2020-10-27 00:50:13 +08:00
# frozen_string_literal: true
2016-04-27 08:12:02 +08:00
#
2017-04-28 10:57:24 +08:00
# Copyright (C) 2016 - present Instructure, Inc.
2016-04-27 08:12:02 +08:00
#
# 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/>.
#
require 'vericite_client'
require 'digest/sha1'
require 'date'
module VeriCite
def self . state_from_similarity_score ( similarity_score )
return 'none' if similarity_score == 0
return 'acceptable' if similarity_score < 25
return 'warning' if similarity_score < 50
return 'problem' if similarity_score < 75
2021-09-23 00:25:11 +08:00
2016-04-27 08:12:02 +08:00
'failure'
end
class Client
attr_accessor :account_id , :shared_secret , :host , :testing , :show_preliminary_score
2021-09-23 00:25:11 +08:00
def initialize ( testing = false )
2016-04-27 08:12:02 +08:00
@host = Canvas :: Plugin . find ( :vericite ) . settings [ :host ] || " api.vericite.com "
account_id = Canvas :: Plugin . find ( :vericite ) . settings [ :account_id ]
shared_secret = Canvas :: Plugin . find ( :vericite ) . settings [ :shared_secret ]
show_preliminary_score = Canvas :: Plugin . find ( :vericite ) . settings [ :show_preliminary_score ] || false
raise " Account ID required " unless account_id
raise " Shared secret required " unless shared_secret
@account_id = account_id
@shared_secret = shared_secret
@show_preliminary_score = show_preliminary_score
@testing = testing
end
def id ( obj )
if @testing
" test_ #{ obj . asset_string } "
else
" #{ account_id } _ #{ obj . asset_string } "
end
end
def email ( item )
# emails @example.com are, guaranteed by RFCs, to be like /dev/null :)
email = if item . is_a? ( User )
item . email
end
2021-10-20 05:23:50 +08:00
email || " #{ item . asset_string } @null.instructure.example.com "
2016-04-27 08:12:02 +08:00
end
def self . default_assignment_vericite_settings
{
:originality_report_visibility = > Canvas :: Plugin . find ( :vericite ) . settings [ :release_to_students ] || 'immediate' ,
2021-09-23 00:25:11 +08:00
:exclude_quoted = > Canvas :: Plugin . find ( :vericite ) . settings [ :exclude_quotes ] ,
2016-04-27 08:12:02 +08:00
:exclude_self_plag = > Canvas :: Plugin . find ( :vericite ) . settings [ :exclude_self_plag ] ,
:store_in_index = > Canvas :: Plugin . find ( :vericite ) . settings [ :store_in_index ] ,
:vericite = > true
}
end
def self . normalize_assignment_vericite_settings ( settings )
unless settings . nil?
valid_keys = VeriCite :: Client . default_assignment_vericite_settings . keys
valid_keys << :created
settings = settings . slice ( * valid_keys )
settings [ :originality_report_visibility ] = 'immediate' unless [ 'immediate' , 'after_grading' , 'after_due_date' , 'never' ] . include? ( settings [ :originality_report_visibility ] )
[ :exclude_quoted , :exclude_self_plag , :store_in_index ] . each do | key |
bool = Canvas :: Plugin . value_to_boolean ( settings [ key ] )
settings [ key ] = bool ? '1' : '0'
end
end
settings
end
def createOrUpdateAssignment ( assignment , settings )
course = assignment . context
today = course . time_zone . today
settings = VeriCite :: Client . normalize_assignment_vericite_settings ( settings )
response = sendRequest ( :create_assignment , settings . merge! ( {
2021-09-23 00:25:11 +08:00
:user = > course ,
:course = > course ,
:assignment = > assignment ,
:utp = > '2' ,
:dtstart = > " #{ today . strftime } 00:00:00 " ,
:dtdue = > " #{ today . strftime } 00:00:00 " ,
:dtpost = > " #{ today . strftime } 00:00:00 " ,
:late_accept_flag = > '1' ,
:post = > true
} ) )
2016-04-27 08:12:02 +08:00
is_response_success? ( response ) ? { assignment_id : response [ :assignment_id ] } : response_error_hash ( response )
end
# if asset_string is passed in, only submit that attachment
2021-09-23 00:25:11 +08:00
def submitPaper ( submission , asset_string = nil )
2016-04-27 08:12:02 +08:00
student = submission . user
assignment = submission . assignment
course = assignment . context
opts = {
:post = > true ,
:utp = > '1' ,
:user = > student ,
:course = > course ,
:assignment = > assignment ,
:tem = > email ( course ) ,
:role = > submission . grants_right? ( student , :grade ) ? " Instructor " : " Learner "
}
responses = { }
if submission . submission_type == 'online_upload'
2021-09-23 00:25:11 +08:00
attachments = submission . attachments . select { | a | a . vericiteable? && ( asset_string . nil? || a . asset_string == asset_string ) }
2016-04-27 08:12:02 +08:00
attachments . each do | a |
# do not resubmit if the score already exists
if submission . vericite_data_hash [ a . asset_string ] [ :similarity_score ] . blank?
paper_id = a . id
paper_title = File . basename ( a . display_name , " .* " )
paper_ext = a . extension
paper_type = a . content_type
if paper_ext == nil
paper_ext = " "
end
paper_size = 100 # File.size(
2016-10-27 04:24:59 +08:00
responses [ a . asset_string ] = sendRequest ( :submit_paper , { :pid = > paper_id , :ptl = > paper_title , :pext = > paper_ext , :ptype = > paper_type , :psize = > paper_size , :pdata = > a . open ( ) } . merge! ( opts ) )
2016-04-27 08:12:02 +08:00
end
end
elsif submission . submission_type == 'online_text_entry' && ( asset_string . nil? || submission . asset_string == asset_string )
paper_id = Digest :: SHA1 . hexdigest submission . plaintext_body
paper_ext = " html "
paper_title = " InlineSubmission "
plain_text = " <html> #{ submission . plaintext_body } </html> "
paper_type = " text/html "
paper_size = plain_text . bytesize
2021-09-23 00:25:11 +08:00
responses [ submission . asset_string ] = sendRequest ( :submit_paper , { :pid = > paper_id , :ptl = > paper_title , :pext = > paper_ext , :ptype = > paper_type , :psize = > paper_size , :pdata = > plain_text } . merge! ( opts ) )
2016-04-27 08:12:02 +08:00
else
raise " Unsupported submission type for VeriCite integration: #{ submission . submission_type } "
end
2021-11-04 05:36:34 +08:00
responses . transform_values! do | res |
is_response_success? ( res ) ? { object_id : res [ :returned_object_id ] } : response_error_hash ( res )
2016-04-27 08:12:02 +08:00
end
responses
end
def generateReport ( submission , asset_string )
user = submission . user
assignment = submission . assignment
course = assignment . context
object_id = submission . vericite_data_hash [ asset_string ] [ :object_id ] rescue nil
res = nil
res = sendRequest ( :get_scores , :oid = > object_id , :utp = > '2' , :user = > user , :course = > course , :assignment = > assignment ) if object_id
data = { }
if res
data [ :similarity_score ] = res [ :similarity_score ]
end
data
end
def submissionReportUrl ( submission , current_user , asset_string )
user = submission . user
assignment = submission . assignment
course = assignment . context
object_id = submission . vericite_data_hash [ asset_string ] [ :object_id ] rescue nil
response = sendRequest ( :generate_report , :oid = > object_id , :utp = > '2' , :current_user = > current_user , :user = > user , :course = > course , :assignment = > assignment )
if response != nil
response [ :report_url ]
else
nil
end
end
def submissionStudentReportUrl ( submission , current_user , asset_string )
user = submission . user
assignment = submission . assignment
course = assignment . context
object_id = submission . vericite_data_hash [ asset_string ] [ :object_id ] rescue nil
response = sendRequest ( :generate_report , :oid = > object_id , :utp = > '1' , :current_user = > current_user , :user = > user , :course = > course , :assignment = > assignment , :tem = > email ( course ) )
if response != nil
response [ :report_url ]
else
nil
end
end
def sendRequest ( command , args )
# default response is "ok" since VeriCite doesn't implement all functions
response = { }
begin
vericite_config = VeriCiteClient :: Configuration . new ( )
vericite_config . host = @host
vericite_config . base_path = '/lms/v1'
api_client = VeriCiteClient :: ApiClient . new ( vericite_config )
vericite_client = VeriCiteClient :: DefaultApi . new ( api_client )
user = args . delete :user
course = args . delete :course
assignment = args . delete :assignment
consumer = @account_id
consumer_secret = @shared_secret
if command == :create_assignment
context_id = course . id
assignment_id = assignment . id
assignment_data = VeriCiteClient :: AssignmentData . new ( )
assignment_data . assignment_title = assignment . title != nil ? assignment . title : assignment_id
assignment_data . assignment_instructions = assignment . description != nil ? assignment . description : " "
assignment_data . assignment_exclude_quotes = args [ " exclude_quoted " ] != nil && args [ " exclude_quoted " ] == '1' ? true : false
assignment_data . assignment_exclude_self_plag = args [ " exclude_self_plag " ] != nil && args [ " exclude_self_plag " ] == '1' ? true : false
assignment_data . assignment_store_in_index = args [ " store_in_index " ] != nil && args [ " store_in_index " ] == '1' ? true : false
assignment_data . assignment_due_date = 0
if assignment . due_at != nil
# convert to epoch time in milli
assignment_data . assignment_due_date = assignment . due_at . to_time . utc . to_i * 1000
end
assignment_data . assignment_grade = assignment . points_possible != nil ? assignment . points_possible : - 1
2021-10-20 05:23:50 +08:00
_data , status_code , _headers = vericite_client . assignments_context_id_assignment_id_post ( context_id , assignment_id , consumer , consumer_secret , assignment_data )
2016-04-27 08:12:02 +08:00
# check status code
response [ :return_code ] = status_code
if ! is_response_success? ( response )
response [ :return_message ] = " An error has occurred while creating the VeriCite assignment. "
response [ :public_error_message ] = response [ :return_message ]
fail " Failed to create assignment: #{ assignment_id } , site #{ context_id } "
end
# this is a flag to signal success
response [ :assignment_id ] = assignment . id
elsif command == :submit_paper
context_id = course . id
assignment_id = assignment . id
user_id = user . id
report_meta_data = VeriCiteClient :: ReportMetaData . new ( )
report_meta_data . user_first_name = user . first_name
report_meta_data . user_last_name = user . last_name
report_meta_data . user_email = email ( user )
report_meta_data . user_role = args [ :role ]
if assignment
report_meta_data . assignment_title = assignment . title != nil ? assignment . title : assignment_id
end
if course
report_meta_data . context_title = course . name != nil ? course . name : context_id
end
external_content_data = VeriCiteClient :: ExternalContentData . new ( )
external_content_data . external_content_id = " #{ consumer } / #{ context_id } / #{ assignment_id } / #{ user_id } / #{ args [ :pid ] } "
external_content_data . file_name = args [ :ptl ]
external_content_data . upload_content_type = args [ :pext ]
external_content_data . upload_content_length = args [ :psize ]
report_meta_data . external_content_data = external_content_data
# @return [Array<ExternalContentUploadInfo>]
2021-10-20 05:23:50 +08:00
data , status_code , _headers = vericite_client . reports_submit_request_context_id_assignment_id_user_id_post ( context_id , assignment_id , user_id , consumer , consumer_secret , report_meta_data )
2016-04-27 08:12:02 +08:00
# check status code
response [ :return_code ] = status_code
if ! is_response_success? ( response )
response [ :return_message ] = " An error has occurred while submitting the paper to VeriCite. "
response [ :public_error_message ] = response [ :return_message ]
fail " Failed to submit paper: #{ external_content_data . external_content_id } "
end
data . each do | externalContentUploadInfo |
2021-09-23 00:25:11 +08:00
# API will return an upload URL to store the submission (throws an exception if it fails)
2017-03-19 08:25:01 +08:00
api_client . uploadfile ( externalContentUploadInfo . url_post , args [ :pdata ] , externalContentUploadInfo . headers )
2016-04-27 08:12:02 +08:00
end
# this is a flag to signal success
response [ :returned_object_id ] = external_content_data . external_content_id
elsif command == :get_scores
context_id = course . id
assignment_id = assignment . id
user_id = user . id
2021-09-23 00:25:11 +08:00
user_score_cache_key_prefix = " vericite_scores/ #{ consumer } / #{ context_id } / #{ assignment_id } / "
2017-05-19 19:11:48 +08:00
users_score_map = { }
# first check if the cache already has the user's score and if we haven't looked up this assignment lately:
users_score_map [ " #{ user_id } " ] = Rails . cache . read ( " #{ user_score_cache_key_prefix } #{ user_id } " )
if users_score_map [ " #{ user_id } " ] . nil? && Rails . cache . read ( user_score_cache_key_prefix ) == nil
2021-09-23 00:25:11 +08:00
# we already looked up this user in Redis, don't bother again (by setting {})
2017-05-19 19:11:48 +08:00
users_score_map [ " #{ user_id } " ] || = { }
2016-04-27 08:12:02 +08:00
# we need to look up the user scores in VeriCite for this course
# @return [Array<ReportScoreReponse>]
2021-09-23 00:25:11 +08:00
data , status_code , _headers = vericite_client . reports_scores_context_id_get ( context_id , consumer , consumer_secret , { :assignment_id = > assignment_id } )
# keep track of the assignment lookup api call
2017-05-19 19:11:48 +08:00
Rails . cache . write ( user_score_cache_key_prefix , true , :expires_in = > 5 . minutes )
2016-04-27 08:12:02 +08:00
# check status code
response [ :return_code ] = status_code
if ! is_response_success? ( response )
2021-09-23 00:25:11 +08:00
response [ :return_message ] = " An error has occurred while getting scores from VeriCite. "
response [ :public_error_message ] = response [ :return_message ]
fail " Failed to get scores for site: #{ context_id } , assignment: #{ assignment_id } , user: #{ user_id } , exId: #{ args [ :oid ] } "
2016-04-27 08:12:02 +08:00
end
# create the user scores map and cache it
data . each do | reportScoreReponse |
if reportScoreReponse . score . is_a? ( Integer ) && reportScoreReponse . score > = 0 &&
2021-09-23 00:25:11 +08:00
( @show_preliminary_score || reportScoreReponse . preliminary . nil? || ! reportScoreReponse . preliminary )
2017-05-19 19:11:48 +08:00
# keep track of this user's report scores
users_score_map [ reportScoreReponse . user ] || = { }
users_score_map [ reportScoreReponse . user ] [ reportScoreReponse . external_content_id ] = Float ( reportScoreReponse . score )
2016-04-27 08:12:02 +08:00
end
end
# cache the user score map for a short period of time
2017-05-19 19:11:48 +08:00
users_score_map . keys . each do | key |
Rails . cache . write ( " #{ user_score_cache_key_prefix } #{ key } " , users_score_map [ key ] , :expires_in = > 5 . minutes )
2016-04-27 08:12:02 +08:00
end
else
# since we didn't have to consult VeriCite, set response status to 200
response [ :return_code ] = 200
end
# the user score map shouldn't be empty now (either grabbed from the cache or VeriCite)
2017-05-19 19:11:48 +08:00
unless users_score_map [ " #{ user_id } " ] . nil?
users_score_map [ " #{ user_id } " ] . each do | key , score |
2016-04-27 08:12:02 +08:00
if key == args [ :oid ] && score > = 0
response [ :similarity_score ] = score
end
end
end
elsif command == :generate_report
context_id = course . id
assignment_id_filter = assignment . id
user_id = user . id
current_user = args . delete :current_user
token_user = current_user . id
token_user_role = 'Learner'
if args [ :utp ] == '2'
2021-09-23 00:25:11 +08:00
# instructor
2016-04-27 08:12:02 +08:00
token_user_role = 'Instructor'
end
# @return [Array<ReportURLLinkReponse>]
2021-10-20 05:23:50 +08:00
data , status_code , _headers = vericite_client . reports_urls_context_id_get ( context_id , assignment_id_filter , consumer , consumer_secret , token_user , token_user_role , { user_id_filter = > user_id , external_content_id_filter = > args [ :oid ] } )
2016-04-27 08:12:02 +08:00
# check status code
response [ :return_code ] = status_code
if ! is_response_success? ( response )
response [ :return_message ] = " An error has occurred while getting the report URL from VeriCite. "
response [ :public_error_message ] = response [ :return_message ]
fail " Failed to get the report url for site: #{ context_id } , assignment: #{ assignment_id } , user: #{ user_id } , exId: #{ args [ :oid ] } , token_user: #{ token_user } , token_user_role: #{ token_user_role } "
end
data . each do | reportURLLinkReponse |
2021-09-23 00:25:11 +08:00
# should only be 1 url
if reportURLLinkReponse . external_content_id == args [ :oid ]
2016-04-27 08:12:02 +08:00
# setting response URL is a signal for success
response [ :report_url ] = reportURLLinkReponse . url
end
end
end
rescue = > e
Rails . logger . error ( " VeriCite: account_id: #{ @account_id } , code: #{ response [ :return_code ] } , error: #{ e } " )
if is_response_success? ( response )
# we do not want to return a success code if there was an error
response [ :return_code ] = 100
end
if ! response . key? ( :return_message )
# we want a generic error message at a minimum
response [ :return_message ] = " VeriCite error during #{ command } command, error: #{ e } "
response [ :public_error_message ] = response [ :return_message ]
end
2021-09-23 00:25:11 +08:00
end # begin
2016-04-27 08:12:02 +08:00
return nil if @testing
response
end
private
SUCCESSFUL_RETURN_CODES = ( 200 .. 299 )
def is_response_success? ( response )
begin
response && response . key? ( :return_code ) && SUCCESSFUL_RETURN_CODES . cover? ( Integer ( response [ :return_code ] ) )
rescue
false
end
end
def response_error_hash ( response )
return { } unless ! is_response_success? ( response )
2021-09-23 00:25:11 +08:00
2016-04-27 08:12:02 +08:00
{
error_code : response [ :return_code ] ,
error_message : response [ :return_message ] ,
public_error_message : response [ :public_error_message ] ,
}
end
end
end