app/controllers/reviews_controller.rb
# frozen_string_literal: true
require 'natsort'
# Presents the code review UI.
class ReviewsController < ApplicationController
before_action :set_organization, except: %i[new create]
def index
if params[:course_id]
fetch :course
@organization = @course.organization
@my_reviews = @course.submissions
.where(user_id: current_user.id)
.where('requests_review OR requires_review OR reviewed')
.order('created_at DESC')
respond_to do |format|
format.html do
add_course_breadcrumb
add_breadcrumb 'Code reviews'
render 'reviews/course_index'
end
format.json do
render json: course_reviews_json
end
end
else
fetch :submission, :files
raise "Submission's exercise has been moved or deleted" unless @submission.exercise
@course = @submission.course
@organization = @course.organization
respond_to do |format|
format.html do
add_course_breadcrumb
add_exercise_breadcrumb
add_submission_breadcrumb
breadcrumb_label = @submission.reviews.count == 1 ? 'Code review' : 'Code reviews'
add_breadcrumb breadcrumb_label, submission_reviews_path(@submission)
render 'reviews/submission_index'
end
end
end
end
def new
fetch :submission, :files
@show_page_presence = true
@course = @submission.course
@organization = @course.organization
add_course_breadcrumb
add_breadcrumb 'Code reviews', organization_course_reviews_path(@organization, @course)
add_breadcrumb 'Code review editor'
@new_review = Review.new(
submission_id: @submission.id,
reviewer_id: current_user.id
)
authorize! :create_review, @course
render 'reviews/submission_index'
end
def create
fetch :submission
@review = Review.new(
submission_id: @submission.id,
reviewer_id: current_user.id,
review_body: params[:review][:review_body]
)
authorize! :create_review, @submission.course
begin
ActiveRecord::Base.connection.transaction do
award_points
mark_as_reviewed
@review.submission.save!
@review.save!
end
rescue StandardError
::Rails.logger.error($!)
respond_with_error('Failed to save code review.')
else
flash[:success] = 'Code review added.'
send_email_about_new_review if params[:send_email]
@course = @submission.course
@organization = @course.organization
redirect_to organization_course_reviews_path(@organization, @course)
end
end
def update
fetch :review
authorize! :update, @review
if params[:review].present? && params[:review].has_key?(:review_body)
update_review
elsif params[:mark_as_read]
mark_as_read(true)
elsif params[:mark_as_unread]
mark_as_read(false)
end
end
def destroy
fetch :review
authorize! :delete, @review
if @review.destroy
flash[:success] = 'Code review deleted.'
redirect_to new_submission_review_path(@review.submission_id)
else
respond_with_error('Failed to delete code review.')
end
end
private
def course_reviews_json
submissions = @my_reviews.includes(reviews: %i[reviewer submission])
exercises = @course.exercises.index_by { |e| e.name }
reviews = submissions.map do |s|
s.reviews.map do |r|
{
submission_id: s.id,
exercise_name: s.exercise_name
}.merge(review_json(exercises, r))
end
end.flatten
{
api_version: ApiVersion::API_VERSION,
reviews: reviews
}
end
def review_json(exercises, review)
available_points = exercises[review.submission.exercise_name].available_points.where(requires_review: true).map(&:name)
points_not_awarded = available_points - review.points_list
{
id: review.id,
marked_as_read: review.marked_as_read,
reviewer_name: review.reviewer.display_name,
review_body: review.review_body,
points: review.points_list.natsort,
points_not_awarded: points_not_awarded.natsort,
url: submission_reviews_url(review.submission_id),
update_url: review_url(review),
created_at: review.created_at,
updated_at: review.updated_at
}
end
def mark_as_read(read)
which = read ? 'read' : 'unread'
fetch :review
authorize! (read ? :mark_as_read : :mark_as_unread), @review
@review.marked_as_read = read
if @review.save
respond_to do |format|
format.html do
flash[:success] = "Code review marked as #{which}."
redirect_to submission_reviews_path(@review.submission)
end
format.json do
render json: { status: 'OK' }
end
end
else
respond_with_error("Failed to mark code review as #{which}.")
end
end
def update_review
fetch :review
authorize! :update, @review
@review.review_body = params[:review][:review_body]
begin
mark_as_reviewed
award_points
@review.submission.save!
@review.save!
rescue StandardError
::Rails.logger.error($!)
respond_with_error('Failed to save code review.')
else
flash[:success] = 'Code review edited. (No notification sent).'
redirect_to new_submission_review_path(@review.submission_id)
end
end
def mark_as_reviewed
sub = @review.submission
sub.reviewed = true
sub.review_dismissed = false
sub.of_same_kind
.where('(requires_review OR requests_review) AND NOT reviewed')
.where(['created_at < ?', sub.created_at])
.update_all(newer_submission_reviewed: true)
end
def fetch(*stuff)
if stuff.include? :course
@course = Course.find(params[:course_id])
authorize! :read, @course
end
if stuff.include? :submission
@submission = Submission.find(params[:submission_id])
authorize! :read, @submission
end
if stuff.include? :review
@review = Review.find(params[:id])
authorize! :read, @review
end
@files = SourceFileList.for_submission(@submission) if stuff.include? :files
end
def send_email_about_new_review
ReviewMailer.review_email(@review).deliver
end
def award_points
submission = @review.submission
exercise = submission.exercise
course = exercise.course
raise 'Exercise of submission has been moved or deleted' unless exercise
available_points = exercise.available_points.where(requires_review: true).map(&:name)
previous_points = course.awarded_points.where(user_id: submission.user_id, name: available_points).map(&:name)
new_points = []
if params[:review][:points].respond_to?(:keys)
for point_name in params[:review][:points].keys
unless exercise.available_points.where(name: point_name).any?
raise "Point does not exist: #{point_name}"
end
new_points << point_name
pt = submission.awarded_points.build(
course_id: submission.course_id,
user_id: submission.user_id,
name: point_name
)
authorize! :create, pt
pt.save!
end
end
@review.points = (@review.points_list + new_points + previous_points).uniq.natsort.join(' ')
submission.points = (submission.points_list + new_points + previous_points).uniq.natsort.join(' ')
end
private
def set_organization
@organization = Organization.find_by(slug: params[:organization_id])
end
end