<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Zobrook2</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Zobrook2"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Zobrook2"/>
	<updated>2026-06-28T22:34:06Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168201</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168201"/>
		<updated>2026-05-04T19:42:08Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
*'''Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]'''&lt;br /&gt;
&lt;br /&gt;
*'''QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]'''&lt;br /&gt;
&lt;br /&gt;
*'''Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Question Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questions_table]'''&lt;br /&gt;
&lt;br /&gt;
Questions are referred to as &amp;quot;Items&amp;quot; in some of the test cases. While they aren't being directly covered by test cases, factories for the Question Model are needed so that the relationship between Questionnaire and QuestionAdvice are properly connected. The Questionnaire model has a one-many relationship with the Questions model, where a Questionnaire can have many Questions while each Question belongs to a single Questionnaire. The QuestionAdvice model has a one-one relationship with the Question model and a QuestionAdvice entry belongs to a single Question.&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====2. self.copy_questionnaire_details====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====3. check_for_question_associations====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====4. get_weighted_score====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====5. compute_weighted_score====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====6. true_false_items?====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====7. max_possible_score====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
====1. self.export_fields====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====2. self.export====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====3. self.to_json_by_question_id====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
====1. path====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====2. add_ta====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====3. remove_ta====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====4. copy_course====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Design ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use factories to load data into database to test that it is being saved and exported properly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/factories/question_advice.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
    factory :question_advice do&lt;br /&gt;
        score {5}&lt;br /&gt;
        association :item&lt;br /&gt;
        advice {'default advice'}&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then, run the test suite using the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/questionnaire_spec.rb spec/models/course_spec.rb spec/models/question_advice_spec.rb &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
&lt;br /&gt;
=== Coverage ===&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Bugs Fixed ===&lt;br /&gt;
&lt;br /&gt;
==== Handling Nil User in add_ta Method (Course Model) ====&lt;br /&gt;
A bug was identified in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model where the &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt; method would crash when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user was passed during unit testing. The issue occurred because the method attempted to access &amp;lt;code&amp;gt;user.id&amp;lt;/code&amp;gt; without first verifying that the user object was present.&lt;br /&gt;
&lt;br /&gt;
To prevent this runtime error, the method was updated to safely handle &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; inputs using safe navigation (&amp;lt;code&amp;gt;&amp;amp;amp;.id&amp;lt;/code&amp;gt;) and an early return pattern.&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Add a Teaching Assistant to the course&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user&amp;amp;.id} does not exist&amp;quot; }&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
  else&lt;br /&gt;
    # remaining logic...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the method no longer raises exceptions when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user is provided and instead returns a controlled error response, improving robustness and test reliability.&lt;br /&gt;
&lt;br /&gt;
==== Corrected Course–User Association Mapping ====&lt;br /&gt;
A bug in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model was fixed where the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association incorrectly referenced a non-existent relationship (&amp;lt;code&amp;gt;course_participants&amp;lt;/code&amp;gt;). The model already defines the correct join association as &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, so the additional reference caused confusion and potential runtime issues.&lt;br /&gt;
&lt;br /&gt;
The association structure correctly uses &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; should be derived from this relationship.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Correct association structure:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :participants&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association correctly resolves through the existing &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt; relationship, maintaining consistency in the model and preventing invalid association lookups.&lt;br /&gt;
&lt;br /&gt;
==== Correct Foreign Key Mapping for QuestionAdvice Association ====&lt;br /&gt;
A fix was applied to the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model to ensure the association correctly references the underlying database schema.&lt;br /&gt;
&lt;br /&gt;
Previously, Rails was implicitly expecting a default foreign key (&amp;lt;code&amp;gt;item_id&amp;lt;/code&amp;gt;), which did not match the actual schema. The &amp;lt;code&amp;gt;question_advice&amp;lt;/code&amp;gt; table stores the foreign key as &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, meaning the association must explicitly specify this key to function correctly.&lt;br /&gt;
&lt;br /&gt;
To resolve this mismatch, the association was updated as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class QuestionAdvice &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :item, foreign_key: :question_id&lt;br /&gt;
  # remaining logic...&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This change ensures that Rails correctly maps each &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; record to its associated &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, restoring proper association behavior and preventing incorrect lookups using the default key.&lt;br /&gt;
&lt;br /&gt;
==== Fixing Invalid ID Reference in remove_ta Method ====&lt;br /&gt;
A bug was discovered in the remove_ta implementation within the Course model where the method was failing to locate existing TaMapping records. The issue was caused by passing the symbol :id as a parameter instead of the actual local id variable.&lt;br /&gt;
&lt;br /&gt;
Because ActiveRecord treats symbols in query hashes literally, it attempted to find a record where the course_id was the literal string/symbol &amp;quot;id&amp;quot;, rather than the integer ID of the course instance. This bug was previously masked in the test suite by heavy mocking of find_by and where. By removing the mocks and using actual database records in the test refactor, the failure was exposed.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This update ensures the query correctly references the course's unique identifier, allowing remove_ta to successfully locate and remove the mapping and subsequently update the user's role.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168167</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168167"/>
		<updated>2026-04-28T22:30:20Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Running Tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
*'''Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]'''&lt;br /&gt;
&lt;br /&gt;
*'''QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]'''&lt;br /&gt;
&lt;br /&gt;
*'''Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Question Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questions_table]'''&lt;br /&gt;
&lt;br /&gt;
Questions are referred to as &amp;quot;Items&amp;quot; in some of the test cases. While they aren't being directly covered by test cases, factories for the Question Model are needed so that the relationship between Questionnaire and QuestionAdvice are properly connected. The Questionnaire model has a one-many relationship with the Questions model, where a Questionnaire can have many Questions while each Question belongs to a single Questionnaire. The QuestionAdvice model has a one-one relationship with the Question model and a QuestionAdvice entry belongs to a single Question.&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Design ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use factories to load data into database to test that it is being saved and exported properly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/question_advice.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
    factory :question_advice do&lt;br /&gt;
        score {5}&lt;br /&gt;
        association :item&lt;br /&gt;
        advice {'default advice'}&lt;br /&gt;
    end&lt;br /&gt;
end&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then, run the test suite using the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/questionnaire_spec.rb spec/models/course_spec.rb spec/models/question_advice_spec.rb &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
&lt;br /&gt;
=== Coverage ===&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Bugs Fixed ===&lt;br /&gt;
&lt;br /&gt;
==== Handling Nil User in add_ta Method (Course Model) ====&lt;br /&gt;
A bug was identified in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model where the &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt; method would crash when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user was passed during unit testing. The issue occurred because the method attempted to access &amp;lt;code&amp;gt;user.id&amp;lt;/code&amp;gt; without first verifying that the user object was present.&lt;br /&gt;
&lt;br /&gt;
To prevent this runtime error, the method was updated to safely handle &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; inputs using safe navigation (&amp;lt;code&amp;gt;&amp;amp;amp;.id&amp;lt;/code&amp;gt;) and an early return pattern.&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# Add a Teaching Assistant to the course&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user&amp;amp;.id} does not exist&amp;quot; }&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
  else&lt;br /&gt;
    # remaining logic...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the method no longer raises exceptions when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user is provided and instead returns a controlled error response, improving robustness and test reliability.&lt;br /&gt;
&lt;br /&gt;
==== Corrected Course–User Association Mapping ====&lt;br /&gt;
A bug in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model was fixed where the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association incorrectly referenced a non-existent relationship (&amp;lt;code&amp;gt;course_participants&amp;lt;/code&amp;gt;). The model already defines the correct join association as &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, so the additional reference caused confusion and potential runtime issues.&lt;br /&gt;
&lt;br /&gt;
The association structure correctly uses &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; should be derived from this relationship.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Correct association structure:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :participants&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association correctly resolves through the existing &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt; relationship, maintaining consistency in the model and preventing invalid association lookups.&lt;br /&gt;
&lt;br /&gt;
==== Correct Foreign Key Mapping for QuestionAdvice Association ====&lt;br /&gt;
A fix was applied to the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model to ensure the association correctly references the underlying database schema.&lt;br /&gt;
&lt;br /&gt;
Previously, Rails was implicitly expecting a default foreign key (&amp;lt;code&amp;gt;item_id&amp;lt;/code&amp;gt;), which did not match the actual schema. The &amp;lt;code&amp;gt;question_advice&amp;lt;/code&amp;gt; table stores the foreign key as &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, meaning the association must explicitly specify this key to function correctly.&lt;br /&gt;
&lt;br /&gt;
To resolve this mismatch, the association was updated as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class QuestionAdvice &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :item, foreign_key: :question_id&lt;br /&gt;
  # remaining logic...&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This change ensures that Rails correctly maps each &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; record to its associated &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, restoring proper association behavior and preventing incorrect lookups using the default key.&lt;br /&gt;
&lt;br /&gt;
==== Fixing Invalid ID Reference in remove_ta Method ====&lt;br /&gt;
A bug was discovered in the remove_ta implementation within the Course model where the method was failing to locate existing TaMapping records. The issue was caused by passing the symbol :id as a parameter instead of the actual local id variable.&lt;br /&gt;
&lt;br /&gt;
Because ActiveRecord treats symbols in query hashes literally, it attempted to find a record where the course_id was the literal string/symbol &amp;quot;id&amp;quot;, rather than the integer ID of the course instance. This bug was previously masked in the test suite by heavy mocking of find_by and where. By removing the mocks and using actual database records in the test refactor, the failure was exposed.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: id)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This update ensures the query correctly references the course's unique identifier, allowing remove_ta to successfully locate and remove the mapping and subsequently update the user's role.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168104</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168104"/>
		<updated>2026-04-27T03:59:00Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Bugs Fixed */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
*'''Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]'''&lt;br /&gt;
&lt;br /&gt;
*'''QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]'''&lt;br /&gt;
&lt;br /&gt;
*'''Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Question Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questions_table]'''&lt;br /&gt;
&lt;br /&gt;
Questions are referred to as &amp;quot;Items&amp;quot; in some of the test cases. While they aren't being directly covered by test cases, factories for the Question Model are needed so that the relationship between Questionnaire and QuestionAdvice are properly connected. The Questionnaire model has a one-many relationship with the Questions model, where a Questionnaire can have many Questions while each Question belongs to a single Questionnaire. The QuestionAdvice model has a one-one relationship with the Question model and a QuestionAdvice entry belongs to a single Question.&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Design ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
&lt;br /&gt;
=== Coverage ===&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Bugs Fixed ===&lt;br /&gt;
&lt;br /&gt;
==== Handling Nil User in add_ta Method (Course Model) ====&lt;br /&gt;
A bug was identified in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model where the &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt; method would crash when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user was passed during unit testing. The issue occurred because the method attempted to access &amp;lt;code&amp;gt;user.id&amp;lt;/code&amp;gt; without first verifying that the user object was present.&lt;br /&gt;
&lt;br /&gt;
To prevent this runtime error, the method was updated to safely handle &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; inputs using safe navigation (&amp;lt;code&amp;gt;&amp;amp;amp;.id&amp;lt;/code&amp;gt;) and an early return pattern.&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# Add a Teaching Assistant to the course&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user&amp;amp;.id} does not exist&amp;quot; }&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
  else&lt;br /&gt;
    # remaining logic...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the method no longer raises exceptions when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user is provided and instead returns a controlled error response, improving robustness and test reliability.&lt;br /&gt;
&lt;br /&gt;
==== Corrected Course–User Association Mapping ====&lt;br /&gt;
A bug in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model was fixed where the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association incorrectly referenced a non-existent relationship (&amp;lt;code&amp;gt;course_participants&amp;lt;/code&amp;gt;). The model already defines the correct join association as &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, so the additional reference caused confusion and potential runtime issues.&lt;br /&gt;
&lt;br /&gt;
The association structure correctly uses &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; should be derived from this relationship.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Correct association structure:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :participants&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association correctly resolves through the existing &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt; relationship, maintaining consistency in the model and preventing invalid association lookups.&lt;br /&gt;
&lt;br /&gt;
==== Correct Foreign Key Mapping for QuestionAdvice Association ====&lt;br /&gt;
A fix was applied to the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model to ensure the association correctly references the underlying database schema.&lt;br /&gt;
&lt;br /&gt;
Previously, Rails was implicitly expecting a default foreign key (&amp;lt;code&amp;gt;item_id&amp;lt;/code&amp;gt;), which did not match the actual schema. The &amp;lt;code&amp;gt;question_advice&amp;lt;/code&amp;gt; table stores the foreign key as &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, meaning the association must explicitly specify this key to function correctly.&lt;br /&gt;
&lt;br /&gt;
To resolve this mismatch, the association was updated as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class QuestionAdvice &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :item, foreign_key: :question_id&lt;br /&gt;
  # remaining logic...&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This change ensures that Rails correctly maps each &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; record to its associated &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, restoring proper association behavior and preventing incorrect lookups using the default key.&lt;br /&gt;
