diff --git a/app/controllers/concerns/course/statistics/live_feedback_concern.rb b/app/controllers/concerns/course/statistics/live_feedback_concern.rb index c9581d2d8de..006c93b2fa6 100644 --- a/app/controllers/concerns/course/statistics/live_feedback_concern.rb +++ b/app/controllers/concerns/course/statistics/live_feedback_concern.rb @@ -6,10 +6,10 @@ def initialize_student_hash(students) students.to_h { |student| [student, nil] } end - def fetch_hash_for_live_feedback_assessment(submissions, live_feedback_codes, students) + def fetch_hash_for_live_feedback_assessment(submissions, assessment_live_feedbacks, students) student_hash = initialize_student_hash(students) - populate_hash(submissions, student_hash, live_feedback_codes) + populate_hash(submissions, student_hash, assessment_live_feedbacks) student_hash end diff --git a/app/controllers/course/assessment/submission/submissions_controller.rb b/app/controllers/course/assessment/submission/submissions_controller.rb index 0628bf3ad30..5054fee2708 100644 --- a/app/controllers/course/assessment/submission/submissions_controller.rb +++ b/app/controllers/course/assessment/submission/submissions_controller.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -class Course::Assessment::Submission::SubmissionsController < \ +class Course::Assessment::Submission::SubmissionsController < Course::Assessment::Submission::Controller include Course::Assessment::Submission::SubmissionsControllerServiceConcern include Signals::EmissionConcern @@ -102,9 +102,42 @@ def generate_live_feedback response_status, response_body = @answer.generate_live_feedback response_body['feedbackUrl'] = ENV.fetch('CODAVERI_URL') + course_user = CourseUser.find_by(course_id: @assessment.course_id, user_id: @submission.creator_id) + live_feedback = Course::Assessment::LiveFeedback.create_with_codes( + @submission.assessment_id, + @answer.question_id, + course_user.id, + response_body['transactionId'], + @answer.actable.files + ) + + if response_status == 200 + params[:live_feedback_id] = live_feedback.id + params[:feedback_files] = response_body['data']['feedbackFiles'] + save_live_feedback + end + + response_body['liveFeedbackId'] = live_feedback.id render json: response_body, status: response_status end + def save_live_feedback + live_feedback = Course::Assessment::LiveFeedback.find_by(id: params[:live_feedback_id]) + return head :bad_request if live_feedback.nil? + + feedback_files = params[:feedback_files] + feedback_files.each do |file| + filename = file[:path] + file[:feedbackLines].each do |feedback_line| + Course::Assessment::LiveFeedbackComment.create( + code_id: live_feedback.code.find_by(filename: filename).id, + line_number: feedback_line[:linenum], + comment: feedback_line[:feedback] + ) + end + end + end + # Reload the current answer or reset it, depending on parameters. # current_answer has the most recent copy of the answer. def reload_answer diff --git a/app/controllers/course/statistics/assessments_controller.rb b/app/controllers/course/statistics/assessments_controller.rb index 652374a519d..c106f900d27 100644 --- a/app/controllers/course/statistics/assessments_controller.rb +++ b/app/controllers/course/statistics/assessments_controller.rb @@ -42,7 +42,7 @@ def live_feedback_statistics preload(course: :course_users).first submissions = Course::Assessment::Submission.where(assessment_id: assessment_params[:id]). preload(creator: :course_users) - live_feedback_codes = Course::Assessment::LiveFeedbackCode.where(assessment_id: assessment_params[:id]) + assessment_live_feedbacks = Course::Assessment::LiveFeedback.where(assessment_id: assessment_params[:id]) @course_users_hash = preload_course_users_hash(@assessment.course) @@ -50,7 +50,7 @@ def live_feedback_statistics create_question_order_hash @student_live_feedback_hash = fetch_hash_for_live_feedback_assessment(submissions, - live_feedback_codes, + assessment_live_feedbacks, @all_students) end diff --git a/client/app/api/course/Assessment/Submissions.js b/client/app/api/course/Assessment/Submissions.js index 61c755ec52f..17316563c0b 100644 --- a/client/app/api/course/Assessment/Submissions.js +++ b/client/app/api/course/Assessment/Submissions.js @@ -132,6 +132,16 @@ export default class SubmissionsAPI extends BaseAssessmentAPI { }); } + saveLiveFeedback(submissionId, liveFeedbackId, feedbackFiles) { + return this.client.post( + `${this.#urlPrefix}/${submissionId}/save_live_feedback`, + { + live_feedback_id: liveFeedbackId, + feedback_files: feedbackFiles, + }, + ); + } + createProgrammingAnnotation(submissionId, answerId, fileId, params) { const url = `${this.#urlPrefix}/${submissionId}/answers/${answerId}/programming/files/${fileId}/annotations`; return this.client.post(url, params); diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveHelpStatisticsTable.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveHelpStatisticsTable.tsx index 43f72ef1e91..0c68a5d2637 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveHelpStatisticsTable.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveHelpStatisticsTable.tsx @@ -111,8 +111,14 @@ const LiveHelpStatisticsTable: FC = (props) => { .sort((a, b) => a.courseUser.name.localeCompare(b.courseUser.name)) .sort( (a, b) => - b.liveFeedbackCount.reduce((sum, count) => sum + (count || 0), 0) - - a.liveFeedbackCount.reduce((sum, count) => sum + (count || 0), 0), + (b.liveFeedbackCount?.reduce( + (sum, count) => sum + (typeof count === 'number' ? count : 0), + 0, + ) ?? 0) - + (a.liveFeedbackCount?.reduce( + (sum, count) => sum + (typeof count === 'number' ? count : 0), + 0, + ) ?? 0), ) .sort( (a, b) => @@ -161,13 +167,9 @@ const LiveHelpStatisticsTable: FC = (props) => { }, title: t(translations.questionIndex, { index: index + 1 }), cell: (datum): ReactNode => { - return typeof datum.liveFeedbackCount?.[index] === 'number' ? ( - renderNonNullAttemptCountCell(datum.liveFeedbackCount?.[index]) - ) : ( -
- - -
- ); + return typeof datum.liveFeedbackCount?.[index] === 'number' + ? renderNonNullAttemptCountCell(datum.liveFeedbackCount?.[index]) + : null; }, sortable: true, csvDownloadable: true, @@ -196,7 +198,7 @@ const LiveHelpStatisticsTable: FC = (props) => { cell: (datum): ReactNode => { const totalFeedbackCount = datum.liveFeedbackCount ? datum.liveFeedbackCount.reduce((sum, count) => sum + (count || 0), 0) - : 0; + : null; return (
{totalFeedbackCount} diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/classNameUtils.ts b/client/app/bundles/course/assessment/pages/AssessmentStatistics/classNameUtils.ts index df1676332e6..5339158663d 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/classNameUtils.ts +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/classNameUtils.ts @@ -1,4 +1,3 @@ -import { green } from 'theme/colors'; import { AttemptInfo } from 'types/course/statistics/assessmentStatistics'; const redBackgroundColorClassName = { diff --git a/client/app/bundles/course/assessment/submission/actions/answers/index.js b/client/app/bundles/course/assessment/submission/actions/answers/index.js index 991a48886f7..92fe57596fb 100644 --- a/client/app/bundles/course/assessment/submission/actions/answers/index.js +++ b/client/app/bundles/course/assessment/submission/actions/answers/index.js @@ -288,6 +288,7 @@ export function generateLiveFeedback({ type: actionTypes.LIVE_FEEDBACK_REQUEST, payload: { questionId, + liveFeedbackId: response.data?.liveFeedbackId, feedbackUrl: response.data?.feedbackUrl, token: response.data?.data?.token, }, @@ -306,9 +307,11 @@ export function generateLiveFeedback({ } export function fetchLiveFeedback({ + submissionId, answerId, questionId, feedbackUrl, + liveFeedbackId, feedbackToken, successMessage, noFeedbackMessage, @@ -318,6 +321,11 @@ export function fetchLiveFeedback({ .fetchLiveFeedback(feedbackUrl, feedbackToken) .then((response) => { if (response.status === 200) { + CourseAPI.assessment.submissions.saveLiveFeedback( + submissionId, + liveFeedbackId, + response.data?.data?.feedbackFiles ?? [], + ); handleFeedbackOKResponse({ dispatch, response, diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx index fbcf31af419..4bf02dfaeb3 100644 --- a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx +++ b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx @@ -209,8 +209,11 @@ class VisibleSubmissionEditIndex extends Component { dispatch, liveFeedback, assessment: { questionIds }, + match: { params }, } = this.props; + const liveFeedbackId = + liveFeedback?.feedbackByQuestion?.[questionId].liveFeedbackId; const feedbackToken = liveFeedback?.feedbackByQuestion?.[questionId].pendingFeedbackToken; const questionIndex = questionIds.findIndex((id) => id === questionId) + 1; @@ -224,9 +227,11 @@ class VisibleSubmissionEditIndex extends Component { ); dispatch( fetchLiveFeedback({ + submissionId: params.submissionId, answerId, questionId, feedbackUrl: liveFeedback?.feedbackUrl, + liveFeedbackId, feedbackToken, successMessage, noFeedbackMessage, diff --git a/client/app/bundles/course/assessment/submission/reducers/liveFeedback.js b/client/app/bundles/course/assessment/submission/reducers/liveFeedback.js index 60fa4eb22e3..c7fed18d824 100644 --- a/client/app/bundles/course/assessment/submission/reducers/liveFeedback.js +++ b/client/app/bundles/course/assessment/submission/reducers/liveFeedback.js @@ -16,6 +16,7 @@ export default function (state = initialState, action) { isRequestingLiveFeedback: true, pendingFeedbackToken: null, answerId: null, + liveFeedbackId: null, feedbackFiles: {}, }; } else { @@ -27,18 +28,20 @@ export default function (state = initialState, action) { }); } case actions.LIVE_FEEDBACK_REQUEST: { - const { token, questionId, feedbackUrl } = action.payload; + const { token, questionId, liveFeedbackId, feedbackUrl } = action.payload; return produce(state, (draft) => { draft.feedbackUrl ??= feedbackUrl; if (!(questionId in draft)) { draft.feedbackByQuestion[questionId] = { isRequestingLiveFeedback: false, + liveFeedbackId, pendingFeedbackToken: token, }; } else { draft.feedbackByQuestion[questionId] = { isRequestingLiveFeedback: false, ...draft.feedbackByQuestion[questionId], + liveFeedbackId, pendingFeedbackToken: token, }; } diff --git a/client/app/types/course/statistics/assessmentStatistics.ts b/client/app/types/course/statistics/assessmentStatistics.ts index 968f1bdda81..3cf7477e585 100644 --- a/client/app/types/course/statistics/assessmentStatistics.ts +++ b/client/app/types/course/statistics/assessmentStatistics.ts @@ -83,5 +83,5 @@ export interface AssessmentLiveFeedbackStatistics { courseUser: StudentInfo; groups: { name: string }[]; workflowState?: WorkflowState; - liveFeedbackCount: number[]; // Will already be ordered by question + liveFeedbackCount?: number[]; // Will already be ordered by question } diff --git a/config/routes.rb b/config/routes.rb index 7f1c156ceaa..cf0c40a1cbc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -245,6 +245,7 @@ post :generate_feedback, on: :member get :fetch_submitted_feedback, on: :member post :generate_live_feedback, on: :member + post :save_live_feedback, on: :member get :download_all, on: :collection get :download_statistics, on: :collection patch :publish_all, on: :collection