Add ProgrammingGroup & ProgrammingGroupMembership

* User can create programming group with other users for exercise
* Submission is shared in a group
* Also adjust specs
This commit is contained in:
kiragrammel
2023-08-10 17:07:04 +02:00
committed by Sebastian Serth
parent 0234414bae
commit 319c3ab3b4
42 changed files with 715 additions and 276 deletions

View File

@ -23,6 +23,15 @@ class ApplicationController < ActionController::Base
@current_user ||= find_or_login_current_user&.store_current_study_group_id(session[:study_group_id])
end
def current_contributor
@current_contributor ||= if session[:pg_id]
current_user.programming_groups.find(session[:pg_id])
else
current_user
end
end
helper_method :current_contributor
def find_or_login_current_user
login_from_authentication_token ||
login_from_lti_session ||

View File

@ -100,6 +100,6 @@ class CommunitySolutionsController < ApplicationController
def set_exercise_and_submission
@exercise = @community_solution.exercise
@submission = current_user.submissions.final.where(exercise_id: @community_solution.exercise.id).order('created_at DESC').first
@submission = current_contributor.submissions.final.where(exercise: @community_solution.exercise).order(created_at: :desc).first
end
end

View File

@ -17,7 +17,7 @@ module FileParameters
# avoid that public files from other contexts can be created
# `next` is similar to an early return and will proceed with the next iteration of the loop
next true if file.context_type == 'Exercise' && file.context_id != exercise.id
next true if file.context_type == 'Submission' && (file.context.contributor_id != current_user.id || file.context.contributor_type != current_user.class.name)
next true if file.context_type == 'Submission' && (file.context.contributor_id != current_contributor.id || file.context.contributor_type != current_contributor.class.name)
next true if file.context_type == 'CommunitySolution' && controller_name != 'community_solutions'
# Optimization: We already queried the ancestor file, let's reuse the object.

View File

@ -21,18 +21,17 @@ module Lti
# exercise_id.nil? ==> the user has logged out. All session data is to be destroyed
# exercise_id.exists? ==> the user has submitted the results of an exercise to the consumer.
# Only the lti_parameters are deleted.
def clear_lti_session_data(exercise_id = nil, _user_id = nil)
def clear_lti_session_data(exercise_id = nil)
if exercise_id.nil?
session.delete(:external_user_id)
session.delete(:study_group_id)
session.delete(:embed_options)
session.delete(:lti_exercise_id)
session.delete(:lti_parameters_id)
end
session.delete(:pg_id)
# March 2022: We temporarily allow reusing the LTI credentials and don't remove them on purpose.
# We allow reusing the LTI credentials and don't remove them on purpose.
# This allows users to jump between remote and web evaluation with the same behavior.
# LtiParameter.where(external_users_id: user_id, exercises_id: exercise_id).destroy_all
# Also it prevents the user from deleting the lti_parameters for their programming team members.
end
private :clear_lti_session_data
@ -159,20 +158,20 @@ module Lti
session: session.to_hash,
exercise_id: submission.exercise_id,
})
normalized_lit_score = submission.normalized_score
normalized_lti_score = submission.normalized_score
if submission.before_deadline?
# Keep the full score
elsif submission.within_grace_period?
# Reduce score by 20%
normalized_lit_score *= 0.8
normalized_lti_score *= 0.8
elsif submission.after_late_deadline?
# Reduce score by 100%
normalized_lit_score *= 0.0
normalized_lti_score *= 0.0
end
begin
response = provider.post_replace_result!(normalized_lit_score)
{code: response.response_code, message: response.post_response.body, status: response.code_major, score_sent: normalized_lit_score}
response = provider.post_replace_result!(normalized_lti_score)
{code: response.response_code, message: response.post_response.body, status: response.code_major, score_sent: normalized_lti_score}
rescue IMS::LTI::XMLParseError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError
# A parsing error might happen if the LTI provider is down and doesn't return a valid XML response
{status: 'error'}

View File

@ -22,8 +22,7 @@ module SubmissionParameters
def merge_user(params)
# The study_group_id might not be present in the session (e.g. for internal users), resulting in session[:study_group_id] = nil which is intended.
params.merge(
contributor_id: current_user.id,
contributor_type: current_user.class.name,
contributor: current_contributor,
study_group_id: current_user.current_study_group_id
)
end

View File

