Extended Exercises by worktime, difficulty and tags, added ProxyExercises as prework for recommendations

Tags can be added to exercises in the edit view. Tags can monitored under /tags.
Added the concept of ProxyExercises which are a collection of Exercises. They can be found under /proxy_exercises
Added Interventions as prework to show interventions later to the user.
Added exercise/[:id]/working_time to return the working time of the user in this exercise and the average working time of all users in this exercise
This commit is contained in:
Thomas Hille
2017-01-29 20:26:45 +01:00
parent 8f927d5ac9
commit 0db11884bc
40 changed files with 675 additions and 5 deletions

View File

@ -6,7 +6,7 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload]
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :run, :statistics, :submit, :reload]
before_action :set_external_user, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update]
@ -54,6 +54,20 @@ class ExercisesController < ApplicationController
def create
@exercise = Exercise.new(exercise_params)
collect_set_and_unset_exercise_tags
myparam = exercise_params
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
for et in checked_exercise_tags
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
et.exercise = @exercise
end
myparam[:exercise_tags] = checked_exercise_tags
myparam.delete :tag_ids
removed_exercise_tags.map {|et| et.destroy}
authorize!
create_and_respond(object: @exercise)
end
@ -63,6 +77,7 @@ class ExercisesController < ApplicationController
end
def edit
collect_set_and_unset_exercise_tags
end
def import_proforma_xml
@ -118,7 +133,8 @@ class ExercisesController < ApplicationController
private :user_by_code_harbor_token
def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
params[:exercise][:expected_worktime_seconds] = params[:exercise][:expected_worktime_minutes].to_i * 60
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :expected_worktime_seconds, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :exercise_params
@ -150,6 +166,12 @@ class ExercisesController < ApplicationController
end
end
def working_times
working_time_accumulated = Time.parse(@exercise.average_working_time_for_only(current_user.id) || "00:00:00").seconds_since_midnight
working_time_avg = Time.parse(@exercise.average_working_time || "00:00:00").seconds_since_midnight
render(json: {working_time_avg: working_time_avg, working_time_accumulated: working_time_accumulated})
end
def index
@search = policy_scope(Exercise).search(params[:q])
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
@ -174,6 +196,8 @@ class ExercisesController < ApplicationController
def new
@exercise = Exercise.new
collect_set_and_unset_exercise_tags
authorize!
end
@ -201,6 +225,16 @@ class ExercisesController < ApplicationController
end
private :set_file_types
def collect_set_and_unset_exercise_tags
@search = policy_scope(Tag).search(params[:q])
@tags = @search.result.order(:name)
exercise_tags = @exercise.exercise_tags
tags_set = exercise_tags.collect{|e| e.tag}.to_set
tags_not_set = Tag.all.to_set.subtract tags_set
@exercise_tags = exercise_tags + tags_not_set.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)}
end
private :collect_set_and_unset_exercise_tags
def show
end
@ -252,7 +286,20 @@ class ExercisesController < ApplicationController
private :transmit_lti_score
def update
update_and_respond(object: @exercise, params: exercise_params)
collect_set_and_unset_exercise_tags
myparam = exercise_params
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
for et in checked_exercise_tags
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
et.exercise = @exercise
end
myparam[:exercise_tags] = checked_exercise_tags
myparam.delete :tag_ids
removed_exercise_tags.map {|et| et.destroy}
update_and_respond(object: @exercise, params: myparam)
end
def redirect_after_submit

View File

