Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
279543d
feat(programming-audit-trail): modify db
syoopie Sep 25, 2024
68210be
feat(programming-audit-trail): preserve old questions and tests
syoopie Oct 22, 2024
63333dc
feat(custom-slider): add custom slider
syoopie Oct 2, 2024
c57737a
feat(accordion): expand accordion functionality
syoopie Oct 3, 2024
22598a7
refactor(all-attempts-display): update accordion
syoopie Oct 3, 2024
0275404
refactor(statistics): change naming of types
syoopie Oct 3, 2024
7824df6
refactor(all-attempts-display): use new slider
syoopie Oct 3, 2024
6a21214
refactor(all-attempts-display): change created_at to submitted_at
syoopie Oct 3, 2024
3b43451
feat(all-attempts-display): add more programming question details
syoopie Oct 15, 2024
58c38e0
refactor(statistics): use seperate API for latest answer
syoopie Oct 22, 2024
fade06b
feat(statistics-marks-table): add card to dialog
syoopie Oct 22, 2024
6b321ad
feat(statistics): add BE support for multiple tests / questions
syoopie Oct 9, 2024
913ecc1
feat(statistics): multiple regrading view for attempts
syoopie Oct 15, 2024
5c299df
feat(regrading): only regrade current_answer on question edit
syoopie Oct 15, 2024
61bbcba
test(stats-answer-controller): rename function calls
syoopie Oct 16, 2024
4b28d7f
test(programming-controller): fix tests
syoopie Oct 18, 2024
398d617
test(programming-management): fix tests
syoopie Oct 21, 2024
7b6a998
test(programming-management): fix template package spec
syoopie Oct 21, 2024
bcd5746
fix(attempt-count-table): fix error when attempt does not exist
syoopie Oct 21, 2024
5d88122
test(test-case-view): fix FE test
syoopie Oct 21, 2024
9183fd7
refactor(pastAnswer): follow audit trail
bivanalhar Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def edit

def update
result = @programming_question.class.transaction do
duplicate_and_replace_programming_question
@question_assessment.skill_ids = programming_question_params[:question_assessment].
try(:[], :skill_ids)
@programming_question.assign_attributes(programming_question_params.
Expand All @@ -54,7 +55,7 @@ def update
end

if result
render_success_json false
render_success_json true
else
render_failure_json
end
Expand Down Expand Up @@ -111,6 +112,53 @@ def destroy

private

def duplicate_and_replace_programming_question
duplicated_programming_question, duplicated_question = duplicate_programming_question
duplicate_associations(duplicated_programming_question)
save_duplicated_question(duplicated_programming_question, duplicated_question)
link_to_original_question(duplicated_programming_question)
@programming_question = duplicated_programming_question
end

def duplicate_programming_question
duplicated_programming_question = @programming_question.dup
duplicated_question = @programming_question.acting_as.dup
# Unique field that needs to be reset
duplicated_programming_question.import_job_id = nil
duplicated_programming_question.question = @programming_question.question

duplicated_question.koditsu_question_id = nil
duplicated_question.is_synced_with_koditsu = false
[duplicated_programming_question, duplicated_question]
end

def duplicate_associations(duplicated_programming_question)
duplicated_programming_question.template_files = @programming_question.template_files.map do |template_file|
duplicated_template_file = template_file.dup
duplicated_template_file.question = duplicated_programming_question
duplicated_template_file
end

duplicated_programming_question.test_cases = @programming_question.test_cases.map do |test_case|
duplicated_test_case = test_case.dup
duplicated_test_case.question = duplicated_programming_question
duplicated_test_case
end
end

def save_duplicated_question(duplicated_programming_question, duplicated_question)
duplicated_programming_question.save!(validate: false)
duplicated_question.save!(validate: false)
duplicated_programming_question.template_files.each(&:save!)
duplicated_programming_question.test_cases.each(&:save!)
end

def link_to_original_question(duplicated_programming_question)
@programming_question.question.actable = duplicated_programming_question
# Update the original programming question's parent_id to link to the new duplicated question
duplicated_programming_question.update_column(:parent_id, @programming_question.id)
end

def format_test_cases
@public_test_cases = []
@private_test_cases = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# frozen_string_literal: true
class Course::Assessment::SubmissionQuestion::SubmissionQuestionsController < \
Course::Assessment::SubmissionQuestion::Controller
helper Course::Statistics::AnswersHelper.name.sub(/Helper$/, '')

def past_answers
answers_to_load = past_answers_params[:answers_to_load]&.to_i || 10
answers = @submission_question.past_answers(answers_to_load)
@question = @submission_question.question
@submission = @submission_question.submission

respond_to do |format|
format.json { render 'past_answers', locals: { answers: answers } }
format.json { render 'past_answers', locals: { answers: answers, question: @question } }
end
end

Expand Down
27 changes: 27 additions & 0 deletions app/controllers/course/statistics/answers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
def show
@submission = @answer.submission
@assessment = @submission.assessment
@question = @answer.question
end

def all_answers
Expand All @@ -25,11 +26,37 @@
question_id: all_answers_params[:question_id]
).
where.not(workflow_state: :attempting)
@question = Course::Assessment::Question.find(all_answers_params[:question_id])
assessment_id = Course::QuestionAssessment.where(question_id: @question.id).first.assessment_id
@assessment = Course::Assessment.find(assessment_id)
@submission = Course::Assessment::Submission.find(all_answers_params[:submission_id])