@ -92,7 +92,7 @@ class ExercisesController < ApplicationController
authorize!
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page], per_page: per_page_param)
@submissions = @feedbacks.map do |feedback|
feedback.exercise.final_submission(feedback.user)
feedback.exercise.final_submission(feedback.user.programming_groups.where(exercise: @exercise).presence || feedback.user)
end
end
@ -298,7 +298,7 @@ class ExercisesController < ApplicationController
private :update_exercise_tips
def implement
user_solved_exercise = @exercise.solved_by?(current_user)
user_solved_exercise = @exercise.solved_by?(current_contributor)
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
Time.zone.now.beginning_of_day).count
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user,
@ -330,7 +330,7 @@ class ExercisesController < ApplicationController
@hide_rfc_button = @embed_options[:disable_rfc]
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
@submission = current_contributor.submissions.order(created_at: :desc).find_by(exercise: @exercise)
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:filepath)
@paths = collect_paths(@files)
end
@ -363,7 +363,7 @@ class ExercisesController < ApplicationController
private :set_available_tips
def working_times
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user)
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_contributor)
working_time_75_percentile = @exercise.get_quantiles([0.75]).first
render(json: {working_time_75_percentile:,
working_time_accumulated:})
@ -375,8 +375,8 @@ class ExercisesController < ApplicationController
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
else
uei = UserExerciseIntervention.new(
user: current_user, exercise: @exercise, intervention:,
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user)
user: current_contributor, exercise: @exercise, intervention:,
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_contributor)
)
uei.save
render(json: {success: 'true'})
@ -465,7 +465,7 @@ class ExercisesController < ApplicationController
def statistics
# Show general statistic page for specific exercise
user_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
contributor_statistics = {'InternalUser' => {}, 'ExternalUser' => {}, 'ProgrammingGroup' => {}}
query = Submission.select('contributor_id, contributor_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
.where(exercise_id: @exercise.id)
@ -481,11 +481,11 @@ class ExercisesController < ApplicationController
end
query.each do |tuple|
user_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
contributor_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
end
render locals: {
user_statistics:,
contributor_statistics:,
}
end
@ -495,7 +495,7 @@ class ExercisesController < ApplicationController
if policy(@exercise).detailed_statistics?
submissions = Submission.where(contributor: @external_user, exercise: @exercise)
.in_study_group_of(current_user)
.order('created_at')
.order(:created_at)
@show_autosaves = params[:show_autosaves] == 'true' || submissions.none? {|s| s.cause != 'autosave' }
submissions = submissions.where.not(cause: 'autosave') unless @show_autosaves
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id,
@ -526,7 +526,8 @@ class ExercisesController < ApplicationController
def submit
@submission = Submission.create(submission_params)
@submission.calculate_score
if @submission.user.external_user? && lti_outcome_service?(@submission.exercise_id, @submission.user.id)
if @submission.users.map {|user| user.external_user? && lti_outcome_service?(@submission.exercise_id, user.id) }.any?
transmit_lti_score
else
redirect_after_submit

View File

@ -5,7 +5,7 @@ class FlowrController < ApplicationController
require_user!
# get the latest submission for this user that also has a test run (i.e. structured_errors if applicable)
submission = Submission.joins(:testruns)
.where(submissions: {contributor: current_user})
.where(submissions: {contributor: current_contributor})
.includes(structured_errors: [structured_error_attributes: [:error_template_attribute]])
.merge(Testrun.order(created_at: :desc)).first

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
class ProgrammingGroupsController < ApplicationController
include CommonBehavior
include LtiHelper
before_action :set_exercise_and_authorize
def new
@programming_group = ProgrammingGroup.new(exercise: @exercise)
authorize!
existing_programming_group = current_user.programming_groups.find_by(exercise: @exercise)
if existing_programming_group
session[:pg_id] = existing_programming_group.id
redirect_to(implement_exercise_path(@exercise),
notice: t("sessions.create_through_lti.session_#{lti_outcome_service?(@exercise.id, current_user.id) ? 'with' : 'without'}_outcome", consumer: @consumer))
end
end
def create
programming_partner_ids = programming_group_params[:programming_partner_ids].split(',').map(&:strip).uniq
users = programming_partner_ids.map do |partner_id|
User.find_by_id_with_type(partner_id)
rescue ActiveRecord::RecordNotFound
partner_id
end
@programming_group = ProgrammingGroup.new(exercise: @exercise, users:)
authorize!
unless programming_partner_ids.include? current_user.id_with_type
@programming_group.add(current_user)
end
create_and_respond(object: @programming_group, path: proc { implement_exercise_path(@exercise) }) do
session[:pg_id] = @programming_group.id
nil
end
end
private
def authorize!
authorize(@programming_group || @programming_groups)
end
def programming_group_params
params.require(:programming_group).permit(:programming_partner_ids)
end
def set_exercise_and_authorize
@exercise = Exercise.find(params[:exercise_id])
authorize(@exercise, :implement?)
end
end

View File

@ -34,6 +34,7 @@ class RemoteEvaluationController < ApplicationController
end
def try_lti
# TODO: Need to consider and support programming groups
if !@submission.user.nil? && lti_outcome_service?(@submission.exercise_id, @submission.user.id)
lti_response = send_score(@submission)
process_lti_response(lti_response)

View File

@ -41,9 +41,9 @@ class UserExerciseFeedbacksController < ApplicationController
Sentry.set_extras(params: uef_params)
@exercise = Exercise.find(uef_params[:exercise_id])
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user: current_user).first
submission = begin
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
current_contributor.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
rescue StandardError
nil
end
@ -69,11 +69,11 @@ class UserExerciseFeedbacksController < ApplicationController
def update
submission = begin
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').final.first
current_contributor.submissions.where(exercise: @exercise).order(created_at: :desc).final.first
rescue StandardError
nil
end
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
rfc = RequestForComment.unsolved.where(exercise: @exercise, user: current_user).first
authorize!
if @exercise && validate_inputs(uef_params)
path =
@ -123,18 +123,15 @@ class UserExerciseFeedbacksController < ApplicationController
params[:user_exercise_feedback][:exercise_id]
end
user_id = current_user.id
user_type = current_user.class.name
latest_submission = Submission
.where(contributor_id: user_id, contributor_type: user_type, exercise_id:)
.where(contributor: current_contributor, exercise_id:)
.order(created_at: :desc).final.first
authorize(latest_submission, :show?)
params[:user_exercise_feedback]
.permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime)
.merge(user_id:,
user_type:,
.merge(user: current_user,
submission: latest_submission,
normalized_score: latest_submission&.normalized_score)
end