@ -0,0 +1,80 @@
class ProxyExercisesController < ApplicationController
include CommonBehavior
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :reload]
def authorize!
authorize(@proxy_exercise || @proxy_exercises)
end
private :authorize!
def clone
proxy_exercise = @proxy_exercise.duplicate(token: nil, exercises: @proxy_exercise.exercises)
proxy_exercise.send(:generate_token)
if proxy_exercise.save
redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
else
flash[:danger] = t('shared.message_failure')
redirect_to(@proxy_exercise)
end
end
def create
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.empty? })
@proxy_exercise = ProxyExercise.new(myparams)
authorize!
create_and_respond(object: @proxy_exercise)
end
def destroy
destroy_and_respond(object: @proxy_exercise)
end
def edit
@search = policy_scope(Exercise).search(params[:q])
@exercises = @search.result.order(:title)
authorize!
end
def proxy_exercise_params
params[:proxy_exercise].permit(:description, :title, :exercise_ids => [])
end
private :proxy_exercise_params
def index
@search = policy_scope(ProxyExercise).search(params[:q])
@proxy_exercises = @search.result.order(:title).paginate(page: params[:page])
authorize!
end
def new
@proxy_exercise = ProxyExercise.new
@search = policy_scope(Exercise).search(params[:q])
@exercises = @search.result.order(:title)
authorize!
end
def set_exercise
@proxy_exercise = ProxyExercise.find(params[:id])
authorize!
end
private :set_exercise
def show
@search = @proxy_exercise.exercises.search
@exercises = @proxy_exercise.exercises.search.result.order(:title) #@search.result.order(:title)
end
#we might want to think about auth here
def reload
end
def update
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.blank? })
update_and_respond(object: @proxy_exercise, params: myparams)
end
end

View File

@ -0,0 +1,55 @@
class TagsController < ApplicationController
include CommonBehavior
before_action :set_tag, only: MEMBER_ACTIONS
def authorize!
authorize(@tag || @tags)
end
private :authorize!
def create
@tag = Tag.new(tag_params)
authorize!
create_and_respond(object: @tag)
end
def destroy
destroy_and_respond(object: @tag)
end
def edit
end
def tag_params
params[:tag].permit(:name)
end
private :tag_params
def index
@tags = Tag.all.paginate(page: params[:page])
authorize!
end
def new
@tag = Tag.new
authorize!
end
def set_tag
@tag = Tag.find(params[:id])
authorize!
end
private :set_tag
def show
end
def update
update_and_respond(object: @tag, params: tag_params)
end
def to_s
name
end
end

View File

@ -8,6 +8,10 @@ module User
has_many :exercises, as: :user
has_many :file_types, as: :user
has_many :submissions, as: :user
has_many :user_proxy_exercise_exercises, as: :user
has_many :user_exercise_interventions, as: :user
has_many :interventions, through: :user_exercise_interventions
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
end

View File

@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base
belongs_to :execution_environment
has_many :submissions
has_and_belongs_to_many :proxy_exercises
has_many :user_proxy_exercise_exercises
has_and_belongs_to_many :exercise_collections
has_many :user_exercise_interventions
has_many :interventions, through: :user_exercise_interventions
has_many :exercise_tags
has_many :tags, through: :exercise_tags
accepts_nested_attributes_for :exercise_tags
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
alias_method :users, :external_users
@ -105,6 +114,7 @@ class Exercise < ActiveRecord::Base
def duplicate(attributes = {})
exercise = dup
exercise.attributes = attributes
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
files.each { |file| exercise.files << file.dup }
exercise
end

View File

@ -0,0 +1,5 @@
class ExerciseCollection < ActiveRecord::Base
has_and_belongs_to_many :exercises
end

View File

@ -0,0 +1,13 @@
class ExerciseTag < ActiveRecord::Base
belongs_to :tag
belongs_to :exercise
before_save :destroy_if_empty_exercise_or_tag
private
def destroy_if_empty_exercise_or_tag
destroy if exercise_id.blank? || tag_id.blank?
end
end

View File

@ -0,0 +1,15 @@
class Intervention < ActiveRecord::Base
NAME = %w(overallSlower longSession syntaxErrors videoNotWatched)
has_many :user_exercise_interventions
has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser"
#belongs_to :user, polymorphic: true
#belongs_to :external_users, source: :user, source_type: ExternalUser
#belongs_to :internal_users, source: :user, source_type: InternalUser, through: :user_interventions
# alias_method :users, :external_users
#has_many :exercises, through: :user_interventions
validates :name, inclusion: {in: NAME}
end