&lt;br /&gt;
==== Fixing Invalid ID Reference in remove_ta Method ====&lt;br /&gt;
A bug was discovered in the remove_ta implementation within the Course model where the method was failing to locate existing TaMapping records. The issue was caused by passing the symbol :id as a parameter instead of the actual local id variable.&lt;br /&gt;
&lt;br /&gt;
Because ActiveRecord treats symbols in query hashes literally, it attempted to find a record where the course_id was the literal string/symbol &amp;quot;id&amp;quot;, rather than the integer ID of the course instance. This bug was previously masked in the test suite by heavy mocking of find_by and where. By removing the mocks and using actual database records in the test refactor, the failure was exposed.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: id)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This update ensures the query correctly references the course's unique identifier, allowing remove_ta to successfully locate and remove the mapping and subsequently update the user's role.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168061</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168061"/>
		<updated>2026-04-24T16:39:38Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Issues in Initial Code */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Design ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
&lt;br /&gt;
=== Coverage ===&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Bugs Fixed ===&lt;br /&gt;
&lt;br /&gt;
==== Handling Nil User in add_ta Method (Course Model) ====&lt;br /&gt;
A bug was identified in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model where the &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt; method would crash when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user was passed during unit testing. The issue occurred because the method attempted to access &amp;lt;code&amp;gt;user.id&amp;lt;/code&amp;gt; without first verifying that the user object was present.&lt;br /&gt;
&lt;br /&gt;
To prevent this runtime error, the method was updated to safely handle &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; inputs using safe navigation (&amp;lt;code&amp;gt;&amp;amp;amp;.id&amp;lt;/code&amp;gt;) and an early return pattern.&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# Add a Teaching Assistant to the course&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user&amp;amp;.id} does not exist&amp;quot; }&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
  else&lt;br /&gt;
    # remaining logic...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the method no longer raises exceptions when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user is provided and instead returns a controlled error response, improving robustness and test reliability.&lt;br /&gt;
&lt;br /&gt;
==== Corrected Course–User Association Mapping ====&lt;br /&gt;
A bug in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model was fixed where the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association incorrectly referenced a non-existent relationship (&amp;lt;code&amp;gt;course_participants&amp;lt;/code&amp;gt;). The model already defines the correct join association as &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, so the additional reference caused confusion and potential runtime issues.&lt;br /&gt;
&lt;br /&gt;
The association structure correctly uses &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; should be derived from this relationship.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Correct association structure:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :participants&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association correctly resolves through the existing &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt; relationship, maintaining consistency in the model and preventing invalid association lookups.&lt;br /&gt;
&lt;br /&gt;
==== Correct Foreign Key Mapping for QuestionAdvice Association ====&lt;br /&gt;
A fix was applied to the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model to ensure the association correctly references the underlying database schema.&lt;br /&gt;
&lt;br /&gt;
Previously, Rails was implicitly expecting a default foreign key (&amp;lt;code&amp;gt;item_id&amp;lt;/code&amp;gt;), which did not match the actual schema. The &amp;lt;code&amp;gt;question_advice&amp;lt;/code&amp;gt; table stores the foreign key as &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, meaning the association must explicitly specify this key to function correctly.&lt;br /&gt;
&lt;br /&gt;
To resolve this mismatch, the association was updated as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class QuestionAdvice &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :item, foreign_key: :question_id&lt;br /&gt;
  # remaining logic...&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This change ensures that Rails correctly maps each &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; record to its associated &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, restoring proper association behavior and preventing incorrect lookups using the default key.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168059</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168059"/>
		<updated>2026-04-24T07:22:16Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Bugs Fixed */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Design ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
&lt;br /&gt;
=== Coverage ===&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Bugs Fixed ===&lt;br /&gt;
&lt;br /&gt;
==== Handling Nil User in add_ta Method (Course Model) ====&lt;br /&gt;
A bug was identified in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model where the &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt; method would crash when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user was passed during unit testing. The issue occurred because the method attempted to access &amp;lt;code&amp;gt;user.id&amp;lt;/code&amp;gt; without first verifying that the user object was present.&lt;br /&gt;
&lt;br /&gt;
To prevent this runtime error, the method was updated to safely handle &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; inputs using safe navigation (&amp;lt;code&amp;gt;&amp;amp;amp;.id&amp;lt;/code&amp;gt;) and an early return pattern.&lt;br /&gt;
&lt;br /&gt;
Fixed implementation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# Add a Teaching Assistant to the course&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user&amp;amp;.id} does not exist&amp;quot; }&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
  else&lt;br /&gt;
    # remaining logic...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the method no longer raises exceptions when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user is provided and instead returns a controlled error response, improving robustness and test reliability.&lt;br /&gt;
&lt;br /&gt;
==== Corrected Course–User Association Mapping ====&lt;br /&gt;
A bug in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model was fixed where the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association incorrectly referenced a non-existent relationship (&amp;lt;code&amp;gt;course_participants&amp;lt;/code&amp;gt;). The model already defines the correct join association as &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, so the additional reference caused confusion and potential runtime issues.&lt;br /&gt;
&lt;br /&gt;
The association structure correctly uses &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; should be derived from this relationship.&lt;br /&gt;
&lt;br /&gt;
Before fix:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Correct association structure:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :participants&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association correctly resolves through the existing &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt; relationship, maintaining consistency in the model and preventing invalid association lookups.&lt;br /&gt;
&lt;br /&gt;
==== Correct Foreign Key Mapping for QuestionAdvice Association ====&lt;br /&gt;
A fix was applied to the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model to ensure the association correctly references the underlying database schema.&lt;br /&gt;
&lt;br /&gt;
Previously, Rails was implicitly expecting a default foreign key (&amp;lt;code&amp;gt;item_id&amp;lt;/code&amp;gt;), which did not match the actual schema. The &amp;lt;code&amp;gt;question_advice&amp;lt;/code&amp;gt; table stores the foreign key as &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, meaning the association must explicitly specify this key to function correctly.&lt;br /&gt;
&lt;br /&gt;
To resolve this mismatch, the association was updated as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class QuestionAdvice &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :item, foreign_key: :question_id&lt;br /&gt;
  # remaining logic...&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This change ensures that Rails correctly maps each &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; record to its associated &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, restoring proper association behavior and preventing incorrect lookups using the default key.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168058</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168058"/>
		<updated>2026-04-24T06:14:10Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Design ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
&lt;br /&gt;
=== Coverage ===&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Bugs Fixed ===&lt;br /&gt;
&lt;br /&gt;
==== Handling Nil User in add_ta Method (Course Model) ====&lt;br /&gt;
A bug was identified in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model where the &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt; method would crash when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user was passed during unit testing. The issue occurred because the method attempted to access &amp;lt;code&amp;gt;user.id&amp;lt;/code&amp;gt; without first verifying that the user object was present.&lt;br /&gt;
&lt;br /&gt;
To prevent this runtime error, the method was updated to safely handle &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; inputs using safe navigation (&amp;lt;code&amp;gt;&amp;amp;amp;.id&amp;lt;/code&amp;gt;) and an early return pattern.&lt;br /&gt;
&lt;br /&gt;
'''Fixed implementation:'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# Add a Teaching Assistant to the course&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user&amp;amp;.id} does not exist&amp;quot; }&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
  else&lt;br /&gt;
    # remaining logic...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the method no longer raises exceptions when a &amp;lt;code&amp;gt;nil&amp;lt;/code&amp;gt; user is provided and instead returns a controlled error response, improving robustness and test reliability.&lt;br /&gt;
&lt;br /&gt;
==== Corrected Course–User Association Mapping ====&lt;br /&gt;
A bug in the &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt; model was fixed where the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association incorrectly referenced a non-existent relationship (&amp;lt;code&amp;gt;course_participants&amp;lt;/code&amp;gt;). The model already defines the correct join association as &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, so the additional reference caused confusion and potential runtime issues.&lt;br /&gt;
&lt;br /&gt;
The association structure correctly uses &amp;lt;code&amp;gt;CourseParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; should be derived from this relationship.&lt;br /&gt;
&lt;br /&gt;
'''Before fix:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Correct association structure:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :participants&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This fix ensures that the &amp;lt;code&amp;gt;users&amp;lt;/code&amp;gt; association correctly resolves through the existing &amp;lt;code&amp;gt;participants&amp;lt;/code&amp;gt; relationship, maintaining consistency in the model and preventing invalid association lookups.&lt;br /&gt;
&lt;br /&gt;
==== Correct Foreign Key Mapping for QuestionAdvice Association ====&lt;br /&gt;
A fix was applied to the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model to ensure the association correctly references the underlying database schema.&lt;br /&gt;
&lt;br /&gt;
Previously, Rails was implicitly expecting a default foreign key (&amp;lt;code&amp;gt;item_id&amp;lt;/code&amp;gt;), which did not match the actual schema. The &amp;lt;code&amp;gt;question_advice&amp;lt;/code&amp;gt; table stores the foreign key as &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, meaning the association must explicitly specify this key to function correctly.&lt;br /&gt;
&lt;br /&gt;
To resolve this mismatch, the association was updated as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class QuestionAdvice &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :item, foreign_key: :question_id&lt;br /&gt;
  # remaining logic...&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This change ensures that Rails correctly maps each &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; record to its associated &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;question_id&amp;lt;/code&amp;gt;, restoring proper association behavior and preventing incorrect lookups using the default key.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168057</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168057"/>
		<updated>2026-04-24T05:49:15Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Running Tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
