canvas-lms/lib/support_helpers/tii.rb

306 lines
8.5 KiB
Ruby

#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module SupportHelpers
module Tii
class TiiFixer < Fixer
def initialize(email, after_time = nil)
@buffer_time = Time.now - 1.hour
@prefix = "TurnItIn"
super(email, after_time)
end
def success_message
"#{fixer_name} fixed #{prettify_broken_count} in #{elapsed_time} seconds!"
end
def broken_objects
@broken_objects ||= Shackles.activate(:slave) { load_broken_objects }
end
private
def broken_count
@count ||= broken_objects.count
end
def prettify_broken_count
"#{broken_count} #{object_type.name.downcase.pluralize(broken_count)}"
end
def like_error
"turnitin_data LIKE '%error%'".freeze
end
def object_type
Submission
end
def updated_field
object_type.arel_table[:updated_at]
end
def load_broken_objects
raise "#{self.class.name} must implement #load_broken_objects"
end
end
class Error2305Fixer < TiiFixer
def fix
broken_objects.each { |a| AssignmentFixer.new(@email, @after_time, a).fix(fix_type_needed) }
end
private
def fix_type_needed
:assignment_fix
end
def like_error
"turnitin_settings LIKE '%2305%'".freeze
end
def object_type
Assignment
end
def load_broken_objects
Assignment.where(updated_field.gt(@after_time)).where(like_error).pluck(:id)
end
end
class MD5Fixer < Error2305Fixer
private
def fix_type_needed
:md5_fix
end
def like_error
"turnitin_settings LIKE '%MD5 not authenticated%'".freeze
end
end
class ShardFixer < TiiFixer
def fix
broken_objects.each { |a| AssignmentFixer.new(@email, @after_time, a).fix }
end
private
def load_broken_objects
# By selecting only the id, we delay the full load until we're
# ready to actually work on the assignment. Lots of little
# loads than one giant one.
Assignment.joins(:submissions).
where(updated_field.gt(@after_time)).
where(updated_field.lt(@buffer_time)).
where("submissions.#{like_error}").
distinct.pluck(:id)
end
def object_type
Assignment
end
def updated_field
Submission.arel_table[:updated_at]
end
end
class LtiAttachmentFixer < TiiFixer
def initialize(email, after_time, submission_id, attachment_id)
@submission = Submission.find(submission_id)
@attachment = Attachment.find(attachment_id)
super(email, after_time)
end
def fix
Turnitin::AttachmentManager.update_attachment(@submission, @attachment)
end
private
def load_broken_objects
[@submission]
end
end
class AssignmentFixer < TiiFixer
def initialize(email, after_time, assignment_id)
@assignment = Assignment.find(assignment_id)
super(email, after_time)
end
def fix(fix_type = fix_type_needed)
return if @assignment.context.turnitin_settings.nil?
case fix_type
when :no_fix
return
when :course_fix
create_course
create_assignment
when :assignment_fix
create_assignment
when :assignment_exists_fix
update_assignment
when :md5_fix
save_assignment
end
resubmit_submissions
end
private
def load_broken_objects
# Non-broken sumissions CAN have turnitin_data that has the
# word error in them that aren't a top level error that we're
# looking for. There aren't a lot, but we'll select them out here.
@assignment.submissions.where(updated_field.gt(@after_time)).
where(updated_field.lt(@buffer_time)).
where(like_error).select { |s| is_bad_submission?(s) }
end
def is_bad_submission?(s)
s.turnitin_data[:status] == "error" || s.turnitin_data.values.any? do |v|
v.is_a?(Hash) && v[:error_code] == 206
end
end
def fix_type_needed
# All the submissions will be broken the same way, so how the
# first is broken is good enough to proceed.
tii = broken_objects.first.try(:turnitin_data)
return :no_fix unless tii
if tii[:student_error].try(:[], :error_code) == 204
:course_fix
elsif tii[:student_error].try(:[], :error_code) == 216
:resubmit_fix
elsif tii[:assignment_error].try(:[], :error_code) == 206
:assignment_fix
elsif tii[:assignment_error].try(:[], :error_code) == 419
:assignment_exists_fix
elsif tii.values.any? { |v| v.is_a?(Hash) && v[:error_code] == 206 }
:assignment_fix
else
:no_fix
end
end
def turnitin_client
@turnitin ||= Turnitin::Client.new(*@assignment.context.turnitin_settings)
end
def create_course
turnitin_client.createCourse(@assignment.context)
end
def create_assignment
@assignment.turnitin_settings.delete(:created)
@assignment.turnitin_settings.delete(:current)
@assignment.save
save_assignment(:create)
end
def update_assignment
@assignment.turnitin_settings[:created] = true
@assignment.save
save_assignment(:update)
end
def save_assignment(save_reason = nil)
res = turnitin_client.createOrUpdateAssignment(@assignment, @assignment.turnitin_settings)
if res[:assignment_id]
@assignment.turnitin_settings[:created] = true
@assignment.turnitin_settings[:current] = true
@assignment.turnitin_settings.delete(:error)
@assignment.save
elsif res[:error_code] == 206 && save_reason == :update
create_assignment
elsif res[:error_code] == 419 && save_reason == :create
update_assignment
else
raise "assignment #{@assignment.id} is still broken: #{res}"
end
end
def resubmit_submissions
broken_objects.each do |b|
b.resubmit_to_turnitin
sleep 3 if Rails.env.production? # TII's API can't keep up if we don't slow down
end
end
end
class StuckInPendingFixer < TiiFixer
def fix
broken_objects.each do |s|
Submission.find(s).resubmit_to_turnitin
sleep 3 if Rails.env.production? # TII's API can't keep up if we don't slow down
end
stuck_with_object_ids.each do |s|
Submission.find(s).check_turnitin_status
sleep 2 if Rails.env.production? # TII's API can't keep up if we don't slow down
end
end
private
def like_error
"turnitin_data LIKE '--- \n:last_processed_attempt: _\n' OR turnitin_data LIKE '--- \n:last_processed_attempt: _\nattachment_________: \n :status: pending\n'".freeze
end
def load_broken_objects
Submission.where(updated_field.gt(@after_time)).
where(updated_field.lt(@buffer_time)).
where(like_error).
pluck(:id)
end
def stuck_with_object_ids
# These should be able to just have "check status" called on them.
Submission.where(updated_field.gt(@after_time)).
where(updated_field.lt(@buffer_time)).
where("turnitin_data LIKE '--- \n:last_processed_attempt: _\nattachment_________: \n :status: pending\n :object_id: \"_________\"\n'").
pluck(:id)
end
end
class ExpiredAccountFixer < StuckInPendingFixer
private
def stuck_with_object_ids
[]
end
def like_error
'turnitin_data LIKE E\'%:status: pending\n:status: error\n:assignment_error: !ruby/hash:ActiveSupport::HashWithIndifferentAccess\n error_code: 217%\''.freeze
end
end
end
end