View File

@ -0,0 +1,27 @@
class ProxyExercise < ActiveRecord::Base
after_initialize :generate_token
has_and_belongs_to_many :exercises
has_many :user_proxy_exercise_exercises
def count_files
exercises.count
end
def generate_token
self.token ||= SecureRandom.hex(4)
end
private :generate_token
def duplicate(attributes = {})
proxy_exercise = dup
proxy_exercise.attributes = attributes
proxy_exercise
end
def to_s
title
end
end

22
app/models/tag.rb Normal file
View File

@ -0,0 +1,22 @@
class Tag < ActiveRecord::Base
has_many :exercise_tags
has_many :exercises, through: :exercise_tags
validates_uniqueness_of :name
def destroy
if (can_be_destroyed?)
super
end
end
def can_be_destroyed?
!exercises.any?
end
def to_s
name
end
end

View File

@ -0,0 +1,8 @@
class UserExerciseFeedback < ActiveRecord::Base
belongs_to :user, polymorphic: true
belongs_to :exercise
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }
end

View File

@ -0,0 +1,11 @@
class UserExerciseIntervention < ActiveRecord::Base
belongs_to :user, polymorphic: true
belongs_to :intervention
belongs_to :exercise
validates :user, presence: true
validates :exercise, presence: true
validates :intervention, presence: true
end

View File

@ -0,0 +1,14 @@
class UserProxyExerciseExercise < ActiveRecord::Base
belongs_to :user, polymorphic: true
belongs_to :exercise
belongs_to :proxy_exercise
validates :user_id, presence: true
validates :user_type, presence: true
validates :exercise_id, presence: true
validates :proxy_exercise_id, presence: true
validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] }
end

View File

@ -16,7 +16,7 @@ class ExercisePolicy < AdminOrAuthorPolicy
define_method(action) { admin? || author?}
end
[:implement?, :submit?, :reload?].each do |action|
[:implement?, :working_times?, :submit?, :reload?].each do |action|
define_method(action) { everyone }
end

View File

@ -0,0 +1,34 @@
class ProxyExercisePolicy < AdminOrAuthorPolicy
def author?
@user == @record.author
end
private :author?
def batch_update?
admin?
end
def show?
@user.internal_user?
end
[:clone?, :destroy?, :edit?, :update?].each do |action|
define_method(action) { admin? || author?}
end
[:reload?].each do |action|
define_method(action) { everyone }
end
class Scope < Scope
def resolve
if @user.admin?
@scope.all
elsif @user.internal_user?
@scope.where('user_id = ? OR public = TRUE', @user.id)
else
@scope.none
end
end
end
end

View File

@ -0,0 +1,34 @@
class TagPolicy < AdminOrAuthorPolicy
def author?
@user == @record.author
end
private :author?
def batch_update?
admin?
end
def show?
@user.internal_user?
end
[:clone?, :destroy?, :edit?, :update?].each do |action|
define_method(action) { admin? || author?}
end
[:reload?].each do |action|
define_method(action) { everyone }
end
class Scope < Scope
def resolve
if @user.admin?
@scope.all
elsif @user.internal_user?
@scope.where('user_id = ? OR public = TRUE', @user.id)
else
@scope.none
end
end
end
end

View File

@ -21,4 +21,4 @@
button style="display:none" id="autosave"
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')

View File