The following steps can be used to run only the tests relevant to this project.&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;spec_helper.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
1. Add the following line to the require section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
require 'simplecov-html'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Modify the SimpleCov.formatter assignment to be the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([&lt;br /&gt;
  SimpleCov::Formatter::HTMLFormatter,&lt;br /&gt;
  SimpleCov::Formatter::JSONFormatter&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3. Replace the &amp;lt;code&amp;gt;if !ENV['COVERAGE_STARTED']&amp;lt;/code&amp;gt; block with the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
if !ENV['COVERAGE_STARTED']&lt;br /&gt;
  SimpleCov.start 'rails' do&lt;br /&gt;
    track_files 'app/models/{questionnaire,question_advice,course}.rb'&lt;br /&gt;
    tracked = %w[&lt;br /&gt;
      app/models/questionnaire.rb&lt;br /&gt;
      app/models/question_advice.rb&lt;br /&gt;
      app/models/course.rb&lt;br /&gt;
    ].map { |path| File.expand_path(path, SimpleCov.root) }&lt;br /&gt;
&lt;br /&gt;
    add_filter do |src|&lt;br /&gt;
      !tracked.include?(src.filename)&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  ENV['COVERAGE_STARTED'] = 'true'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Bugs Fixed ==&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168056</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168056"/>
		<updated>2026-04-24T05:42:08Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Coverage and Results */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
== Bugs Fixed ==&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Coverage results are shown in the table below:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 100.00% || 59 || 39 || 39 || 0 || 1.31&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 100.00% || 24 || 13 || 13 || 0 || 1.54&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 100.00% || 141 || 64 || 64 || 0 || 6.36&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168055</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168055"/>
		<updated>2026-04-24T05:39:04Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Conclusion */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
== Bugs Fixed ==&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168054</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168054"/>
		<updated>2026-04-24T05:38:02Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* References */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
== Bugs Fixed ==&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168053</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168053"/>
		<updated>2026-04-24T05:37:18Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Running Tests ==&lt;br /&gt;
&lt;br /&gt;
== Bugs Fixed ==&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168052</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168052"/>
		<updated>2026-04-24T05:35:06Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Github */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Repository: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
Link to Reimplementation Pull Request: [https://github.com/expertiza/reimplementation-back-end/pull/336]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168051</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168051"/>
		<updated>2026-04-24T05:22:22Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Issues in Existing Code */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Initial Code ==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The Course model contains inconsistent association naming. One declaration references the `participants` relationship, while another attempts to use `course_participants`, which is not defined. This mismatch can lead to runtime errors when accessing associated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
The file &amp;lt;code&amp;gt;spec/models/questionnaire_spec.rb&amp;lt;/code&amp;gt; provides test coverage primarily for validations, associations, and the &amp;lt;code&amp;gt;copy_questionnaire_details&amp;lt;/code&amp;gt; method. However, multiple instance methods remain untested, and the &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt; model lacks sufficient coverage overall.&lt;br /&gt;
&lt;br /&gt;
Similarly, &amp;lt;code&amp;gt;spec/models/course_spec.rb&amp;lt;/code&amp;gt; focuses mainly on validations and the &amp;lt;code&amp;gt;path&amp;lt;/code&amp;gt; method. Important instance methods such as &amp;lt;code&amp;gt;add_ta&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;remove_ta&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;copy_course&amp;lt;/code&amp;gt; are not covered by unit tests.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168050</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168050"/>
		<updated>2026-04-24T05:20:04Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Method Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Descriptions ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Course model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168049</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168049"/>
		<updated>2026-04-24T05:19:20Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Method Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. validate_questionnaire&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items?&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. self.export_fields&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.export&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
1. path&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Course model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168048</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168048"/>
		<updated>2026-04-24T05:16:43Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Test Methods */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Course model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods Overview ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Setup ===&lt;br /&gt;
This suite provides a comprehensive setup for testing questionnaire behavior, including validation rules, scoring logic, duplication, and utility methods.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Multiple questionnaire instances:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  let(:questionnaire1) { build(:questionnaire, max_question_score: 20) }&lt;br /&gt;
  let(:questionnaire2) { build(:questionnaire, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions with weights:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, weight: 1) }&lt;br /&gt;
  let(:question2) { create(:item, weight: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Mocked dependencies:&lt;br /&gt;
  * AssignmentQuestionnaire for scoring logic&lt;br /&gt;
  * Participant/review objects using doubles&lt;br /&gt;
&lt;br /&gt;
This setup supports testing:&lt;br /&gt;
* Field validations (name, min/max scores)&lt;br /&gt;
* Associations with items&lt;br /&gt;
* Deep copy functionality (including nested advice records)&lt;br /&gt;
* Scoring algorithms and weighted calculations&lt;br /&gt;
* Serialization and helper methods&lt;br /&gt;
&lt;br /&gt;
The structure ensures isolation of logic while simulating realistic relationships between models.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Setup ===&lt;br /&gt;
This suite focuses on relationships between questionnaires, questions (items), and advice entries. It builds a structured dataset with multiple linked records.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults&lt;br /&gt;
* Questionnaire with scoring bounds:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:questionnaire) { create(:questionnaire, min_question_score: 0, max_question_score: 10) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Associated questions:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:question1) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  let(:question2) { create(:item, questionnaire: questionnaire) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* QuestionAdvice entries:&lt;br /&gt;
  * Default values (implicit score/advice)&lt;br /&gt;
  * Custom values for testing overrides&lt;br /&gt;
&lt;br /&gt;
This setup enables validation of:&lt;br /&gt;
* Default attribute values&lt;br /&gt;
* Associations&lt;br /&gt;
* Export functionality&lt;br /&gt;
* JSON formatting methods&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Setup ===&lt;br /&gt;
The Course test suite establishes core domain objects using factories, including an instructor, institution, and course. User callbacks are stubbed to avoid unintended initialization side effects.&lt;br /&gt;
&lt;br /&gt;
Key setup features:&lt;br /&gt;
* Stubbed User defaults:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  allow_any_instance_of(User).to receive(:set_defaults)&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Core entities:&lt;br /&gt;
  &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
  let(:instructor) { create(:instructor) }&lt;br /&gt;
  let(:institution) { create(:institution) }&lt;br /&gt;
  let(:course) { create(:course, instructor: instructor, institution: institution) }&lt;br /&gt;
  &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* Additional supporting objects:&lt;br /&gt;
  * Users with roles (student, TA, instructor)&lt;br /&gt;
  * CourseParticipant and TaMapping records&lt;br /&gt;
&lt;br /&gt;
This setup supports testing validations, associations, and course management methods such as TA assignment and course duplication.&lt;br /&gt;
&lt;br /&gt;
=== Course Model Test Methods ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168047</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168047"/>
		<updated>2026-04-24T05:08:01Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Test Methods */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Course model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| name returns correct values&lt;br /&gt;
| Verifies that questionnaire names are correctly assigned and retrieved.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.name).to eq('abc')&lt;br /&gt;
expect(questionnaire1.name).to eq('xyz')&lt;br /&gt;
expect(questionnaire2.name).to eq('pqr')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name presence validation&lt;br /&gt;
| Ensures the questionnaire name cannot be blank.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.name = '  '&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| instructor_id retrieval&lt;br /&gt;
| Confirms the questionnaire returns the correct instructor ID.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.instructor_id).to eq(instructor.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| maximum_score validation (type and positivity)&lt;br /&gt;
| Validates max score is an integer, positive, and greater than minimum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.max_question_score).to eq(10)&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = -10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.max_question_score = 0&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 1&lt;br /&gt;
expect(questionnaire).to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| minimum_score validation&lt;br /&gt;
| Ensures minimum score is valid, integer, and less than maximum score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
questionnaire.min_question_score = 5&lt;br /&gt;
expect(questionnaire.min_question_score).to eq(5)&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 10&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&lt;br /&gt;
questionnaire.min_question_score = 'a'&lt;br /&gt;
expect(questionnaire).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| association with items&lt;br /&gt;
| Verifies that a questionnaire has many associated questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
expect(questionnaire.items.reload).to include(question1, question2)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details creates copy&lt;br /&gt;
| Ensures a questionnaire copy is created with correct attributes.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  Questionnaire.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.instructor_id)&lt;br /&gt;
  .to eq(questionnaire.instructor_id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.name)&lt;br /&gt;
  .to eq(&amp;quot;Copy of #{questionnaire.name}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.created_at)&lt;br /&gt;
  .to be_within(1.second).of(Time.zone.now)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies items&lt;br /&gt;
| Verifies all questions are duplicated in the copied questionnaire.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
expect(copied_questionnaire.items.count).to eq(2)&lt;br /&gt;
expect(copied_questionnaire.items.first.txt).to eq(question1.txt)&lt;br /&gt;
expect(copied_questionnaire.items.second.txt).to eq(question2.txt)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_questionnaire_details copies advice&lt;br /&gt;
| Ensures associated advice records are duplicated with questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice.insert(&lt;br /&gt;
  {&lt;br /&gt;
    question_id: question1.id,&lt;br /&gt;
    score: 3,&lt;br /&gt;
    advice: 'Good rationale',&lt;br /&gt;
    created_at: Time.zone.now,&lt;br /&gt;
    updated_at: Time.zone.now&lt;br /&gt;
  }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
copied_questionnaire =&lt;br /&gt;
  described_class.copy_questionnaire_details(&lt;br /&gt;
    { id: questionnaire.id, instructor_id: instructor.id }&lt;br /&gt;
  )&lt;br /&gt;
&lt;br /&gt;
copied_question = copied_questionnaire.items.first&lt;br /&gt;
copied_advice = QuestionAdvice.where(question_id: copied_question.id)&lt;br /&gt;
&lt;br /&gt;
expect(copied_advice.count).to eq(1)&lt;br /&gt;
expect(copied_advice.first.score).to eq(3)&lt;br /&gt;
expect(copied_advice.first.advice).to eq('Good rationale')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| symbol method&lt;br /&gt;
| Confirms the questionnaire returns the correct symbol.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(questionnaire.symbol).to eq(:review)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_assessments_for&lt;br /&gt;
| Verifies retrieval of participant reviews.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
reviews = [double('review1'), double('review2')]&lt;br /&gt;
participant = double('participant', reviews: reviews)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.get_assessments_for(participant)).to eq(reviews)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| check_for_question_associations restriction&lt;br /&gt;
| Prevents deletion when associated questions exist and allows otherwise.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
questionnaire.items.reload&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire.check_for_question_associations&lt;br /&gt;
}.to raise_error(ActiveRecord::DeleteRestrictionError)&lt;br /&gt;
&lt;br /&gt;
questionnaire_without_items = build(:questionnaire)&lt;br /&gt;
&lt;br /&gt;
expect {&lt;br /&gt;
  questionnaire_without_items.check_for_question_associations&lt;br /&gt;
}.not_to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| as_json serialization&lt;br /&gt;
| Ensures JSON output includes required fields.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
json = questionnaire.as_json&lt;br /&gt;
expect(json).to include('id', 'name', 'instructor')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| get_weighted_score behavior&lt;br /&gt;
| Uses appropriate symbol depending on round configuration.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 42)&lt;br /&gt;
&lt;br /&gt;
aq = double('assignment_questionnaire', used_in_round: nil)&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 42, questionnaire_id: questionnaire.id)&lt;br /&gt;
  .and_return(aq)&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire).to receive(:compute_weighted_score)&lt;br /&gt;
  .with(:review, assignment, {})&lt;br /&gt;
&lt;br /&gt;
questionnaire.get_weighted_score(assignment, {})&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| compute_weighted_score&lt;br /&gt;
| Calculates weighted score or returns zero if no average exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
assignment = double('assignment', id: 77)&lt;br /&gt;
&lt;br /&gt;
allow(AssignmentQuestionnaire).to receive(:find_by)&lt;br /&gt;
  .with(assignment_id: 77)&lt;br /&gt;
  .and_return(double('aq', questionnaire_weight: 25))&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: nil } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(0)&lt;br /&gt;
&lt;br /&gt;
scores = { review: { scores: { avg: 8 } } }&lt;br /&gt;
expect(questionnaire.compute_weighted_score(:review, assignment, scores)).to eq(2.0)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| true_false_items?&lt;br /&gt;
| Detects presence or absence of checkbox-type questions.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Checkbox')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(true)&lt;br /&gt;
&lt;br /&gt;
allow(questionnaire).to receive(:items)&lt;br /&gt;
  .and_return([double('item', type: 'Criterion')])&lt;br /&gt;
expect(questionnaire.true_false_items?).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| max_possible_score&lt;br /&gt;
| Computes total possible score based on weights and max score.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
question1&lt;br /&gt;
question2&lt;br /&gt;
&lt;br /&gt;
# question1.weight = 1, question2.weight = 10&lt;br /&gt;
# max_question_score = 10&lt;br /&gt;
&lt;br /&gt;
expect(questionnaire.max_possible_score.to_i).to eq(110)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168046</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168046"/>
		<updated>2026-04-24T05:04:01Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* QuestionAdvice */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Course model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire ===&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| question association returns correct question_id&lt;br /&gt;
| Verifies that each QuestionAdvice correctly references its associated question.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.question_id).to eq(question1.id)&lt;br /&gt;
expect(question_advice2.question_id).to eq(question2.id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| score returns correct value&lt;br /&gt;
| Ensures that the score attribute of QuestionAdvice is returned correctly, including defaults.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.score).to eq(5)&lt;br /&gt;
expect(question_advice2.score).to eq(6)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| advice returns correct value&lt;br /&gt;
| Ensures that the advice attribute is returned correctly, including default advice text.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
expect(question_advice1.advice).to eq('default advice')&lt;br /&gt;
expect(question_advice2.advice).to eq('advice for question 2')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export_fields mapping&lt;br /&gt;
| Verifies that export_fields returns the correct column mappings.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.export_fields({})&lt;br /&gt;
expect(output).to eq([&amp;quot;id&amp;quot;, &amp;quot;question_id&amp;quot;, &amp;quot;score&amp;quot;, &amp;quot;advice&amp;quot;, &amp;quot;created_at&amp;quot;, &amp;quot;updated_at&amp;quot;])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| export method stores values&lt;br /&gt;
| Tests that the export method correctly appends QuestionAdvice records to a CSV array.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
csv = []&lt;br /&gt;
QuestionAdvice.export(csv, questionnaire.id, nil)&lt;br /&gt;
expect(csv.length).to eq(3)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question1&lt;br /&gt;
| Verifies JSON output for all QuestionAdvice entries associated with question 1.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question1.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 5, advice: 'default advice' },&lt;br /&gt;
  { score: 1, advice: 'advice for question 3' }&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| to_json_by_question_id for question2&lt;br /&gt;
| Verifies JSON output for QuestionAdvice entries associated with question 2.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
output = QuestionAdvice.to_json_by_question_id(question2.id)&lt;br /&gt;
expect(output).to eq([&lt;br /&gt;
  { score: 6, advice: 'advice for question 2'}&lt;br /&gt;
])&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Course ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168045</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168045"/>
		<updated>2026-04-24T04:58:10Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Model Documentation==&lt;br /&gt;