fetch_all_actable_questions(@question)
end

private

def all_answers_params
params.permit(:submission_id, :question_id)
end

def fetch_all_actable_questions(question)
unless versioned_question?(question)
@all_actable_questions = [question.actable]
return
end

question = question.actable
@all_actable_questions = [question]
while question.parent
@all_actable_questions << question.parent
question = question.parent

Check warning on line 53 in app/controllers/course/statistics/answers_controller.rb

View check run for this annotation

Codecov / codecov/patch

app/controllers/course/statistics/answers_controller.rb#L49-L53

Added lines #L49 - L53 were not covered by tests
end
end

# at the moment, we only support versioning for Programming Question. In the future, we might extend the support
# for all auto-gradable questions, such as MCQ/MRQ.
def versioned_question?(question)
question.actable.is_a?(Course::Assessment::Question::Programming)
end
end
16 changes: 16 additions & 0 deletions app/helpers/course/statistics/answers_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Course::Statistics::AnswersHelper
def get_historical_auto_gradings(programming_auto_grading)
historical = []
current = programming_auto_grading

while current&.parent_id
parent = Course::Assessment::Answer::ProgrammingAutoGrading.find_by(id: current.parent_id)
historical.unshift(parent) if parent
current = parent

Check warning on line 11 in app/helpers/course/statistics/answers_helper.rb

View check run for this annotation

Codecov / codecov/patch

app/helpers/course/statistics/answers_helper.rb#L9-L11

Added lines #L9 - L11 were not covered by tests
end

historical
end
end
3 changes: 3 additions & 0 deletions app/models/course/assessment/answer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ class Course::Assessment::Answer < ApplicationRecord
def auto_grade!(redirect_to_path: nil, reduce_priority: false)
raise IllegalStateError if attempting?

# When evaluated or graded, only autograde most recent answer
return if !submitted? && !current_answer?

