Add CommunitySolution

* Also slightly refactor some JS files
This commit is contained in:
Sebastian Serth
2021-11-21 19:18:52 +01:00
parent d559cfb323
commit da4e10b990
21 changed files with 509 additions and 31 deletions

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
class CommunitySolutionsController < ApplicationController
include CommonBehavior
include RedirectBehavior
include SubmissionParameters
before_action :set_community_solution, only: %i[edit update]
before_action :set_community_solution_lock, only: %i[edit]
before_action :set_exercise_and_submission, only: %i[edit update]
# GET /community_solutions
def index
@community_solutions = CommunitySolution.all
authorize!
end
# GET /community_solutions/1/edit
def edit
authorize!
# Be safe. Only allow access to this page if user has valid lock
redirect_after_submit unless @community_solution_lock.present? && @community_solution_lock.active? && @community_solution_lock.user == current_user && @community_solution_lock.community_solution == @community_solution
# We don't want to perform any of the following steps if we rendered already (e.g. due to a redirect)
return if performed?
last_contribution = CommunitySolutionContribution.where(community_solution: @community_solution, timely_contribution: true, autosave: false, proposed_changes: true).order(created_at: :asc).last
@files = []
if last_contribution.blank?
last_contribution = @community_solution.exercise
new_readme_file = {content: '', file_type: FileType.find_by(file_extension: '.txt'), hidden: false, read_only: false, name: 'ReadMe', role: 'regular_file', context: @community_solution}
@files << CodeOcean::File.create!(new_readme_file)
end
all_visible_files = last_contribution.files.select(&:visible)
# Add the ReadMe file first
@files += all_visible_files.select {|f| CodeOcean::File.find_by(id: f.file_id).context_type == 'CommunitySolution' }
# Then, add all remaining files and sort them by name with extension
@files += (all_visible_files - @files).sort_by(&:name_with_extension)
# Own Submission as a reference
@own_files = @submission.collect_files.select(&:visible).sort_by(&:name_with_extension)
# Remove the file_id from the second graph. Otherwise, the comparison and file-tree selection does not work as expected
@own_files.map do |file|
file.file_id = nil
file.read_only = true
end
end
# PATCH/PUT /community_solutions/1
def update
authorize!
contribution_params = submission_params
cause = contribution_params.delete(:cause)
contribution_params[:proposed_changes] = cause == 'change-community-solution'
contribution_params[:autosave] = cause == 'autosave-community-solution'
contribution_params.delete(:exercise_id)
contribution_params[:community_solution] = @community_solution
# Acquire lock here! This is expensive but required for synchronization
@community_solution_lock = ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute("LOCK #{CommunitySolutionLock.table_name} IN ACCESS EXCLUSIVE MODE")
lock = CommunitySolutionLock.where(user: current_user, community_solution: @community_solution).order(locked_until: :asc).last
if lock.active?
contribution_params[:timely_contribution] = true
# Update lock: Either expand the time (autosave) or return it (change / accept)
new_lock_time = contribution_params[:autosave] ? 5.minutes.from_now : Time.zone.now
lock.update!(locked_until: new_lock_time)
else
contribution_params[:timely_contribution] = false
end
# This is returned
lock
end
contribution_params[:community_solution_lock] = @community_solution_lock
contribution_params[:working_time] = @community_solution_lock.working_time
CommunitySolutionContribution.create(contribution_params)
redirect_after_submit
end
private
def authorize!
authorize(@community_solution)
end
# Use callbacks to share common setup or constraints between actions.
def set_community_solution
@community_solution = CommunitySolution.find(params[:id])
end
def set_community_solution_lock
@community_solution_lock = CommunitySolutionLock.find(params[:lock_id])
end
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
end
end

View File

@ -6,7 +6,7 @@ module FileParameters
params.reject do |_, file_attributes|
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
# avoid that public files from other contexts can be created
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id)
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id) || (file.context_type == 'CommunitySolution' && controller_name != 'community_solutions')
end
else
[]

View File

@ -6,6 +6,11 @@ module RedirectBehavior
def redirect_after_submit
Rails.logger.debug { "Redirecting user with score:s #{@submission.normalized_score}" }
if @submission.normalized_score.to_d == 1.0.to_d
if redirect_to_community_solution?
redirect_to_community_solution
return
end
# if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external,
# otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.)
# redirect 10 percent pseudorandomly to the feedback page
@ -63,6 +68,40 @@ module RedirectBehavior
private
def redirect_to_community_solution
url = edit_community_solution_path(@community_solution, lock_id: @community_solution_lock.id)
respond_to do |format|
format.html { redirect_to(url) }
format.json { render(json: {redirect: url}) }
end
end
def redirect_to_community_solution?
return false unless Java21Study.allow_redirect_to_community_solution?(current_user, @exercise)
@community_solution = CommunitySolution.find_by(exercise: @exercise)
return false if @community_solution.blank?
last_contribution = CommunitySolutionContribution.where(community_solution: @community_solution).order(created_at: :asc).last
# Only redirect if last contribution is from another user.
eligible = last_contribution.blank? || last_contribution.user != current_user
return false unless eligible
# Acquire lock here! This is expensive but required for synchronization
@community_solution_lock = ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute("LOCK #{CommunitySolutionLock.table_name} IN ACCESS EXCLUSIVE MODE")
# This is returned
CommunitySolutionLock.find_or_create_by(community_solution: @community_solution, locked_until: Time.zone.now...) do |lock|
lock.user = current_user
lock.locked_until = 5.minutes.from_now
end
end
@community_solution_lock.user == current_user
end
def redirect_to_user_feedback
uef = UserExerciseFeedback.find_by(exercise: @exercise, user: current_user)
url = if uef