diff --git a/app/controllers/course/admin/assessment_settings_controller.rb b/app/controllers/course/admin/assessment_settings_controller.rb
index 2b9a8947188..30229c9e745 100644
--- a/app/controllers/course/admin/assessment_settings_controller.rb
+++ b/app/controllers/course/admin/assessment_settings_controller.rb
@@ -69,6 +69,8 @@ def category_params
# :allow_randomization,
:allow_mrq_options_randomization,
:programming_max_time_limit,
+ :rubric_grading_prompt_enabled,
+ :rubric_grading_prompt,
assessment_categories_attributes: [
:id,
:title,
diff --git a/app/models/concerns/course/assessment/submission/answers_concern.rb b/app/models/concerns/course/assessment/submission/answers_concern.rb
index 1e65b440d48..6aea3d6163b 100644
--- a/app/models/concerns/course/assessment/submission/answers_concern.rb
+++ b/app/models/concerns/course/assessment/submission/answers_concern.rb
@@ -52,9 +52,6 @@ def bulk_save_new_answer_actables(new_answers_group_by_actables)
ActiveRecord::Base.transaction do
new_answers_group_by_actables.each_key do |key|
key.constantize.import! new_answers_group_by_actables[key], recursive: true
- if key.constantize == Course::Assessment::Answer::RubricBasedResponse
- new_answers_group_by_actables[key].each(&:create_category_grade_instances)
- end
end
end
end
diff --git a/app/models/course.rb b/app/models/course.rb
index e428ef5d993..eb1b8ee0edd 100644
--- a/app/models/course.rb
+++ b/app/models/course.rb
@@ -262,6 +262,27 @@ def programming_max_time_limit=(time)
settings(:course_assessments_component).programming_max_time_limit = time
end
+ # Whether the course-wide grading prompt is applied during rubric AI grading. Independent of the prompt
+ # text so toggling off preserves the text for later re-enabling.
+ def rubric_grading_prompt_enabled
+ settings(:course_assessments_component).rubric_grading_prompt_enabled || false
+ end
+
+ def rubric_grading_prompt_enabled=(enabled)
+ settings(:course_assessments_component).rubric_grading_prompt_enabled =
+ ActiveRecord::Type::Boolean.new.cast(enabled)
+ end
+
+ # Course-wide prompt prepended before each question's own grading prompt during rubric AI grading, applied
+ # only when rubric_grading_prompt_enabled (see Course::Rubric::RubricAdapter#grading_prompt).
+ def rubric_grading_prompt
+ settings(:course_assessments_component).rubric_grading_prompt
+ end
+
+ def rubric_grading_prompt=(prompt)
+ settings(:course_assessments_component).rubric_grading_prompt = prompt.presence
+ end
+
def codaveri_feedback_workflow
settings(:course_codaveri_component).feedback_workflow
end
diff --git a/app/models/course/assessment/answer/rubric_based_response.rb b/app/models/course/assessment/answer/rubric_based_response.rb
index be8abf26f8a..dfe4074003f 100644
--- a/app/models/course/assessment/answer/rubric_based_response.rb
+++ b/app/models/course/assessment/answer/rubric_based_response.rb
@@ -8,11 +8,11 @@ class Course::Assessment::Answer::RubricBasedResponse < ApplicationRecord
# record, so they are stashed during #assign_params and persisted once the answer itself saves.
after_save :persist_grading_selections, if: -> { @pending_grading_selections.present? }
+ # Deprecated link to the v1 response selection model. Remains because v1 tables will not be removed.
+ # Replaced by an indirect link via Answer -> Rubric::AnswerEvaluation -> Rubric::AnswerEvaluation::Selection
has_many :selections, class_name: 'Course::Assessment::Answer::RubricBasedResponseSelection',
dependent: :destroy, foreign_key: :answer_id, inverse_of: :answer
- accepts_nested_attributes_for :selections, allow_destroy: true
-
# Specific implementation of Course::Assessment::Answer#reset_answer
def reset_answer
self.answer_text = question.actable.template_text || ''
@@ -86,23 +86,6 @@ def ensure_grading_evaluation!
end
end
- def create_category_grade_instances
- answer.class.transaction do
- new_category_selections = question.specific.categories.map do |category|
- {
- answer_id: id,
- category_id: category.id,
- criterion_id: nil,
- grade: nil,
- explanation: nil
- }
- end
-
- selections = Course::Assessment::Answer::RubricBasedResponseSelection.insert_all(new_category_selections)
- raise ActiveRecord::Rollback if !new_category_selections.empty? && (selections.nil? || selections.rows.empty?)
- end
- end
-
private
def set_default
diff --git a/app/models/course/assessment/question.rb b/app/models/course/assessment/question.rb
index 377cd70b71c..24502a77fae 100644
--- a/app/models/course/assessment/question.rb
+++ b/app/models/course/assessment/question.rb
@@ -201,8 +201,10 @@ def copy_attributes(other)
end
# Duplicates this question's grading contexts (see Course::Assessment::Question::GradingContext). Called from
- # each rubric-gradable actable's #initialize_duplicate with +other+ = the source actable being duplicated.
- # * As a CONSUMER: deep-copy the contexts this question pulls from onto the duplicate.
+ # the #initialize_duplicate of every type that can consume contexts (RBR, forum) OR be a sibling-answer
+ # source (text response), with +other+ = the source actable being duplicated.
+ # * As a CONSUMER: deep-copy the contexts this question pulls from onto the duplicate (no-op for source-only
+ # types, which have none).
# * As a SOURCE: re-point any already-duplicated contexts that reference this question, so a duplicate
# consumer pulls from the duplicate source. Together with GradingContext#initialize_duplicate, the source
# linkage is preserved regardless of which of the two questions is duplicated first.
diff --git a/app/models/course/assessment/question/forum_post_response.rb b/app/models/course/assessment/question/forum_post_response.rb
index 66dcb9f6a5c..815ea261337 100644
--- a/app/models/course/assessment/question/forum_post_response.rb
+++ b/app/models/course/assessment/question/forum_post_response.rb
@@ -59,6 +59,12 @@ def attempt(submission, last_attempt = nil)
def initialize_duplicate(duplicator, other)
copy_attributes(other)
+
+ # active_rubric lives on the polymorphic question; the dup'd acting_as carries the source's
+ # active_rubric_id over, so replace it with a duplicate of the source rubric (re-homed to the destination
+ # course by Course::Rubric#initialize_duplicate) instead of sharing the source's. Only rubric-mode forum
+ # questions have one; a default-mode question leaves it nil.
+ self.active_rubric = duplicator.duplicate(other.active_rubric) if other.active_rubric
initialize_grading_context_duplicates(duplicator, other)
end
diff --git a/app/models/course/assessment/question/text_response.rb b/app/models/course/assessment/question/text_response.rb
index be369b00aea..9e79611460d 100644
--- a/app/models/course/assessment/question/text_response.rb
+++ b/app/models/course/assessment/question/text_response.rb
@@ -146,6 +146,10 @@ def initialize_duplicate(duplicator, other)
else
self.solutions = duplicator.duplicate(other.solutions)
end
+
+ # A text response is never a context CONSUMER, but it can be a sibling-answer SOURCE, so it still needs
+ # the source fix-up pass when duplicated (see Course::Assessment::Question#initialize_grading_context_duplicates).
+ initialize_grading_context_duplicates(duplicator, other)
end
def build_at_least_one_group_one_point
diff --git a/app/models/course/rubric/rubric_adapter.rb b/app/models/course/rubric/rubric_adapter.rb
index 7526e2fdfd9..2ab91eba7f4 100644
--- a/app/models/course/rubric/rubric_adapter.rb
+++ b/app/models/course/rubric/rubric_adapter.rb
@@ -19,8 +19,11 @@ def formatted_rubric_categories
end.join("\n\n")
end
+ # The course-wide grading prompt (set in the assessment settings page) is prepended before the question's
+ # own grading prompt when enabled and present, so shared grading guidance applies to every rubric-graded
+ # question in the course. Either part may be blank; the separator only appears when both are present.
def grading_prompt
- @rubric.grading_prompt
+ [course_grading_prompt, @rubric.grading_prompt.presence].compact.join("\n\n")
end
def model_answer
@@ -62,4 +65,13 @@ def build_category_schema(category, field_name)
'description' => "Selected criterion and explanation for #{field_name} #{category.name}"
}
end
+
+ private
+
+ def course_grading_prompt
+ course = @rubric.course
+ return nil unless course.rubric_grading_prompt_enabled
+
+ course.rubric_grading_prompt.presence
+ end
end
diff --git a/app/views/course/admin/assessment_settings/edit.json.jbuilder b/app/views/course/admin/assessment_settings/edit.json.jbuilder
index ee89c76d0f5..a4ab1558686 100644
--- a/app/views/course/admin/assessment_settings/edit.json.jbuilder
+++ b/app/views/course/admin/assessment_settings/edit.json.jbuilder
@@ -6,6 +6,8 @@ json.showStdoutAndStderr current_course.show_stdout_and_stderr || false
json.allowRandomization current_course.allow_randomization || false
json.allowMrqOptionsRandomization current_course.allow_mrq_options_randomization || false
json.maxProgrammingTimeLimit current_course.programming_max_time_limit if can?(:manage, :all)
+json.rubricGradingPromptEnabled current_course.rubric_grading_prompt_enabled
+json.rubricGradingPrompt current_course.rubric_grading_prompt || ''
json.canCreateCategories can?(:create, Course::Assessment::Category.new(course: current_course))
diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentSettingsForm.tsx b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentSettingsForm.tsx
index 7394731f87a..f1451855227 100644
--- a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentSettingsForm.tsx
+++ b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentSettingsForm.tsx
@@ -31,6 +31,13 @@ const AssessmentsSettingsForm = forwardRef<
.nullable()
.typeError(t(translations.maxTimeLimitRequired))
.min(1, t(translations.positiveMaxTimeLimitRequired)),
+ rubricGradingPrompt: yup.string().when('rubricGradingPromptEnabled', {
+ is: true,
+ then: yup
+ .string()
+ .trim()
+ .required(t(translations.rubricGradingPromptRequired)),
+ }),
});
return (
@@ -42,7 +49,7 @@ const AssessmentsSettingsForm = forwardRef<
onSubmit={props.onSubmit}
validates={validationSchema}
>
- {(control): JSX.Element => (
+ {(control, watch): JSX.Element => (
<>
{/* Randomized Assessment is temporarily hidden (PR#5406) */}
@@ -137,6 +144,49 @@ const AssessmentsSettingsForm = forwardRef<
)}
+
+ (
+
+ )}
+ />
+
+
+ {t(translations.rubricGradingPromptHint)}
+
+
+ (
+
+ )}
+ />
+
+
({
id: category.id,
title: category.title,
diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/translations.ts b/client/app/bundles/course/admin/pages/AssessmentSettings/translations.ts
index bcab1d77a72..d5f57ae555a 100644
--- a/client/app/bundles/course/admin/pages/AssessmentSettings/translations.ts
+++ b/client/app/bundles/course/admin/pages/AssessmentSettings/translations.ts
@@ -58,6 +58,24 @@ export default defineMessages({
id: 'course.admin.AssessmentSettings.enableMcqChoicesRandomisations',
defaultMessage: 'Randomise MCQ choices',
},
+ rubricGrading: {
+ id: 'course.admin.AssessmentSettings.rubricGrading',
+ defaultMessage: 'AI Rubric Grading',
+ },
+ useRubricGradingPrompt: {
+ id: 'course.admin.AssessmentSettings.useRubricGradingPrompt',
+ defaultMessage: 'Use course-wide grading prompt',
+ },
+ rubricGradingPromptHint: {
+ id: 'course.admin.AssessmentSettings.rubricGradingPromptHint',
+ defaultMessage:
+ "When a question with a grading rubric is autograded with AI, these instructions will be inserted before the question's specific grading prompt.",
+ },
+ rubricGradingPromptRequired: {
+ id: 'course.admin.AssessmentSettings.rubricGradingPromptRequired',
+ defaultMessage:
+ 'Please enter a grading prompt, or disable the course-wide grading prompt.',
+ },
deleteCategoryPromptAction: {
id: 'course.admin.AssessmentSettings.deleteCategoryPromptAction',
defaultMessage: 'Delete {title} category',
diff --git a/client/app/types/course/admin/assessments.ts b/client/app/types/course/admin/assessments.ts
index 61655d43484..ba7c35e8b44 100644
--- a/client/app/types/course/admin/assessments.ts
+++ b/client/app/types/course/admin/assessments.ts
@@ -6,6 +6,8 @@ export interface AssessmentSettingsData {
categories: AssessmentCategory[];
canCreateCategories: boolean;
maxProgrammingTimeLimit?: number;
+ rubricGradingPromptEnabled: boolean;
+ rubricGradingPrompt?: string;
}
export interface AssessmentCategory {
@@ -62,6 +64,8 @@ export interface AssessmentSettingsPostData {
allow_randomization?: AssessmentSettingsData['allowRandomization'];
allow_mrq_options_randomization?: AssessmentSettingsData['allowMrqOptionsRandomization'];
programming_max_time_limit: AssessmentSettingsData['maxProgrammingTimeLimit'];
+ rubric_grading_prompt_enabled?: AssessmentSettingsData['rubricGradingPromptEnabled'];
+ rubric_grading_prompt?: AssessmentSettingsData['rubricGradingPrompt'];
assessment_categories_attributes?: {
id: AssessmentCategory['id'];
title: AssessmentCategory['title'];
diff --git a/client/locales/en.json b/client/locales/en.json
index aa30c20c106..eae07bb46bf 100644
--- a/client/locales/en.json
+++ b/client/locales/en.json
@@ -500,6 +500,15 @@
"course.admin.AssessmentSettings.programmingQuestionSettings": {
"defaultMessage": "Programming Question settings"
},
+ "course.admin.AssessmentSettings.rubricGrading": {
+ "defaultMessage": "AI Rubric Grading"
+ },
+ "course.admin.AssessmentSettings.rubricGradingPromptHint": {
+ "defaultMessage": "When a question with a grading rubric is autograded with AI, these instructions will be inserted before the question's specific grading prompt."
+ },
+ "course.admin.AssessmentSettings.rubricGradingPromptRequired": {
+ "defaultMessage": "Please enter a grading prompt, or disable the course-wide grading prompt."
+ },
"course.admin.AssessmentSettings.seconds": {
"defaultMessage": "s"
},
@@ -515,6 +524,9 @@
"course.admin.AssessmentSettings.toTab": {
"defaultMessage": "to {tab}"
},
+ "course.admin.AssessmentSettings.useRubricGradingPrompt": {
+ "defaultMessage": "Use course-wide grading prompt"
+ },
"course.admin.CodaveriSettings.codaveriModel": {
"defaultMessage": "Model"
},
diff --git a/client/locales/ko.json b/client/locales/ko.json
index 10e11ce6b7b..67f76356040 100644
--- a/client/locales/ko.json
+++ b/client/locales/ko.json
@@ -500,6 +500,15 @@
"course.admin.AssessmentSettings.programmingQuestionSettings": {
"defaultMessage": "프로그래밍 질문 설정"
},
+ "course.admin.AssessmentSettings.rubricGrading": {
+ "defaultMessage": "AI 루브릭 채점"
+ },
+ "course.admin.AssessmentSettings.rubricGradingPromptHint": {
+ "defaultMessage": "채점 루브릭이 있는 문제를 AI로 자동 채점할 때, 이 지침이 해당 문제의 개별 채점 프롬프트 앞에 삽입됩니다."
+ },
+ "course.admin.AssessmentSettings.rubricGradingPromptRequired": {
+ "defaultMessage": "채점 프롬프트를 입력하거나 코스 전체 채점 프롬프트를 비활성화하세요."
+ },
"course.admin.AssessmentSettings.seconds": {
"defaultMessage": "초"
},
@@ -515,6 +524,9 @@
"course.admin.AssessmentSettings.toTab": {
"defaultMessage": "{tab}로"
},
+ "course.admin.AssessmentSettings.useRubricGradingPrompt": {
+ "defaultMessage": "코스 전체 채점 프롬프트 사용"
+ },
"course.admin.CodaveriSettings.Some": {
"defaultMessage": "일부"
},
diff --git a/client/locales/zh.json b/client/locales/zh.json
index 7649ff1bab5..f6fd093c667 100644
--- a/client/locales/zh.json
+++ b/client/locales/zh.json
@@ -500,6 +500,15 @@
"course.admin.AssessmentSettings.programmingQuestionSettings": {
"defaultMessage": "编程题设置"
},
+ "course.admin.AssessmentSettings.rubricGrading": {
+ "defaultMessage": "AI 评分标准评分"
+ },
+ "course.admin.AssessmentSettings.rubricGradingPromptHint": {
+ "defaultMessage": "当带有评分标准的题目由 AI 自动评分时,这些说明将被插入到该题目专属评分提示之前。"
+ },
+ "course.admin.AssessmentSettings.rubricGradingPromptRequired": {
+ "defaultMessage": "请输入评分提示,或停用课程级评分提示。"
+ },
"course.admin.AssessmentSettings.seconds": {
"defaultMessage": "秒"
},
@@ -515,6 +524,9 @@
"course.admin.AssessmentSettings.toTab": {
"defaultMessage": "到 {tab}"
},
+ "course.admin.AssessmentSettings.useRubricGradingPrompt": {
+ "defaultMessage": "使用课程级评分提示"
+ },
"course.admin.CodaveriSettings.codaveriModel": {
"defaultMessage": "模型"
},
diff --git a/spec/controllers/course/admin/assessment_settings_controller_spec.rb b/spec/controllers/course/admin/assessment_settings_controller_spec.rb
index 66a10223739..3f86eb9a48b 100644
--- a/spec/controllers/course/admin/assessment_settings_controller_spec.rb
+++ b/spec/controllers/course/admin/assessment_settings_controller_spec.rb
@@ -28,6 +28,21 @@
end
end
+ describe '#update persisting the course-wide rubric grading prompt' do
+ subject do
+ patch :update, as: :json, params: {
+ course_id: course,
+ course: { rubric_grading_prompt_enabled: true, rubric_grading_prompt: 'Grade generously' }
+ }
+ end
+
+ it 'stores the prompt and enable flag on the course settings' do
+ expect(subject).to render_template(:edit)
+ expect(course.reload.rubric_grading_prompt).to eq('Grade generously')
+ expect(course.reload.rubric_grading_prompt_enabled).to be(true)
+ end
+ end
+
describe 'moving actions' do
let!(:category1) { create(:course_assessment_category, course: course) }
let!(:category2) { create(:course_assessment_category, course: course) }
diff --git a/spec/factories/course_assessment_answer_rubric_based_response.rb b/spec/factories/course_assessment_answer_rubric_based_response.rb
index 72995018f26..ce9d5de0eac 100644
--- a/spec/factories/course_assessment_answer_rubric_based_response.rb
+++ b/spec/factories/course_assessment_answer_rubric_based_response.rb
@@ -12,17 +12,5 @@
assessment: assessment).question
end
answer_text { 'This is a sample response to the rubric question.' }
-
- trait :with_selections do
- after(:build) do |answer, _evaluator|
- question = answer.question.specific
- question.categories.each do |category|
- selection = build(:course_assessment_answer_rubric_based_response_selection,
- answer: answer,
- category: category)
- answer.selections << selection
- end
- end
- end
end
end
diff --git a/spec/factories/course_assessment_answer_rubric_based_response_selection.rb b/spec/factories/course_assessment_answer_rubric_based_response_selection.rb
deleted file mode 100644
index 3ccde5c3185..00000000000
--- a/spec/factories/course_assessment_answer_rubric_based_response_selection.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-FactoryBot.define do
- factory :course_assessment_answer_rubric_based_response_selection,
- class: Course::Assessment::Answer::RubricBasedResponseSelection do
- association :answer, factory: :course_assessment_answer_rubric_based_response
-
- transient do
- question { answer.question.specific }
- end
-
- category do
- question.categories.first
- end
-
- criterion { nil }
- grade { nil }
- explanation { nil }
- end
-end
diff --git a/spec/models/course/assessment/question/forum_post_response_spec.rb b/spec/models/course/assessment/question/forum_post_response_spec.rb
index d33239aeadf..6f3ea289ae9 100644
--- a/spec/models/course/assessment/question/forum_post_response_spec.rb
+++ b/spec/models/course/assessment/question/forum_post_response_spec.rb
@@ -59,6 +59,29 @@
end
end
+ describe 'duplication of active_rubric' do
+ let(:course) { create(:course) }
+ let(:assessment) { create(:assessment, course: course) }
+ let(:question) do
+ create(:course_assessment_question_forum_post_response, assessment: assessment)
+ end
+
+ before do
+ rubric = create(:course_rubric, course: course, questions: [question.acting_as])
+ question.acting_as.update_columns(grading_mode: 'rubric', active_rubric_id: rubric.id)
+ end
+
+ subject(:duplicate) do
+ Duplicator.new([], destination_course: course, current_course: course).duplicate(question)
+ end
+
+ it 'gives the duplicate its own rubric of identical content rather than sharing the source rubric' do
+ expect(duplicate.active_rubric).to be_present
+ expect(duplicate.active_rubric).not_to eq(question.active_rubric)
+ expect(duplicate.active_rubric.canonical_content_hash).to eq(question.active_rubric.content_hash)
+ end
+ end
+
describe 'validations' do
subject { build(:course_assessment_question_forum_post_response) }
diff --git a/spec/models/course/assessment/question/grading_context_spec.rb b/spec/models/course/assessment/question/grading_context_spec.rb
index 1d893223b65..4d31968aebf 100644
--- a/spec/models/course/assessment/question/grading_context_spec.rb
+++ b/spec/models/course/assessment/question/grading_context_spec.rb
@@ -70,31 +70,36 @@ def duplicate(objects)
end
end
- context 'with a sibling_question_answer context' do
- let(:source_question) do
- create(:course_assessment_question_forum_post_response, assessment: assessment)
- end
-
- before do
- described_class.create!(
- question: consumer.acting_as, context_type: 'sibling_question_answer',
- source: source_question.acting_as, identifier: 'sibling'
- )
- end
+ # A sibling source can be any question that provides_grading_context? (a rubric-graded forum question, or a
+ # plain text response that is only a source), so the linkage must survive duplication for both kinds.
+ {
+ 'a forum question' => :course_assessment_question_forum_post_response,
+ 'a text response' => :course_assessment_question_text_response
+ }.each do |source_desc, source_factory|
+ context "with a sibling_question_answer context sourced from #{source_desc}" do
+ let(:source_question) { create(source_factory, assessment: assessment) }
+
+ before do
+ described_class.create!(
+ question: consumer.acting_as, context_type: 'sibling_question_answer',
+ source: source_question.acting_as, identifier: 'sibling'
+ )
+ end
- # The Duplicator imposes no order on the two co-duplicated questions, so the source linkage must hold
- # whichever is processed first.
- [%i[source_question consumer], %i[consumer source_question]].each do |order|
- it "re-points the duplicate context at the duplicate source (order: #{order.join(' then ')})" do
- originals = order.map { |name| send(name) }
- dup_by_original = originals.zip(duplicate(originals)).to_h
- dup_consumer = dup_by_original[consumer]
- dup_source = dup_by_original[source_question]
-
- context = dup_consumer.grading_contexts.find { |c| c.context_type == 'sibling_question_answer' }
- expect(context.identifier).to eq('sibling')
- expect(context.source).to eq(dup_source.acting_as)
- expect(context.source).not_to eq(source_question.acting_as)
+ # The Duplicator imposes no order on the two co-duplicated questions, so the source linkage must hold
+ # whichever is processed first.
+ [%i[source_question consumer], %i[consumer source_question]].each do |order|
+ it "re-points the duplicate context at the duplicate source (order: #{order.join(' then ')})" do
+ originals = order.map { |name| send(name) }
+ dup_by_original = originals.zip(duplicate(originals)).to_h
+ dup_consumer = dup_by_original[consumer]
+ dup_source = dup_by_original[source_question]
+
+ context = dup_consumer.grading_contexts.find { |c| c.context_type == 'sibling_question_answer' }
+ expect(context.identifier).to eq('sibling')
+ expect(context.source).to eq(dup_source.acting_as)
+ expect(context.source).not_to eq(source_question.acting_as)
+ end
end
end
end
diff --git a/spec/models/course/rubric/rubric_adapter_spec.rb b/spec/models/course/rubric/rubric_adapter_spec.rb
new file mode 100644
index 00000000000..c082c85d6bc
--- /dev/null
+++ b/spec/models/course/rubric/rubric_adapter_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+RSpec.describe Course::Rubric::RubricAdapter do
+ let(:instance) { Instance.default }
+ with_tenant(:instance) do
+ let(:course) { create(:course) }
+ let(:rubric) { create(:course_rubric, course: course, grading_prompt: 'Question guidance') }
+
+ subject(:grading_prompt) { described_class.new(rubric).grading_prompt }
+
+ context 'when the course-wide grading prompt is disabled' do
+ before { course.update!(rubric_grading_prompt: 'Course-wide guidance', rubric_grading_prompt_enabled: false) }
+
+ it 'returns just the question grading prompt' do
+ expect(grading_prompt).to eq('Question guidance')
+ end
+ end
+
+ context 'when the course-wide grading prompt is enabled' do
+ before { course.update!(rubric_grading_prompt: 'Course-wide guidance', rubric_grading_prompt_enabled: true) }
+
+ it 'prepends the course prompt before the question prompt' do
+ expect(grading_prompt).to eq("Course-wide guidance\n\nQuestion guidance")
+ end
+
+ context 'and the question grading prompt is blank' do
+ let(:rubric) { create(:course_rubric, course: course, grading_prompt: '') }
+
+ it 'returns just the course prompt, without a dangling separator' do
+ expect(grading_prompt).to eq('Course-wide guidance')
+ end
+ end
+
+ context 'but the course prompt itself is blank' do
+ before { course.update!(rubric_grading_prompt: '') }
+
+ it 'returns just the question grading prompt' do
+ expect(grading_prompt).to eq('Question guidance')
+ end
+ end
+ end
+ end
+end