&lt;br /&gt;
Questionnaire Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires]&lt;br /&gt;
&lt;br /&gt;
QuestionAdvice Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Question_advices]&lt;br /&gt;
&lt;br /&gt;
Course Model: [https://wiki.expertiza.ncsu.edu/index.php?title=Courses_table]&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The next section outlines the existing implementation of the methods to be unit tested.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Course model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for unit testing with RSpec.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Course ====&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test Methods ==&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire ===&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice ===&lt;br /&gt;
&lt;br /&gt;
=== Course ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Test Method !! Description !! Code Snippet&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of name&lt;br /&gt;
| Ensures that a course must have a name to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.name = ''&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| validates presence of directory_path&lt;br /&gt;
| Ensures that a course must have a directory path to be valid.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
course.directory_path = ' '&lt;br /&gt;
expect(course).not_to be_valid&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| returns users through participants&lt;br /&gt;
| Verifies that users associated through course participants are accessible.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
student = create(:user, :student)&lt;br /&gt;
create(:course_participant, course: course, user: student)&lt;br /&gt;
expect(course.users).to include(student)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path raises error without instructor&lt;br /&gt;
| Ensures an error is raised when generating a path without an associated instructor.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(nil)&lt;br /&gt;
expect { course.path }.to raise_error&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| path returns directory with instructor&lt;br /&gt;
| Verifies a directory path is returned when instructor and institution exist.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(course).to receive(:instructor_id).and_return(6)&lt;br /&gt;
allow(User).to receive(:find).with(6).and_return(user1)&lt;br /&gt;
expect(course.path.directory?).to be_truthy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta success&lt;br /&gt;
| Adds a TA and updates the user role accordingly.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
expect(result[:data]).to include('course_id' =&amp;gt; course.id, 'user_id' =&amp;gt; ta_user.id)&lt;br /&gt;
expect(ta_user.reload.role.name).to eq('Teaching Assistant')&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta already exists&lt;br /&gt;
| Prevents adding a TA who is already assigned to the course.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta nil user&lt;br /&gt;
| Returns an error when attempting to add a non-existent user.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.add_ta(nil)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| add_ta save failure&lt;br /&gt;
| Returns validation errors when TA mapping fails to save.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
allow(TaMapping).to receive(:create).and_return(double(save: false))&lt;br /&gt;
result = course.add_ta(ta_user)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta no mapping&lt;br /&gt;
| Returns an error when no TA mapping exists.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(false)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| remove_ta success&lt;br /&gt;
| Removes a TA mapping and updates role if it was the last assignment.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
ta_mapping = TaMapping.create!(user_id: ta_user.id, course_id: course.id)&lt;br /&gt;
allow(course.ta_mappings).to receive(:find_by).and_return(ta_mapping)&lt;br /&gt;
allow(TaMapping).to receive(:where).with(user_id: ta_user.id).and_return([ta_mapping])&lt;br /&gt;
stub_const('Role::STUDENT', student_role)&lt;br /&gt;
result = course.remove_ta(ta_user.id)&lt;br /&gt;
expect(result[:success]).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| copy_course success&lt;br /&gt;
| Creates a duplicate course with modified name and directory path.&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
result = course.copy_course&lt;br /&gt;
expect(result).to be(true)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Coverage and Results ==&lt;br /&gt;
&lt;br /&gt;
Test Results:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! % covered !! Lines !! Relevant Lines !! Lines covered !! Lines missed !! Avg. Hits / Line&lt;br /&gt;
|-&lt;br /&gt;
| app/models/questionnaire.rb || 64.06% || 141 || 64 || 41 || 23 || 3.02&lt;br /&gt;
|-&lt;br /&gt;
| app/models/question_advice.rb || 38.46% || 24 || 13 || 5 || 8 || 0.38&lt;br /&gt;
|-&lt;br /&gt;
| app/models/course.rb || 43.59% || 59 || 39 || 17 || 22 || 0.46&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167928</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167928"/>
		<updated>2026-04-14T00:48:55Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Key Relationships */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Stores all questions belonging to this questionnaire. Deleting the questionnaire removes its items.  &lt;br /&gt;
  '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor who owns this questionnaire.  &lt;br /&gt;
  '''belongs_to :instructor&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Links advice to a specific question (item).  &lt;br /&gt;
  '''belongs_to :item'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor responsible for the course.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Associates the course with an institution.  &lt;br /&gt;
  '''belongs_to :institution, foreign_key: 'institution_id'''&lt;br /&gt;
&lt;br /&gt;
* Stores all assignments in the course; removed when the course is deleted.  &lt;br /&gt;
  '''has_many :assignments, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Manages enrolled users through a join table.  &lt;br /&gt;
  '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  '''has_many :users, through: :course_participants, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
* Manages teaching assistants for the course.  &lt;br /&gt;
  '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
  '''has_many :tas, through: :ta_mappings, source: :ta'''&lt;br /&gt;
&lt;br /&gt;
* Stores teams within the course.  &lt;br /&gt;
  '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories and fixtures to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167926</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167926"/>
		<updated>2026-04-14T00:47:53Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Key Relationships */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Stores all questions belonging to this questionnaire. Deleting the questionnaire removes its items.  &lt;br /&gt;
  '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor who owns this questionnaire.  &lt;br /&gt;
  '''belongs_to :instructor&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Links advice to a specific question (item).  &lt;br /&gt;
  '''belongs_to :item'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor responsible for the course.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Associates the course with an institution.  &lt;br /&gt;
  '''belongs_to :institution, foreign_key: 'institution_id'''&lt;br /&gt;
&lt;br /&gt;
* Stores all assignments in the course; removed when the course is deleted.  &lt;br /&gt;
  '''has_many :assignments, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Manages enrolled users through a join table.  &lt;br /&gt;
  '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  '''has_many :users, through: :participants, source: :user'''&lt;br /&gt;
&lt;br /&gt;
* Manages teaching assistants for the course.  &lt;br /&gt;
  '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
  '''has_many :tas, through: :ta_mappings, source: :ta'''&lt;br /&gt;
&lt;br /&gt;
* Stores teams within the course.  &lt;br /&gt;
  '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167925</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167925"/>
		<updated>2026-04-14T00:47:33Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Key Relationships */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Stores all questions belonging to this questionnaire. Deleting the questionnaire removes its items.  &lt;br /&gt;
  '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor who owns this questionnaire.  &lt;br /&gt;
  '''belongs_to :instructor&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Links advice to a specific question (item).  &lt;br /&gt;
  '''belongs_to :item, foreign_key: 'question_id'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor responsible for the course.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Associates the course with an institution.  &lt;br /&gt;
  '''belongs_to :institution, foreign_key: 'institution_id'''&lt;br /&gt;
&lt;br /&gt;
* Stores all assignments in the course; removed when the course is deleted.  &lt;br /&gt;
  '''has_many :assignments, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Manages enrolled users through a join table.  &lt;br /&gt;
  '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  '''has_many :users, through: :participants, source: :user'''&lt;br /&gt;
&lt;br /&gt;
* Manages teaching assistants for the course.  &lt;br /&gt;
  '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
  '''has_many :tas, through: :ta_mappings, source: :ta'''&lt;br /&gt;
&lt;br /&gt;
* Stores teams within the course.  &lt;br /&gt;
  '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167924</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167924"/>
		<updated>2026-04-14T00:47:10Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Project Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Stores all questions belonging to this questionnaire. Deleting the questionnaire removes its items.  &lt;br /&gt;
  '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor who owns this questionnaire.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Links advice to a specific question (item).  &lt;br /&gt;
  '''belongs_to :item, foreign_key: 'question_id'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor responsible for the course.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Associates the course with an institution.  &lt;br /&gt;
  '''belongs_to :institution, foreign_key: 'institution_id'''&lt;br /&gt;
&lt;br /&gt;
* Stores all assignments in the course; removed when the course is deleted.  &lt;br /&gt;
  '''has_many :assignments, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Manages enrolled users through a join table.  &lt;br /&gt;
  '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  '''has_many :users, through: :participants, source: :user'''&lt;br /&gt;
&lt;br /&gt;
* Manages teaching assistants for the course.  &lt;br /&gt;
  '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
  '''has_many :tas, through: :ta_mappings, source: :ta'''&lt;br /&gt;
&lt;br /&gt;
* Stores teams within the course.  &lt;br /&gt;
  '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167923</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167923"/>
		<updated>2026-04-14T00:37:39Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Stores all questions belonging to this questionnaire. Deleting the questionnaire removes its items.  &lt;br /&gt;
  '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor who owns this questionnaire.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Connects questionnaires to assignments through a join table.  &lt;br /&gt;
  '''has_many :assignment_questionnaires'''&lt;br /&gt;
&lt;br /&gt;
* Allows access to assignments that use this questionnaire.  &lt;br /&gt;
  '''has_many :assignments, through: :assignment_questionnaires'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Links advice to a specific question (item).  &lt;br /&gt;
  '''belongs_to :item, foreign_key: 'question_id'''&lt;br /&gt;
&lt;br /&gt;
* Provides access to the parent questionnaire through the item.  &lt;br /&gt;
  '''has_one :questionnaire, through: :item'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor responsible for the course.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Associates the course with an institution.  &lt;br /&gt;
  '''belongs_to :institution, foreign_key: 'institution_id'''&lt;br /&gt;
&lt;br /&gt;
* Stores all assignments in the course; removed when the course is deleted.  &lt;br /&gt;
  '''has_many :assignments, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Manages enrolled users through a join table.  &lt;br /&gt;
  '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  '''has_many :users, through: :participants, source: :user'''&lt;br /&gt;
&lt;br /&gt;
* Manages teaching assistants for the course.  &lt;br /&gt;
  '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
  '''has_many :tas, through: :ta_mappings, source: :ta'''&lt;br /&gt;
&lt;br /&gt;
* Stores teams within the course.  &lt;br /&gt;
  '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
* Direct access to participant records.  &lt;br /&gt;
  '''has_many :course_participants'''&lt;br /&gt;
&lt;br /&gt;
* Accesses questionnaires used in the course via assignments.  &lt;br /&gt;
  '''has_many :questionnaires, through: :assignments'''&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire ====&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice ====&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167922</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167922"/>
		<updated>2026-04-14T00:34:01Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Project Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Stores all questions belonging to this questionnaire. Deleting the questionnaire removes its items.  &lt;br /&gt;
  '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor who owns this questionnaire.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Connects questionnaires to assignments through a join table.  &lt;br /&gt;
  '''has_many :assignment_questionnaires'''&lt;br /&gt;
&lt;br /&gt;
* Allows access to assignments that use this questionnaire.  &lt;br /&gt;
  '''has_many :assignments, through: :assignment_questionnaires'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Links advice to a specific question (item).  &lt;br /&gt;
  '''belongs_to :item, foreign_key: 'question_id'''&lt;br /&gt;
&lt;br /&gt;
* Provides access to the parent questionnaire through the item.  &lt;br /&gt;
  '''has_one :questionnaire, through: :item'''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Identifies the instructor responsible for the course.  &lt;br /&gt;
  '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''&lt;br /&gt;
&lt;br /&gt;
* Associates the course with an institution.  &lt;br /&gt;
  '''belongs_to :institution, foreign_key: 'institution_id'''&lt;br /&gt;
&lt;br /&gt;
* Stores all assignments in the course; removed when the course is deleted.  &lt;br /&gt;
  '''has_many :assignments, dependent: :destroy'''&lt;br /&gt;
&lt;br /&gt;
* Manages enrolled users through a join table.  &lt;br /&gt;
  '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  '''has_many :users, through: :participants, source: :user'''&lt;br /&gt;
&lt;br /&gt;
* Manages teaching assistants for the course.  &lt;br /&gt;
  '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
  '''has_many :tas, through: :ta_mappings, source: :ta'''&lt;br /&gt;
&lt;br /&gt;
* Stores teams within the course.  &lt;br /&gt;
  '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''&lt;br /&gt;
&lt;br /&gt;
* Direct access to participant records.  &lt;br /&gt;
  '''has_many :course_participants'''&lt;br /&gt;
&lt;br /&gt;
* Accesses questionnaires used in the course via assignments.  &lt;br /&gt;
  '''has_many :questionnaires, through: :assignments'''&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167921</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167921"/>
		<updated>2026-04-14T00:32:12Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Coverage */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''  &lt;br /&gt;
  Represents all questions (items) that belong to this questionnaire. Each item defines a single question in the rubric. When a questionnaire is deleted, all associated items are also removed to maintain data integrity.&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''  &lt;br /&gt;
  Associates the questionnaire with the instructor (User) who created it. This relationship is used for ownership, permissions, and ensuring uniqueness of questionnaires per instructor.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :assignment_questionnaires'''  &lt;br /&gt;
  Defines the join relationship between assignments and questionnaires, allowing a questionnaire to be reused across multiple assignments.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :assignments, through: :assignment_questionnaires'''  &lt;br /&gt;
  Enables access to all assignments that use this questionnaire through the join table, supporting flexible rubric reuse.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :item, foreign_key: 'question_id'''  &lt;br /&gt;
  Links each advice entry to a specific question (item), ensuring feedback is tied to the correct rubric question.&lt;br /&gt;
&lt;br /&gt;
* '''has_one :questionnaire, through: :item'''  &lt;br /&gt;
  Provides indirect access to the parent questionnaire through the associated item, allowing retrieval of questionnaire context.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''  &lt;br /&gt;
  Associates the course with its instructor, who is responsible for managing course content, assignments, and participants.&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :institution, foreign_key: 'institution_id'''  &lt;br /&gt;
  Connects the course to an institution (e.g., university), which helps organize courses and supports directory structure generation.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :assignments, dependent: :destroy'''  &lt;br /&gt;
  A course can include multiple assignments. Deleting the course removes all associated assignments to prevent orphaned data.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
* '''has_many :users, through: :participants, source: :user'''  &lt;br /&gt;
  Represents all users enrolled in the course (students, TAs, etc.) via a join table that stores role and enrollment data.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
* '''has_many :tas, through: :ta_mappings, source: :ta'''  &lt;br /&gt;
  Defines the relationship between courses and teaching assistants using a join table, allowing multiple TAs per course.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  Represents student teams within the course, typically used for group assignments. All teams are deleted when the course is removed.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :course_participants'''  &lt;br /&gt;
  Direct access to the join table storing participant metadata such as roles and permissions.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :questionnaires, through: :assignments'''  &lt;br /&gt;
  Indicates that questionnaires are indirectly associated with a course through its assignments, enabling retrieval of all rubrics used in the course.&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage and Current Results ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167920</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167920"/>
		<updated>2026-04-14T00:30:58Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Project Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
&lt;br /&gt;
==== Questionnaire Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* '''has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy'''  &lt;br /&gt;
  Represents all questions (items) that belong to this questionnaire. Each item defines a single question in the rubric. When a questionnaire is deleted, all associated items are also removed to maintain data integrity.&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''  &lt;br /&gt;
  Associates the questionnaire with the instructor (User) who created it. This relationship is used for ownership, permissions, and ensuring uniqueness of questionnaires per instructor.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :assignment_questionnaires'''  &lt;br /&gt;
  Defines the join relationship between assignments and questionnaires, allowing a questionnaire to be reused across multiple assignments.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :assignments, through: :assignment_questionnaires'''  &lt;br /&gt;
  Enables access to all assignments that use this questionnaire through the join table, supporting flexible rubric reuse.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
&lt;br /&gt;
==== QuestionAdvice Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :item, foreign_key: 'question_id'''  &lt;br /&gt;
  Links each advice entry to a specific question (item), ensuring feedback is tied to the correct rubric question.&lt;br /&gt;
&lt;br /&gt;
* '''has_one :questionnaire, through: :item'''  &lt;br /&gt;
  Provides indirect access to the parent questionnaire through the associated item, allowing retrieval of questionnaire context.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
&lt;br /&gt;
==== Course Attributes ====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''text'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships ====&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'''  &lt;br /&gt;
  Associates the course with its instructor, who is responsible for managing course content, assignments, and participants.&lt;br /&gt;
&lt;br /&gt;
* '''belongs_to :institution, foreign_key: 'institution_id'''  &lt;br /&gt;
  Connects the course to an institution (e.g., university), which helps organize courses and supports directory structure generation.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :assignments, dependent: :destroy'''  &lt;br /&gt;
  A course can include multiple assignments. Deleting the course removes all associated assignments to prevent orphaned data.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
* '''has_many :users, through: :participants, source: :user'''  &lt;br /&gt;
  Represents all users enrolled in the course (students, TAs, etc.) via a join table that stores role and enrollment data.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :ta_mappings, dependent: :destroy'''  &lt;br /&gt;
* '''has_many :tas, through: :ta_mappings, source: :ta'''  &lt;br /&gt;
  Defines the relationship between courses and teaching assistants using a join table, allowing multiple TAs per course.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course'''  &lt;br /&gt;
  Represents student teams within the course, typically used for group assignments. All teams are deleted when the course is removed.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :course_participants'''  &lt;br /&gt;
  Direct access to the join table storing participant metadata such as roles and permissions.&lt;br /&gt;
&lt;br /&gt;
* '''has_many :questionnaires, through: :assignments'''  &lt;br /&gt;
  Indicates that questionnaires are indirectly associated with a course through its assignments, enabling retrieval of all rubrics used in the course.&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage. The fixtures are stored in question_advice.yml under the fixtures folder.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#question_advice.yml&lt;br /&gt;
&lt;br /&gt;
question_advice_1:  &lt;br /&gt;
  id: 1&lt;br /&gt;
  question_id: 1&lt;br /&gt;
  score: 5&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 1.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
question_advice_2:&lt;br /&gt;
  id: 2&lt;br /&gt;
  question_id: 2&lt;br /&gt;
  score: 3&lt;br /&gt;
  advice: &amp;quot;This is some advice for question 2.&amp;quot;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167918</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167918"/>
		<updated>2026-04-14T00:25:14Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Method Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
==== QuestionAdvice Attributes:==== &lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships:==== &lt;br /&gt;
&lt;br /&gt;
belongs_to :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry.&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
====Course Attributes:====&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
====Key Relationships:====&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
belongs_to :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
has_many :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams.&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section discusses the current implementation of methods that our group will be unit testing.&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. validate_questionnaire ====&lt;br /&gt;
Validates a questionnaire by ensuring:&lt;br /&gt;
* Maximum score is a positive integer&lt;br /&gt;
* Minimum score is a non-negative integer&lt;br /&gt;
* Minimum score is less than the maximum score&lt;br /&gt;
* Questionnaire name is unique per instructor&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.copy_questionnaire_details ====&lt;br /&gt;
Clones a questionnaire, including its items and associated advice.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. check_for_question_associations ====&lt;br /&gt;
Checks whether the questionnaire has associated items before deletion.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new(&amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. get_weighted_score ====&lt;br /&gt;
Calculates the weighted score by generating a symbol and calling a helper method.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                           symbol&lt;br /&gt;
                         else&lt;br /&gt;
                           (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                         end&lt;br /&gt;
&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 5. compute_weighted_score ====&lt;br /&gt;
Helper method that computes the weighted score for an assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
  aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
  if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
    0&lt;br /&gt;
  else&lt;br /&gt;
    scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 6. true_false_items? ====&lt;br /&gt;
Checks if the questionnaire contains any true/false (checkbox) items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
  items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
  false&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 7. max_possible_score ====&lt;br /&gt;
Calculates the maximum possible score based on item weights and max question score.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
  results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                         .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                         .where('questionnaires.id = ?', id)&lt;br /&gt;
  results[0].max_score&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. self.export_fields ====&lt;br /&gt;
Returns all column names of QuestionAdvice as strings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
  QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. self.export ====&lt;br /&gt;
Exports QuestionAdvice entries to a CSV file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
  questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
&lt;br /&gt;
  questionnaire.items.each do |item|&lt;br /&gt;
    QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
      csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. self.to_json_by_question_id ====&lt;br /&gt;
Exports QuestionAdvice entries to JSON format by question ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
  question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
&lt;br /&gt;
  question_advices.map do |advice|&lt;br /&gt;
    { score: advice.score, advice: advice.advice }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
&lt;br /&gt;
==== 1. path ====&lt;br /&gt;
Returns the submission directory path for the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
  raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
&lt;br /&gt;
  Rails.root + '/' +&lt;br /&gt;
    Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' +&lt;br /&gt;
    directory_path + '/'&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. add_ta ====&lt;br /&gt;
Adds a Teaching Assistant (TA) to the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
  if user.nil?&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
    return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  else&lt;br /&gt;
    ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
    ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
    user.update(role: ta_role) if ta_role&lt;br /&gt;
&lt;br /&gt;
    if ta_mapping.save&lt;br /&gt;
      return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
    else&lt;br /&gt;
      return { success: false, message: ta_mapping.errors }&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 3. remove_ta ====&lt;br /&gt;
Removes a Teaching Assistant from the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
  ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
  return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
&lt;br /&gt;
  ta = User.find(ta_mapping.user_id)&lt;br /&gt;
  ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
&lt;br /&gt;
  if ta_count.zero?&lt;br /&gt;
    ta.update(role: Role::STUDENT)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  ta_mapping.destroy&lt;br /&gt;
  { success: true, ta_name: ta.name }&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 4. copy_course ====&lt;br /&gt;
Creates a duplicate of the course.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
  new_course = dup&lt;br /&gt;
  new_course.directory_path += '_copy'&lt;br /&gt;
  new_course.name += '_copy'&lt;br /&gt;
  new_course.save&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code is the current version of the factory for the course models.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :course do&lt;br /&gt;
    name { Faker::Educator.course_name }&lt;br /&gt;
    info { Faker::Lorem.paragraph }&lt;br /&gt;
    private { false }&lt;br /&gt;
    directory_path { Faker::File.dir }&lt;br /&gt;
    association :institution, factory: :institution&lt;br /&gt;
    association :instructor, factory: :user&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167915</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167915"/>
		<updated>2026-04-14T00:21:24Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Course Model */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
==== QuestionAdvice Attributes:==== &lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships:==== &lt;br /&gt;
&lt;br /&gt;
belongs_to :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry.&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
====Course Attributes:====&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
====Key Relationships:====&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
belongs_to :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
has_many :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
has_many :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams.&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
The following code is currently how the factory for the Questionnaire model is set up.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following code serves as the factory for generating instances of items which are essentially questions in a questionnaire object.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167913</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167913"/>
		<updated>2026-04-14T00:19:42Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Key Relationships: */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
==== QuestionAdvice Attributes:==== &lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships:==== &lt;br /&gt;
&lt;br /&gt;
belongs_to :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry.&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167912</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167912"/>
		<updated>2026-04-14T00:19:21Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Key Relationships: */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
==== QuestionAdvice Attributes:==== &lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships:==== &lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry.&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/questionnaires.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :questionnaire do&lt;br /&gt;
    sequence(:name) { |n| &amp;quot;Questionnaire #{n}&amp;quot; }&lt;br /&gt;
    private { false }&lt;br /&gt;
    min_question_score { 0 }&lt;br /&gt;
    max_question_score { 10 }&lt;br /&gt;
    association :instructor&lt;br /&gt;
    association :assignment&lt;br /&gt;
&lt;br /&gt;
    # Trait for questionnaire with questions&lt;br /&gt;
    trait :with_questions do&lt;br /&gt;
      after(:create) do |questionnaire|&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 1, seq: 1, txt: &amp;quot;que 1&amp;quot;, question_type: &amp;quot;Scale&amp;quot;)&lt;br /&gt;
        create(:item, questionnaire: questionnaire, weight: 10, seq: 2, txt: &amp;quot;que 2&amp;quot;, question_type: &amp;quot;Checkbox&amp;quot;)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# spec/factories/items.rb&lt;br /&gt;
FactoryBot.define do&lt;br /&gt;
  factory :item do&lt;br /&gt;
    sequence(:txt) { |n| &amp;quot;Question #{n}&amp;quot; }&lt;br /&gt;
    sequence(:seq) { |n| n }&lt;br /&gt;
    weight { 1 }&lt;br /&gt;
    question_type { &amp;quot;Scale&amp;quot; }&lt;br /&gt;
    break_before { true }&lt;br /&gt;
    association :questionnaire&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167910</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167910"/>
		<updated>2026-04-14T00:19:15Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* QuestionAdvice Model */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
==== QuestionAdvice Attributes:==== &lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships:==== &lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167909</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167909"/>
		<updated>2026-04-14T00:18:55Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Questionnaire Model */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
QuestionAdvice Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167908</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167908"/>
		<updated>2026-04-14T00:18:39Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Questionnaire Model */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
has_many :items, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire. All associated items will be deleted when this is deleted.&lt;br /&gt;
&lt;br /&gt;
belongs_to :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
QuestionAdvice Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teaching assistants&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many teams&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167903</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167903"/>
		<updated>2026-04-14T00:13:57Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Questionnaire Model */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
====Questionnaire Attributes:====&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Key Relationships: ====&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :items, class_name: &amp;quot;Item&amp;quot;, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
QuestionAdvice Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many TAs&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167902</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167902"/>
		<updated>2026-04-14T00:12:27Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Conclusion */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
Questionnaire Attributes:&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :items, class_name: &amp;quot;Item&amp;quot;, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
QuestionAdvice Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many TAs&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
Currently for Questionnaire, the data for unit testing is currently being created using Factory so we will continue to write tests that ensure over 80% code coverage. We will also use factories for the Course model as the method logic is dependent on varying data. It allows us to adjust parameters and test for edge cases easier.&lt;br /&gt;
&lt;br /&gt;
For QuestionAdvice use fixtures to load data into database to test that it is being saved and exported properly. As it stands, there isn't that much logical complexity with the data inside QuestionAdvice itself as it is more concerned with exporting data into csv or json files for storage.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion ==&lt;br /&gt;
This section will be updated with final results at the end of the project.&lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167899</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167899"/>
		<updated>2026-04-14T00:00:05Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Github */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
Questionnaire Attributes:&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :items, class_name: &amp;quot;Item&amp;quot;, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
QuestionAdvice Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many TAs&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
For this project, we will be using factories to generate our models for testing.&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
simplecov report&lt;br /&gt;
&lt;br /&gt;
test results&lt;br /&gt;
&lt;br /&gt;
== Conclusion == &lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project repo: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167895</id>
		<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=167895"/>
		<updated>2026-04-13T23:48:43Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Project Description */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project Description ==&lt;br /&gt;
&lt;br /&gt;
This project is oriented around the testing of three models representing the individual reviews that each student will be conducting for an assignment. The Questionnaire model represents a review rubric used in assignments. It holds a set of scored questions with configurable min/max score bounds, belongs to an instructor, and supports being copied (along with all its questions and advice) for reuse. It also provides scoring utilities used during assignment evaluation. The QuestionAdvice model represents the score-specific feedback hints attached to a question in a questionnaire (e.g., &amp;quot;if you give a score of 3, here's what that means&amp;quot;). It provides class-level methods to export advice records to CSV and to fetch advice for a given question as JSON. The Course model represents a course in the system. It manages the course's filesystem submission path, handles adding and removing Teaching Assistants (updating their role accordingly via TaMapping), and supports duplicating a course via copy_course. The emphasis is on making sure that the methods associated with the model classes for Questionnaires, QuestionAdvice, and Course are tested using RSpec unit testing and resolving any bugs that are present in the current implementation.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2617 — Testing Questionnaire and Course Models&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Aanand Sreekumaran Nair Jayakumari&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || DJ Hansen, Zack Brooks, Matt Nguyen&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;ApplicationRecord&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;Questionnaire&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;QuestionAdvice&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model ===&lt;br /&gt;
Questionnaire Attributes:&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* min_question_score: '''integer'''&lt;br /&gt;
* max_question_score: '''integer'''&lt;br /&gt;
* questionnaire_type: '''string'''&lt;br /&gt;
* display_type: '''string'''&lt;br /&gt;
* instruction_loc: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :items, class_name: &amp;quot;Item&amp;quot;, foreign_key: &amp;quot;questionnaire_id&amp;quot;, dependent: :destroy &lt;br /&gt;
&lt;br /&gt;
The collection of items (Questions) associated with this Questionnaire&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor &lt;br /&gt;
&lt;br /&gt;
Each questionnaire entry belongs to an instructor&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model ===&lt;br /&gt;
QuestionAdvice Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* question_id: '''integer'''&lt;br /&gt;
* score: '''integer'''&lt;br /&gt;
* advice: '''text'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :item&lt;br /&gt;
&lt;br /&gt;
Each QuestionAdvice entry belongs to an Item (Question) entry&lt;br /&gt;
&lt;br /&gt;
=== Course Model ===&lt;br /&gt;
Course Attributes:&lt;br /&gt;
&lt;br /&gt;
* id: '''integer'''&lt;br /&gt;
* name: '''string'''&lt;br /&gt;
* directory_path: '''string'''&lt;br /&gt;
* info: '''info'''&lt;br /&gt;
* private: '''boolean'''&lt;br /&gt;
* created_at: '''datetime'''&lt;br /&gt;
* updated_at: '''datetime'''&lt;br /&gt;
* instructor_id: '''integer'''&lt;br /&gt;
* institution_id: '''integer'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Key Relationships:&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :instructor, class_name: 'User', foreign_key: 'instructor_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to a user who is the instructor&lt;br /&gt;
&lt;br /&gt;
'''belongs_to''' :institution, foreign_key: 'institution_id'&lt;br /&gt;
&lt;br /&gt;
Each Course belongs to an institution&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :assignments, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many assignments that will all be removed if the course is removed &lt;br /&gt;
&lt;br /&gt;
'''has_many''' :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
Each Course can have many participants that will all removed if the course is removed&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :ta_mappings, dependent: :destroy&lt;br /&gt;
&lt;br /&gt;
Each Course can have many TAs&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :tas, through: :ta_mappings, source: :ta&lt;br /&gt;
&lt;br /&gt;
'''has_many''' :teams, class_name: 'CourseTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
&lt;br /&gt;
== Method Description ==&lt;br /&gt;
The following section will discuss the current implementation of methods that our group will best unit testing. &lt;br /&gt;
&lt;br /&gt;
=== Questionnaire Model Methods ===&lt;br /&gt;
1. validate_questionnaire - validates the questionnaire that is being created to make sure that both the maximum and minimum score are a positive integer and that the minimum score must be less than the maximum score. Also checks to see if this new questionnaire is unique and not already in the Questionnaire table.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def validate_questionnaire&lt;br /&gt;
  errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score &amp;lt; 1&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score &amp;lt; 0&lt;br /&gt;
  errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score &amp;gt;= max_question_score&lt;br /&gt;
  results = Questionnaire.where('id &amp;lt;&amp;gt; ? and name = ? and instructor_id = ?', id, name, instructor_id)&lt;br /&gt;
  errors.add(:name, 'Questionnaire names must be unique.') if results.present?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. self.copy_questionnaire_details - clones the contents of a questionnaire, including the items and associated advice&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.copy_questionnaire_details(params)&lt;br /&gt;
  orig_questionnaire = Questionnaire.find(params[:id])&lt;br /&gt;
  items = Item.where(questionnaire_id: params[:id])&lt;br /&gt;
  questionnaire = orig_questionnaire.dup&lt;br /&gt;
  questionnaire.instructor_id = params[:instructor_id]&lt;br /&gt;
  questionnaire.name = 'Copy of ' + orig_questionnaire.name&lt;br /&gt;
  questionnaire.created_at = Time.zone.now&lt;br /&gt;
  questionnaire.save!&lt;br /&gt;
  items.each do |question|&lt;br /&gt;
    new_question = question.dup&lt;br /&gt;
    new_question.questionnaire_id = questionnaire.id&lt;br /&gt;
    new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) &amp;amp;&amp;amp; new_question.size.nil?&lt;br /&gt;
    new_question.save!&lt;br /&gt;
    advice = QuestionAdvice.where(question_id: question.id)&lt;br /&gt;
    next if advice.empty?&lt;br /&gt;
&lt;br /&gt;
    advice.each do |advice|&lt;br /&gt;
      new_advice = advice.dup&lt;br /&gt;
      new_advice.question_id = new_question.id&lt;br /&gt;
      new_advice.save!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
  questionnaire&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. check_for_question_associations - Check_for_question_associations checks if questionnaire has associated items or not&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def check_for_question_associations&lt;br /&gt;
  if items.any?&lt;br /&gt;
    raise ActiveRecord::DeleteRestrictionError.new( &amp;quot;Cannot delete record because dependent items exist&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
4. get_weighted_score - gets the actual weighted score of the assignment by creating a symbol and calling the compute_weighted_score helper method&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def get_weighted_score(assignment, scores)&lt;br /&gt;
  # create symbol for &amp;quot;varying rubrics&amp;quot; feature -Yang&lt;br /&gt;
  round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round&lt;br /&gt;
  questionnaire_symbol = if round.nil?&lt;br /&gt;
                          symbol&lt;br /&gt;
                        else&lt;br /&gt;
                          (symbol.to_s + round.to_s).to_sym&lt;br /&gt;
                        end&lt;br /&gt;
  compute_weighted_score(questionnaire_symbol, assignment, scores)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. compute_weighted_score - helper method that computes the weighted score for the assignment using the questionnaire &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def compute_weighted_score(symbol, assignment, scores)&lt;br /&gt;
      # aq = assignment_questionnaires.find_by(assignment_id: assignment.id)&lt;br /&gt;
      aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
      if scores[symbol][:scores][:avg].nil?&lt;br /&gt;
        0&lt;br /&gt;
      else&lt;br /&gt;
        scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. true_false_items? - Does this questionnaire contain true/false items?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def true_false_items?&lt;br /&gt;
      items.each { |question| return true if question.type == 'Checkbox' }&lt;br /&gt;
      false&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. max_possible_score - calculates the maximum possible score of the questionnaire by inner joining Questionnaire table with Items and then getting the sum of the items and multiplying them by the questionnaire maximum score &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def max_possible_score&lt;br /&gt;
      results = Questionnaire.joins('INNER JOIN items ON items.questionnaire_id = questionnaires.id')&lt;br /&gt;
                            .select('SUM(items.weight) * questionnaires.max_question_score as max_score')&lt;br /&gt;
                            .where('questionnaires.id = ?', id)&lt;br /&gt;
      results[0].max_score&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== QuestionAdvice Model Methods ===&lt;br /&gt;
1. self.export_fields - exports all of the columns of QuestionAdvice as string&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export_fields(_options)&lt;br /&gt;
      QuestionAdvice.columns.map(&amp;amp;:name)&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. self.export - exports the QuestionAdvice entry to a csv file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.export(csv, parent_id, _options)&lt;br /&gt;
      questionnaire = Questionnaire.find(parent_id)&lt;br /&gt;
      questionnaire.items.each do |item|&lt;br /&gt;
        QuestionAdvice.where(question_id: item.id).each do |advice|&lt;br /&gt;
          csv &amp;lt;&amp;lt; advice.attributes.values&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. self.to_json_by_question_id - exports the QuestionAdvice entries to a JSON file based on question id&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def self.to_json_by_question_id(question_id)&lt;br /&gt;
      question_advices = QuestionAdvice.where(question_id: question_id).order(:id)&lt;br /&gt;
      question_advices.map do |advice|&lt;br /&gt;
        { score: advice.score, advice: advice.advice }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Course Model Methods ===&lt;br /&gt;
1. path - Returns the submission directory for the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def path&lt;br /&gt;
    raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil?&lt;br /&gt;
    Rails.root + '/' + Institution.find(institution_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + User.find(instructor_id).name.gsub(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;) + '/' + directory_path + '/'&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. add_ta - Add a Teaching Assistant to the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def add_ta(user)&lt;br /&gt;
    if user.nil?&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} does not exist&amp;quot; }&lt;br /&gt;
    elsif TaMapping.exists?(user_id: user.id, course_id: id)&lt;br /&gt;
      return { success: false, message: &amp;quot;The user with id #{user.id} is already a TA for this course.&amp;quot; }&lt;br /&gt;
    else&lt;br /&gt;
      ta_mapping = TaMapping.create(user_id: user.id, course_id: id)&lt;br /&gt;
      ta_role = Role.find_by(name: 'Teaching Assistant')&lt;br /&gt;
      user.update(role: ta_role) if ta_role&lt;br /&gt;
      if ta_mapping.save&lt;br /&gt;
        return { success: true, data: ta_mapping.slice(:course_id, :user_id) }&lt;br /&gt;
      else&lt;br /&gt;
        return { success: false, message: ta_mapping.errors }&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. remove_ta - Removes Teaching Assistant from the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def remove_ta(user_id)&lt;br /&gt;
    ta_mapping = ta_mappings.find_by(user_id: user_id, course_id: :id)&lt;br /&gt;
    return { success: false, message: &amp;quot;No TA mapping found for the specified course and TA&amp;quot; } if ta_mapping.nil?&lt;br /&gt;
    ta = User.find(ta_mapping.user_id)&lt;br /&gt;
    ta_count = TaMapping.where(user_id: user_id).size - 1&lt;br /&gt;
    if ta_count.zero?&lt;br /&gt;
      ta.update(role: Role::STUDENT)&lt;br /&gt;
    end&lt;br /&gt;
    ta_mapping.destroy&lt;br /&gt;
    { success: true, ta_name: ta.name }&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. copy_course - Creates a copy of the course&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def copy_course&lt;br /&gt;
    new_course = dup&lt;br /&gt;
    new_course.directory_path += '_copy'&lt;br /&gt;
    new_course.name += '_copy'&lt;br /&gt;
    new_course.save&lt;br /&gt;
  end&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Issues in Existing Code==&lt;br /&gt;
&lt;br /&gt;
=== Bugs ===&lt;br /&gt;
The following statements in the Questionnaire model have inconsistent naming as the first statement relies on the relationship &amp;quot;participants&amp;quot; while the second statement relies on the relationship &amp;quot;course_participants&amp;quot; which doesn't exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :course&lt;br /&gt;
has_many :users, through: :course_participants, inverse_of: :course&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Coverage Issues ===&lt;br /&gt;
&lt;br /&gt;
spec/models/questionnaire_spec.rb exists but only covers validations, associations, and copy_questionnaire_details. Several instance methods and the entire QuestionAdvice model have less coverage.&lt;br /&gt;
&lt;br /&gt;
spec/models/course_spec.rb only tests validations and the path method. Three instance methods - add_ta, remove_ta, and copy_course - have zero unit test coverage.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
== Coverage ==&lt;br /&gt;
&lt;br /&gt;
== Conclusion == &lt;br /&gt;
&lt;br /&gt;
== Github == &lt;br /&gt;
Link to Github Project page: [https://github.com/MATTMINWIN/reimplementation-back-end]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167708</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167708"/>
		<updated>2026-04-03T20:23:37Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Dynamic Strategy Configuration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Frontend Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the functional behavior and UI logic for the '''Review Strategy''' tab. The interface manages how reviewers are assigned to submissions, incorporating conditional logic based on global settings defined in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Review Strategy interface allows instructors to toggle between manual and automated assignment models. This version renames legacy strategies to improve clarity and introduces dynamic field visibility based on the system state.&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor initiates configuration via the '''Review strategy''' dropdown:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;): Used for manual or rule-based distributions.&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;): Used for algorithmic, real-time distributions.&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
When '''Static''' is selected, the following interface elements are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:static page.png|right|thumb|600px|Reimplementation of static assignment page]]&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors select from three methods under the prompt: ''&amp;quot;How should reviewers be assigned?&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
# '''Round robin:''' Includes an info tooltip explaining sequential distribution.&lt;br /&gt;
# '''Random:''' Assignments are distributed randomly.&lt;br /&gt;
# '''Import File:''' &lt;br /&gt;
#* '''UI Logic:''' Selecting this option reveals an '''Import Button'''. &lt;br /&gt;
#* '''Visibility:''' The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields are hidden when this method is active.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
Field labels and visibility depend on the '''Calibration for training''' setting in the General tab:&lt;br /&gt;
&lt;br /&gt;
* '''Standard Label:''' &amp;quot;Number of reviews each reviewer is required to do.&amp;quot;&lt;br /&gt;
* '''Calibration Label:''' If Calibration is enabled, the label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and a new field, '''&amp;quot;Number of calibrated reviews&amp;quot;''', becomes visible.&lt;br /&gt;
* '''Pre-submission Assignments:''' A checkbox labeled &amp;quot;Assign reviewers to review projects that have not yet been submitted&amp;quot; (Defaults to '''Checked''').&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Visible only when required parameters are defined. Clicking this triggers the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
When '''Dynamic''' is selected, the following configuration fields are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:dynamic page.png|right|thumb|600px|Reimplementation of dynamic assignment page]]&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input field.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' This field is automatically pre-populated with the value entered in the &amp;quot;required&amp;quot; field.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** '''Visibility:''' Only appears if '''Has topics?''' is enabled on the General tab.&lt;br /&gt;
** '''Default:''' Pre-populated with '''1'''.&lt;br /&gt;
** '''Tooltip:''' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
These settings are persistent across both Static and Dynamic strategies, though some are subject to conditional visibility.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Visibility !! Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see the identity of their reviewers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to assigned peers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Only if '''Has teams?''' is checked || Each team member selects a role and reviews based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Only if '''Has teams?''' is checked || Assignment logic treats the team as a single unit rather than individuals.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Implementation ==&lt;br /&gt;
The frontend captures the current state of all visible fields and transmits the configuration to the backend API. This ensures the specific assignment parameters and conditional logic states are correctly persisted in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167707</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167707"/>
		<updated>2026-04-03T20:23:28Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Dynamic Strategy Configuration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Frontend Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the functional behavior and UI logic for the '''Review Strategy''' tab. The interface manages how reviewers are assigned to submissions, incorporating conditional logic based on global settings defined in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Review Strategy interface allows instructors to toggle between manual and automated assignment models. This version renames legacy strategies to improve clarity and introduces dynamic field visibility based on the system state.&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor initiates configuration via the '''Review strategy''' dropdown:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;): Used for manual or rule-based distributions.&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;): Used for algorithmic, real-time distributions.&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
When '''Static''' is selected, the following interface elements are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:static page.png|right|thumb|600px|Reimplementation of static assignment page]]&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors select from three methods under the prompt: ''&amp;quot;How should reviewers be assigned?&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
# '''Round robin:''' Includes an info tooltip explaining sequential distribution.&lt;br /&gt;
# '''Random:''' Assignments are distributed randomly.&lt;br /&gt;
# '''Import File:''' &lt;br /&gt;
#* '''UI Logic:''' Selecting this option reveals an '''Import Button'''. &lt;br /&gt;
#* '''Visibility:''' The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields are hidden when this method is active.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
Field labels and visibility depend on the '''Calibration for training''' setting in the General tab:&lt;br /&gt;
&lt;br /&gt;
* '''Standard Label:''' &amp;quot;Number of reviews each reviewer is required to do.&amp;quot;&lt;br /&gt;
* '''Calibration Label:''' If Calibration is enabled, the label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and a new field, '''&amp;quot;Number of calibrated reviews&amp;quot;''', becomes visible.&lt;br /&gt;
* '''Pre-submission Assignments:''' A checkbox labeled &amp;quot;Assign reviewers to review projects that have not yet been submitted&amp;quot; (Defaults to '''Checked''').&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Visible only when required parameters are defined. Clicking this triggers the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
When '''Dynamic''' is selected, the following configuration fields are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:dynamic page.png|right|thumb|700px|Reimplementation of dynamic assignment page]]&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input field.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' This field is automatically pre-populated with the value entered in the &amp;quot;required&amp;quot; field.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** '''Visibility:''' Only appears if '''Has topics?''' is enabled on the General tab.&lt;br /&gt;
** '''Default:''' Pre-populated with '''1'''.&lt;br /&gt;
** '''Tooltip:''' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
These settings are persistent across both Static and Dynamic strategies, though some are subject to conditional visibility.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Visibility !! Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see the identity of their reviewers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to assigned peers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Only if '''Has teams?''' is checked || Each team member selects a role and reviews based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Only if '''Has teams?''' is checked || Assignment logic treats the team as a single unit rather than individuals.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Implementation ==&lt;br /&gt;
The frontend captures the current state of all visible fields and transmits the configuration to the backend API. This ensures the specific assignment parameters and conditional logic states are correctly persisted in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167706</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167706"/>
		<updated>2026-04-03T20:23:17Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Dynamic Strategy Configuration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Frontend Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the functional behavior and UI logic for the '''Review Strategy''' tab. The interface manages how reviewers are assigned to submissions, incorporating conditional logic based on global settings defined in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Review Strategy interface allows instructors to toggle between manual and automated assignment models. This version renames legacy strategies to improve clarity and introduces dynamic field visibility based on the system state.&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor initiates configuration via the '''Review strategy''' dropdown:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;): Used for manual or rule-based distributions.&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;): Used for algorithmic, real-time distributions.&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
When '''Static''' is selected, the following interface elements are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:static page.png|right|thumb|600px|Reimplementation of static assignment page]]&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors select from three methods under the prompt: ''&amp;quot;How should reviewers be assigned?&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
# '''Round robin:''' Includes an info tooltip explaining sequential distribution.&lt;br /&gt;
# '''Random:''' Assignments are distributed randomly.&lt;br /&gt;
# '''Import File:''' &lt;br /&gt;
#* '''UI Logic:''' Selecting this option reveals an '''Import Button'''. &lt;br /&gt;
#* '''Visibility:''' The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields are hidden when this method is active.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
Field labels and visibility depend on the '''Calibration for training''' setting in the General tab:&lt;br /&gt;
&lt;br /&gt;
* '''Standard Label:''' &amp;quot;Number of reviews each reviewer is required to do.&amp;quot;&lt;br /&gt;
* '''Calibration Label:''' If Calibration is enabled, the label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and a new field, '''&amp;quot;Number of calibrated reviews&amp;quot;''', becomes visible.&lt;br /&gt;
* '''Pre-submission Assignments:''' A checkbox labeled &amp;quot;Assign reviewers to review projects that have not yet been submitted&amp;quot; (Defaults to '''Checked''').&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Visible only when required parameters are defined. Clicking this triggers the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
When '''Dynamic''' is selected, the following configuration fields are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:dynamic page.png|right|thumb|550px|Reimplementation of dynamic assignment page]]&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input field.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' This field is automatically pre-populated with the value entered in the &amp;quot;required&amp;quot; field.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** '''Visibility:''' Only appears if '''Has topics?''' is enabled on the General tab.&lt;br /&gt;
** '''Default:''' Pre-populated with '''1'''.&lt;br /&gt;
** '''Tooltip:''' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
These settings are persistent across both Static and Dynamic strategies, though some are subject to conditional visibility.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Visibility !! Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see the identity of their reviewers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to assigned peers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Only if '''Has teams?''' is checked || Each team member selects a role and reviews based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Only if '''Has teams?''' is checked || Assignment logic treats the team as a single unit rather than individuals.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Implementation ==&lt;br /&gt;
The frontend captures the current state of all visible fields and transmits the configuration to the backend API. This ensures the specific assignment parameters and conditional logic states are correctly persisted in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Dynamic_page.png&amp;diff=167705</id>
		<title>File:Dynamic page.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Dynamic_page.png&amp;diff=167705"/>
		<updated>2026-04-03T20:22:45Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167704</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167704"/>
		<updated>2026-04-03T20:22:30Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Dynamic Strategy Configuration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Frontend Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the functional behavior and UI logic for the '''Review Strategy''' tab. The interface manages how reviewers are assigned to submissions, incorporating conditional logic based on global settings defined in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Review Strategy interface allows instructors to toggle between manual and automated assignment models. This version renames legacy strategies to improve clarity and introduces dynamic field visibility based on the system state.&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor initiates configuration via the '''Review strategy''' dropdown:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;): Used for manual or rule-based distributions.&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;): Used for algorithmic, real-time distributions.&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
When '''Static''' is selected, the following interface elements are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:static page.png|right|thumb|600px|Reimplementation of static assignment page]]&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors select from three methods under the prompt: ''&amp;quot;How should reviewers be assigned?&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
# '''Round robin:''' Includes an info tooltip explaining sequential distribution.&lt;br /&gt;
# '''Random:''' Assignments are distributed randomly.&lt;br /&gt;
# '''Import File:''' &lt;br /&gt;
#* '''UI Logic:''' Selecting this option reveals an '''Import Button'''. &lt;br /&gt;
#* '''Visibility:''' The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields are hidden when this method is active.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
Field labels and visibility depend on the '''Calibration for training''' setting in the General tab:&lt;br /&gt;
&lt;br /&gt;
* '''Standard Label:''' &amp;quot;Number of reviews each reviewer is required to do.&amp;quot;&lt;br /&gt;
* '''Calibration Label:''' If Calibration is enabled, the label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and a new field, '''&amp;quot;Number of calibrated reviews&amp;quot;''', becomes visible.&lt;br /&gt;
* '''Pre-submission Assignments:''' A checkbox labeled &amp;quot;Assign reviewers to review projects that have not yet been submitted&amp;quot; (Defaults to '''Checked''').&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Visible only when required parameters are defined. Clicking this triggers the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
When '''Dynamic''' is selected, the following configuration fields are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:dynamic page.png|right|thumb|600px|Reimplementation of dynamic assignment page]]&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input field.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' This field is automatically pre-populated with the value entered in the &amp;quot;required&amp;quot; field.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** '''Visibility:''' Only appears if '''Has topics?''' is enabled on the General tab.&lt;br /&gt;
** '''Default:''' Pre-populated with '''1'''.&lt;br /&gt;
** '''Tooltip:''' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
These settings are persistent across both Static and Dynamic strategies, though some are subject to conditional visibility.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Visibility !! Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see the identity of their reviewers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to assigned peers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Only if '''Has teams?''' is checked || Each team member selects a role and reviews based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Only if '''Has teams?''' is checked || Assignment logic treats the team as a single unit rather than individuals.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Implementation ==&lt;br /&gt;
The frontend captures the current state of all visible fields and transmits the configuration to the backend API. This ensures the specific assignment parameters and conditional logic states are correctly persisted in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Static_page.png&amp;diff=167703</id>
		<title>File:Static page.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Static_page.png&amp;diff=167703"/>
		<updated>2026-04-03T20:21:20Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167702</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167702"/>
		<updated>2026-04-03T20:20:57Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Static Strategy Configuration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Frontend Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the functional behavior and UI logic for the '''Review Strategy''' tab. The interface manages how reviewers are assigned to submissions, incorporating conditional logic based on global settings defined in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Review Strategy interface allows instructors to toggle between manual and automated assignment models. This version renames legacy strategies to improve clarity and introduces dynamic field visibility based on the system state.&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor initiates configuration via the '''Review strategy''' dropdown:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;): Used for manual or rule-based distributions.&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;): Used for algorithmic, real-time distributions.&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
When '''Static''' is selected, the following interface elements are exposed:&lt;br /&gt;
&lt;br /&gt;
[[File:static page.png|right|thumb|600px|Reimplementation of static assignment page]]&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors select from three methods under the prompt: ''&amp;quot;How should reviewers be assigned?&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
# '''Round robin:''' Includes an info tooltip explaining sequential distribution.&lt;br /&gt;
# '''Random:''' Assignments are distributed randomly.&lt;br /&gt;
# '''Import File:''' &lt;br /&gt;
#* '''UI Logic:''' Selecting this option reveals an '''Import Button'''. &lt;br /&gt;
#* '''Visibility:''' The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields are hidden when this method is active.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
Field labels and visibility depend on the '''Calibration for training''' setting in the General tab:&lt;br /&gt;
&lt;br /&gt;
* '''Standard Label:''' &amp;quot;Number of reviews each reviewer is required to do.&amp;quot;&lt;br /&gt;
* '''Calibration Label:''' If Calibration is enabled, the label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and a new field, '''&amp;quot;Number of calibrated reviews&amp;quot;''', becomes visible.&lt;br /&gt;
* '''Pre-submission Assignments:''' A checkbox labeled &amp;quot;Assign reviewers to review projects that have not yet been submitted&amp;quot; (Defaults to '''Checked''').&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Visible only when required parameters are defined. Clicking this triggers the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
When '''Dynamic''' is selected, the following configuration fields are exposed:&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input field.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' This field is automatically pre-populated with the value entered in the &amp;quot;required&amp;quot; field.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** '''Visibility:''' Only appears if '''Has topics?''' is enabled on the General tab.&lt;br /&gt;
** '''Default:''' Pre-populated with '''1'''.&lt;br /&gt;
** '''Tooltip:''' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
These settings are persistent across both Static and Dynamic strategies, though some are subject to conditional visibility.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Visibility !! Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see the identity of their reviewers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to assigned peers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Only if '''Has teams?''' is checked || Each team member selects a role and reviews based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Only if '''Has teams?''' is checked || Assignment logic treats the team as a single unit rather than individuals.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Implementation ==&lt;br /&gt;
The frontend captures the current state of all visible fields and transmits the configuration to the backend API. This ensures the specific assignment parameters and conditional logic states are correctly persisted in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167678</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167678"/>
		<updated>2026-03-31T03:24:02Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Frontend Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the functional behavior and UI logic for the '''Review Strategy''' tab. The interface manages how reviewers are assigned to submissions, incorporating conditional logic based on global settings defined in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Review Strategy interface allows instructors to toggle between manual and automated assignment models. This version renames legacy strategies to improve clarity and introduces dynamic field visibility based on the system state.&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor initiates configuration via the '''Review strategy''' dropdown:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;): Used for manual or rule-based distributions.&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;): Used for algorithmic, real-time distributions.&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
When '''Static''' is selected, the following interface elements are exposed:&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors select from three methods under the prompt: ''&amp;quot;How should reviewers be assigned?&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
# '''Round robin:''' Includes an info tooltip explaining sequential distribution.&lt;br /&gt;
# '''Random:''' Assignments are distributed randomly.&lt;br /&gt;
# '''Import File:''' &lt;br /&gt;
#* '''UI Logic:''' Selecting this option reveals an '''Import Button'''. &lt;br /&gt;
#* '''Visibility:''' The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields are hidden when this method is active.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
Field labels and visibility depend on the '''Calibration for training''' setting in the General tab:&lt;br /&gt;
&lt;br /&gt;
* '''Standard Label:''' &amp;quot;Number of reviews each reviewer is required to do.&amp;quot;&lt;br /&gt;
* '''Calibration Label:''' If Calibration is enabled, the label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and a new field, '''&amp;quot;Number of calibrated reviews&amp;quot;''', becomes visible.&lt;br /&gt;
* '''Pre-submission Assignments:''' A checkbox labeled &amp;quot;Assign reviewers to review projects that have not yet been submitted&amp;quot; (Defaults to '''Checked''').&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Visible only when required parameters are defined. Clicking this triggers the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
When '''Dynamic''' is selected, the following configuration fields are exposed:&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input field.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' This field is automatically pre-populated with the value entered in the &amp;quot;required&amp;quot; field.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** '''Visibility:''' Only appears if '''Has topics?''' is enabled on the General tab.&lt;br /&gt;
** '''Default:''' Pre-populated with '''1'''.&lt;br /&gt;
** '''Tooltip:''' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
These settings are persistent across both Static and Dynamic strategies, though some are subject to conditional visibility.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Visibility !! Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see the identity of their reviewers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to assigned peers.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Only if '''Has teams?''' is checked || Each team member selects a role and reviews based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Only if '''Has teams?''' is checked || Assignment logic treats the team as a single unit rather than individuals.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Implementation ==&lt;br /&gt;
The frontend captures the current state of all visible fields and transmits the configuration to the backend API. This ensures the specific assignment parameters and conditional logic states are correctly persisted in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167677</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167677"/>
		<updated>2026-03-31T03:20:39Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Technical Communication */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Required Frontend Changes =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The &amp;quot;Review Strategy&amp;quot; tab manages how reviewers are assigned to submissions. This update renames existing strategies and introduces conditional UI logic based on settings in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor is presented with a '''Review strategy''' dropdown with two options:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;)&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
If '''Static''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors must choose '''&amp;quot;How should reviewers be assigned?&amp;quot;''' from three options:&lt;br /&gt;
# '''Round robin:''' (Includes an info button explaining sequential distribution).&lt;br /&gt;
# '''Random:''' (Reviewers assigned randomly).&lt;br /&gt;
# '''Import File:''' (Specifies custom mappings). &lt;br /&gt;
#* '''Requirement:''' If this is chosen, an '''Import Button''' must appear. The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields (below) should disappear.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' &lt;br /&gt;
** ''Note:'' If '''Calibration for training''' is checked on the General tab, this field label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and an additional field '''&amp;quot;Number of calibrated reviews&amp;quot;''' appears.&lt;br /&gt;
* '''Assign reviewers to review projects that have not yet been submitted:''' A checkbox that defaults to '''Checked'''.&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Must be visible when needed parameters are specified to trigger the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
If '''Dynamic''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' Should be pre-populated automatically with the value entered in the &amp;quot;required&amp;quot; field above.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** ''Visibility:'' Only appears if '''Has topics?''' is checked on the General tab. &lt;br /&gt;
** ''Default:'' Pre-populated with '''1'''.&lt;br /&gt;
** ''Info Button Text:'' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
The following fields appear regardless of whether Static or Dynamic is chosen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Requirement !! Info Button Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see who is reviewing them.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to others.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Visible only if '''Has teams?''' is checked || Every team member chooses a role and reviews contributions based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Visible only if '''Has teams?''' is checked || Teams (not individuals) are assigned to review other teams.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Communication ==&lt;br /&gt;
The frontend captures all the above states and transmits them to the backend API to ensure the assignment configuration is persisted correctly in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167676</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167676"/>
		<updated>2026-03-31T03:20:29Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: /* Technical Communication */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Required Frontend Changes =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The &amp;quot;Review Strategy&amp;quot; tab manages how reviewers are assigned to submissions. This update renames existing strategies and introduces conditional UI logic based on settings in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor is presented with a '''Review strategy''' dropdown with two options:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;)&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
If '''Static''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors must choose '''&amp;quot;How should reviewers be assigned?&amp;quot;''' from three options:&lt;br /&gt;
# '''Round robin:''' (Includes an info button explaining sequential distribution).&lt;br /&gt;
# '''Random:''' (Reviewers assigned randomly).&lt;br /&gt;
# '''Import File:''' (Specifies custom mappings). &lt;br /&gt;
#* '''Requirement:''' If this is chosen, an '''Import Button''' must appear. The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields (below) should disappear.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' &lt;br /&gt;
** ''Note:'' If '''Calibration for training''' is checked on the General tab, this field label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and an additional field '''&amp;quot;Number of calibrated reviews&amp;quot;''' appears.&lt;br /&gt;
* '''Assign reviewers to review projects that have not yet been submitted:''' A checkbox that defaults to '''Checked'''.&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Must be visible when needed parameters are specified to trigger the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
If '''Dynamic''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' Should be pre-populated automatically with the value entered in the &amp;quot;required&amp;quot; field above.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** ''Visibility:'' Only appears if '''Has topics?''' is checked on the General tab. &lt;br /&gt;
** ''Default:'' Pre-populated with '''1'''.&lt;br /&gt;
** ''Info Button Text:'' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
The following fields appear regardless of whether Static or Dynamic is chosen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Requirement !! Info Button Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see who is reviewing them.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to others.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Visible only if '''Has teams?''' is checked || Every team member chooses a role and reviews contributions based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Visible only if '''Has teams?''' is checked || Teams (not individuals) are assigned to review other teams.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Communication ==&lt;br /&gt;
The frontend captures all the above states and transmit them to the backend API to ensure the assignment configuration is persisted correctly in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167669</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167669"/>
		<updated>2026-03-31T03:07:53Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Required Frontend Changes =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The &amp;quot;Review Strategy&amp;quot; tab manages how reviewers are assigned to submissions. This update renames existing strategies and introduces conditional UI logic based on settings in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor is presented with a '''Review strategy''' dropdown with two options:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;)&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
If '''Static''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors must choose '''&amp;quot;How should reviewers be assigned?&amp;quot;''' from three options:&lt;br /&gt;
# '''Round robin:''' (Includes an info button explaining sequential distribution).&lt;br /&gt;
# '''Random:''' (Reviewers assigned randomly).&lt;br /&gt;
# '''Import File:''' (Specifies custom mappings). &lt;br /&gt;
#* '''Requirement:''' If this is chosen, an '''Import Button''' must appear. The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields (below) should disappear.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' &lt;br /&gt;
** ''Note:'' If '''Calibration for training''' is checked on the General tab, this field label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and an additional field '''&amp;quot;Number of calibrated reviews&amp;quot;''' appears.&lt;br /&gt;
* '''Assign reviewers to review projects that have not yet been submitted:''' A checkbox that defaults to '''Checked'''.&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Must be visible when needed parameters are specified to trigger the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
If '''Dynamic''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' Should be pre-populated automatically with the value entered in the &amp;quot;required&amp;quot; field above.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** ''Visibility:'' Only appears if '''Has topics?''' is checked on the General tab. &lt;br /&gt;
** ''Default:'' Pre-populated with '''1'''.&lt;br /&gt;
** ''Info Button Text:'' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
The following fields appear regardless of whether Static or Dynamic is chosen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Requirement !! Info Button Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see who is reviewing them.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to others.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Visible only if '''Has teams?''' is checked || Every team member chooses a role and reviews contributions based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Visible only if '''Has teams?''' is checked || Teams (not individuals) are assigned to review other teams.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Communication ==&lt;br /&gt;
The frontend must capture all the above states and transmit them to the backend API via the {{code|transformAssignmentRequest}} utility to ensure the assignment configuration is persisted correctly in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167668</id>
		<title>CSC/ECE 517 Spring 2026 - E2600. Reimplement review mapping controller</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2600._Reimplement_review_mapping_controller&amp;diff=167668"/>
		<updated>2026-03-31T03:07:14Z</updated>

		<summary type="html">&lt;p&gt;Zobrook2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Review Strategy Tab: Required Frontend Changes =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The &amp;quot;Review Strategy&amp;quot; tab manages how reviewers are assigned to submissions. This update renames existing strategies and introduces conditional UI logic based on settings in the &amp;quot;General&amp;quot; tab (e.g., Calibration, Topics, and Teams).&lt;br /&gt;
&lt;br /&gt;
== Primary Strategy Selection ==&lt;br /&gt;
The instructor is presented with a '''Review strategy''' dropdown with two options:&lt;br /&gt;
* '''Static''' (formerly &amp;quot;Instructor-Selected&amp;quot;)&lt;br /&gt;
* '''Dynamic''' (formerly &amp;quot;Auto-Selected&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
== Static Strategy Configuration ==&lt;br /&gt;
If '''Static''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
=== Assignment Methods ===&lt;br /&gt;
Instructors must choose '''&amp;quot;How should reviewers be assigned?&amp;quot;''' from three options:&lt;br /&gt;
# '''Round robin:''' (Includes an info button explaining sequential distribution).&lt;br /&gt;
# '''Random:''' (Reviewers assigned randomly).&lt;br /&gt;
# '''Import File:''' (Specifies custom mappings). &lt;br /&gt;
#* '''Requirement:''' If this is chosen, an '''Import Button''' must appear. The &amp;quot;Number of reviews&amp;quot; and &amp;quot;Pre-submission&amp;quot; fields (below) should disappear.&lt;br /&gt;
&lt;br /&gt;
=== Review Counts &amp;amp; Logic ===&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' &lt;br /&gt;
** ''Note:'' If '''Calibration for training''' is checked on the General tab, this field label changes to: '''&amp;quot;Number of uncalibrated reviews each reviewer is required to do&amp;quot;''' and an additional field '''&amp;quot;Number of calibrated reviews&amp;quot;''' appears.&lt;br /&gt;
* '''Assign reviewers to review projects that have not yet been submitted:''' A checkbox that defaults to '''Checked'''.&lt;br /&gt;
&lt;br /&gt;
=== Execution ===&lt;br /&gt;
* '''Assign Reviewers Button:''' Must be visible when needed parameters are specified to trigger the backend mapping logic.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
== Dynamic Strategy Configuration ==&lt;br /&gt;
If '''Dynamic''' is selected, the following fields are exposed:&lt;br /&gt;
&lt;br /&gt;
* '''Number of reviews each reviewer is required to do:''' Integer input.&lt;br /&gt;
* '''Max. number of reviews each reviewer is allowed to do:''' Should be pre-populated automatically with the value entered in the &amp;quot;required&amp;quot; field above.&lt;br /&gt;
* '''Review topic threshold (k):''' &lt;br /&gt;
** ''Visibility:'' Only appears if '''Has topics?''' is checked on the General tab. &lt;br /&gt;
** ''Default:'' Pre-populated with '''1'''.&lt;br /&gt;
** ''Info Button Text:'' &amp;quot;A topic is reviewable if the minimum number of reviews already done for the submissions on that topic is within k of the minimum number of reviews done on the least-reviewed submission on any topic.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
== Global Review Settings ==&lt;br /&gt;
The following fields appear regardless of whether Static or Dynamic is chosen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! Requirement !! Info Button Description&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing anonymous?''' || Always visible || Students cannot see who is reviewing them.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are self-reviews required?''' || Always visible || Reviewers/teams must review their own work in addition to others.&lt;br /&gt;
|-&lt;br /&gt;
| '''Is reviewing role-based?''' || Visible only if '''Has teams?''' is checked || Every team member chooses a role and reviews contributions based on a role-specific rubric.&lt;br /&gt;
|-&lt;br /&gt;
| '''Are reviews to be done by teams?''' || Visible only if '''Has teams?''' is checked || Teams (not individuals) are assigned to review other teams.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Technical Communication ==&lt;br /&gt;
The frontend must capture all the above states and transmit them to the backend API via the {{code|transformAssignmentRequest}} utility to ensure the assignment configuration is persisted correctly in the database.&lt;/div&gt;</summary>
		<author><name>Zobrook2</name></author>
	</entry>
</feed>