@ -32,6 +32,25 @@
label
= f.check_box(:allow_auto_completion)
= t('activerecord.attributes.exercise.allow_auto_completion')
.form-group
= f.label(t('activerecord.attributes.exercise.difficulty'))
= f.number_field :expected_difficulty, in: 1..10, step: 1
.form-group
= f.label(t('activerecord.attributes.exercise.worktime'))
= f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1
h2 Tags
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.exercise.selection')
th = sort_link(@search, :title, t('activerecord.attributes.tag.name'))
th = t('activerecord.attributes.tag.difficulty')
= collection_check_boxes :exercise, :tag_ids, @exercise_tags, :tag_id, :id do |b|
tr
td = b.check_box
td = b.object.tag.name
td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1
h2 = t('activerecord.attributes.exercise.files')
ul#files.list-unstyled.panel-group
= f.fields_for :files do |files_form|

View File

@ -22,3 +22,4 @@
#questions-column
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
= qa_js_tag

View File

@ -16,6 +16,9 @@ h1 = Exercise.model_name.human(count: 2)
th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment'))
th = t('.test_files')
th = t('activerecord.attributes.exercise.maximum_score')
th = t('activerecord.attributes.exercise.tags')
th = t('activerecord.attributes.exercise.difficulty')
th = t('activerecord.attributes.exercise.worktime')
th
= t('activerecord.attributes.exercise.public')
- if policy(Exercise).batch_update?
@ -29,6 +32,9 @@ h1 = Exercise.model_name.human(count: 2)
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td = exercise.files.teacher_defined_tests.count
td = exercise.maximum_score
td = exercise.exercise_tags.count
td = exercise.expected_difficulty
td = (exercise.expected_worktime_seconds / 60).ceil
td.public data-value=exercise.public? = symbol_for(exercise.public?)
td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?

View File

@ -19,6 +19,9 @@ h1
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
= row(label: 'exercise.embedding_parameters') do
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
= row(label: 'exercise.difficulty', value: @exercise.expected_difficulty)
= row(label: 'exercise.worktime', value: "#{@exercise.expected_worktime_seconds/60} min")
= row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", "))
h2 = t('activerecord.attributes.exercise.files')

View File

@ -0,0 +1,24 @@
= form_for(@proxy_exercise, multipart: true) do |f|
= render('shared/form_errors', object: @proxy_exercise)
.form-group
= f.label(:title)
= f.text_field(:title, class: 'form-control', required: true)
.form-group
= f.label(:description)
= f.pagedown_editor :description
h3 Exercises
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.exercise.selection')
th = sort_link(@search, :title, t('activerecord.attributes.submission.exercise'))
th = sort_link(@search, :created_at, t('shared.created_at'))
= collection_check_boxes :proxy_exercise, :exercise_ids, @exercises, :id, :title do |b|
tr
td = b.check_box
td = link_to(b.object, b.object)
td = l(b.object.created_at, format: :short)
.actions = render('shared/submit_button', f: f, object: @proxy_exercise)

View File

@ -0,0 +1,3 @@
h1 = t('activerecord.models.proxy_exercise.one', model: ProxyExercise.model_name.human)+ ": " + @proxy_exercise.title
= render('form')

View File

@ -0,0 +1,35 @@
h1 = ProxyExercise.model_name.human(count: 2)
= render(layout: 'shared/form_filters') do |f|
.form-group
= f.label(:title_cont, t('activerecord.attributes.proxy_exercise.title'), class: 'sr-only')
= f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.proxy_exercise.title'))
.table-responsive
table.table
thead
tr
th = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title'))
th = "Token"
th = t('activerecord.attributes.proxy_exercise.files_count')
th colspan=6 = t('shared.actions')
tbody
- @proxy_exercises.each do |proxy_exercise|
tr data-id=proxy_exercise.id
td = link_to(proxy_exercise.title,proxy_exercise)
td = proxy_exercise.token
td = proxy_exercise.count_files
td = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit?
td
.btn-group
button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button')
span.caret
span.sr-only Toggle Dropdown
ul.dropdown-menu.pull-right role="menu"
li = link_to(t('shared.show'), proxy_exercise) if policy(proxy_exercise).show?
li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(proxy_exercise).destroy?
li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(proxy_exercise).clone?
= render('shared/pagination', collection: @proxy_exercises)
p = render('shared/new_button', model: ProxyExercise)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: ProxyExercise.model_name.human)
= render('form')

