From 0db11884bcdc5a3607d2f22372f7a4985b3d9bae Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Sun, 29 Jan 2017 20:26:45 +0100 Subject: [PATCH] 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 --- app/controllers/exercises_controller.rb | 53 +++++++++++- app/controllers/proxy_exercises_controller.rb | 80 +++++++++++++++++++ app/controllers/tags_controller.rb | 55 +++++++++++++ app/models/concerns/user.rb | 4 + app/models/exercise.rb | 10 +++ app/models/exercise_collection.rb | 5 ++ app/models/exercise_tag.rb | 13 +++ app/models/intervention.rb | 15 ++++ app/models/proxy_exercise.rb | 27 +++++++ app/models/tag.rb | 22 +++++ app/models/user_exercise_feedback.rb | 8 ++ app/models/user_exercise_intervention.rb | 11 +++ app/models/user_proxy_exercise_exercise.rb | 14 ++++ app/policies/exercise_policy.rb | 2 +- app/policies/proxy_exercise_policy.rb | 34 ++++++++ app/policies/tag_policy.rb | 34 ++++++++ app/views/exercises/_editor.html.slim | 2 +- app/views/exercises/_form.html.slim | 19 +++++ app/views/exercises/implement.html.slim | 1 + app/views/exercises/index.html.slim | 6 ++ app/views/exercises/show.html.slim | 3 + app/views/proxy_exercises/_form.html.slim | 24 ++++++ app/views/proxy_exercises/edit.html.slim | 3 + app/views/proxy_exercises/index.html.slim | 35 ++++++++ app/views/proxy_exercises/new.html.slim | 3 + .../proxy_exercises/reload.json.jbuilder | 3 + app/views/proxy_exercises/show.html.slim | 23 ++++++ app/views/tags/_form.html.slim | 6 ++ app/views/tags/edit.html.slim | 3 + app/views/tags/index.html.slim | 19 +++++ app/views/tags/new.html.slim | 3 + app/views/tags/show.html.slim | 6 ++ config/locales/de.yml | 17 ++++ config/locales/en.yml | 17 ++++ config/routes.rb | 17 ++++ ...70205163247_create_exercise_collections.rb | 14 ++++ .../20170205165450_create_proxy_exercises.rb | 23 ++++++ .../20170205210357_create_interventions.rb | 16 ++++ db/migrate/20170206141210_add_tags.rb | 19 +++++ .../20170206152503_add_user_feedback.rb | 11 +++ 40 files changed, 675 insertions(+), 5 deletions(-) create mode 100644 app/controllers/proxy_exercises_controller.rb create mode 100644 app/controllers/tags_controller.rb create mode 100644 app/models/exercise_collection.rb create mode 100644 app/models/exercise_tag.rb create mode 100644 app/models/intervention.rb create mode 100644 app/models/proxy_exercise.rb create mode 100644 app/models/tag.rb create mode 100644 app/models/user_exercise_feedback.rb create mode 100644 app/models/user_exercise_intervention.rb create mode 100644 app/models/user_proxy_exercise_exercise.rb create mode 100644 app/policies/proxy_exercise_policy.rb create mode 100644 app/policies/tag_policy.rb create mode 100644 app/views/proxy_exercises/_form.html.slim create mode 100644 app/views/proxy_exercises/edit.html.slim create mode 100644 app/views/proxy_exercises/index.html.slim create mode 100644 app/views/proxy_exercises/new.html.slim create mode 100644 app/views/proxy_exercises/reload.json.jbuilder create mode 100644 app/views/proxy_exercises/show.html.slim create mode 100644 app/views/tags/_form.html.slim create mode 100644 app/views/tags/edit.html.slim create mode 100644 app/views/tags/index.html.slim create mode 100644 app/views/tags/new.html.slim create mode 100644 app/views/tags/show.html.slim create mode 100644 db/migrate/20170205163247_create_exercise_collections.rb create mode 100644 db/migrate/20170205165450_create_proxy_exercises.rb create mode 100644 db/migrate/20170205210357_create_interventions.rb create mode 100644 db/migrate/20170206141210_add_tags.rb create mode 100644 db/migrate/20170206152503_add_user_feedback.rb diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 79314208..f27324ad 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -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 diff --git a/app/controllers/proxy_exercises_controller.rb b/app/controllers/proxy_exercises_controller.rb new file mode 100644 index 00000000..02fe0220 --- /dev/null +++ b/app/controllers/proxy_exercises_controller.rb @@ -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 diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 00000000..ff6925dd --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -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 diff --git a/app/models/concerns/user.rb b/app/models/concerns/user.rb index ee72a715..330ccacd 100644 --- a/app/models/concerns/user.rb +++ b/app/models/concerns/user.rb @@ -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 diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 29f260c2..37e421a7 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -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 diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb new file mode 100644 index 00000000..2dca0e9d --- /dev/null +++ b/app/models/exercise_collection.rb @@ -0,0 +1,5 @@ +class ExerciseCollection < ActiveRecord::Base + + has_and_belongs_to_many :exercises + +end \ No newline at end of file diff --git a/app/models/exercise_tag.rb b/app/models/exercise_tag.rb new file mode 100644 index 00000000..4b8ab3e5 --- /dev/null +++ b/app/models/exercise_tag.rb @@ -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 \ No newline at end of file diff --git a/app/models/intervention.rb b/app/models/intervention.rb new file mode 100644 index 00000000..960a4188 --- /dev/null +++ b/app/models/intervention.rb @@ -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 \ No newline at end of file diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb new file mode 100644 index 00000000..1f6d47c1 --- /dev/null +++ b/app/models/proxy_exercise.rb @@ -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 \ No newline at end of file diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100644 index 00000000..002ec687 --- /dev/null +++ b/app/models/tag.rb @@ -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 \ No newline at end of file diff --git a/app/models/user_exercise_feedback.rb b/app/models/user_exercise_feedback.rb new file mode 100644 index 00000000..d3ec09d5 --- /dev/null +++ b/app/models/user_exercise_feedback.rb @@ -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 \ No newline at end of file diff --git a/app/models/user_exercise_intervention.rb b/app/models/user_exercise_intervention.rb new file mode 100644 index 00000000..60824c34 --- /dev/null +++ b/app/models/user_exercise_intervention.rb @@ -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 \ No newline at end of file diff --git a/app/models/user_proxy_exercise_exercise.rb b/app/models/user_proxy_exercise_exercise.rb new file mode 100644 index 00000000..e7defae6 --- /dev/null +++ b/app/models/user_proxy_exercise_exercise.rb @@ -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 \ No newline at end of file diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 55f7d16b..c89ad86a 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -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 diff --git a/app/policies/proxy_exercise_policy.rb b/app/policies/proxy_exercise_policy.rb new file mode 100644 index 00000000..28de525e --- /dev/null +++ b/app/policies/proxy_exercise_policy.rb @@ -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 diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb new file mode 100644 index 00000000..8325b9fa --- /dev/null +++ b/app/policies/tag_policy.rb @@ -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 diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 56986673..ff18968c 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -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') \ No newline at end of file diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index c4159254..0811b447 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -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| diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 87ff4e1f..300775a6 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -22,3 +22,4 @@ #questions-column #questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}" = qa_js_tag + diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 9ad5cc25..bd8fe880 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -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? diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index 902f8135..1efbd612 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -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') diff --git a/app/views/proxy_exercises/_form.html.slim b/app/views/proxy_exercises/_form.html.slim new file mode 100644 index 00000000..bd57bf06 --- /dev/null +++ b/app/views/proxy_exercises/_form.html.slim @@ -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) \ No newline at end of file diff --git a/app/views/proxy_exercises/edit.html.slim b/app/views/proxy_exercises/edit.html.slim new file mode 100644 index 00000000..8aa200c9 --- /dev/null +++ b/app/views/proxy_exercises/edit.html.slim @@ -0,0 +1,3 @@ +h1 = t('activerecord.models.proxy_exercise.one', model: ProxyExercise.model_name.human)+ ": " + @proxy_exercise.title + += render('form') diff --git a/app/views/proxy_exercises/index.html.slim b/app/views/proxy_exercises/index.html.slim new file mode 100644 index 00000000..2a8067c1 --- /dev/null +++ b/app/views/proxy_exercises/index.html.slim @@ -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) diff --git a/app/views/proxy_exercises/new.html.slim b/app/views/proxy_exercises/new.html.slim new file mode 100644 index 00000000..ae59a292 --- /dev/null +++ b/app/views/proxy_exercises/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: ProxyExercise.model_name.human) + += render('form') diff --git a/app/views/proxy_exercises/reload.json.jbuilder b/app/views/proxy_exercises/reload.json.jbuilder new file mode 100644 index 00000000..8e5d4e3c --- /dev/null +++ b/app/views/proxy_exercises/reload.json.jbuilder @@ -0,0 +1,3 @@ +json.set! :files do + json.array! @exercise.files.visible, :content, :id +end diff --git a/app/views/proxy_exercises/show.html.slim b/app/views/proxy_exercises/show.html.slim new file mode 100644 index 00000000..c1888d79 --- /dev/null +++ b/app/views/proxy_exercises/show.html.slim @@ -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) diff --git a/app/views/tags/_form.html.slim b/app/views/tags/_form.html.slim new file mode 100644 index 00000000..4f02a28f --- /dev/null +++ b/app/views/tags/_form.html.slim @@ -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) diff --git a/app/views/tags/edit.html.slim b/app/views/tags/edit.html.slim new file mode 100644 index 00000000..23c76720 --- /dev/null +++ b/app/views/tags/edit.html.slim @@ -0,0 +1,3 @@ +h1 = @tag.name + += render('form') diff --git a/app/views/tags/index.html.slim b/app/views/tags/index.html.slim new file mode 100644 index 00000000..2d4916af --- /dev/null +++ b/app/views/tags/index.html.slim @@ -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) diff --git a/app/views/tags/new.html.slim b/app/views/tags/new.html.slim new file mode 100644 index 00000000..a933bede --- /dev/null +++ b/app/views/tags/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: Hint.model_name.human) + += render('form') diff --git a/app/views/tags/show.html.slim b/app/views/tags/show.html.slim new file mode 100644 index 00000000..81eda745 --- /dev/null +++ b/app/views/tags/show.html.slim @@ -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) diff --git a/config/locales/de.yml b/config/locales/de.yml index 5b4329df..09c6932c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 5541d68f..e3c0bc6f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index b4606f74..87cde74c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20170205163247_create_exercise_collections.rb b/db/migrate/20170205163247_create_exercise_collections.rb new file mode 100644 index 00000000..ef27f756 --- /dev/null +++ b/db/migrate/20170205163247_create_exercise_collections.rb @@ -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 diff --git a/db/migrate/20170205165450_create_proxy_exercises.rb b/db/migrate/20170205165450_create_proxy_exercises.rb new file mode 100644 index 00000000..fb2704ce --- /dev/null +++ b/db/migrate/20170205165450_create_proxy_exercises.rb @@ -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 diff --git a/db/migrate/20170205210357_create_interventions.rb b/db/migrate/20170205210357_create_interventions.rb new file mode 100644 index 00000000..1b7a8121 --- /dev/null +++ b/db/migrate/20170205210357_create_interventions.rb @@ -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 diff --git a/db/migrate/20170206141210_add_tags.rb b/db/migrate/20170206141210_add_tags.rb new file mode 100644 index 00000000..fd95b4dc --- /dev/null +++ b/db/migrate/20170206141210_add_tags.rb @@ -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 diff --git a/db/migrate/20170206152503_add_user_feedback.rb b/db/migrate/20170206152503_add_user_feedback.rb new file mode 100644 index 00000000..f62ccd9d --- /dev/null +++ b/db/migrate/20170206152503_add_user_feedback.rb @@ -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