diff --git a/app/controllers/concerns/redirect_behavior.rb b/app/controllers/concerns/redirect_behavior.rb index da1b80bb..50a6131a 100644 --- a/app/controllers/concerns/redirect_behavior.rb +++ b/app/controllers/concerns/redirect_behavior.rb @@ -5,6 +5,13 @@ module RedirectBehavior def redirect_after_submit Rails.logger.debug { "Redirecting user with score:s #{@submission.normalized_score}" } + + # TEMPORARY: For the pythonjunior2023 course, we want to have a lot of feedback! + if @submission.redirect_to_survey? + redirect_to_pair_programming_survey + return + end + if @submission.normalized_score.to_d == BigDecimal('1.0') if redirect_to_community_solution? redirect_to_community_solution @@ -98,6 +105,16 @@ module RedirectBehavior @community_solution_lock.user == current_user end + # TEMPORARY: For the pythonjunior2023 course, we introduced a pair programming survey + def redirect_to_pair_programming_survey + url = new_pair_programming_exercise_feedback_path(pair_programming_exercise_feedback: {exercise_id: @exercise.id, submission_id: @submission.id}) + + respond_to do |format| + format.html { redirect_to(url) } + format.json { render(json: {redirect: url}) } + end + end + def redirect_to_user_feedback uef = UserExerciseFeedback.find_by(exercise: @exercise, user: current_user) url = if uef diff --git a/app/controllers/pair_programming_exercise_feedbacks_controller.rb b/app/controllers/pair_programming_exercise_feedbacks_controller.rb new file mode 100644 index 00000000..7c069525 --- /dev/null +++ b/app/controllers/pair_programming_exercise_feedbacks_controller.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +class PairProgrammingExerciseFeedbacksController < ApplicationController + include CommonBehavior + include RedirectBehavior + + before_action :set_presets, only: %i[new create] + + def comment_presets + [[0, t('pair_programming_exercise_feedback.difficulty_easy')], + [1, t('pair_programming_exercise_feedback.difficulty_some_what_easy')], + [2, t('pair_programming_exercise_feedback.difficulty_ok')], + [3, t('pair_programming_exercise_feedback.difficulty_some_what_difficult')], + [4, t('pair_programming_exercise_feedback.difficult_too_difficult')]] + end + + def time_presets + [[0, t('pair_programming_exercise_feedback.estimated_time_less_5')], + [1, t('pair_programming_exercise_feedback.estimated_time_5_to_10')], + [2, t('pair_programming_exercise_feedback.estimated_time_10_to_20')], + [3, t('pair_programming_exercise_feedback.estimated_time_20_to_30')], + [4, t('pair_programming_exercise_feedback.estimated_time_more_30')]] + end + + def new + exercise_id = if params[:pair_programming_exercise_feedback].nil? + params[:exercise_id] + else + params[:pair_programming_exercise_feedback][:exercise_id] + end + @exercise = Exercise.find(exercise_id) + + @submission = Submission.find(params[:pair_programming_exercise_feedback][:submission_id]) + authorize(@submission, :show?) + + @uef = PairProgrammingExerciseFeedback.new(user: current_user, exercise: @exercise, programming_group:, submission: @submission) + authorize! + end + + def create + Sentry.set_extras(params: uef_params) + + @exercise = Exercise.find(uef_params[:exercise_id]) + + if @exercise + @uef = PairProgrammingExerciseFeedback.new(exercise: @exercise, programming_group:, study_group_id: current_user.current_study_group_id) + @uef.update(uef_params) + authorize! + if validate_inputs(uef_params) && @uef.save + redirect_after_submit + else + flash.now[:danger] = t('shared.message_failure') + redirect_back fallback_location: pair_programming_exercise_feedback_path(@uef) + end + end + end + + private + + def authorize! + authorize(@uef || @uefs) + end + + def set_presets + @texts = comment_presets.to_a + @times = time_presets.to_a + end + + def uef_params + return if params[:pair_programming_exercise_feedback].blank? + + @submission = Submission.find(params[:pair_programming_exercise_feedback][:submission_id]) + + authorize(@submission, :show?) + + params[:pair_programming_exercise_feedback] + .permit(:difficulty, :user_estimated_worktime, :exercise_id) + .merge(user: current_user, + submission: @submission, + normalized_score: @submission&.normalized_score) + end + + def validate_inputs(uef_params) + if uef_params[:difficulty].to_i.negative? || uef_params[:difficulty].to_i >= comment_presets.size + false + else + !(uef_params[:user_estimated_worktime].to_i.negative? || uef_params[:user_estimated_worktime].to_i >= time_presets.size) + end + rescue StandardError + false + end + + def programming_group + current_contributor if current_contributor.programming_group? + end +end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 39a325cf..fa79eb35 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -25,6 +25,7 @@ class Exercise < ApplicationRecord has_many :tags, through: :exercise_tags accepts_nested_attributes_for :exercise_tags has_many :user_exercise_feedbacks + has_many :pair_programming_exercise_feedbacks has_many :exercise_tips has_many :tips, through: :exercise_tips @@ -590,6 +591,8 @@ class Exercise < ApplicationRecord private :valid_submission_deadlines? def needs_more_feedback?(submission) + return false if PairProgramming23Study.experiment_course?(submission.study_group_id) + if submission.normalized_score.to_d == BigDecimal('1.0') user_exercise_feedbacks.final.size <= MAX_GROUP_EXERCISE_FEEDBACKS else diff --git a/app/models/pair_programming_exercise_feedback.rb b/app/models/pair_programming_exercise_feedback.rb new file mode 100644 index 00000000..f1d4ac36 --- /dev/null +++ b/app/models/pair_programming_exercise_feedback.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class PairProgrammingExerciseFeedback < ApplicationRecord + include Creation + + belongs_to :exercise + belongs_to :submission + belongs_to :study_group + belongs_to :programming_group, optional: true + has_one :execution_environment, through: :exercise + + scope :intermediate, -> { where.not(normalized_score: 1.00) } + scope :final, -> { where(normalized_score: 1.00) } + + def to_s + 'Pair Programming Exercise Feedback' + end +end diff --git a/app/models/programming_group.rb b/app/models/programming_group.rb index d87cc9d9..45171ed3 100644 --- a/app/models/programming_group.rb +++ b/app/models/programming_group.rb @@ -11,6 +11,7 @@ class ProgrammingGroup < ApplicationRecord has_many :runners, as: :contributor, dependent: :destroy has_many :events has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor' + has_many :pair_programming_exercise_feedbacks belongs_to :exercise validate :min_group_size diff --git a/app/models/study_group.rb b/app/models/study_group.rb index 1fb22813..841f8377 100644 --- a/app/models/study_group.rb +++ b/app/models/study_group.rb @@ -11,6 +11,7 @@ class StudyGroup < ApplicationRecord has_many :lti_parameters, dependent: :delete_all has_many :events has_many :events_synchronized_editor, class_name: 'Event::SynchronizedEditor' + has_many :pair_programming_exercise_feedbacks belongs_to :consumer def users diff --git a/app/models/submission.rb b/app/models/submission.rb index 368eb507..c515a5e5 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -17,6 +17,8 @@ class Submission < ApplicationRecord has_many :testruns has_many :structured_errors, dependent: :destroy has_many :comments, through: :files + has_one :user_exercise_feedback + has_one :pair_programming_exercise_feedback belongs_to :external_users, lambda { where(submissions: {contributor_type: 'ExternalUser'}).includes(:submissions) @@ -118,12 +120,18 @@ class Submission < ApplicationRecord end def redirect_to_feedback? + return false if PairProgramming23Study.experiment_course?(study_group_id) + # Redirect 10% of users to the exercise feedback page. Ensure, that always the same # users get redirected per exercise and different users for different exercises. If # desired, the number of feedbacks can be limited with exercise.needs_more_feedback?(submission) (contributor_id + exercise.created_at.to_i) % 10 == 1 end + def redirect_to_survey? + cause == 'submit' && pair_programming_exercise_feedback.blank? && PairProgramming23Study.experiment_course?(study_group_id) + end + def own_unsolved_rfc(user) Pundit.policy_scope(user, RequestForComment).joins(:submission).where(submission: {contributor:}).unsolved.find_by(exercise:) end diff --git a/app/policies/pair_programming_exercise_feedback_policy.rb b/app/policies/pair_programming_exercise_feedback_policy.rb new file mode 100644 index 00000000..cf58c9f0 --- /dev/null +++ b/app/policies/pair_programming_exercise_feedback_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class PairProgrammingExerciseFeedbackPolicy < AdminOnlyPolicy + def create? + everyone + end + + def new? + everyone + end +end diff --git a/app/views/pair_programming_exercise_feedbacks/_form.html.slim b/app/views/pair_programming_exercise_feedbacks/_form.html.slim new file mode 100644 index 00000000..41c2d217 --- /dev/null +++ b/app/views/pair_programming_exercise_feedbacks/_form.html.slim @@ -0,0 +1,23 @@ += form_for(@uef) do |f| + div + h1 id="exercise-headline" + = t('activerecord.models.user_exercise_feedback.one') + " " + @exercise.title + = render('shared/form_errors', object: @uef) + p + == t('pair_programming_exercise_feedback.description') + .mb-3 + h5.mt-4 = t('pair_programming_exercise_feedback.difficulty') + = f.collection_radio_buttons :difficulty, @texts, :first, :last do |b| + .form-check + label.form-check-label + = b.radio_button(class: 'form-check-input') + = b.text + h5.mt-4 = t('pair_programming_exercise_feedback.working_time') + = f.collection_radio_buttons :user_estimated_worktime, @times, :first, :last do |b| + .form-check + label.form-check-label + = b.radio_button(class: 'form-check-input') + = b.text + = f.hidden_field(:exercise_id, :value => @exercise.id) + = f.hidden_field(:submission_id, :value => @submission.id) + .actions = render('shared/submit_button', f: f, object: @uef) diff --git a/app/views/pair_programming_exercise_feedbacks/new.html.slim b/app/views/pair_programming_exercise_feedbacks/new.html.slim new file mode 100644 index 00000000..7e5cfff1 --- /dev/null +++ b/app/views/pair_programming_exercise_feedbacks/new.html.slim @@ -0,0 +1 @@ += render('form') diff --git a/config/locales/de.yml b/config/locales/de.yml index 444200b2..f3b00c95 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -229,6 +229,9 @@ de: internal_user: one: Interner Nutzer other: Interne Nutzer + pair_programming_exercise_feedback: + one: Feedback + other: Feedbacks programming_group: one: Programmiergruppe other: Programmiergruppen @@ -975,6 +978,21 @@ de: previous_label: '← Vorherige Seite' file_template: no_template_label: "Leere Datei" + pair_programming_exercise_feedback: + difficulty_easy: "Die Aufgabe war viel zu einfach." + difficulty_some_what_easy: "Die Aufgabe war etwas zu einfach." + difficulty_ok: "Die Aufgabenschwierigkeit war genau richtig." + difficulty_some_what_difficult: "Die Aufgabe war etwas zu schwierig." + difficult_too_difficult: "Die Aufgabe war viel zu schwierig." + difficulty: "Schwierigkeit der Aufgabe:" + description: "Vielen Dank für Deine Abgabe! Bevor du die Aufgabe beendest, würden wir uns freuen, wenn Du uns hier Feedback zur Aufgabe gibst." + estimated_time_less_5: "weniger als 5 Minuten" + estimated_time_5_to_10: "zwischen 5 und 10 Minuten" + estimated_time_10_to_20: "zwischen 10 und 20 Minuten" + estimated_time_20_to_30: "zwischen 20 und 30 Minuten" + estimated_time_more_30: "mehr als 30 Minuten" + working_time: "Geschätze Bearbeitungszeit für diese Aufgabe:" + no_feedback: "Es wurde noch kein Feedback zu dieser Aufgabe gegeben." user_exercise_feedback: difficulty_easy: "Die Aufgabe war zu einfach" difficulty_some_what_easy: "Die Aufgabe war etwas zu einfach" diff --git a/config/locales/en.yml b/config/locales/en.yml index ce116a2b..ff2987c8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -229,6 +229,9 @@ en: internal_user: one: Internal User other: Internal Users + pair_programming_exercise_feedback: + one: Feedback + other: Feedbacks programming_group: one: Programming Group other: Programming Groups @@ -975,6 +978,21 @@ en: previous_label: '← Previous Page' file_template: no_template_label: "Empty File" + pair_programming_exercise_feedback: + difficulty_easy: "The exercise was far too easy." + difficulty_some_what_easy: "The exercise was a bit too easy." + difficulty_ok: "The exercise difficulty was just right." + difficulty_some_what_difficult: "The exercise was a bit too difficult." + difficult_too_difficult: "The exercise was far too difficult." + difficulty: "Difficulty of the exercise:" + description: "Thank you for your submission! Before you finish the task, we kindly ask you for feedback for this exercise." + estimated_time_less_5: "less than 5 minutes" + estimated_time_5_to_10: "between 5 and 10 minutes" + estimated_time_10_to_20: "between 10 and 20 minutes" + estimated_time_20_to_30: "between 20 and 30 minutes" + estimated_time_more_30: "more than 30 minutes" + working_time: "Estimated time working on this exercise:" + no_feedback: "There is no feedback for this exercise yet." user_exercise_feedback: difficulty_easy: "the exercise was too easy" difficulty_some_what_easy: "the exercise was somewhat easy" diff --git a/config/routes.rb b/config/routes.rb index 384c60d7..b1a340a4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -119,6 +119,8 @@ Rails.application.routes.draw do resources :user_exercise_feedbacks, except: %i[show index] + resources :pair_programming_exercise_feedbacks, only: %i[new create] + resources :external_users, only: %i[index show], concerns: :statistics do resources :exercises do get :statistics, to: 'exercises#external_user_statistics', on: :member diff --git a/db/migrate/20230905190519_add_pair_programming_exercise_feedbacks.rb b/db/migrate/20230905190519_add_pair_programming_exercise_feedbacks.rb new file mode 100644 index 00000000..7c6b196e --- /dev/null +++ b/db/migrate/20230905190519_add_pair_programming_exercise_feedbacks.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddPairProgrammingExerciseFeedbacks < ActiveRecord::Migration[7.0] + def change + create_table :pair_programming_exercise_feedbacks do |t| + t.references :exercise, null: false, index: true, foreign_key: true + t.references :submission, null: false, index: true, foreign_key: true + t.references :user, polymorphic: true, null: false, index: true + t.references :programming_group, null: true, index: {name: 'pp_feedback_programming_group'}, foreign_key: true + t.references :study_group, null: false, index: true, foreign_key: true + t.integer :difficulty + t.integer :user_estimated_worktime + t.integer :reason_work_alone + t.float :normalized_score + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5955d25a..0b16392c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_04_180123) do +ActiveRecord::Schema[7.0].define(version: 2023_09_05_190519) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" enable_extension "pgcrypto" @@ -382,6 +382,26 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_04_180123) do t.index ["study_group_id"], name: "index_lti_parameters_on_study_group_id" end + create_table "pair_programming_exercise_feedbacks", force: :cascade do |t| + t.bigint "exercise_id", null: false + t.bigint "submission_id", null: false + t.string "user_type", null: false + t.bigint "user_id", null: false + t.bigint "programming_group_id" + t.bigint "study_group_id", null: false + t.integer "difficulty" + t.integer "user_estimated_worktime" + t.integer "reason_work_alone" + t.float "normalized_score" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["exercise_id"], name: "index_pair_programming_exercise_feedbacks_on_exercise_id" + t.index ["programming_group_id"], name: "pp_feedback_programming_group" + t.index ["study_group_id"], name: "index_pair_programming_exercise_feedbacks_on_study_group_id" + t.index ["submission_id"], name: "index_pair_programming_exercise_feedbacks_on_submission_id" + t.index ["user_type", "user_id"], name: "index_pair_programming_exercise_feedbacks_on_user" + end + create_table "programming_group_memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.bigint "programming_group_id", null: false t.string "user_type", null: false @@ -643,6 +663,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_04_180123) do add_foreign_key "lti_parameters", "exercises" add_foreign_key "lti_parameters", "external_users" add_foreign_key "lti_parameters", "study_groups" + add_foreign_key "pair_programming_exercise_feedbacks", "exercises" + add_foreign_key "pair_programming_exercise_feedbacks", "programming_groups" + add_foreign_key "pair_programming_exercise_feedbacks", "study_groups" + add_foreign_key "pair_programming_exercise_feedbacks", "submissions" add_foreign_key "programming_group_memberships", "programming_groups" add_foreign_key "programming_groups", "exercises" add_foreign_key "remote_evaluation_mappings", "study_groups"