canvas-lms/ui/shared/grading/SubmissionHelper.ts

163 lines
6.4 KiB
TypeScript

// @ts-nocheck
/*
* Copyright (C) 2019 - 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/>.
*/
import {camelizeProperties, underscoreProperties} from '@canvas/convert-case'
import {originalityReportSubmissionKey} from './originalityReportHelper'
import type {
SubmissionOriginalityData,
SimilarityEntry,
CamelizedSubmissionWithOriginalityReport,
} from '@canvas/grading/grading.d'
export function isGraded(submission) {
// TODO: remove when we no longer camelize data in Gradebook
const workflow_state = submission.workflow_state || submission.workflowState
return (submission.score != null && workflow_state === 'graded') || submission.excused
}
export function isPostable(submission) {
// TODO: remove when we no longer camelize data in Gradebook
const posted_at = submission.posted_at || submission.postedAt
const has_postable_comments = submission.has_postable_comments || submission.hasPostableComments
return !posted_at && (isGraded(submission) || !!has_postable_comments)
}
export function isHideable(submission) {
// TODO: remove when we no longer camelize data in Gradebook
const posted_at = submission.posted_at || submission.postedAt
return !!posted_at
}
// This function returns an object containing plagiarism/originality-related
// data for the given submission, or null if the submission has no relevant
// info. The returned object contains the following keys:
// - type: either 'turnitin', 'vericite' or 'originality_report'
// - entries: an array of individual reports, containing:
// - id: an identifier for the individual report (e.g., 'submission_1', 'attachment_41')
// - data: the contents of the report, typically containing 'status' and
// 'similarity_score' fields among other things
//
// The array of entries is sorted in the following order:
// - "error" reports (reports that had a problem running)
// - "pending" reports (reports still being processed)
// - scored reports, with higher scores (indicating more likely plagiarism) first
export function extractSimilarityInfo(submission) {
const sub = camelizeProperties(submission) as CamelizedSubmissionWithOriginalityReport
let plagiarismData
let type: 'vericite' | 'turnitin' | 'originality_report' | null = null
if (sub.vericiteData?.provider === 'vericite') {
type = 'vericite'
plagiarismData = sub.vericiteData
} else if (sub.turnitinData != null) {
type = 'turnitin'
plagiarismData = sub.turnitinData
}
if (sub.hasOriginalityReport) {
type = 'originality_report'
}
if (plagiarismData == null || type == null) {
return null
}
const entries = getSimilarityEntries(sub, plagiarismData)
if (entries.length === 0) {
return null
}
return {type, entries}
}
function getSimilarityEntries(submission, plagiarismData) {
const entries: SimilarityEntry[] = []
if (submission.submissionType === 'online_upload' && submission.attachments != null) {
// A submission with attachments may have a plagiarism report for each
// attachment. Also, an attachment's data might also be found in an
// "attachment" object nested inside the actual attachment object we got.
submission.attachments.forEach(attachment => {
const id = attachment.attachment?.id || attachment.id
const idKey = `attachment_${id}`
const attachmentData = plagiarismData[idKey]
if (attachmentData != null) {
entries.push({id: idKey, data: attachmentData})
}
})
} else if (submission.submissionType === 'online_text_entry') {
// A text entry submission will only have one active report, but the report
// may be keyed by the submission version (or not). Try to use the data for
// the current version (as returned by originalityReportSubmissionKey), but
// if that's not available, check the "base" submission instead.
const originalityReportKey = originalityReportSubmissionKey(underscoreProperties(submission))
const dataForKey = plagiarismData[originalityReportKey]
const baseSubmissionId = `submission_${submission.id}`
const dataForBaseSubmission = plagiarismData[baseSubmissionId]
if (dataForKey != null) {
entries.push({id: originalityReportKey, data: dataForKey})
} else if (dataForBaseSubmission != null) {
entries.push({id: baseSubmissionId, data: dataForBaseSubmission})
}
}
entries.sort(similarityEntryComparator)
return entries
}
function similarityEntryComparator(a: SimilarityEntry, b: SimilarityEntry): number {
const orderedStatuses = ['error', 'pending', 'scored', 'none']
// We only display a single plagiarism report in New Gradebook. If a
// submission has multiple reports, show the one with the "worst" status
// (e.g., a report that encountered an error should be prioritized over one
// that was successfully scored).
const {status: aStatus, similarity_score: aScore} = a.data
const {status: bStatus, similarity_score: bScore} = b.data
// If both entries have been scored, show the one with the higher similarity
// score (i.e., the one more likely to have been plagiarized).
if (aStatus === 'scored' && aScore != null && bStatus === 'scored' && bScore != null) {
return bScore - aScore
}
// Otherwise, just compare by status.
return orderedStatuses.indexOf(aStatus || 'none') - orderedStatuses.indexOf(bStatus || 'none')
}
export function similarityIcon(similarityData: SubmissionOriginalityData): string {
const {status, similarity_score} = similarityData
let iconClass
if (status === 'error') {
iconClass = 'icon-warning'
} else if (status === 'pending') {
iconClass = 'icon-clock'
} else if (similarity_score > 60) {
iconClass = 'icon-empty icon-Solid'
} else if (similarity_score > 20) {
iconClass = 'icon-oval-half icon-Solid'
} else {
iconClass = 'icon-certified icon-Solid'
}
// xsslint safeString.identifier iconClass
return `<i class="${iconClass}"></i>`
}