ensure_auto_grading!
if grade_inline?
Course::Assessment::Answer::AutoGradingService.grade(self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Course::Assessment::Answer::ProgrammingAutoGrading < ApplicationRecord

validates :exit_code, numericality: { only_integer: true }, allow_nil: true

belongs_to :parent, class_name: 'Course::Assessment::Answer::ProgrammingAutoGrading', optional: true

has_one :programming_answer, through: :answer,
source: :actable,
source_type: 'Course::Assessment::Answer::Programming'
Expand Down
1 change: 1 addition & 0 deletions app/models/course/assessment/question/programming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Course::Assessment::Question::Programming < ApplicationRecord # rubocop:di

belongs_to :import_job, class_name: 'TrackableJob::Job', inverse_of: nil, optional: true
belongs_to :language, class_name: 'Coursemology::Polyglot::Language', inverse_of: nil
belongs_to :parent, class_name: 'Course::Assessment::Question::Programming', optional: true
has_one_attachment
has_many :template_files, class_name: 'Course::Assessment::Question::ProgrammingTemplateFile',
dependent: :destroy, foreign_key: :question_id, inverse_of: :question
Expand Down
3 changes: 3 additions & 0 deletions app/services/course/assessment/answer/auto_grading_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ class << self
#
# @param [Course::Assessment::Answer] answer The answer to be graded.
def grade(answer)
old_auto_grading_actable = answer.auto_grading&.actable
answer = if answer.question.auto_gradable?
pick_grader(answer.question).grade(answer)
else
assign_maximum_grade(answer)
end

answer.save!
answer.auto_grading.actable.update!(parent_id: old_auto_grading_actable.id) if old_auto_grading_actable
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
json.language question.language.name
json.editorMode question.language.ace_mode
json.fileSubmission question.multiple_file_submission

json.memoryLimit question.memory_limit if question.memory_limit
json.timeLimit question.time_limit if question.time_limit
json.attemptLimit question.attempt_limit if question.attempt_limit

json.fileSubmission question.multiple_file_submission?

json.autogradable question.auto_gradable?
json.isCodaveri question.is_codaveri
json.liveFeedbackEnabled question.live_feedback_enabled

json.isCodaveri question.is_codaveri?
json.liveFeedbackEnabled question.live_feedback_enabled?
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# frozen_string_literal: true
json.answers answers do |answer|
json.partial! answer, answer: answer
json.partial! 'course/statistics/answers/answer', answer: answer, question: question
end
11 changes: 11 additions & 0 deletions app/views/course/statistics/answers/_all_questions.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

json.allQuestions @all_actable_questions do |question|
json.id question.id
json.title question.title
json.maximumGrade question.maximum_grade
json.description format_ckeditor_rich_text(question.description)
json.type question.question_type

json.partial! question, question: question, can_grade: false, answer: @all_answers.first
end
27 changes: 27 additions & 0 deletions app/views/course/statistics/answers/_answer.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true
specific_answer = answer.specific

json.id answer.id
json.grade answer.grade
json.questionType question.question_type

if answer.actable_type == Course::Assessment::Answer::Programming.name
json.partial! 'course/statistics/answers/programming_answer', answer: specific_answer, can_grade: false
else
json.partial! specific_answer, answer: specific_answer, can_grade: false
end

if answer.actable_type == Course::Assessment::Answer::Programming.name
files = answer.specific.files
json.partial! 'course/assessment/answer/programming/annotations', programming_files: files,
can_grade: false
posts = files.flat_map(&:annotations).map(&:discussion_topic).flat_map(&:posts)

json.posts posts do |post|
json.partial! post, post: post if post.published?
end
end

json.submittedAt answer.submitted_at&.iso8601
json.currentAnswer answer.current_answer
json.workflowState answer.workflow_state
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true
question = @question.specific

# If a non current_answer is being loaded, use it instead of loading the last_attempt.
is_current_answer = answer.current_answer?
latest_answer = last_attempt(answer)
attempt = is_current_answer ? latest_answer : answer
auto_grading = attempt&.auto_grading&.specific

json.fields do
json.questionId answer.question_id
json.id answer.acting_as.id
json.files_attributes answer.files do |file|
json.(file, :id, :filename)
json.content file.content
json.highlightedContent highlight_code_block(file.content, question.language)
end
end

can_read_tests = can?(:read_tests, @submission)
show_private = can_read_tests || (@submission.published? && @assessment.show_private?)
show_evaluation = can_read_tests || (@submission.published? && @assessment.show_evaluation?)

test_cases_by_type = question.test_cases_by_type
test_cases_and_results = get_test_cases_and_results(test_cases_by_type, auto_grading)

show_stdout_and_stderr = (can_read_tests || current_course.show_stdout_and_stderr) &&
auto_grading && auto_grading&.exit_code != 0

displayed_test_case_types = ['public_test']
displayed_test_case_types << 'private_test' if show_private
displayed_test_case_types << 'evaluation_test' if show_evaluation

historical_auto_gradings = get_historical_auto_gradings(auto_grading)

json.testCases (historical_auto_gradings + [auto_grading]).each do |ag|
next if ag.nil? # To account for autogradings with no test results (programming with no autograding)

question = ag.test_results.first.test_case ? ag.test_results.first.test_case.question : @question.actable
test_cases_by_type = question.test_cases_by_type
test_cases_and_results = get_test_cases_and_results(test_cases_by_type, ag)

json.canReadTests can_read_tests
json.questionId question.id
displayed_test_case_types.each do |test_case_type|
show_public = (test_case_type == 'public_test') && current_course.show_public_test_cases_output
show_testcase_outputs = can_read_tests || show_public
json.set! test_case_type do
if test_cases_and_results[test_case_type].present?
json.array! test_cases_and_results[test_case_type] do |test_case, test_result|
json.identifier test_case.identifier if can_read_tests
json.expression test_case.expression
json.expected test_case.expected
if test_result
json.output get_output(test_result) if show_testcase_outputs
json.passed test_result.passed?
end
end

end
end
json.(ag, :stdout, :stderr) if show_stdout_and_stderr
end
end

if answer.codaveri_feedback_job_id && question.is_codaveri
codaveri_job = answer.codaveri_feedback_job
json.codaveriFeedback do
json.jobId answer.codaveri_feedback_job_id
json.jobStatus codaveri_job.status
json.jobUrl job_path(codaveri_job) if codaveri_job.status == 'submitted'
json.errorMessage codaveri_job.error['message'] if codaveri_job.error
end
end
7 changes: 3 additions & 4 deletions app/views/course/statistics/answers/all_answers.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true
json.partial! 'all_questions'

json.allAnswers @all_answers do |answer|
json.id answer.id
json.createdAt answer.created_at&.iso8601
json.currentAnswer answer.current_answer
json.workflowState answer.workflow_state
json.partial! 'answer', answer: answer, question: @question
end

posts = @submission_question.discussion_topic.posts
Expand Down
6 changes: 2 additions & 4 deletions client/app/api/course/Statistics/AnswerStatistics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QuestionType } from 'types/course/assessment/question';
import { AnswerStatisticsData } from 'types/course/statistics/assessmentStatistics';
import { Answer } from 'types/course/statistics/assessmentStatistics';

