From ba829bb5a3a60995f3c6a2a62d7e7a913c4f1409 Mon Sep 17 00:00:00 2001 From: JosephKS10 Date: Fri, 12 Sep 2025 11:30:35 +1000 Subject: [PATCH 1/5] Task 1: Add tutorial self enrolment columns and indexes to task_definitions --- ...rial_self_enrolment_to_task_definitions.rb | 17 +++ db/schema.rb | 110 ++++++++++++------ 2 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb diff --git a/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb b/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb new file mode 100644 index 0000000000..267ec73e00 --- /dev/null +++ b/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb @@ -0,0 +1,17 @@ +class AddTutorialSelfEnrolmentToTaskDefinitions < ActiveRecord::Migration[7.1] + def change + add_column :task_definitions, :tutorial_self_enrolment_enabled, :boolean, default: false, null: false + add_reference :task_definitions, :tutorial_self_enrolment_stream, foreign_key: { to_table: :tutorial_streams }, null: true + + # Add index for better query performance + add_index :task_definitions, :tutorial_self_enrolment_enabled + add_index :task_definitions, :tutorial_self_enrolment_stream_id + end + + def down + remove_index :task_definitions, :tutorial_self_enrolment_enabled + remove_index :task_definitions, :tutorial_self_enrolment_stream_id + remove_reference :task_definitions, :tutorial_self_enrolment_stream + remove_column :task_definitions, :tutorial_self_enrolment_enabled + end +end diff --git a/db/schema.rb b/db/schema.rb index 6daa71ebf1..d11893aa71 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_05_28_223908) do - create_table "activity_types", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| +ActiveRecord::Schema[7.1].define(version: 2025_09_12_012227) do + create_table "activity_types", charset: "utf8mb3", collation: "utf8mb3_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: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "auth_tokens", charset: "utf8mb3", collation: "utf8mb3_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: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "breaks", charset: "utf8mb3", collation: "utf8mb3_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: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "campuses", charset: "utf8mb3", collation: "utf8mb3_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: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "comments_read_receipts", charset: "utf8mb3", collation: "utf8mb3_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 @@ -54,7 +54,7 @@ t.index ["user_id"], name: "index_comments_read_receipts_on_user_id" end - create_table "discussion_comments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "discussion_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "time_started" t.datetime "time_completed" t.integer "number_of_prompts" @@ -62,7 +62,7 @@ t.datetime "updated_at", null: false end - create_table "group_memberships", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "group_memberships", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "group_id" t.bigint "project_id" t.boolean "active", default: true @@ -72,7 +72,7 @@ t.index ["project_id"], name: "index_group_memberships_on_project_id" end - create_table "group_sets", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "group_sets", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "name" t.boolean "allow_students_to_create_groups", default: true @@ -85,7 +85,7 @@ t.index ["unit_id"], name: "index_group_sets_on_unit_id" end - create_table "group_submissions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "group_submissions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "group_id" t.string "notes" t.bigint "submitted_by_project_id" @@ -97,7 +97,7 @@ t.index ["task_definition_id"], name: "index_group_submissions_on_task_definition_id" end - create_table "groups", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "groups", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "group_set_id" t.bigint "tutorial_id" t.string "name" @@ -109,7 +109,7 @@ t.index ["tutorial_id"], name: "index_groups_on_tutorial_id" end - create_table "learning_outcome_task_links", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "learning_outcome_task_links", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.text "description" t.integer "rating" t.bigint "task_definition_id" @@ -122,7 +122,7 @@ t.index ["task_id"], name: "index_learning_outcome_task_links_on_task_id" end - create_table "learning_outcomes", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "learning_outcomes", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.integer "ilo_number" t.string "name" @@ -131,7 +131,7 @@ t.index ["unit_id"], name: "index_learning_outcomes_on_unit_id" end - create_table "logins", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "logins", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "timestamp" t.bigint "user_id" t.datetime "created_at", null: false @@ -139,7 +139,21 @@ t.index ["user_id"], name: "index_logins_on_user_id" end - create_table "overseer_assessments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "marking_sessions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "marker_id", null: false + t.bigint "unit_id", null: false + t.string "ip_address" + t.datetime "start_time" + t.datetime "end_time" + t.integer "duration_minutes" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["marker_id", "unit_id", "ip_address", "updated_at"], name: "index_marking_sessions_on_user_unit_ip_and_time" + t.index ["marker_id"], name: "index_marking_sessions_on_marker_id" + t.index ["unit_id"], name: "index_marking_sessions_on_unit_id" + end + + create_table "overseer_assessments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.string "submission_timestamp", null: false t.string "result_task_status" @@ -150,7 +164,7 @@ t.index ["task_id"], name: "index_overseer_assessments_on_task_id" end - create_table "overseer_images", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "overseer_images", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "tag", null: false t.datetime "created_at", null: false @@ -160,7 +174,7 @@ t.datetime "last_pulled_date" end - create_table "projects", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "projects", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "project_role" t.datetime "created_at", null: false @@ -187,14 +201,29 @@ t.index ["user_id"], name: "index_projects_on_user_id" end - create_table "roles", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "roles", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_comments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "session_activities", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "marking_session_id", null: false + t.string "action" + t.bigint "project_id" + t.bigint "task_id" + t.bigint "task_definition_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["action", "task_id", "created_at"], name: "index_session_activities_on_action_task_created_at" + t.index ["marking_session_id"], name: "index_session_activities_on_marking_session_id" + t.index ["project_id"], name: "index_session_activities_on_project_id" + t.index ["task_definition_id"], name: "index_session_activities_on_task_definition_id" + t.index ["task_id"], name: "index_session_activities_on_task_id" + end + + create_table "task_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false t.string "comment", limit: 4096 @@ -225,7 +254,7 @@ t.index ["user_id"], name: "index_task_comments_on_user_id" end - create_table "task_definitions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_definitions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "name" t.string "description", limit: 4096 @@ -250,13 +279,17 @@ t.bigint "overseer_image_id" t.string "tii_group_id" t.string "moss_language" + t.boolean "tutorial_self_enrolment_enabled", default: false, null: false + t.bigint "tutorial_self_enrolment_stream_id" t.index ["group_set_id"], name: "index_task_definitions_on_group_set_id" t.index ["overseer_image_id"], name: "index_task_definitions_on_overseer_image_id" + t.index ["tutorial_self_enrolment_enabled"], name: "index_task_definitions_on_tutorial_self_enrolment_enabled" + t.index ["tutorial_self_enrolment_stream_id"], name: "index_task_definitions_on_tutorial_self_enrolment_stream_id" t.index ["tutorial_stream_id"], name: "index_task_definitions_on_tutorial_stream_id" t.index ["unit_id"], name: "index_task_definitions_on_unit_id" end - create_table "task_engagements", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_engagements", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "engagement_time" t.string "engagement" t.bigint "task_id" @@ -265,7 +298,7 @@ t.index ["task_id"], name: "index_task_engagements_on_task_id" end - create_table "task_pins", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_pins", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false t.datetime "created_at", null: false @@ -275,7 +308,7 @@ t.index ["user_id"], name: "fk_rails_915df186ed" end - create_table "task_similarities", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_similarities", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id" t.bigint "other_task_id" t.integer "pct" @@ -290,14 +323,14 @@ t.index ["tii_submission_id"], name: "index_task_similarities_on_tii_submission_id" end - create_table "task_statuses", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_statuses", charset: "utf8mb3", collation: "utf8mb3_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: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_submissions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "submission_time" t.datetime "assessment_time" t.string "outcome" @@ -309,7 +342,7 @@ t.index ["task_id"], name: "index_task_submissions_on_task_id" end - create_table "tasks", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tasks", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_definition_id" t.bigint "project_id" t.bigint "task_status_id" @@ -335,7 +368,7 @@ t.index ["task_status_id"], name: "index_tasks_on_task_status_id" end - create_table "teaching_periods", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "teaching_periods", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "period", null: false t.datetime "start_date", null: false t.datetime "end_date", null: false @@ -394,7 +427,7 @@ t.index ["tii_task_similarity_id"], name: "index_tii_submissions_on_tii_task_similarity_id" end - create_table "tutorial_enrolments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tutorial_enrolments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "project_id", null: false @@ -404,7 +437,7 @@ t.index ["tutorial_id"], name: "index_tutorial_enrolments_on_tutorial_id" end - create_table "tutorial_streams", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tutorial_streams", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false t.datetime "created_at", null: false @@ -418,7 +451,7 @@ t.index ["unit_id"], name: "index_tutorial_streams_on_unit_id" end - create_table "tutorials", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tutorials", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "meeting_day" t.string "meeting_time" @@ -437,7 +470,7 @@ t.index ["unit_role_id"], name: "index_tutorials_on_unit_role_id" end - create_table "unit_roles", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "unit_roles", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "user_id" t.bigint "tutorial_id" t.datetime "created_at", null: false @@ -450,7 +483,7 @@ t.index ["user_id"], name: "index_unit_roles_on_user_id" end - create_table "units", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "units", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" t.string "description", limit: 4096 t.datetime "start_date" @@ -480,7 +513,7 @@ t.index ["teaching_period_id"], name: "index_units_on_teaching_period_id" end - create_table "users", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "users", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" @@ -513,7 +546,7 @@ t.index ["role_id"], name: "index_users_on_role_id" end - create_table "webcal_unit_exclusions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "webcal_unit_exclusions", charset: "utf8mb3", collation: "utf8mb3_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 @@ -521,7 +554,7 @@ t.index ["webcal_id"], name: "fk_rails_d5fab02cb7" end - create_table "webcals", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "webcals", charset: "utf8mb3", collation: "utf8mb3_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" @@ -531,4 +564,11 @@ t.index ["user_id"], name: "index_webcals_on_user_id", unique: true end + add_foreign_key "marking_sessions", "units" + add_foreign_key "marking_sessions", "users", column: "marker_id" + add_foreign_key "session_activities", "marking_sessions" + add_foreign_key "session_activities", "projects" + add_foreign_key "session_activities", "task_definitions" + add_foreign_key "session_activities", "tasks" + add_foreign_key "task_definitions", "tutorial_streams", column: "tutorial_self_enrolment_stream_id" end From ea33184443c91e5a31f6b0e5aaf471623f91de5f Mon Sep 17 00:00:00 2001 From: JosephKS10 Date: Sat, 13 Sep 2025 00:32:25 +1000 Subject: [PATCH 2/5] Made changes to the code for Task 1 --- ...rial_self_enrolment_to_task_definitions.rb | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb b/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb index 267ec73e00..52d3887e50 100644 --- a/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb +++ b/db/migrate/20250912012227_add_tutorial_self_enrolment_to_task_definitions.rb @@ -1,17 +1,13 @@ class AddTutorialSelfEnrolmentToTaskDefinitions < ActiveRecord::Migration[7.1] def change - add_column :task_definitions, :tutorial_self_enrolment_enabled, :boolean, default: false, null: false - add_reference :task_definitions, :tutorial_self_enrolment_stream, foreign_key: { to_table: :tutorial_streams }, null: true + unless column_exists?(:task_definitions, :tutorial_self_enrolment_enabled) + add_column :task_definitions, :tutorial_self_enrolment_enabled, :boolean, default: false, null: false + add_index :task_definitions, :tutorial_self_enrolment_enabled + end - # Add index for better query performance - add_index :task_definitions, :tutorial_self_enrolment_enabled - add_index :task_definitions, :tutorial_self_enrolment_stream_id - end - - def down - remove_index :task_definitions, :tutorial_self_enrolment_enabled - remove_index :task_definitions, :tutorial_self_enrolment_stream_id - remove_reference :task_definitions, :tutorial_self_enrolment_stream - remove_column :task_definitions, :tutorial_self_enrolment_enabled + unless column_exists?(:task_definitions, :tutorial_self_enrolment_stream_id) + add_reference :task_definitions, :tutorial_self_enrolment_stream, foreign_key: { to_table: :tutorial_streams }, null: true + add_index :task_definitions, :tutorial_self_enrolment_stream_id + end end end From 5fc9b7a996e0d875423e0b53325f0f93a15187a1 Mon Sep 17 00:00:00 2001 From: Mostafa Nouri Date: Sat, 13 Sep 2025 15:31:28 +1000 Subject: [PATCH 3/5] updated task_defenition and toturial_stream --- app/models/task_definition.rb | 13 ++++ app/models/tutorial_stream.rb | 2 + db/schema.rb | 101 ++++++++++++++++++---------- test/models/task_definition_test.rb | 35 ++++++++++ 4 files changed, 114 insertions(+), 37 deletions(-) diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 7e78bd2a71..88717d9ea5 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -13,6 +13,7 @@ class TaskDefinition < ApplicationRecord belongs_to :group_set, optional: true belongs_to :tutorial_stream, optional: true belongs_to :overseer_image, optional: true + belongs_to :tutorial_self_enrolment_stream, class_name: "TutorialStream", optional: true has_many :tasks, dependent: :destroy # Destroying a task definition will also nuke any instances has_many :group_submissions, dependent: :destroy # Destroying a task definition will also nuke any group submissions @@ -41,6 +42,8 @@ class TaskDefinition < ApplicationRecord validates :weighting, presence: true + validate :self_enrolment_stream_unit_must_match + include TaskDefinitionTiiModule include TaskDefinitionSimilarityModule @@ -56,6 +59,16 @@ def tutorial_stream_present? end end + def tutorial_self_enrolment_enabled? + tutorial_self_enrolment_enabled + end + + def self_enrolment_stream_unit_must_match + if tutorial_self_enrolment_enabled? && tutorial_self_enrolment_stream.present? && tutorial_self_enrolment_stream.unit != unit + errors.add(:tutorial_self_enrolment_stream, "must belong to the same unit") + end + end + # In the rollover process, copy this definition into another unit # Copy this task into the other unit def copy_to(other_unit) diff --git a/app/models/tutorial_stream.rb b/app/models/tutorial_stream.rb index 2b0bb38208..f1cdaf0812 100644 --- a/app/models/tutorial_stream.rb +++ b/app/models/tutorial_stream.rb @@ -9,6 +9,8 @@ class TutorialStream < ApplicationRecord has_many :tutorials, dependent: :destroy has_many :task_definitions, -> { order 'start_date ASC, abbreviation ASC' } + has_many :self_enrolment_task_definitions, class_name: "TaskDefinition", foreign_key: :tutorial_self_enrolment_stream_id, dependent: :nullify + validates :unit, presence: true validates :activity_type, presence: true diff --git a/db/schema.rb b/db/schema.rb index d11893aa71..87ac580b6f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -24,6 +24,8 @@ t.datetime "auth_token_expiry", null: false t.bigint "user_id" t.string "authentication_token", null: false + t.integer "token_type", default: 0, null: false + t.index ["token_type"], name: "index_auth_tokens_on_token_type" t.index ["user_id"], name: "index_auth_tokens_on_user_id" end @@ -54,6 +56,15 @@ t.index ["user_id"], name: "index_comments_read_receipts_on_user_id" end + create_table "d2l_assessment_mappings", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "unit_id", null: false + t.string "org_unit_id" + t.integer "grade_object_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["unit_id"], name: "index_d2l_assessment_mappings_on_unit_id", unique: true + end + create_table "discussion_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "time_started" t.datetime "time_completed" @@ -82,6 +93,7 @@ t.datetime "updated_at" t.integer "capacity" t.boolean "locked", default: false, null: false + t.index ["name", "unit_id"], name: "index_group_sets_on_name_and_unit_id", unique: true t.index ["unit_id"], name: "index_group_sets_on_unit_id" end @@ -106,6 +118,7 @@ t.integer "capacity_adjustment", default: 0, null: false t.boolean "locked", default: false, null: false t.index ["group_set_id"], name: "index_groups_on_group_set_id" + t.index ["name", "group_set_id"], name: "index_groups_on_name_and_group_set_id", unique: true t.index ["tutorial_id"], name: "index_groups_on_tutorial_id" end @@ -128,6 +141,7 @@ t.string "name" t.string "description", limit: 4096 t.string "abbreviation" + t.index ["abbreviation", "unit_id"], name: "index_learning_outcomes_on_abbreviation_and_unit_id", unique: true t.index ["unit_id"], name: "index_learning_outcomes_on_unit_id" end @@ -139,20 +153,6 @@ t.index ["user_id"], name: "index_logins_on_user_id" end - create_table "marking_sessions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "marker_id", null: false - t.bigint "unit_id", null: false - t.string "ip_address" - t.datetime "start_time" - t.datetime "end_time" - t.integer "duration_minutes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["marker_id", "unit_id", "ip_address", "updated_at"], name: "index_marking_sessions_on_user_unit_ip_and_time" - t.index ["marker_id"], name: "index_marking_sessions_on_marker_id" - t.index ["unit_id"], name: "index_marking_sessions_on_unit_id" - end - create_table "overseer_assessments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.string "submission_timestamp", null: false @@ -172,6 +172,8 @@ t.text "pulled_image_text" t.integer "pulled_image_status" t.datetime "last_pulled_date" + t.index ["name"], name: "index_overseer_images_on_name", unique: true + 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| @@ -208,21 +210,6 @@ t.datetime "updated_at", null: false end - create_table "session_activities", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "marking_session_id", null: false - t.string "action" - t.bigint "project_id" - t.bigint "task_id" - t.bigint "task_definition_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["action", "task_id", "created_at"], name: "index_session_activities_on_action_task_created_at" - t.index ["marking_session_id"], name: "index_session_activities_on_marking_session_id" - t.index ["project_id"], name: "index_session_activities_on_project_id" - t.index ["task_definition_id"], name: "index_session_activities_on_task_definition_id" - t.index ["task_id"], name: "index_session_activities_on_task_id" - end - create_table "task_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false @@ -243,10 +230,11 @@ t.integer "extension_weeks" t.string "extension_response" t.bigint "reply_to_id" - t.bigint "overseer_assessment_id" + t.bigint "commentable_id" + t.string "commentable_type" t.index ["assessor_id"], name: "index_task_comments_on_assessor_id" + t.index ["commentable_type", "commentable_id"], name: "index_task_comments_on_commentable_type_and_commentable_id" t.index ["discussion_comment_id"], name: "index_task_comments_on_discussion_comment_id" - t.index ["overseer_assessment_id"], name: "index_task_comments_on_overseer_assessment_id" t.index ["recipient_id"], name: "fk_rails_1dbb49165b" t.index ["reply_to_id"], name: "index_task_comments_on_reply_to_id" t.index ["task_id"], name: "index_task_comments_on_task_id" @@ -279,9 +267,16 @@ t.bigint "overseer_image_id" t.string "tii_group_id" t.string "moss_language" + t.boolean "scorm_enabled", default: false + t.boolean "scorm_allow_review", default: false + t.boolean "scorm_bypass_test", default: false + t.boolean "scorm_time_delay_enabled", default: false + t.integer "scorm_attempt_limit", default: 0 t.boolean "tutorial_self_enrolment_enabled", default: false, null: false t.bigint "tutorial_self_enrolment_stream_id" + t.index ["abbreviation", "unit_id"], name: "index_task_definitions_on_abbreviation_and_unit_id", unique: true t.index ["group_set_id"], name: "index_task_definitions_on_group_set_id" + t.index ["name", "unit_id"], name: "index_task_definitions_on_name_and_unit_id", unique: true t.index ["overseer_image_id"], name: "index_task_definitions_on_overseer_image_id" t.index ["tutorial_self_enrolment_enabled"], name: "index_task_definitions_on_tutorial_self_enrolment_enabled" t.index ["tutorial_self_enrolment_stream_id"], name: "index_task_definitions_on_tutorial_self_enrolment_stream_id" @@ -361,6 +356,7 @@ t.integer "contribution_pts", default: 3 t.integer "quality_pts", default: -1 t.integer "extensions", default: 0, null: false + t.integer "scorm_extensions", default: 0, null: false t.index ["group_submission_id"], name: "index_tasks_on_group_submission_id" t.index ["project_id", "task_definition_id"], name: "tasks_uniq_proj_task_def", unique: true t.index ["project_id"], name: "index_tasks_on_project_id" @@ -377,6 +373,17 @@ t.index ["period", "year"], name: "index_teaching_periods_on_period_and_year", unique: true end + create_table "test_attempts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "task_id" + t.datetime "attempted_time", null: false + t.boolean "terminated", default: false + t.boolean "completion_status", default: false + t.boolean "success_status", default: false + t.float "score_scaled", default: 0.0 + t.text "cmi_datamodel" + t.index ["task_id"], name: "index_test_attempts_on_task_id" + end + create_table "tii_actions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "entity_type" t.bigint "entity_id" @@ -464,6 +471,7 @@ t.integer "capacity", default: -1 t.bigint "campus_id" t.bigint "tutorial_stream_id" + t.index ["abbreviation", "unit_id"], name: "index_tutorials_on_abbreviation_and_unit_id", unique: true t.index ["campus_id"], name: "index_tutorials_on_campus_id" t.index ["tutorial_stream_id"], name: "index_tutorials_on_tutorial_stream_id" t.index ["unit_id"], name: "index_tutorials_on_unit_id" @@ -507,12 +515,32 @@ t.bigint "overseer_image_id" t.datetime "portfolio_auto_generation_date" t.string "tii_group_context_id" + t.boolean "archived", default: false 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" t.index ["teaching_period_id"], name: "index_units_on_teaching_period_id" end + create_table "user_oauth_states", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "state" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["state"], name: "index_user_oauth_states_on_state", unique: true + t.index ["user_id"], name: "index_user_oauth_states_on_user_id" + end + + create_table "user_oauth_tokens", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "user_id", null: false + t.integer "provider", default: 0, null: false + t.text "token" + t.datetime "expires_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_user_oauth_tokens_on_user_id" + end + create_table "users", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -542,8 +570,11 @@ t.string "tii_eula_version" t.datetime "tii_eula_date" t.boolean "tii_eula_version_confirmed", default: false, null: false + t.index ["email"], name: "index_users_on_email", unique: true t.index ["login_id"], name: "index_users_on_login_id", unique: true t.index ["role_id"], name: "index_users_on_role_id" + t.index ["student_id"], name: "index_users_on_student_id", unique: true + 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| @@ -564,11 +595,7 @@ t.index ["user_id"], name: "index_webcals_on_user_id", unique: true end - add_foreign_key "marking_sessions", "units" - add_foreign_key "marking_sessions", "users", column: "marker_id" - add_foreign_key "session_activities", "marking_sessions" - add_foreign_key "session_activities", "projects" - add_foreign_key "session_activities", "task_definitions" - add_foreign_key "session_activities", "tasks" add_foreign_key "task_definitions", "tutorial_streams", column: "tutorial_self_enrolment_stream_id" + add_foreign_key "user_oauth_states", "users" + add_foreign_key "user_oauth_tokens", "users" end diff --git a/test/models/task_definition_test.rb b/test/models/task_definition_test.rb index d20634d8cc..7300d64bb0 100644 --- a/test/models/task_definition_test.rb +++ b/test/models/task_definition_test.rb @@ -269,4 +269,39 @@ def test_delete_unneeded_group_submission_on_group_set_change unit.destroy end + def test_tutorial_self_enrolment_valid_and_invalid + unit = FactoryBot.create(:unit) + activity = FactoryBot.create(:activity_type) + stream1 = FactoryBot.create(:tutorial_stream, unit: unit, activity_type: activity) + stream2 = FactoryBot.create(:tutorial_stream, unit: unit, activity_type: activity) + + #valid case: self-enrolment stream in same unit + td_valid = FactoryBot.build(:task_definition, + unit: unit, + tutorial_stream: stream1, + tutorial_self_enrolment_enabled: true, + tutorial_self_enrolment_stream: stream2 + ) + + assert td_valid.valid?, "TaskDefinition should be valid when self enrolment stream is in the same unit" + td_valid.save! + + assert td_valid.tutorial_self_enrolment_enabled? + assert_equal stream2, td_valid.tutorial_self_enrolment_stream + + # invalid case: self-enrolment stream from another unit + other_unit = FactoryBot.create(:unit) + other_stream = FactoryBot.create(:tutorial_stream, unit: other_unit, activity_type: activity) + + td_invalid = FactoryBot.build(:task_definition, + unit: unit, + tutorial_stream: stream1, + tutorial_self_enrolment_enabled: true, + tutorial_self_enrolment_stream: other_stream + ) + + refute td_invalid.valid?, "TaskDefinition should not be valid with self enrolment stream from another unit" + assert_includes td_invalid.errors[:tutorial_self_enrolment_stream], "must belong to the same unit" + end + end From 95fb2951eabab7666f3ede764f532d40f6bbb7a5 Mon Sep 17 00:00:00 2001 From: Mostafa Nouri Date: Sat, 13 Sep 2025 18:29:57 +1000 Subject: [PATCH 4/5] expose to the API entities to expose tutorial self-enrolment fields to the frontend --- app/api/entities/task_definition_entity.rb | 5 +++ test/api/tasks_api_test.rb | 36 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index 94ba180d48..108f29568a 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -44,5 +44,10 @@ def staff?(my_role) expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) } expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) } expose :moss_language, if: ->(unit, options) { staff?(options[:my_role]) } + expose :tutorial_self_enrolment_enabled + expose :tutorial_self_enrolment_stream_id + expose :tutorial_self_enrolment_stream_abbr, expose_nil: true do |task_definition, _options| + task_definition.tutorial_self_enrolment_stream&.abbreviation + end end end diff --git a/test/api/tasks_api_test.rb b/test/api/tasks_api_test.rb index 1e84f3f3c2..3bd1aef153 100644 --- a/test/api/tasks_api_test.rb +++ b/test/api/tasks_api_test.rb @@ -406,5 +406,39 @@ def test_can_submit_ipynb td.destroy end - + def test_exposes_tutorial_self_enrolment_fields + unit = FactoryBot.create(:unit) + activity = FactoryBot.create(:activity_type) + stream = FactoryBot.create(:tutorial_stream, unit: unit, activity_type: activity) + + # Task with self enrolment enabled + td_enabled = FactoryBot.create( + :task_definition, + unit: unit, + tutorial_stream: stream, + tutorial_self_enrolment_enabled: true, + tutorial_self_enrolment_stream: stream + ) + + json = Entities::TaskDefinitionEntity.represent(td_enabled, my_role: nil).as_json + + assert_equal true, json[:tutorial_self_enrolment_enabled], "expected enrolment flag to be true" + assert_equal stream.id, json[:tutorial_self_enrolment_stream_id], "expected stream id to match" + assert_equal stream.abbreviation, json[:tutorial_self_enrolment_stream_abbr], "expected abbr to match" + + # Task with self enrolment disabled + td_disabled = FactoryBot.create( + :task_definition, + unit: unit, + tutorial_stream: stream, + tutorial_self_enrolment_enabled: false, + tutorial_self_enrolment_stream: nil + ) + + json = Entities::TaskDefinitionEntity.represent(td_disabled, my_role: nil).as_json + + assert_equal false, json[:tutorial_self_enrolment_enabled], "expected enrolment flag to be false" + assert_nil json[:tutorial_self_enrolment_stream_id], "expected no stream id" + assert_nil json[:tutorial_self_enrolment_stream_abbr], "expected no stream abbr" + end end From e3f054bb8c3e5325bb6bd34dc5cb2c1f47f5de02 Mon Sep 17 00:00:00 2001 From: JosephKS10 Date: Thu, 18 Sep 2025 19:35:58 +1000 Subject: [PATCH 5/5] API Controller Implementation for student self enrolment --- app/api/api_root.rb | 3 + app/api/student_tutorial_enrolment_api.rb | 94 +++++++++++++++++++ app/api/task_definitions_api.rb | 56 ++++++++++- app/models/task_definition.rb | 6 ++ .../student_tutorial_enrolment_api_test.rb | 90 ++++++++++++++++++ .../task_definition_self_enrolment_test.rb | 44 +++++++++ 6 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 app/api/student_tutorial_enrolment_api.rb create mode 100644 test/api/student_tutorial_enrolment_api_test.rb create mode 100644 test/models/task_definition_self_enrolment_test.rb diff --git a/app/api/api_root.rb b/app/api/api_root.rb index bde5f02365..dccc1f1ebf 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -86,6 +86,8 @@ class ApiRoot < Grape::API mount UsersApi mount WebcalApi mount WebcalPublicApi + mount StudentTutorialEnrolmentApi + # # Add auth details to all end points @@ -122,6 +124,7 @@ class ApiRoot < Grape::API AuthenticationHelpers.add_auth_to UnitRolesApi AuthenticationHelpers.add_auth_to UnitsApi AuthenticationHelpers.add_auth_to WebcalApi + AuthenticationHelpers.add_auth_to StudentTutorialEnrolmentApi add_swagger_documentation \ base_path: nil, diff --git a/app/api/student_tutorial_enrolment_api.rb b/app/api/student_tutorial_enrolment_api.rb new file mode 100644 index 0000000000..0d674d5f27 --- /dev/null +++ b/app/api/student_tutorial_enrolment_api.rb @@ -0,0 +1,94 @@ +require 'grape' + +class StudentTutorialEnrolmentApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + before do + authenticated? + end + + resource :projects do + route_param :project_id do + resource :tutorial_enrolments do + desc 'Enrol student in tutorial for self-enrolment task' + params do + requires :task_definition_id, type: Integer, desc: 'Task definition with self enrolment enabled' + requires :tutorial_id, type: Integer, desc: 'Tutorial to enrol in' + end + post do + project = Project.find(params[:project_id]) + + # Authorization check + error!('Unauthorized', 401) unless project.student == current_user + + task_def = TaskDefinition.find(params[:task_definition_id]) + tutorial = Tutorial.find(params[:tutorial_id]) + + # Validation checks + error!('Task does not allow self enrolment', 400) unless task_def.tutorial_self_enrolment_enabled? + error!('Tutorial not available for this task', 400) unless task_def.available_tutorials_for_self_enrolment.include?(tutorial) + if tutorial.respond_to?(:at_capacity?) && tutorial.at_capacity? + error!('Tutorial is full', 400) + end + + # Check if already enrolled in this stream + existing_enrolment = project.tutorial_enrolments.joins(:tutorial) + .where(tutorials: { tutorial_stream_id: task_def.tutorial_self_enrolment_stream_id }) + .first + + if existing_enrolment + # Update existing enrolment + existing_enrolment.update!(tutorial: tutorial) + message = 'Tutorial enrolment updated successfully' + else + # Create new enrolment + project.enrol_in(tutorial) + message = 'Tutorial enrolment created successfully' + end + + { + success: true, + message: message, + tutorial: { + id: tutorial.id, + abbreviation: tutorial.abbreviation, + campus_id: tutorial.campus_id + } + } + end + + desc 'Get current tutorial enrolment for task' + params do + requires :task_definition_id, type: Integer, desc: 'Task definition with self enrolment enabled' + end + get do + project = Project.find(params[:project_id]) + + # Authorization check + error!('Unauthorized', 401) unless project.student == current_user || authorise?(current_user, project, :get) + + task_def = TaskDefinition.find(params[:task_definition_id]) + + # Find enrolment in the relevant stream + enrolment = project.tutorial_enrolments.joins(:tutorial) + .where(tutorials: { tutorial_stream_id: task_def.tutorial_self_enrolment_stream_id }) + .first + + if enrolment + { + enrolled: true, + tutorial: { + id: enrolment.tutorial.id, + abbreviation: enrolment.tutorial.abbreviation, + campus_id: enrolment.tutorial.campus_id + } + } + else + { enrolled: false } + end + end + end + end + end +end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 03536c9efd..01fc3d69f9 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -33,6 +33,8 @@ class TaskDefinitionsApi < Grape::API optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer' optional :moss_language, type: String, desc: 'The language to use for code similarity checks' + optional :tutorial_self_enrolment_enabled, type: Boolean, desc: 'Enable tutorial self enrolment' + optional :tutorial_self_enrolment_stream_abbr, type: String, desc: 'Tutorial stream abbreviation for self enrolment' end end post '/units/:unit_id/task_definitions/' do @@ -61,11 +63,24 @@ class TaskDefinitionsApi < Grape::API :max_quality_pts, :assessment_enabled, :overseer_image_id, - :moss_language + :moss_language, + :tutorial_self_enrolment_enabled, + upload_requirements: [] ) task_params[:unit_id] = unit.id - task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) unless params[:task_def][:upload_requirements].nil? + # Handle upload_requirements - check if it's already an array or needs parsing + if params[:task_def][:upload_requirements].present? + if params[:task_def][:upload_requirements].is_a?(Array) + task_params[:upload_requirements] = params[:task_def][:upload_requirements] + elsif params[:task_def][:upload_requirements].is_a?(String) + begin + task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) + rescue JSON::ParserError + error!({ error: 'Invalid upload_requirements format' }, 400) + end + end + end task_def = TaskDefinition.new(task_params) @@ -76,6 +91,12 @@ class TaskDefinitionsApi < Grape::API task_def.tutorial_stream = tutorial_stream end + # Handle tutorial self-enrolment stream + if params[:task_def][:tutorial_self_enrolment_stream_abbr].present? + stream = unit.tutorial_streams.find_by!(abbreviation: params[:task_def][:tutorial_self_enrolment_stream_abbr]) + task_def.tutorial_self_enrolment_stream = stream + end + # # Link in group set if specified # @@ -111,6 +132,8 @@ class TaskDefinitionsApi < Grape::API optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer' optional :moss_language, type: String, desc: 'The language to use for code similarity checks' + optional :tutorial_self_enrolment_enabled, type: Boolean, desc: 'Enable tutorial self enrolment' + optional :tutorial_self_enrolment_stream_abbr, type: String, desc: 'Tutorial stream abbreviation for self enrolment' end end put '/units/:unit_id/task_definitions/:id' do @@ -138,10 +161,23 @@ class TaskDefinitionsApi < Grape::API :max_quality_pts, :assessment_enabled, :overseer_image_id, - :moss_language + :moss_language, + :tutorial_self_enrolment_enabled, + upload_requirements: [] ) - task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) unless params[:task_def][:upload_requirements].nil? + # Handle upload_requirements - check if it's already an array or needs parsing + if params[:task_def][:upload_requirements].present? + if params[:task_def][:upload_requirements].is_a?(Array) + task_params[:upload_requirements] = params[:task_def][:upload_requirements] + elsif params[:task_def][:upload_requirements].is_a?(String) + begin + task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) + rescue JSON::ParserError + error!({ error: 'Invalid upload_requirements format' }, 400) + end + end + end # Ensure changes to a TD defined as a "draft task definition" are validated if unit.draft_task_definition_id == params[:id] @@ -163,6 +199,18 @@ class TaskDefinitionsApi < Grape::API task_def.save! end + # Handle tutorial self-enrolment stream updates + if params[:task_def].key?(:tutorial_self_enrolment_stream_abbr) + if params[:task_def][:tutorial_self_enrolment_stream_abbr].present? + stream = task_def.unit.tutorial_streams.find_by!(abbreviation: params[:task_def][:tutorial_self_enrolment_stream_abbr]) + task_def.tutorial_self_enrolment_stream = stream + task_def.save! + else + task_def.tutorial_self_enrolment_stream = nil + task_def.save! + end + end + # # Link in group set if specified # diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 88717d9ea5..19a1dd78af 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -63,6 +63,12 @@ def tutorial_self_enrolment_enabled? tutorial_self_enrolment_enabled end + def available_tutorials_for_self_enrolment + return Tutorial.none unless tutorial_self_enrolment_enabled? && tutorial_self_enrolment_stream + + tutorial_self_enrolment_stream.tutorials.where(unit: unit) + end + def self_enrolment_stream_unit_must_match if tutorial_self_enrolment_enabled? && tutorial_self_enrolment_stream.present? && tutorial_self_enrolment_stream.unit != unit errors.add(:tutorial_self_enrolment_stream, "must belong to the same unit") diff --git a/test/api/student_tutorial_enrolment_api_test.rb b/test/api/student_tutorial_enrolment_api_test.rb new file mode 100644 index 0000000000..b581f43f87 --- /dev/null +++ b/test/api/student_tutorial_enrolment_api_test.rb @@ -0,0 +1,90 @@ +require "test_helper" + +class StudentTutorialEnrolmentApiTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + + def app + Rails.application + end + + def setup + @student = FactoryBot.create(:user, role: Role.student) + @unit = FactoryBot.create(:unit) + @project = FactoryBot.create(:project, unit: @unit, user: @student) + @activity_type = FactoryBot.create(:activity_type) + @tutorial_stream = FactoryBot.create(:tutorial_stream, unit: @unit, activity_type: @activity_type) + @tutorial = FactoryBot.create(:tutorial, unit: @unit, tutorial_stream: @tutorial_stream) + + @tutorial.update!(campus_id: @project.campus_id) + + @task_def = FactoryBot.create(:task_definition, + unit: @unit, + tutorial_stream: @tutorial_stream, + tutorial_self_enrolment_enabled: true, + tutorial_self_enrolment_stream: @tutorial_stream + ) + + # Set headers with AuthHelper + add_auth_header_for(user: @student) + end + + test "POST enrolment creates new tutorial enrolment" do + post "/api/projects/#{@project.id}/tutorial_enrolments", { + task_definition_id: @task_def.id, + tutorial_id: @tutorial.id + } + + assert_equal 201, last_response.status + response_data = last_response_body + assert response_data["success"] + assert_equal "Tutorial enrolment created successfully", response_data["message"] + end + + test "POST enrolment fails for disabled self enrolment" do + @task_def.update!(tutorial_self_enrolment_enabled: false) + + post "/api/projects/#{@project.id}/tutorial_enrolments", { + task_definition_id: @task_def.id, + tutorial_id: @tutorial.id + } + + assert_equal 400, last_response.status + end + + test "POST enrolment fails for unauthorized user" do + other_student = FactoryBot.create(:user, role: Role.student) + add_auth_header_for(user: other_student) + + post "/api/projects/#{@project.id}/tutorial_enrolments", { + task_definition_id: @task_def.id, + tutorial_id: @tutorial.id + } + + assert_equal 401, last_response.status + end + + test "GET enrolment returns current status when enrolled" do + @project.enrol_in(@tutorial) + + get "/api/projects/#{@project.id}/tutorial_enrolments", { + task_definition_id: @task_def.id + } + + assert_equal 200, last_response.status + response_data = last_response_body + assert response_data["enrolled"] + assert_equal @tutorial.id, response_data["tutorial"]["id"] + end + + test "GET enrolment returns not enrolled status" do + get "/api/projects/#{@project.id}/tutorial_enrolments", { + task_definition_id: @task_def.id + } + + assert_equal 200, last_response.status + response_data = last_response_body + assert_not response_data["enrolled"] + end +end diff --git a/test/models/task_definition_self_enrolment_test.rb b/test/models/task_definition_self_enrolment_test.rb new file mode 100644 index 0000000000..4f6d03385f --- /dev/null +++ b/test/models/task_definition_self_enrolment_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +class TaskDefinitionSelfEnrolmentTest < ActiveSupport::TestCase + def setup + @unit = FactoryBot.create(:unit) + @activity_type = FactoryBot.create(:activity_type) + @tutorial_stream = FactoryBot.create(:tutorial_stream, unit: @unit, activity_type: @activity_type) + @tutorial = FactoryBot.create(:tutorial, unit: @unit, tutorial_stream: @tutorial_stream) + + @task_def = FactoryBot.create(:task_definition, + unit: @unit, + tutorial_stream: @tutorial_stream, + tutorial_self_enrolment_enabled: true, + tutorial_self_enrolment_stream: @tutorial_stream + ) + end + + test "available_tutorials_for_self_enrolment returns correct tutorials" do + available = @task_def.available_tutorials_for_self_enrolment + assert_includes available, @tutorial + assert_equal 1, available.count + end + + test "available_tutorials_for_self_enrolment returns none when disabled" do + @task_def.update!(tutorial_self_enrolment_enabled: false) + available = @task_def.available_tutorials_for_self_enrolment + assert_equal 0, available.count + end + + test "available_tutorials_for_self_enrolment returns none without stream" do + @task_def.update!(tutorial_self_enrolment_stream: nil) + available = @task_def.available_tutorials_for_self_enrolment + assert_equal 0, available.count + end + + test "only returns tutorials from correct stream" do + other_stream = FactoryBot.create(:tutorial_stream, unit: @unit, activity_type: @activity_type) + other_tutorial = FactoryBot.create(:tutorial, unit: @unit, tutorial_stream: other_stream) + + available = @task_def.available_tutorials_for_self_enrolment + assert_includes available, @tutorial + assert_not_includes available, other_tutorial + end +end