diff --git a/app/api/api_root.rb b/app/api/api_root.rb index f3d1904fa3..7de6c81051 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -97,6 +97,7 @@ class ApiRoot < Grape::API mount Courseflow::CourseMapUnitApi mount Courseflow::SpecializationApi mount Courseflow::RequirementSetApi + mount Courseflow::RequirementApi mount UnitDefinitionApi # diff --git a/app/api/courseflow/course_map_api.rb b/app/api/courseflow/course_map_api.rb index fb4c03db7f..8682104883 100644 --- a/app/api/courseflow/course_map_api.rb +++ b/app/api/courseflow/course_map_api.rb @@ -14,12 +14,13 @@ class CourseMapApi < Grape::API params do requires :userId, type: Integer, desc: "User ID" end - get '/coursemap/userId/:userId' do - course_map = CourseMap.find_by(userId: params[:userId]) + get '/coursemap/userId/:userId' do + course_map = CourseMap.find_by(userId: params[:userId]) || CourseMap.find_by(userId: nil) + if course_map present course_map, with: Entities::CourseMapEntity else - error!({ error: "Course map #{params[:userId]} not found" }, 404) + error!({ error: "Course map for user #{params[:userId]} not found" }, 404) end end diff --git a/app/api/courseflow/entities/requirement_entity.rb b/app/api/courseflow/entities/requirement_entity.rb new file mode 100644 index 0000000000..b31d0ec4ab --- /dev/null +++ b/app/api/courseflow/entities/requirement_entity.rb @@ -0,0 +1,15 @@ +module Courseflow + module Entities + class RequirementEntity < Grape::Entity + expose :id + expose :unitId + expose :courseId + expose :type + expose :category + expose :description + expose :minimum + expose :maximum + expose :requirementSetGroupId + end + end +end diff --git a/app/api/courseflow/requirement_api.rb b/app/api/courseflow/requirement_api.rb new file mode 100644 index 0000000000..63dbb0cc3a --- /dev/null +++ b/app/api/courseflow/requirement_api.rb @@ -0,0 +1,90 @@ +require 'grape' +module Courseflow + class RequirementApi < Grape::API + format :json + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + before do + authenticated? + end + + desc 'Get all requirements' + get '/requirement' do + present Requirement.all, with: Entities::RequirementEntity + end + + desc 'Get requirements by unit ID' + params do + requires :unitId, type: Integer, desc: 'Unit ID' + end + get '/requirement/unitId/:unitId' do + present Requirement.where(unitId: params[:unitId]), with: Entities::RequirementEntity + end + + desc 'Get requirements by course ID' + params do + requires :courseId, type: Integer, desc: 'Course ID' + end + get '/requirement/courseId/:courseId' do + present Requirement.where(courseId: params[:courseId]), with: Entities::RequirementEntity + end + + desc 'Create a new requirement' + params do + requires :unitId, type: Integer + requires :courseId, type: Integer + requires :type, type: String + requires :category, type: String + requires :description, type: String + optional :minimum, type: Integer + optional :maximum, type: Integer + requires :requirementSetGroupId, type: Integer + end + post '/requirement' do + error!({ error: 'Not authorised' }, 403) unless authorise?(current_user, User, :handle_courseflow) + req = Requirement.new(declared(params, include_missing: false)) + if req.save + status 201 + present req, with: Entities::RequirementEntity + else + error!({ error: 'Failed to create requirement', details: req.errors.full_messages }, 400) + end + end + + desc 'Update a requirement' + params do + requires :id, type: Integer, desc: 'Requirement ID' + optional :unitId, type: Integer + optional :courseId, type: Integer + optional :type, type: String + optional :category, type: String + optional :description, type: String + optional :minimum, type: Integer + optional :maximum, type: Integer + optional :requirementSetGroupId, type: Integer + end + put '/requirement/:id' do + error!({ error: 'Not authorised' }, 403) unless authorise?(current_user, User, :handle_courseflow) + req = Requirement.find_by(id: params[:id]) + error!({ error: 'Requirement not found' }, 404) unless req + if req.update(declared(params, include_missing: false).except(:id)) + present req, with: Entities::RequirementEntity + else + error!({ error: 'Failed to update requirement', details: req.errors.full_messages }, 400) + end + end + + desc 'Delete a requirement' + params do + requires :id, type: Integer, desc: 'Requirement ID' + end + delete '/requirement/:id' do + error!({ error: 'Not authorised' }, 403) unless authorise?(current_user, User, :handle_courseflow) + req = Requirement.find_by(id: params[:id]) + error!({ error: 'Requirement not found' }, 404) unless req + req.destroy + status 204 + end + end +end diff --git a/app/models/courseflow/course_map.rb b/app/models/courseflow/course_map.rb index b2f1494952..b784df7599 100644 --- a/app/models/courseflow/course_map.rb +++ b/app/models/courseflow/course_map.rb @@ -1,7 +1,11 @@ module Courseflow class CourseMap < ApplicationRecord # Validation rules for attributes in the course map model - validates :userId, presence: true + validates :userId, presence: true, unless: :template? validates :courseId, presence: true + + def template? + userId.nil? + end end end diff --git a/app/models/courseflow/requirement.rb b/app/models/courseflow/requirement.rb new file mode 100644 index 0000000000..4a4101c9bb --- /dev/null +++ b/app/models/courseflow/requirement.rb @@ -0,0 +1,11 @@ +module Courseflow + class Requirement < ApplicationRecord + self.inheritance_column = :_type_disabled + + validates :unitId, presence: true + validates :courseId, presence: true + validates :category, presence: true + validates :description, presence: true + validates :requirementSetGroupId, presence: true + end +end diff --git a/db/migrate/20250515025346_create_requirement.rb b/db/migrate/20250515025346_create_requirement.rb new file mode 100644 index 0000000000..3dda33887a --- /dev/null +++ b/db/migrate/20250515025346_create_requirement.rb @@ -0,0 +1,16 @@ +class CreateRequirement < ActiveRecord::Migration[7.1] + def change + create_table :requirements do |t| + t.integer :unitId + t.integer :courseId + t.string :type + t.string :category + t.string :description + t.integer :minimum + t.integer :maximum + t.integer :requirementSetGroupId + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 13783ff85e..45edac32a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_09_10_063917) do - create_table "activity_types", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| +ActiveRecord::Schema[7.1].define(version: 2025_05_15_025346) do + create_table "activity_types", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false t.datetime "created_at", null: false @@ -20,21 +20,21 @@ t.index ["name"], name: "index_activity_types_on_name", unique: true end - create_table "auth_tokens", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "auth_tokens", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "auth_token_expiry", null: false t.bigint "user_id" t.string "authentication_token", null: false t.index ["user_id"], name: "index_auth_tokens_on_user_id" end - create_table "breaks", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "breaks", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "start_date", null: false t.integer "number_of_weeks", null: false t.bigint "teaching_period_id" t.index ["teaching_period_id"], name: "index_breaks_on_teaching_period_id" end - create_table "campuses", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "campuses", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name", null: false t.integer "mode", null: false t.string "abbreviation", null: false @@ -44,7 +44,7 @@ t.index ["name"], name: "index_campuses_on_name", unique: true end - create_table "comments_read_receipts", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "comments_read_receipts", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "task_comment_id", null: false t.bigint "user_id", null: false t.datetime "created_at", null: false @@ -81,7 +81,7 @@ t.datetime "updated_at", null: false end - create_table "discussion_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "discussion_comments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "time_started" t.datetime "time_completed" t.integer "number_of_prompts" @@ -89,7 +89,7 @@ t.datetime "updated_at", null: false end - create_table "group_memberships", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "group_memberships", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "group_id" t.bigint "project_id" t.boolean "active", default: true @@ -99,7 +99,7 @@ t.index ["project_id"], name: "index_group_memberships_on_project_id" end - create_table "group_sets", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "group_sets", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "name" t.boolean "allow_students_to_create_groups", default: true @@ -113,7 +113,7 @@ t.index ["unit_id"], name: "index_group_sets_on_unit_id" end - create_table "group_submissions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "group_submissions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "group_id" t.string "notes" t.bigint "submitted_by_project_id" @@ -125,7 +125,7 @@ t.index ["task_definition_id"], name: "index_group_submissions_on_task_definition_id" end - create_table "groups", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "groups", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "group_set_id" t.bigint "tutorial_id" t.string "name" @@ -138,7 +138,7 @@ t.index ["tutorial_id"], name: "index_groups_on_tutorial_id" end - create_table "learning_outcome_task_links", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "learning_outcome_task_links", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.text "description" t.integer "rating" t.bigint "task_definition_id" @@ -151,7 +151,7 @@ t.index ["task_id"], name: "index_learning_outcome_task_links_on_task_id" end - create_table "learning_outcomes", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "learning_outcomes", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.integer "ilo_number" t.string "name" @@ -161,7 +161,7 @@ t.index ["unit_id"], name: "index_learning_outcomes_on_unit_id" end - create_table "logins", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "logins", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "timestamp" t.bigint "user_id" t.datetime "created_at", null: false @@ -169,7 +169,7 @@ t.index ["user_id"], name: "index_logins_on_user_id" end - create_table "overseer_assessments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "overseer_assessments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.string "submission_timestamp", null: false t.string "result_task_status" @@ -180,7 +180,7 @@ t.index ["task_id"], name: "index_overseer_assessments_on_task_id" end - create_table "overseer_images", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "overseer_images", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "tag", null: false t.datetime "created_at", null: false @@ -192,7 +192,7 @@ t.index ["tag"], name: "index_overseer_images_on_tag", unique: true end - create_table "projects", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "projects", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "project_role" t.datetime "created_at", null: false @@ -228,7 +228,20 @@ t.datetime "updated_at", null: false end - create_table "roles", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "requirements", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.integer "unitId" + t.integer "courseId" + t.string "type" + t.string "category" + t.string "description" + t.integer "minimum" + t.integer "maximum" + t.integer "requirementSetGroupId" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "roles", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", null: false @@ -241,7 +254,7 @@ t.datetime "updated_at", null: false end - create_table "task_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_comments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false t.string "comment", limit: 4096 @@ -272,7 +285,7 @@ t.index ["user_id"], name: "index_task_comments_on_user_id" end - create_table "task_definitions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_definitions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "name" t.string "description", limit: 4096 @@ -305,7 +318,7 @@ t.index ["unit_id"], name: "index_task_definitions_on_unit_id" end - create_table "task_engagements", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_engagements", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "engagement_time" t.string "engagement" t.bigint "task_id" @@ -314,7 +327,7 @@ t.index ["task_id"], name: "index_task_engagements_on_task_id" end - create_table "task_pins", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_pins", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false t.datetime "created_at", null: false @@ -324,7 +337,7 @@ t.index ["user_id"], name: "fk_rails_915df186ed" end - create_table "task_similarities", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_similarities", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "task_id" t.bigint "other_task_id" t.integer "pct" @@ -339,14 +352,14 @@ t.index ["tii_submission_id"], name: "index_task_similarities_on_tii_submission_id" end - create_table "task_statuses", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_statuses", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name" t.string "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_submissions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "task_submissions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "submission_time" t.datetime "assessment_time" t.string "outcome" @@ -358,7 +371,7 @@ t.index ["task_id"], name: "index_task_submissions_on_task_id" end - create_table "tasks", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "tasks", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "task_definition_id" t.bigint "project_id" t.bigint "task_status_id" @@ -384,7 +397,7 @@ t.index ["task_status_id"], name: "index_tasks_on_task_status_id" end - create_table "teaching_periods", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "teaching_periods", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "period", null: false t.datetime "start_date", null: false t.datetime "end_date", null: false @@ -443,7 +456,7 @@ t.index ["tii_task_similarity_id"], name: "index_tii_submissions_on_tii_task_similarity_id" end - create_table "tutorial_enrolments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "tutorial_enrolments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "project_id", null: false @@ -453,7 +466,7 @@ t.index ["tutorial_id"], name: "index_tutorial_enrolments_on_tutorial_id" end - create_table "tutorial_streams", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "tutorial_streams", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false t.datetime "created_at", null: false @@ -467,7 +480,7 @@ t.index ["unit_id"], name: "index_tutorial_streams_on_unit_id" end - create_table "tutorials", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "tutorials", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "meeting_day" t.string "meeting_time" @@ -496,7 +509,7 @@ t.datetime "updated_at", null: false end - create_table "unit_roles", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "unit_roles", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "user_id" t.bigint "tutorial_id" t.datetime "created_at", null: false @@ -509,7 +522,7 @@ t.index ["user_id"], name: "index_unit_roles_on_user_id" end - create_table "units", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "units", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "name" t.string "description", limit: 4096 t.datetime "start_date" @@ -533,8 +546,8 @@ t.bigint "overseer_image_id" t.datetime "portfolio_auto_generation_date" t.string "tii_group_context_id" - t.bigint "unit_definition_id" t.boolean "archived", default: false + t.bigint "unit_definition_id" t.index ["draft_task_definition_id"], name: "index_units_on_draft_task_definition_id" t.index ["main_convenor_id"], name: "index_units_on_main_convenor_id" t.index ["overseer_image_id"], name: "index_units_on_overseer_image_id" @@ -542,7 +555,7 @@ t.index ["unit_definition_id"], name: "index_units_on_unit_definition_id" end - create_table "users", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "users", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" @@ -578,7 +591,7 @@ t.index ["username"], name: "index_users_on_username", unique: true end - create_table "webcal_unit_exclusions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "webcal_unit_exclusions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.bigint "webcal_id", null: false t.bigint "unit_id", null: false t.index ["unit_id", "webcal_id"], name: "index_webcal_unit_exclusions_on_unit_id_and_webcal_id", unique: true @@ -586,7 +599,7 @@ t.index ["webcal_id"], name: "fk_rails_d5fab02cb7" end - create_table "webcals", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "webcals", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| t.string "guid", limit: 36, null: false t.boolean "include_start_dates", default: false, null: false t.bigint "user_id" diff --git a/lib/tasks/test_data_courseflow.rake b/lib/tasks/test_data_courseflow.rake new file mode 100644 index 0000000000..4211e8f7fe --- /dev/null +++ b/lib/tasks/test_data_courseflow.rake @@ -0,0 +1,392 @@ +namespace :db do + namespace :test_data do + desc "Creates test data for frontend development including units, prerequisites, and course association." + task create_frontend_data: :environment do + puts "Starting to create frontend test data..." + + # 1. Ensure Roles exist (essential for user creation) + if Role.count == 0 + puts "-> Generating user roles as they don't exist." + # Simplified role creation based on common roles. Adjust if specific descriptions are needed. + # This logic is similar to what might be in 'db:init' or an equivalent setup task. + roles_to_create = [ + { name: 'Student', description: "Students can enroll in units and submit work." }, + { name: 'Tutor', description: "Tutors can supervise tutorials and provide feedback." }, + { name: 'Convenor', description: "Convenors can manage units and act as tutors/students." }, + { name: 'Admin', description: "Admins have full access, can create convenors, etc." }, + { name: 'Auditor', description: "Auditors have read-only admin access." } + ] + roles_to_create.each do |role_attrs| + Role.find_or_create_by!(name: role_attrs[:name]) do |r| + r.description = role_attrs[:description] + end + print "." + end + puts " Roles generated." + else + puts "Roles already exist." + end + + # 2. Find or Create a Convenor User + convenor_role = Role.find_by!(name: 'Convenor') + admin_role = Role.find_by!(name: 'Admin') # Needed if creating users with system role + + convenor_user = User.find_by(email: 'aconvenor@doubtfire.com') + unless convenor_user + puts "Creating a test convenor user..." + convenor_user = User.create!( + email: 'testconvenor@example.com', + username: 'testconvenor', + login_id: 'testconvenor', # Often same as username + first_name: 'Test', + last_name: 'Convenor', + nickname: 'TestCon', + system_role: 'Convenor', + password: 'password', + password_confirmation: 'password' + ) + puts "Test convenor user created: #{convenor_user.email}" + else + puts "Test convenor user found: #{convenor_user.email}" + end + + # 3. Find or Create a Teaching Period + current_year = Date.today.year + teaching_period = TeachingPeriod.find_or_create_by!(period: 'Trimester 1', year: current_year) do |tp| + tp.start_date = Date.new(current_year, 3, 1) + tp.end_date = Date.new(current_year, 6, 30) + tp.active_until = Date.new(current_year, 7, 31) + puts "Created Teaching Period: #{tp.period} #{tp.year}" + end + puts "Using Teaching Period: #{teaching_period.period} #{teaching_period.year}" + + # 4. Define Unit Data + puts "Defining unit data..." + units_data = [ + # 0-Credit Point Compulsory Units + { code: 'DAI001', name: 'Academic Integrity and Respect at Deakin', credit_points: 0, prerequisites: [] }, + { code: 'STP010', name: 'Career Tools for Employability', credit_points: 0, prerequisites: [] }, + { code: 'SIT010', name: 'Introduction to Online Learning', credit_points: 0, prerequisites: [] }, + + # Core Units (12) + { code: 'SIT102', name: 'Introduction to Programming', credit_points: 1, prerequisites: [] }, + { code: 'SIT111', name: 'Computer Systems', credit_points: 1, prerequisites: [] }, + { code: 'SIT182', name: 'Real World Practices for Cyber Security', credit_points: 1, prerequisites: [] }, + { code: 'SIT112', name: 'Introduction to Data Science and Artificial Intelligence', credit_points: 1, prerequisites: [] }, + { code: 'SIT103', name: 'Database Fundamentals', credit_points: 1, prerequisites: [] }, + { code: 'SIT120', name: 'Introduction to Responsive Web Apps', credit_points: 1, prerequisites: [] }, + { code: 'SIT224', name: 'Information Technology Systems and Innovation', credit_points: 1, prerequisites: [] }, + { code: 'MIS201', name: 'Digital Business Analysis', credit_points: 1, prerequisites: [] }, + { code: 'SIT216', name: 'User-Centered Design', credit_points: 1, prerequisites: [] }, + { code: 'SIT223', name: 'Professional Practice in Information Technology', credit_points: 1, prerequisites: [] }, + { code: 'SIT317', name: 'Information Technology Innovations and Entrepreneurship', credit_points: 1, prerequisites: [] }, + { code: 'SIT328', name: 'Communicating Information Technology Projects', credit_points: 1, prerequisites: ['MIS201'] }, + + # Capstone Units + { code: 'SIT374', name: 'Team Project (A) - Project Management and Practices', credit_points: 1, prerequisites: ['SIT223'] }, + { code: 'SIT344', name: 'Professional Practice', credit_points: 2, prerequisites: ['SIT223'] }, + { code: 'SIT306', name: 'Project Delivery', credit_points: 1, prerequisites: ['SIT374'] }, # Assumed prerequisite + { code: 'SIT378', name: 'Team Project (B) - Execution and Delivery', credit_points: 1, prerequisites: ['SIT374'] }, # Assumed prerequisite + + # Other Units (for prerequisites and electives) + { code: 'SIT232', name: 'Object-Orient ed Development', credit_points: 1, prerequisites: ['SIT102'] }, + { code: 'SIT323', name: 'Cloud Native Application Development', credit_points: 1, prerequisites: ['SIT103', 'SIT232'] } + ] + + created_units_map = {} # To store Unit model instances { unit_code => unit_instance } + + # 5. Create UnitDefinitions and Units + puts "Creating UnitDefinitions and Units..." + units_data.each do |unit_data_hash| + # Create UnitDefinition + unit_def = UnitDefinition.find_or_create_by!(code: unit_data_hash[:code]) do |ud| + ud.name = unit_data_hash[:name] + ud.description = "This unit covers #{unit_data_hash[:name]}. Credit Points: #{unit_data_hash[:credit_points]}." + ud.version = '1.0' + puts "Created UnitDefinition: #{ud.code} - #{ud.name}" + end + + # Find or Create Unit instance + unit_instance = Unit.find_or_initialize_by(unit_definition_id: unit_def.id, teaching_period: teaching_period) + + # Attributes to set/update + unit_attributes_to_set = { + name: unit_def.name, + code: unit_def.code, + description: unit_def.description, + start_date: teaching_period.start_date, + end_date: teaching_period.end_date, + active: (teaching_period.active_until > DateTime.now), + credit_points: unit_data_hash[:credit_points], + prerequisites: unit_data_hash[:prerequisites].to_json, + corequisites: unit_data_hash[:corequisites].to_json + } + + if unit_instance.new_record? + puts "Creating Unit: #{unit_attributes_to_set[:code]} - #{unit_attributes_to_set[:name]} in #{teaching_period.period} #{teaching_period.year}" + else + puts "Updating existing Unit: #{unit_instance.code} in #{teaching_period.period} #{teaching_period.year} with new details." + end + + unit_instance.assign_attributes(unit_attributes_to_set) + unit_instance.save! # Save changes for both new and existing records + + # Assign the convenor_user as the main convenor for this unit_instance + unit_convenor_role = UnitRole.find_or_create_by!(user: convenor_user, unit: unit_instance, role: convenor_role) do |ur| + puts "Assigned #{convenor_user.username} as Convenor for Unit #{unit_instance.code}" + end + + # Update the unit with the main_convenor_id if it's not already set or different + if unit_instance.main_convenor_id != unit_convenor_role.id + unit_instance.update!(main_convenor_id: unit_convenor_role.id) + puts "Set main convenor for Unit #{unit_instance.code} to UnitRole ID #{unit_convenor_role.id}" + end + + created_units_map[unit_data_hash[:code]] = unit_instance + puts "Processed Unit: #{unit_instance.code}" + end + + # 6. Find or Create Course S326 + puts "Initialize course S326 by code." + course_s326 = Courseflow::Course.find_or_create_by!(code: 'S326') do |c| + c.name = 'Bachelor of Information Technology' + c.code = 'S326' + c.year = current_year + c.version = '1.0' + c.url = 'https://www.deakin.edu.au/course/bachelor-information-technology' + puts "Created Course: #{c.code} - #{c.name}" + end + puts "Using Course: #{course_s326.code} (ID: #{course_s326.id})" + + # 7. Create Courseflow::CourseMap for Course S326 + # This represents a specific map or plan for the course, associated with a user. + course_map = Courseflow::CourseMap.find_or_create_by!(courseId: course_s326.id, userId: 1) do |cm| + # Add any other default attributes for CourseMap if necessary + puts "Created CourseMap for Course #{course_s326.code} (ID: #{course_s326.id}) and User aadmin (ID: 1)" + end + puts "Using CourseMap ID: #{course_map.id} (CourseID: #{course_map.courseId}, UserID: 1)" + + # 8. Create Course Rules and Unit Prerequisites + puts "\n--- Creating S326 Course Rules ---" + + # Rule: 0-Credit Point Compulsory Units + compulsory_units = ['DAI001', 'STP010', 'SIT010'] + compulsory_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'compulsory_units', + description: 'Must pass all 0-credit point compulsory units', + minimum: 3, + maximum: 3, + requirementSetGroupId: 0 # Temporary value + ) + compulsory_req.update!(requirementSetGroupId: compulsory_req.id) + compulsory_units.each do |code| + Courseflow::RequirementSet.create!( + requirementSetGroupId: compulsory_req.requirementSetGroupId, + requirementId: compulsory_req.id, + unitId: created_units_map[code].id, + description: "Compulsory unit #{code} - #{created_units_map[code].name}" + ) + end + puts "CREATED: Rule for 0-credit point compulsory units." + + # Rule: Core Units + core_units = ['MIS201', 'SIT102', 'SIT103', 'SIT111', 'SIT112', 'SIT120', 'SIT182', 'SIT216', 'SIT223', 'SIT224', 'SIT317', 'SIT328'] + core_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'core_units', + description: 'Must pass all 12 core units', + minimum: 12, + maximum: 12, + requirementSetGroupId: 0 # Temporary value + ) + core_req.update!(requirementSetGroupId: core_req.id) + core_units.each do |code| + Courseflow::RequirementSet.create!( + requirementSetGroupId: core_req.requirementSetGroupId, + requirementId: core_req.id, + unitId: created_units_map[code].id, + description: "Core unit #{code} - #{created_units_map[code].name}" + ) + end + puts "CREATED: Rule for 12 core units." + + # Rule: Capstone Units (Choice using chained RequirementSets) + capstone_choice_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'capstone_choice', + description: 'Must complete ONE capstone option', + minimum: 1, + maximum: 1, + requirementSetGroupId: 0 # Temporary value + ) + capstone_choice_req.update!(requirementSetGroupId: capstone_choice_req.id) + + # Option A: SIT344 (2cp) + capstone_option_a_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'capstone_option', + description: 'Capstone Option A: SIT344 (2cp)', + requirementSetGroupId: 0 # Temporary value + ) + capstone_option_a_req.update!(requirementSetGroupId: capstone_option_a_req.id) + + # Option B: SIT306, SIT374, SIT378 (3cp) + capstone_option_b_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'capstone_option', + description: 'Capstone Option B: SIT306, SIT374, SIT378', + minimum: 3, + maximum: 3, + requirementSetGroupId: 0 # Temporary value + ) + capstone_option_b_req.update!(requirementSetGroupId: capstone_option_b_req.id) + + Courseflow::RequirementSet.create!( + requirementSetGroupId: capstone_option_a_req.id, + requirementId: capstone_option_a_req.id, + unitId: created_units_map['SIT344'].id, + description: "Complete SIT344" + ) + # RequirementSet for Option B + ['SIT306', 'SIT374', 'SIT378'].each do |code| + Courseflow::RequirementSet.create!( + requirementSetGroupId: capstone_option_b_req.id, + requirementId: capstone_option_b_req.id, + unitId: created_units_map[code].id, + description: "Part of Capstone B" + ) + end + # 3. Create the RequirementSet for the main choice. Instead of pointing to units + # or child groups, it now points directly to the Requirement records for the options. + Courseflow::RequirementSet.create!( + requirementSetGroupId: capstone_choice_req.id, + requirementId: capstone_option_a_req.id, # <-- Points to the Requirement for Option A + unitId: nil, # <-- This is now valid because of our model change + description: "Capstone Option A - SIT344 Professional Practice" + ) + Courseflow::RequirementSet.create!( + requirementSetGroupId: capstone_choice_req.id, + requirementId: capstone_option_b_req.id, # <-- Points to the Requirement for Option B + unitId: nil, # <-- This is now valid + description: "Capstone Option B - SIT306/SIT374/SIT378 Team Projects" + ) + + puts "CREATED: Rule for Capstone unit choice." + + # Rule: Level Restrictions + level1_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'level_restriction', + description: 'Must pass no more than 10 credit points at level 1', + maximum: 10, + requirementSetGroupId: 0 # Temporary value + ) + level1_req.update!(requirementSetGroupId: level1_req.id) + + level3_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'level_restriction', + description: 'Must pass at least 6 credit points at level 3', + minimum: 6, + requirementSetGroupId: 0 # Temporary value + ) + level3_req.update!(requirementSetGroupId: level3_req.id) + puts "CREATED: Rules for unit level restrictions." + + # Rule: Total Credit Points + cp_req = Courseflow::Requirement.create!( + courseId: course_s326.id, + type: 'course', + category: 'credit_points', + description: 'Must pass 24 credit points for course', + minimum: 24, + maximum: 24, + requirementSetGroupId: 0 # Temporary value + ) + cp_req.update!(requirementSetGroupId: cp_req.id) + puts "CREATED: Rule for total course credit points." + # Create Individual Unit Prerequisites + puts "\n--- Creating Individual Unit Prerequisites ---" + units_data.each do |unit_data_hash| + next if unit_data_hash[:prerequisites].empty? + target_unit_model = created_units_map[unit_data_hash[:code]] + + requirement = Courseflow::Requirement.find_or_create_by!( + unitId: target_unit_model.id, + courseId: course_s326.id, + type: 'unit', + category: 'prerequisite', + description: "Prerequisites for #{target_unit_model.code}", + requirementSetGroupId: 0 + ) do |req| + req.minimum = unit_data_hash[:prerequisites].length + req.maximum = unit_data_hash[:prerequisites].length + end + requirement.update!(requirementSetGroupId: requirement.id) if requirement.requirementSetGroupId.nil? + + unit_data_hash[:prerequisites].each do |prereq_code| + prerequisite_unit_model = created_units_map[prereq_code] + next unless prerequisite_unit_model + Courseflow::RequirementSet.find_or_create_by!( + requirementSetGroupId: requirement.requirementSetGroupId, + requirementId: requirement.id, + unitId: prerequisite_unit_model.id, + description: "Prerequisite #{prereq_code} for #{target_unit_model.code}" + ) + end + puts "CREATED: Prerequisites for #{target_unit_model.code}." + end + + # 9. Define the specific slotting information for units + unit_specific_slots = { + 'SIT102' => { year_slot: 1, teaching_period_slot: 1, unit_slot: 1 }, + 'SIT111' => { year_slot: 1, teaching_period_slot: 1, unit_slot: 2 }, + 'SIT182' => { year_slot: 1, teaching_period_slot: 1, unit_slot: 3 }, + 'SIT112' => { year_slot: 1, teaching_period_slot: 1, unit_slot: 4 }, + 'SIT224' => { year_slot: 1, teaching_period_slot: 2, unit_slot: 1 }, + 'SIT103' => { year_slot: 1, teaching_period_slot: 2, unit_slot: 2 }, + 'SIT120' => { year_slot: 1, teaching_period_slot: 2, unit_slot: 3 }, + 'MIS201' => { year_slot: 1, teaching_period_slot: 2, unit_slot: 4 }, + 'SIT216' => { year_slot: 2, teaching_period_slot: 1, unit_slot: 1 }, + 'SIT317' => { year_slot: 2, teaching_period_slot: 2, unit_slot: 1 }, + 'SIT223' => { year_slot: 2, teaching_period_slot: 2, unit_slot: 2 }, + 'SIT374' => { year_slot: 3, teaching_period_slot: 1, unit_slot: 1 }, + 'SIT328' => { year_slot: 3, teaching_period_slot: 1, unit_slot: 2 }, + 'SIT344' => { year_slot: 3, teaching_period_slot: 2, unit_slot: 3 }, + } + + # 10. Create Courseflow::CourseMapUnit entries for each unit in this CourseMap + # These entries link the units to the specific course map, effectively defining its content. + # We'll assign default slotting for demonstration. + puts "Creating CourseMapUnit entries for CourseMap ID #{course_map.id} based on specific slotting..." + created_units_map.each_value do |unit_instance| + slot_info = unit_specific_slots[unit_instance.code] + + if slot_info + Courseflow::CourseMapUnit.find_or_create_by!( + courseMapId: course_map.id, + unitId: unit_instance.id + ) do |cmu| + cmu.yearSlot = slot_info[:year_slot] + cmu.teachingPeriodSlot = slot_info[:teaching_period_slot] + cmu.unitSlot = slot_info[:unit_slot] + # Add any other default attributes for CourseMapUnit if necessary + puts "Created/Found CourseMapUnit for Unit #{unit_instance.code} in CourseMap #{course_map.id} (Y#{cmu.yearSlot}, TP#{cmu.teachingPeriodSlot}, S#{cmu.unitSlot})" + end + else + puts "Skipping CourseMapUnit creation for Unit #{unit_instance.code} as it's not in the specific slotting data." + end + end + + puts "Frontend test data creation finished successfully." + end + end +end diff --git a/test/api/courseflow/requirement_api_test.rb b/test/api/courseflow/requirement_api_test.rb new file mode 100644 index 0000000000..136385b939 --- /dev/null +++ b/test/api/courseflow/requirement_api_test.rb @@ -0,0 +1,85 @@ +require 'test_helper' + +class RequirementApiTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + + def app + Rails.application + end + + setup do + @unit = FactoryBot.create(:unit) + @course = FactoryBot.create(:course) + @attrs = { + unitId: @unit.id, + courseId: @course.id, + type: 'count', + category: 'prerequisite', + description: 'Must complete SIT102 first', + minimum: 1, + maximum: 1, + requirementSetGroupId: 1 + } + add_auth_header_for user: User.first + end + + def teardown + Courseflow::Requirement.destroy_all + @unit.destroy + @course.destroy + end + + def test_get_all_requirements + req = FactoryBot.create(:requirement, @attrs) + get "/api/requirement" + assert_equal 200, last_response.status + ensure + req.destroy + end + + def test_get_requirements_by_unit + req = FactoryBot.create(:requirement, @attrs) + get "/api/requirement/unitId/#{@unit.id}" + assert_equal 200, last_response.status + ensure + req.destroy + end + + def test_get_requirements_by_course + req = FactoryBot.create(:requirement, @attrs) + get "/api/requirement/courseId/#{@course.id}" + assert_equal 200, last_response.status + ensure + req.destroy + end + + def test_create_requirement + post_json "/api/requirement", @attrs + assert_equal 201, last_response.status + assert_equal 'prerequisite', last_response_body['category'] + end + + def test_update_requirement + req = FactoryBot.create(:requirement, @attrs) + put_json "/api/requirement/#{req.id}", description: 'Updated' + assert_equal 200, last_response.status + assert_equal 'Updated', last_response_body['description'] + ensure + req.destroy + end + + def test_delete_requirement + req = FactoryBot.create(:requirement, @attrs) + delete "/api/requirement/#{req.id}" + assert_equal 204, last_response.status + assert_not Courseflow::Requirement.exists?(req.id) + end + + def test_unauthorized_create_requirement + clear_auth_header + post_json "/api/requirement", @attrs + assert_equal 419, last_response.status + end +end diff --git a/test/api/courseflow/template_course_map_test.rb b/test/api/courseflow/template_course_map_test.rb new file mode 100644 index 0000000000..5264a71a68 --- /dev/null +++ b/test/api/courseflow/template_course_map_test.rb @@ -0,0 +1,204 @@ +require 'test_helper' + +class TemplateCourseMapTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::JsonHelper + include TestHelpers::AuthHelper + + def app + Rails.application + end + + def setup + # Create teaching period if needed + @teaching_period = TeachingPeriod.first || FactoryBot.create(:teaching_period) + + # Create users needed for testing + @convenor_user1 = User.find_by(username: 'acain') || FactoryBot.create(:user, username: 'acain', nickname: 'Macite') + @convenor_user2 = User.find_by(username: 'aconvenor') || FactoryBot.create(:user, username: 'aconvenor', nickname: 'The Giant') + @tutor_user = User.find_by(username: 'cliff') || FactoryBot.create(:user, username: 'cliff', nickname: 'Cliff') + + # Create units required for course map + create_sit_units + + # Create a template course map (userId=nil) + @template_course_map = Courseflow::CourseMap.create(courseId: 1, userId: nil) + + # Create the course map units based on your seeds.rb structure + create_course_map_units + + # Create a test student user that has no course map + @student_without_course_map = FactoryBot.create(:user, :student) + end + + def teardown + # Clean up course map units + Courseflow::CourseMapUnit.where(courseMapId: @template_course_map.id).destroy_all + # Clean up course map + @template_course_map.destroy if @template_course_map + + end + + def create_sit_units + # Define the SIT units + @sit_units = { + sit111: { code: "SIT111", name: "Computer Systems" }, + sit192: { code: "SIT192", name: "Discrete Mathematics" }, + sit112: { code: "SIT112", name: "Introduction to Data Science and Artificial Intelligence" }, + sit102: { code: "SIT102", name: "Introduction to Programming" }, + sit232: { code: "SIT232", name: "Object-Oriented Development" }, + sit103: { code: "SIT103", name: "Database Fundamentals" }, + sit292: { code: "SIT292", name: "Linear Algebra for Data Analysis" }, + sit202: { code: "SIT202", name: "Computer Networks and Communication" }, + sit221: { code: "SIT221", name: "Data Structures and Algorithms" }, + sit215: { code: "SIT215", name: "Computational Intelligence" }, + sit223: { code: "SIT223", name: "Professional Practice in Information Technology" }, + sit320: { code: "SIT320", name: "Advanced Algorithms" }, + sit344: { code: "SIT344", name: "Professional Practice" }, + sit378: { code: "SIT378", name: "Team Project (B)" }, + sit315: { code: "SIT315", name: "Concurrent and Distributed Programming" } + } + + # Store the created units for later reference + @created_units = {} + + # Create each unit if it doesn't exist + @sit_units.each do |key, unit_data| + # Skip if the unit already exists + existing_unit = Unit.find_by(code: unit_data[:code]) + if existing_unit + @created_units[key] = existing_unit + next + end + + # Create the unit + unit = Unit.create!( + code: unit_data[:code], + name: unit_data[:name], + description: "#{unit_data[:name]} unit description", + teaching_period: @teaching_period + ) + + # Employ staff + unit.employ_staff(@convenor_user1, Role.convenor) + unit.employ_staff(@convenor_user2, Role.convenor) + unit.employ_staff(@tutor_user, Role.tutor) + + # Store for later use + @created_units[key] = unit + end + end + + def create_course_map_units + units_data = [ + { unit: :sit111, year: 2024, teaching_period: 1, slot: 1 }, + { unit: :sit192, year: 2024, teaching_period: 1, slot: 2 }, + { unit: :sit112, year: 2024, teaching_period: 1, slot: 3 }, + { unit: :sit102, year: 2024, teaching_period: 1, slot: 4 }, + + { unit: :sit232, year: 2024, teaching_period: 2, slot: 1 }, + { unit: :sit103, year: 2024, teaching_period: 2, slot: 2 }, + { unit: :sit292, year: 2024, teaching_period: 2, slot: 3 }, + { unit: :sit202, year: 2024, teaching_period: 2, slot: 4 }, + + { unit: :sit221, year: 2025, teaching_period: 1, slot: 1 }, + { unit: :sit215, year: 2025, teaching_period: 1, slot: 2 }, + + { unit: :sit223, year: 2025, teaching_period: 2, slot: 1 }, + { unit: :sit320, year: 2025, teaching_period: 2, slot: 2 }, + { unit: :sit315, year: 2025, teaching_period: 2, slot: 3 }, + + { unit: :sit344, year: 2026, teaching_period: 1, slot: 1 }, + + { unit: :sit378, year: 2026, teaching_period: 2, slot: 1 } + ] + + units_data.each do |unit_data| + Courseflow::CourseMapUnit.create( + courseMapId: @template_course_map.id, + unitId: @created_units[unit_data[:unit]].id, + yearSlot: unit_data[:year], + teachingPeriodSlot: unit_data[:teaching_period], + unitSlot: unit_data[:slot] + ) + end + end + + # + # Tests + # + + def test_get_template_course_map_when_user_has_no_course_map + add_auth_header_for user: @student_without_course_map + + get "/api/coursemap/userId/#{@student_without_course_map.id}" + + assert_equal 200, last_response.status + + response_data = JSON.parse(last_response.body) + + assert_equal @template_course_map.id, response_data['id'] + assert_equal @template_course_map.courseId, response_data['courseId'] + assert_nil response_data['userId'] # Template has nil userId + end + + def test_user_course_map_prioritized_over_template + student_with_map = FactoryBot.create(:user, :student) + personal_course_map = Courseflow::CourseMap.create(courseId: 1, userId: student_with_map.id) + + Courseflow::CourseMapUnit.create( + courseMapId: personal_course_map.id, + unitId: @created_units[:sit111].id, + yearSlot: 2030, + teachingPeriodSlot: 3, + unitSlot: 5 + ) + + add_auth_header_for user: student_with_map + + get "/api/coursemap/userId/#{student_with_map.id}" + + assert_equal 200, last_response.status + + response_data = JSON.parse(last_response.body) + + # Verify that the personal course map was returned, not the template + assert_equal personal_course_map.id, response_data['id'] + assert_equal personal_course_map.courseId, response_data['courseId'] + assert_equal student_with_map.id, response_data['userId'] + ensure + Courseflow::CourseMapUnit.where(courseMapId: personal_course_map.id).destroy_all if personal_course_map + personal_course_map.destroy if personal_course_map + end + + def test_course_map_units_for_template + add_auth_header_for user: @student_without_course_map + + # Request the course map units for the template + get "/api/coursemapunit/courseMapId/#{@template_course_map.id}" + + assert_equal 200, last_response.status + + response_data = JSON.parse(last_response.body) + + # Verify that we got all course map units + assert_equal 15, response_data.length + + # Verify first unit is SIT111 in the first year and period + first_unit = response_data.find { |unit| unit['unitSlot'] == 1 && unit['yearSlot'] == 2024 && unit['teachingPeriodSlot'] == 1 } + assert_not_nil first_unit + assert_equal @created_units[:sit111].id, first_unit['unitId'] + end + + def test_template_course_map_structure + course_map_units = Courseflow::CourseMapUnit.where(courseMapId: @template_course_map.id) + + # Verify we have units in each expected year and period + assert_equal 4, course_map_units.where(yearSlot: 2024, teachingPeriodSlot: 1).count + assert_equal 4, course_map_units.where(yearSlot: 2024, teachingPeriodSlot: 2).count + assert_equal 2, course_map_units.where(yearSlot: 2025, teachingPeriodSlot: 1).count + assert_equal 3, course_map_units.where(yearSlot: 2025, teachingPeriodSlot: 2).count + assert_equal 1, course_map_units.where(yearSlot: 2026, teachingPeriodSlot: 1).count + assert_equal 1, course_map_units.where(yearSlot: 2026, teachingPeriodSlot: 2).count + end +end diff --git a/test/factories/requirement_factory.rb b/test/factories/requirement_factory.rb new file mode 100644 index 0000000000..b9da0983e7 --- /dev/null +++ b/test/factories/requirement_factory.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :requirement, class: 'Courseflow::Requirement' do + unitId { FactoryBot.create(:unit).id } + courseId { FactoryBot.create(:course).id } + type { 'count' } + category { 'prerequisite' } + description { 'Must complete SIT102 first' } + minimum { 1 } + maximum { 1 } + requirementSetGroupId { 1 } + end +end