import { APIResponse } from 'api/types';

Expand All @@ -10,9 +10,7 @@ export default class AnswerStatisticsAPI extends BaseCourseAPI {
return `/courses/${this.courseId}/statistics/answers`;
}

fetch(
answerId: number,
): APIResponse<AnswerStatisticsData<keyof typeof QuestionType>> {
fetch(answerId: number): APIResponse<Answer<keyof typeof QuestionType>> {
return this.client.get(`${this.#urlPrefix}/${answerId}`);
}
}
4 changes: 2 additions & 2 deletions client/app/bundles/course/assessment/operations/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dispatch } from 'store';
import { QuestionType } from 'types/course/assessment/question';
import {
AncestorAssessmentStats,
AnswerStatisticsData,
Answer,
AssessmentLiveFeedbackStatistics,
SubmissionQuestionDetails,
} from 'types/course/statistics/assessmentStatistics';
Expand Down Expand Up @@ -57,7 +57,7 @@ export const fetchSubmissionQuestionDetails = async (

export const fetchAnswer = async (
answerId: number,
): Promise<AnswerStatisticsData<keyof typeof QuestionType>> => {
): Promise<Answer<keyof typeof QuestionType>> => {
const response = await CourseAPI.statistics.answer.fetch(answerId);

return response.data;
Expand Down
Loading