View File

@ -0,0 +1,3 @@
json.set! :files do
json.array! @exercise.files.visible, :content, :id
end

View File

@ -0,0 +1,23 @@
- content_for :head do
= javascript_include_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js')
= stylesheet_link_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css')
h1
= @proxy_exercise.title
- if policy(@proxy_exercise).edit?
= render('shared/edit_button', object: @proxy_exercise)
= row(label: 'exercise.title', value: @proxy_exercise.title)
= row(label: 'proxy_exercise.files_count', value: @exercises.count)
= row(label: 'exercise.description', value: @proxy_exercise.description)
h3 Exercises
.table-responsive
table.table
thead
tr
th = sort_link(@search, :title, t('activerecord.attributes.submission.exercise'))
th = sort_link(@search, :created_at, t('shared.created_at'))
- @proxy_exercise.exercises.each do |exercise|
tr
td = link_to(exercise.title, exercise)
td = l(exercise.created_at, format: :short)

View File

@ -0,0 +1,6 @@
= form_for(@tag) do |f|
= render('shared/form_errors', object: @tag)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.actions = render('shared/submit_button', f: f, object: @tag)

View File

@ -0,0 +1,3 @@
h1 = @tag.name
= render('form')

View File

@ -0,0 +1,19 @@
h1 = Tag.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.hint.name')
/th = t('activerecord.attributes.hint.locale')
/th colspan=3 = t('shared.actions')
tbody
- @tags.each do |tag|
tr
td = tag.name
td = link_to(t('shared.show'), tag)
td = link_to(t('shared.edit'), edit_tag_path(tag))
td = link_to(t('shared.destroy'), tag, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if tag.can_be_destroyed?
= render('shared/pagination', collection: @tags)
p = render('shared/new_button', model: Tag, path: new_tag_path)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: Hint.model_name.human)
= render('form')

View File

@ -0,0 +1,6 @@
h1
= @tag.name
= render('shared/edit_button', object: @tag)
= row(label: 'tag.name', value: @tag.name)
= row(label: 'tag.usage', value: @tag.exercises.count)

View File

@ -27,6 +27,7 @@ de:
exercise:
description: Beschreibung
embedding_parameters: Parameter für LTI-Einbettung
tags: Tags
execution_environment: Ausführungsumgebung
execution_environment_id: Ausführungsumgebung
files: Dateien
@ -34,10 +35,16 @@ de:
instructions: Anweisungen
maximum_score: Erreichbare Punktzahl
public: Öffentlich
selection: Ausgewählt
title: Titel
user: Autor
allow_auto_completion: "Autovervollständigung aktivieren"
allow_file_creation: "Dateierstellung erlauben"
difficulty: Schwierigkeitsgrad
worktime: "vermutete Arbeitszeit in Minuten"
proxy_exercise:
title: Title
files_count: Anzahl der Aufgaben
external_user:
consumer: Konsument
email: E-Mail
@ -91,6 +98,10 @@ de:
files: Dateien
score: Punktzahl
user: Autor
tag:
name: Name
usage: Verwendet
difficulty: Anteil an der Aufgabe
file_template:
name: "Name"
file_type: "Dateityp"
@ -111,6 +122,9 @@ de:
exercise:
one: Aufgabe
other: Aufgaben
proxy_exercise:
one: Proxy Aufgabe
other: Proxy Aufgaben
external_user:
one: Externer Nutzer
other: Externe Nutzer
@ -290,6 +304,9 @@ de:
tests: Unit Tests
time_difference: 'Arbeitszeit bis hier*'
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
proxy_exercises:
index:
clone: Duplizieren
external_users:
statistics:
title: Statistiken für Externe Benutzer

View File

