diff --git a/app/views/course/assessment/answer/rubric_based_responses/_rubric_based_response.json.jbuilder b/app/views/course/assessment/answer/rubric_based_responses/_rubric_based_response.json.jbuilder index b58e2b3a8da..346cd40997d 100644 --- a/app/views/course/assessment/answer/rubric_based_responses/_rubric_based_response.json.jbuilder +++ b/app/views/course/assessment/answer/rubric_based_responses/_rubric_based_response.json.jbuilder @@ -30,21 +30,25 @@ if attempt.submitted? && !attempt.auto_grading end end -json.categoryGrades answer.selections.includes(:criterion).map do |selection| - criterion = selection.criterion - - json.id selection.id - json.gradeId criterion&.id - json.categoryId selection.category_id - json.grade criterion ? criterion.grade : selection.grade - json.explanation criterion ? nil : selection.explanation +if can_grade || (@assessment.show_rubric_to_students? && @submission.published?) + json.categoryGrades answer.selections.includes(:criterion).map do |selection| + criterion = selection.criterion + + json.id selection.id + json.gradeId criterion&.id + json.categoryId selection.category_id + json.grade criterion ? criterion.grade : selection.grade + json.explanation criterion ? nil : selection.explanation + end end -posts = answer.submission.submission_questions.find_by(question_id: answer.question_id)&.discussion_topic&.posts -ai_generated_comment = posts&.select(&:is_ai_generated)&.last -if ai_generated_comment - json.aiGeneratedComment do - json.partial! ai_generated_comment +if can_grade + posts = answer.submission.submission_questions.find_by(question_id: answer.question_id)&.discussion_topic&.posts + ai_generated_comment = posts&.select(&:is_ai_generated)&.last + if ai_generated_comment + json.aiGeneratedComment do + json.partial! ai_generated_comment + end end end diff --git a/app/views/course/assessment/question/rubric_based_responses/_rubric_based_response.json.jbuilder b/app/views/course/assessment/question/rubric_based_responses/_rubric_based_response.json.jbuilder index 2cf1fcbfda3..c6d67e63a6b 100644 --- a/app/views/course/assessment/question/rubric_based_responses/_rubric_based_response.json.jbuilder +++ b/app/views/course/assessment/question/rubric_based_responses/_rubric_based_response.json.jbuilder @@ -1,14 +1,18 @@ # frozen_string_literal: true -json.aiGradingEnabled question.ai_grading_enabled? -json.categories question.categories.each do |category| - json.id category.id - json.name category.name - json.maximumGrade category.criterions.map(&:grade).compact.max - json.isBonusCategory category.is_bonus_category +json.aiGradingEnabled question.ai_grading_enabled? if can_grade +if can_grade || (@assessment.show_rubric_to_students? && answer.submission.published?) + json.categories question.categories.each do |category| + json.id category.id + json.name category.name + json.maximumGrade category.criterions.map(&:grade).compact.max + json.isBonusCategory category.is_bonus_category - json.grades category.criterions.each do |criterion| - json.id criterion.id - json.grade criterion.grade - json.explanation format_ckeditor_rich_text(criterion.explanation) + json.grades category.criterions.each do |criterion| + json.id criterion.id + json.grade criterion.grade + json.explanation format_ckeditor_rich_text(criterion.explanation) + end end +else + json.categories [] end diff --git a/app/views/course/assessment/submission/answer/answers/show.json.jbuilder b/app/views/course/assessment/submission/answer/answers/show.json.jbuilder index 011cfb40ad0..ed800d0ce23 100644 --- a/app/views/course/assessment/submission/answer/answers/show.json.jbuilder +++ b/app/views/course/assessment/submission/answer/answers/show.json.jbuilder @@ -15,9 +15,9 @@ json.question do json.description format_ckeditor_rich_text(question.description) json.type question.question_type - json.partial! question, question: question.specific, can_grade: false, answer: @answer + json.partial! question, question: question.specific, can_grade: can_grade, answer: @answer end -json.partial! specific_answer, answer: specific_answer, can_grade: false +json.partial! specific_answer, answer: specific_answer, can_grade: can_grade if can_grade || @answer.submission.published? json.grading do @@ -25,6 +25,7 @@ if can_grade || @answer.submission.published? end end +# hide unpublished annotations in answer details if @answer.actable_type == Course::Assessment::Answer::Programming.name files = @answer.specific.files json.partial! 'course/assessment/answer/programming/annotations', programming_files: files, diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/AnswerDisplay/LastAttempt.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/AnswerDisplay/LastAttempt.tsx index 2a04ffa2df7..9c16e03867f 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/AnswerDisplay/LastAttempt.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/AnswerDisplay/LastAttempt.tsx @@ -84,6 +84,16 @@ const LastAttemptIndex: FC = (props) => { diff --git a/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsSequenceView.tsx b/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsSequenceView.tsx index c11c9ddfbb7..c3986281356 100644 --- a/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsSequenceView.tsx +++ b/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsSequenceView.tsx @@ -18,8 +18,11 @@ import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; import useTranslation from 'lib/hooks/useTranslation'; import { formatLongDateTime } from 'lib/moment'; +import { workflowStates } from '../../constants'; import { historyActions } from '../../reducers/history'; +import { getAssessment } from '../../selectors/assessments'; import { getSubmissionQuestionHistory } from '../../selectors/history'; +import { getSubmission } from '../../selectors/submissions'; import translations from '../../translations'; import AnswerDetails from '../AnswerDetails/AnswerDetails'; import TextResponseSolutions from '../TextResponseSolutions'; @@ -38,6 +41,10 @@ const AllAttemptsSequenceView: FC = (props) => { const { answerDataById, allAnswers, selectedAnswerIds, question } = useAppSelector(getSubmissionQuestionHistory(submissionId, questionId)); + const assessment = useAppSelector(getAssessment); + const submission = useAppSelector(getSubmission); + const published = submission.workflowState === workflowStates.Published; + useEffect(() => { const answerIdsToFetch = selectedAnswerIds.filter( @@ -94,6 +101,23 @@ const AllAttemptsSequenceView: FC = (props) => { diff --git a/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsTimelineView.tsx b/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsTimelineView.tsx index 69958b1e69f..12849e33f85 100644 --- a/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsTimelineView.tsx +++ b/client/app/bundles/course/assessment/submission/components/AllAttempts/AllAttemptsTimelineView.tsx @@ -5,16 +5,20 @@ import CustomSlider from 'lib/components/extensions/CustomSlider'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; import { formatLongDateTime } from 'lib/moment'; +import { workflowStates } from '../../constants'; +import { getAssessment } from '../../selectors/assessments'; import { getSubmissionQuestionHistory } from '../../selectors/history'; +import { getSubmission } from '../../selectors/submissions'; import AnswerDetails from '../AnswerDetails/AnswerDetails'; interface Props { questionId: number; submissionId: number; + graderView: boolean; } const AllAttemptsTimelineView: FC = (props) => { - const { submissionId, questionId } = props; + const { submissionId, questionId, graderView } = props; const dispatch = useAppDispatch(); @@ -22,6 +26,10 @@ const AllAttemptsTimelineView: FC = (props) => { getSubmissionQuestionHistory(submissionId, questionId), ); + const assessment = useAppSelector(getAssessment); + const submission = useAppSelector(getSubmission); + const published = submission.workflowState === workflowStates.Published; + // sliderIndex is the uncommited index that is updated on drag // displayedIndex is updated on drop or with any non-mouse (keyboard) events // we distinguish these because we don't want to query each answer as user drags the slider @@ -102,6 +110,21 @@ const AllAttemptsTimelineView: FC = (props) => {
diff --git a/client/app/bundles/course/assessment/submission/components/AllAttempts/index.tsx b/client/app/bundles/course/assessment/submission/components/AllAttempts/index.tsx index 7dc3591dc9a..b442e2ef24e 100644 --- a/client/app/bundles/course/assessment/submission/components/AllAttempts/index.tsx +++ b/client/app/bundles/course/assessment/submission/components/AllAttempts/index.tsx @@ -111,6 +111,7 @@ const AllAttemptsContent: FC = (props) => { )} {viewType === 'timeline' && ( diff --git a/client/app/bundles/course/assessment/submission/components/AnswerDetails/AnswerDetails.tsx b/client/app/bundles/course/assessment/submission/components/AnswerDetails/AnswerDetails.tsx index 30c44ae2e8b..2888627617b 100644 --- a/client/app/bundles/course/assessment/submission/components/AnswerDetails/AnswerDetails.tsx +++ b/client/app/bundles/course/assessment/submission/components/AnswerDetails/AnswerDetails.tsx @@ -8,7 +8,7 @@ import { formatLongDateTime } from 'lib/moment'; import messagesTranslations from 'lib/translations/messages'; import { HistoryFetchStatus } from '../../reducers/history'; -import { AnswerDetailsProps } from '../../types'; +import { AnswerDetailsProps, DisplaySettings } from '../../types'; import FileUploadDetails from './FileUploadDetails'; import ForumPostResponseDetails from './ForumPostResponseDetails'; @@ -100,19 +100,26 @@ const FetchedAnswerDetails = ( type AnswerDetailsComponentProps = { status: HistoryFetchStatus; + displaySettings: DisplaySettings; } & Partial>; const AnswerDetails = ( props: AnswerDetailsComponentProps, ): JSX.Element => { - const { answer, question, status } = props; + const { answer, question, status, displaySettings } = props; const { t } = useTranslation(); const isAnswerRenderable = answer && question && status === HistoryFetchStatus.COMPLETED; if (isAnswerRenderable) { - return ; + return ( + + ); } if (status === HistoryFetchStatus.ERRORED) { return ( diff --git a/client/app/bundles/course/assessment/submission/components/AnswerDetails/MultipleChoiceDetails.tsx b/client/app/bundles/course/assessment/submission/components/AnswerDetails/MultipleChoiceDetails.tsx index f5920867a0d..4cd3d45e758 100644 --- a/client/app/bundles/course/assessment/submission/components/AnswerDetails/MultipleChoiceDetails.tsx +++ b/client/app/bundles/course/assessment/submission/components/AnswerDetails/MultipleChoiceDetails.tsx @@ -7,7 +7,8 @@ import { AnswerDetailsProps } from '../../types'; const MultipleChoiceDetails = ( props: AnswerDetailsProps, ): JSX.Element => { - const { question, answer } = props; + const { question, answer, displaySettings } = props; + const { showMcqMrqSolution } = displaySettings; return ( <> {question.options.map((option) => ( @@ -25,7 +26,7 @@ const MultipleChoiceDetails = ( , ): JSX.Element => { - const { question, answer } = props; + const { question, answer, displaySettings } = props; + const { showMcqMrqSolution } = displaySettings; return ( <> {question.options.map((option) => { @@ -26,7 +27,7 @@ const MultipleResponseDetails = ( , ): JSX.Element => { - const { answer } = props; + const { answer, displaySettings } = props; const annotations = answer.annotations ?? []; + const { + showPrivateTestCases, + showEvaluationTestCases, + showPublicTestCasesOutput, + showPrivateTestCasesOutput, + showEvaluationTestCasesOutput, + showStdoutAndStderr, + } = displaySettings; + const dispatch = useAppDispatch(); useEffect(() => { @@ -35,8 +43,18 @@ const ProgrammingAnswerDetails = ( file={file} /> ))} - - + + {/* might not need this component because unpublished annotations (i.e Codaveri) are not shown in Answer Details */} + {/* students can see this status bar in past attempts view, which is not relevant to them */} + {/* */} ); }; diff --git a/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCaseRow.tsx b/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCaseRow.tsx index 3266022da80..65c9fb1a2be 100644 --- a/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCaseRow.tsx +++ b/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCaseRow.tsx @@ -7,6 +7,7 @@ import ExpandableCode from 'lib/components/core/ExpandableCode'; interface Props { result: TestCaseResult; + showTestCaseOutput: boolean; } const TestCaseClassName = { @@ -16,7 +17,7 @@ const TestCaseClassName = { }; const TestCaseRow: FC = (props) => { - const { result } = props; + const { result, showTestCaseOutput } = props; const nameRegex = /\/?(\w+)$/; const idMatch = result.identifier?.match(nameRegex); @@ -56,9 +57,11 @@ const TestCaseRow: FC = (props) => { {result.expected || ''} - - {result.output || ''} - + {showTestCaseOutput && ( + + {result.output || ''} + + )} {testCaseIcon} diff --git a/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCases.tsx b/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCases.tsx index 7963e767d42..7d332e213aa 100644 --- a/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCases.tsx +++ b/client/app/bundles/course/assessment/submission/components/AnswerDetails/ProgrammingComponent/TestCases.tsx @@ -70,11 +70,18 @@ const translations = defineMessages({ interface Props { testCase: TestCase; + showPrivateTestCases: boolean; + showEvaluationTestCases: boolean; + showPublicTestCasesOutput: boolean; + showPrivateTestCasesOutput: boolean; + showEvaluationTestCasesOutput: boolean; + showStdoutAndStderr: boolean; } interface TestCaseComponentProps { testCaseResults: TestCaseResult[]; testCaseType: string; + showTestCaseOutput: boolean; } interface OutputStreamProps { @@ -83,11 +90,12 @@ interface OutputStreamProps { } const TestCaseComponent: FC = (props) => { - const { testCaseResults, testCaseType } = props; + const { testCaseResults, testCaseType, showTestCaseOutput } = props; const { t } = useTranslation(); + // result.output might be undefined for private and evaluation test cases for students const isProgrammingAnswerEvaluated = - testCaseResults.filter((result) => !!result.output).length > 0; + testCaseResults.filter((result) => result.passed !== undefined).length > 0; const numPassedTestCases = testCaseResults.filter( (result) => result.passed, @@ -178,9 +186,11 @@ const TestCaseComponent: FC = (props) => { - - - + {showTestCaseOutput && ( + + + + )} @@ -188,7 +198,11 @@ const TestCaseComponent: FC = (props) => { {testCaseResults.map((result) => ( - + ))} @@ -222,37 +236,59 @@ const OutputStream: FC = (props) => { }; const TestCases: FC = (props) => { - const { testCase } = props; + const { + testCase, + showPrivateTestCases, + showEvaluationTestCases, + showPublicTestCasesOutput, + showPrivateTestCasesOutput, + showEvaluationTestCasesOutput, + showStdoutAndStderr, + } = props; return (
{testCase.public_test && testCase.public_test.length > 0 && ( )} - {testCase.private_test && testCase.private_test.length > 0 && ( - - )} - - {testCase.evaluation_test && testCase.evaluation_test.length > 0 && ( - - )} + {showPrivateTestCases && + testCase.private_test && + testCase.private_test.length > 0 && ( + + )} + + {showEvaluationTestCases && + testCase.evaluation_test && + testCase.evaluation_test.length > 0 && ( + + )} - + {showStdoutAndStderr && ( + <> + - + + + )}
); }; diff --git a/client/app/bundles/course/assessment/submission/components/AnswerDetails/RubricBasedResponseDetails.tsx b/client/app/bundles/course/assessment/submission/components/AnswerDetails/RubricBasedResponseDetails.tsx index e2cc18dfa39..c1ad4fc1539 100644 --- a/client/app/bundles/course/assessment/submission/components/AnswerDetails/RubricBasedResponseDetails.tsx +++ b/client/app/bundles/course/assessment/submission/components/AnswerDetails/RubricBasedResponseDetails.tsx @@ -7,19 +7,23 @@ import { AnswerDetailsProps } from '../../types'; const RubricBasedResponseDetails = ( props: AnswerDetailsProps, ): JSX.Element => { - const { question, answer } = props; + const { question, answer, displaySettings } = props; + const { showRubricBreakdown } = displaySettings; return ( <> - {}} // Placeholder function since RubricPanel is not editable here - /> + {showRubricBreakdown && answer.categoryGrades && ( + {}} // Placeholder function since RubricPanel is not editable here + /> + )} ); }; diff --git a/client/app/bundles/course/assessment/submission/components/ReadOnlyEditor/index.jsx b/client/app/bundles/course/assessment/submission/components/ReadOnlyEditor/index.jsx index 315e53973d7..e94e905f6f0 100644 --- a/client/app/bundles/course/assessment/submission/components/ReadOnlyEditor/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/ReadOnlyEditor/index.jsx @@ -56,6 +56,17 @@ class ReadOnlyEditor extends Component { this.setState({ expanded: newExpanded }); } + + // Update editor mode when annotations length changes + if (prevProps.annotations.length !== this.props.annotations.length) { + const newEditorMode = + this.props.annotations.length > 0 + ? EDITOR_MODE_WIDE + : EDITOR_MODE_NARROW; + if (this.state.editorMode !== newEditorMode) { + this.setState({ editorMode: newEditorMode }); + } + } } setAllCommentStateCollapsed() { diff --git a/client/app/bundles/course/assessment/submission/components/answers/MultipleChoice/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/MultipleChoice/index.jsx index fb1bf3ccb25..9794f4834b5 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/MultipleChoice/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/MultipleChoice/index.jsx @@ -12,6 +12,7 @@ const MultipleChoiceOptions = ({ readOnly, showMcqMrqSolution, graderView, + published, question, field: { onChange, value }, }) => ( @@ -27,7 +28,9 @@ const MultipleChoiceOptions = ({ { const { id: prevId } = prevProps.question; const { id: nextId } = nextProps.question; - const { graderView: prevGraderView } = prevProps.graderView; - const { graderView: nextGraderView } = nextProps.graderView; + const prevGraderView = prevProps.graderView; + const nextGraderView = nextProps.graderView; const isQuestionIdUnchanged = prevId === nextId; const isGraderViewUnchanged = prevGraderView === nextGraderView; return ( @@ -78,6 +82,7 @@ const MultipleChoice = (props) => { const { answerId, graderView, + published, question, readOnly, saveAnswerAndUpdateClientVersion, @@ -99,7 +104,7 @@ const MultipleChoice = (props) => { }, }} fieldState={fieldState} - {...{ question, readOnly, showMcqMrqSolution, graderView }} + {...{ question, readOnly, showMcqMrqSolution, graderView, published }} /> )} /> @@ -109,6 +114,7 @@ const MultipleChoice = (props) => { MultipleChoice.propTypes = { answerId: PropTypes.number.isRequired, graderView: PropTypes.bool.isRequired, + published: PropTypes.bool.isRequired, question: questionShape.isRequired, readOnly: PropTypes.bool.isRequired, saveAnswerAndUpdateClientVersion: PropTypes.func.isRequired, diff --git a/client/app/bundles/course/assessment/submission/components/answers/MultipleResponse/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/MultipleResponse/index.jsx index d8968397a80..f4702bb9e68 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/MultipleResponse/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/MultipleResponse/index.jsx @@ -12,6 +12,7 @@ const MultipleResponseOptions = ({ readOnly, showMcqMrqSolution, graderView, + published, question, field: { onChange, value }, }) => ( @@ -27,7 +28,9 @@ const MultipleResponseOptions = ({ { const { id: prevId } = prevProps.question; const { id: nextId } = nextProps.question; - const { graderView: prevGraderView } = prevProps.graderView; - const { graderView: nextGraderView } = nextProps.graderView; + const prevGraderView = prevProps.graderView; + const nextGraderView = nextProps.graderView; const isQuestionIdUnchanged = prevId === nextId; const isGraderViewUnchanged = prevGraderView === nextGraderView; return ( @@ -93,6 +97,7 @@ const MultipleResponse = (props) => { const { answerId, graderView, + published, question, readOnly, saveAnswerAndUpdateClientVersion, @@ -114,7 +119,7 @@ const MultipleResponse = (props) => { }, }} fieldState={fieldState} - {...{ question, readOnly, showMcqMrqSolution, graderView }} + {...{ question, readOnly, showMcqMrqSolution, graderView, published }} /> )} /> @@ -124,6 +129,7 @@ const MultipleResponse = (props) => { MultipleResponse.propTypes = { answerId: PropTypes.number.isRequired, graderView: PropTypes.bool.isRequired, + published: PropTypes.bool.isRequired, question: questionShape.isRequired, readOnly: PropTypes.bool.isRequired, saveAnswerAndUpdateClientVersion: PropTypes.func.isRequired, diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx index f26bb95dddf..43af99f06d1 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx @@ -7,6 +7,7 @@ const MultipleChoiceAdapter = (props: McqAnswerProps): JSX.Element => { answerId, readOnly, graderView, + published, showMcqMrqSolution, saveAnswerAndUpdateClientVersion, } = props; @@ -15,6 +16,7 @@ const MultipleChoiceAdapter = (props: McqAnswerProps): JSX.Element => { key={`question_${question.id}`} answerId={answerId!} graderView={graderView} + published={published} question={question} readOnly={readOnly} saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion} diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx index 01877b712e1..5fa485b820f 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx @@ -7,6 +7,7 @@ const MultipleResponseAdapter = (props: MrqAnswerProps): JSX.Element => { answerId, readOnly, graderView, + published, showMcqMrqSolution, saveAnswerAndUpdateClientVersion, } = props; @@ -15,6 +16,7 @@ const MultipleResponseAdapter = (props: MrqAnswerProps): JSX.Element => { key={`question_${question.id}`} answerId={answerId!} graderView={graderView} + published={published} question={question} readOnly={readOnly} saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion} diff --git a/client/app/bundles/course/assessment/submission/components/answers/index.tsx b/client/app/bundles/course/assessment/submission/components/answers/index.tsx index 045661a7e5a..24ee8a065d9 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/index.tsx +++ b/client/app/bundles/course/assessment/submission/components/answers/index.tsx @@ -28,6 +28,7 @@ import translations from '../../translations'; interface SubmissionAnswerProps { answerId: number | null; graderView: boolean; + published: boolean; allErrors: ErrorType[]; question: SubmissionQuestionData; questionType: T; @@ -57,6 +58,7 @@ const SubmissionAnswer = ( answerId, allErrors, graderView, + published, question, questionType, readOnly, @@ -117,6 +119,7 @@ const SubmissionAnswer = ( saveAnswerAndUpdateClientVersion, graderView, showMcqMrqSolution, + published, }, MultipleResponse: { answerId, @@ -125,6 +128,7 @@ const SubmissionAnswer = ( saveAnswerAndUpdateClientVersion, graderView, showMcqMrqSolution, + published, }, Programming: { answerId, diff --git a/client/app/bundles/course/assessment/submission/components/answers/types.ts b/client/app/bundles/course/assessment/submission/components/answers/types.ts index 772a51b07ab..2a76c523f8d 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/types.ts +++ b/client/app/bundles/course/assessment/submission/components/answers/types.ts @@ -17,11 +17,13 @@ export interface ScribingAnswerProps export interface McqAnswerProps extends AnswerCommonProps<'MultipleChoice'> { showMcqMrqSolution: boolean; graderView: boolean; + published: boolean; } export interface MrqAnswerProps extends AnswerCommonProps<'MultipleResponse'> { showMcqMrqSolution: boolean; graderView: boolean; + published: boolean; } export interface ProgrammingAnswerProps diff --git a/client/app/bundles/course/assessment/submission/containers/QuestionGrade.tsx b/client/app/bundles/course/assessment/submission/containers/QuestionGrade.tsx index 1068be46401..19b10e52020 100644 --- a/client/app/bundles/course/assessment/submission/containers/QuestionGrade.tsx +++ b/client/app/bundles/course/assessment/submission/containers/QuestionGrade.tsx @@ -92,7 +92,7 @@ const QuestionGrade: FC = (props) => { question.type === QuestionType.RubricBasedResponse; const isRubricVisible = isRubricBasedResponse && - (!submission.isStudent || assessment.showRubricToStudents); + (graderView || (published && assessment.showRubricToStudents)); const isRubricBasedResponseAndAutogradable = isRubricBasedResponse && (question as SubmissionQuestionData) diff --git a/client/app/bundles/course/assessment/submission/containers/ReadOnlyEditor.jsx b/client/app/bundles/course/assessment/submission/containers/ReadOnlyEditor.jsx index 075ba4d1484..e389bd8768d 100644 --- a/client/app/bundles/course/assessment/submission/containers/ReadOnlyEditor.jsx +++ b/client/app/bundles/course/assessment/submission/containers/ReadOnlyEditor.jsx @@ -5,9 +5,11 @@ import { Warning } from '@mui/icons-material'; import { Paper, Typography } from '@mui/material'; import PropTypes from 'prop-types'; +import { POST_WORKFLOW_STATE } from 'lib/constants/sharedConstants'; + import ProgrammingFileDownloadChip from '../components/answers/Programming/ProgrammingFileDownloadChip'; import ReadOnlyEditorComponent from '../components/ReadOnlyEditor'; -import { fileShape, topicShape } from '../propTypes'; +import { fileShape, postShape, topicShape } from '../propTypes'; const translations = defineMessages({ sizeTooBig: { @@ -18,16 +20,39 @@ const translations = defineMessages({ class ReadOnlyEditorContainer extends Component { shouldComponentUpdate(nextProps) { - return nextProps.annotations !== this.props.annotations; + return ( + nextProps.annotations !== this.props.annotations || + nextProps.posts !== this.props.posts || + nextProps.graderView !== this.props.graderView + ); } render() { - const { answerId, file, annotations } = this.props; + const { answerId, file, annotations, posts, graderView } = this.props; if (file.highlightedContent !== null) { + const filteredAnnotations = graderView + ? Object.values(annotations) + : // Students should only see published posts + Object.values(annotations) + .map((annotation) => { + const publishedPostIds = + annotation.postIds.filter( + (postId) => + posts[postId]?.workflowState === + POST_WORKFLOW_STATE.published, + ) || []; + + return { + ...annotation, + postIds: publishedPostIds, + }; + }) + .filter((annotation) => annotation.postIds.length > 0); + return ( ; setIsFirstRendering: (isFirstRendering: boolean) => void; + readOnly?: boolean; } const RubricPanel: FC = (props) => { const { t } = useTranslation(); - const { answerId, answerCategoryGrades, question, setIsFirstRendering } = - props; + const { + answerId, + answerCategoryGrades, + question, + setIsFirstRendering, + readOnly, + } = props; const categoryGrades = useMemo(() => { - const categoryGradeHash = answerCategoryGrades.reduce( + const categoryGradeHash = (answerCategoryGrades ?? []).reduce( (obj, category) => ({ ...obj, [category.categoryId]: { @@ -42,7 +48,7 @@ const RubricPanel: FC = (props) => { {}, ); - return question.categories.reduce( + return (question.categories ?? []).reduce( (obj, category) => ({ ...obj, [category.id]: { @@ -82,13 +88,14 @@ const RubricPanel: FC = (props) => { - {question?.categories.map((category) => ( + {(question?.categories ?? []).map((category) => ( ))} diff --git a/client/app/bundles/course/assessment/submission/containers/RubricPanelRow.tsx b/client/app/bundles/course/assessment/submission/containers/RubricPanelRow.tsx index b6d91d25172..be8caccf2f5 100644 --- a/client/app/bundles/course/assessment/submission/containers/RubricPanelRow.tsx +++ b/client/app/bundles/course/assessment/submission/containers/RubricPanelRow.tsx @@ -31,6 +31,7 @@ interface RubricPanelRowProps { category: RubricBasedResponseCategoryQuestionData; categoryGrades: Record; setIsFirstRendering: (isFirstRendering: boolean) => void; + readOnly?: boolean; } function buildCategoryGradeExplanationMap( @@ -144,7 +145,13 @@ const MaxGradeCell: FC<{ maxGrade?: number }> = ({ maxGrade }) => ( ); const RubricPanelRow: FC = (props) => { - const { answerId, question, category, categoryGrades } = props; + const { + answerId, + question, + category, + categoryGrades, + readOnly = false, + } = props; const dispatch = useAppDispatch(); const submission = useAppSelector(getSubmission); @@ -158,7 +165,7 @@ const RubricPanelRow: FC = (props) => { const attempting = workflowState === workflowStates.Attempting; const published = workflowState === workflowStates.Published; - const editable = !attempting && graderView; + const editable = !attempting && graderView && !readOnly; const bonusAwarded = new Date(submittedAt) < new Date(bonusEndAt) ? bonusPoints : 0; diff --git a/client/app/bundles/course/assessment/submission/containers/TestCaseView/index.jsx b/client/app/bundles/course/assessment/submission/containers/TestCaseView/index.jsx index 7cda3dc3a08..13fe741a3ae 100644 --- a/client/app/bundles/course/assessment/submission/containers/TestCaseView/index.jsx +++ b/client/app/bundles/course/assessment/submission/containers/TestCaseView/index.jsx @@ -126,9 +126,10 @@ export class VisibleTestCaseView extends Component { ); } - renderTestCaseRow(testCase) { + renderTestCaseRow(testCase, testCaseType) { const { testCases: { canReadTests }, + graderView, } = this.props; const { showPublicTestCasesOutput } = this.props; @@ -175,7 +176,9 @@ export class VisibleTestCaseView extends Component { {testCase.expected || ''}
- {(canReadTests || showPublicTestCasesOutput) && ( + {((graderView && canReadTests) || + (showPublicTestCasesOutput && + testCaseType === 'publicTestCases')) && ( {testCase.output || ''} @@ -199,8 +202,9 @@ export class VisibleTestCaseView extends Component { return null; } + // testCase.output might be undefined for private and evaluation test cases for students const isProgrammingAnswerEvaluated = - testCases.filter((testCase) => !!testCase.output).length > 0; + testCases.filter((testCase) => testCase.passed !== undefined).length > 0; const numPassedTestCases = testCases.filter( (testCase) => testCase.passed, @@ -298,7 +302,9 @@ export class VisibleTestCaseView extends Component { - {((graderView && canReadTests) || showPublicTestCasesOutput) && ( + {((graderView && canReadTests) || + (showPublicTestCasesOutput && + testCaseType === 'publicTestCases')) && ( @@ -309,7 +315,9 @@ export class VisibleTestCaseView extends Component { - {testCases.map(this.renderTestCaseRow.bind(this))} + {testCases.map((testCase) => + this.renderTestCaseRow(testCase, testCaseType), + )} diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/components/QuestionContent.tsx b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/components/QuestionContent.tsx index 8c0318e9f04..4430936ef76 100644 --- a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/components/QuestionContent.tsx +++ b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/components/QuestionContent.tsx @@ -47,6 +47,7 @@ const QuestionContent: FC = (props) => { const { isSaving } = submissionFlags; const attempting = workflowState === workflowStates.Attempting; + const published = workflowState === workflowStates.Published; const questionId = questionIds[stepIndex]; const question = questions[questionId]; @@ -71,6 +72,7 @@ const QuestionContent: FC = (props) => { questionType: question.type, submissionId: submission.id, graderView, + published, showMcqMrqSolution, openAnswerHistoryView, questionNumber: stepIndex + 1, diff --git a/client/app/bundles/course/assessment/submission/types.ts b/client/app/bundles/course/assessment/submission/types.ts index c4813b59511..8b85520ff67 100644 --- a/client/app/bundles/course/assessment/submission/types.ts +++ b/client/app/bundles/course/assessment/submission/types.ts @@ -180,9 +180,21 @@ export interface AnswerDetailsMap { RubricBasedResponse: RubricBasedResponseAnswerData; } +export interface DisplaySettings { + showPrivateTestCases: boolean; + showEvaluationTestCases: boolean; + showMcqMrqSolution: boolean; + showRubricBreakdown: boolean; + showPublicTestCasesOutput: boolean; + showPrivateTestCasesOutput: boolean; + showEvaluationTestCasesOutput: boolean; + showStdoutAndStderr: boolean; +} + export interface AnswerDetailsProps { question: SubmissionQuestionData; answer: AnswerDetailsMap[T]; + displaySettings: DisplaySettings; } export type AnswerDataWithQuestion = diff --git a/client/app/types/course/assessment/submission/answer/rubricBasedResponse.ts b/client/app/types/course/assessment/submission/answer/rubricBasedResponse.ts index d6fef78b27c..c073c4d6795 100644 --- a/client/app/types/course/assessment/submission/answer/rubricBasedResponse.ts +++ b/client/app/types/course/assessment/submission/answer/rubricBasedResponse.ts @@ -25,7 +25,7 @@ export interface RubricBasedResponseAnswerData extends AnswerBaseData { path?: string; }; latestAnswer?: RubricBasedResponseAnswerData; - categoryGrades: { + categoryGrades?: { id: number | null | undefined; categoryId: number; grade: number; diff --git a/client/app/types/course/assessment/submission/question/types.ts b/client/app/types/course/assessment/submission/question/types.ts index f403699d444..171c905fe44 100644 --- a/client/app/types/course/assessment/submission/question/types.ts +++ b/client/app/types/course/assessment/submission/question/types.ts @@ -78,7 +78,7 @@ export interface RubricBasedResponseCategoryQuestionData } interface RubricBasedResponseQuestionData { - aiGradingEnabled: boolean; + aiGradingEnabled?: boolean; categories: RubricBasedResponseCategoryQuestionData[]; }