@ -48,6 +48,7 @@ en:
exercise:
description: Description
embedding_parameters: LTI Embedding Parameters
tags: Tags
execution_environment: Execution Environment
execution_environment_id: Execution Environment
files: Files
@ -55,10 +56,16 @@ en:
instructions: Instructions
maximum_score: Maximum Score
public: Public
selection: Selected
title: Title
user: Author
allow_auto_completion: "Allow auto completion"
allow_file_creation: "Allow file creation"
difficulty: Difficulty
worktime: "Expected worktime in minutes"
proxy_exercise:
title: Title
files_count: Exercises Count
external_user:
consumer: Consumer
email: Email
@ -112,6 +119,10 @@ en:
files: Files
score: Score
user: Author
tag:
name: Name
usage: Used
difficulty: Share on the Exercise
file_template:
name: "Name"
file_type: "File Type"
@ -132,6 +143,9 @@ en:
exercise:
one: Exercise
other: Exercises
proxy_exercise:
one: Proxy Exercise
other: Proxy Exercises
external_user:
one: External User
other: External Users
@ -311,6 +325,9 @@ en:
tests: Unit Test Results
time_difference: 'Working Time until here*'
addendum: '* Deltas longer than 30 minutes are ignored.'
proxy_exercises:
index:
clone: Duplicate
external_users:
statistics:
title: External User Statistics

View File

@ -60,12 +60,29 @@ Rails.application.routes.draw do
member do
post :clone
get :implement
get :working_times
get :statistics
get :reload
post :submit
end
end
resources :proxy_exercises do
member do
post :clone
get :reload
post :submit
end
end
resources :tags do
member do
post :clone
get :reload
post :submit
end
end
resources :external_users, only: [:index, :show], concerns: :statistics do
resources :exercises, concerns: :statistics
end

View File

@ -0,0 +1,14 @@
class CreateExerciseCollections < ActiveRecord::Migration
def change
create_table :exercise_collections do |t|
t.string :name
t.timestamps
end
create_table :exercise_collections_exercises, id: false do |t|
t.belongs_to :exercise_collection, index: true
t.belongs_to :exercise, index: true
end
end
end

View File

@ -0,0 +1,23 @@
class CreateProxyExercises < ActiveRecord::Migration
def change
create_table :proxy_exercises do |t|
t.string :title
t.string :description
t.string :token
t.timestamps
end
create_table :exercises_proxy_exercises, id: false do |t|
t.belongs_to :proxy_exercise, index: true
t.belongs_to :exercise, index: true
t.timestamps
end
create_table :user_proxy_exercise_exercises do |t|
t.belongs_to :user, polymorphic: true, index: true
t.belongs_to :proxy_exercise, index: true
t.belongs_to :exercise, index: true
t.timestamps
end
end
end

View File

@ -0,0 +1,16 @@
class CreateInterventions < ActiveRecord::Migration
def change
create_table :user_exercise_interventions do |t|
t.belongs_to :user, polymorphic: true
t.belongs_to :exercise
t.belongs_to :intervention
t.timestamps
end
create_table :interventions do |t|
t.string :name
t.text :markup
t.timestamps
end
end
end

View File

@ -0,0 +1,19 @@
class AddTags < ActiveRecord::Migration
def change
add_column :exercises, :expected_worktime_seconds, :integer, default: 0
add_column :exercises, :expected_difficulty, :integer, default: 1
create_table :tags do |t|
t.string :name, null: false
t.timestamps
end
create_table :exercise_tags do |t|
t.belongs_to :exercise
t.belongs_to :tag
t.integer :factor, default: 0
end
end
end

View File

@ -0,0 +1,11 @@
class AddUserFeedback < ActiveRecord::Migration
def change
create_table :user_exercise_feedbacks do |t|
t.belongs_to :exercise, null: false
t.belongs_to :user, polymorphic: true, null: false
t.integer :difficulty
t.integer :working_time_seconds
t.string :feedback_text
end
end
end