diff --git a/Gemfile b/Gemfile index 020dbe491..d3d733e54 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '3.4.5' gem 'mysql2', '~> 0.5.7' gem 'sqlite3', '~> 1.4' # Alternative for development -gem 'puma', '~> 5.0' +gem 'puma', '~> 6.0' gem 'rails', '~> 8.0', '>= 8.0.1' gem 'mini_portile2', '~> 2.8' # Helps with native gem compilation gem 'observer' # Required for Ruby 3.4.5 compatibility with Rails 8.0 diff --git a/Gemfile.lock b/Gemfile.lock index 5e7341cb0..6fdf41173 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,6 +106,7 @@ GEM term-ansicolor thor crass (1.0.6) + csv (3.3.5) danger (9.5.3) base64 (~> 0.2) claide (~> 1.0) @@ -128,6 +129,7 @@ GEM debug (1.11.0) irb (~> 1.10) reline (>= 0.3.8) + delegate (0.4.0) diff-lcs (1.6.2) docile (1.4.1) domain_name (0.6.20240107) @@ -149,8 +151,11 @@ GEM faraday (>= 0.8) faraday-net_http (3.4.1) net-http (>= 0.5.0) + faraday-retry (2.3.2) + faraday (~> 2.0) find_with_order (1.3.1) activerecord (>= 3) + forwardable (1.3.3) git (2.3.3) activesupport (>= 5.0) addressable (~> 2.8) @@ -197,10 +202,13 @@ GEM mime-types-data (~> 3.2025, >= 3.2025.0507) mime-types-data (3.2025.0924) mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (5.25.5) mize (0.6.1) + monitor (0.2.0) msgpack (1.8.0) multi_json (1.17.0) + mutex_m (0.3.0) mysql2 (0.5.7) bigdecimal nap (1.1.0) @@ -217,7 +225,7 @@ GEM net-protocol netrc (0.11.0) nio4r (2.5.9) - nokogiri (1.15.2-aarch64-linux) + nokogiri (1.18.10-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) @@ -230,6 +238,7 @@ GEM faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) + ostruct (0.6.3) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) @@ -350,6 +359,7 @@ GEM addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) securerandom (0.4.1) + set (1.1.2) shoulda-matchers (6.5.0) activesupport (>= 5.2.0) simplecov (0.22.0) @@ -358,7 +368,10 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) + singleton (0.3.0) spring (4.4.0) + sqlite3 (1.7.3) + mini_portile2 (~> 2.8.0) stringio (3.1.7) sync (0.5.0) term-ansicolor (1.11.3) @@ -398,18 +411,30 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10.0) bcrypt (~> 3.1.7) + bigdecimal bootsnap (>= 1.18.4) coveralls + csv danger database_cleaner-active_record + date debug + delegate factory_bot_rails faker + faraday-retry find_with_order + forwardable jwt (~> 2.7, >= 2.7.1) lingua - mysql2 (~> 0.5.5) + logger + mini_portile2 (~> 2.8) + monitor + mutex_m + mysql2 (~> 0.5.7) observer + ostruct + psych (~> 5.2) puma (~> 6.0) rack-cors rails (~> 8.0, >= 8.0.1) @@ -418,14 +443,19 @@ DEPENDENCIES rswag-specs rswag-ui rubocop + set shoulda-matchers simplecov simplecov_json_formatter + singleton spring + sqlite3 (~> 1.4) + timeout tzinfo-data + uri RUBY VERSION - ruby 3.2.7p253 + ruby 3.4.5p51 BUNDLED WITH 2.4.14 diff --git a/app/controllers/sign_up_topics_controller.rb b/app/controllers/sign_up_topics_controller.rb index 11ddf5c7b..9294548e6 100644 --- a/app/controllers/sign_up_topics_controller.rb +++ b/app/controllers/sign_up_topics_controller.rb @@ -16,6 +16,35 @@ def index # render json: {message: 'All selected topics have been loaded successfully.', sign_up_topics: @stopics}, status: 200 end + def rubric_list + if params[:assignment_id].nil? + render json: { message: 'Assignment ID is required!' }, status: :unprocessable_entity + return + end + + assignment = Assignment.find_by(id: params[:assignment_id]) + if assignment.nil? + render json: { message: 'Assignment not found!' }, status: :not_found + return + end + + topics = assignment.sign_up_topics.includes(:questionnaire) + render json: topics.map { |t| serialize_topic_with_rubric(t) }, status: :ok + end + + private + def serialize_topic_with_rubric(topic) + { + id: topic.id, + name: topic.topic_name, + identifier: topic.topic_identifier, + max_choosers: topic.max_choosers, + questionnaire_id: topic.questionnaire_id, + questionnaire_name: topic.questionnaire&.name, + assignment_id: topic.assignment_id + } + end + # POST /sign_up_topics # The create method allows the instructor to create a new topic # params[:sign_up_topic][:topic_identifier] follows a json format diff --git a/app/helpers/topic_rubrics_helper.rb b/app/helpers/topic_rubrics_helper.rb new file mode 100644 index 000000000..c263ad1ab --- /dev/null +++ b/app/helpers/topic_rubrics_helper.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module TopicRubricsHelper + # Assign a questionnaire to a specific topic + def self.assign_rubric_to_topic(topic_id, questionnaire_id, assignment_id, round = nil) + topic = SignUpTopic.find(topic_id) + assignment = Assignment.find(assignment_id) + questionnaire = Questionnaire.find(questionnaire_id) + + # Validate that the questionnaire belongs to this assignment + unless assignment.questionnaires.include?(questionnaire) + raise ArgumentError, 'Questionnaire must be associated with this assignment' + end + + # Check if assignment_questionnaire already exists + assignment_questionnaire = AssignmentQuestionnaire.find_or_initialize_by( + assignment_id: assignment_id, + questionnaire_id: questionnaire_id, + topic_id: topic_id, + used_in_round: round + ) + + assignment_questionnaire.save! + assignment_questionnaire + end + + # Remove rubric assignment from a topic + def self.remove_rubric_from_topic(topic_id, assignment_id, round = nil) + AssignmentQuestionnaire.where( + assignment_id: assignment_id, + topic_id: topic_id, + used_in_round: round + ).destroy_all + end + + # Get the rubric for a topic (with fallback to default) + def self.get_rubric_for_topic(topic_id, round = nil) + topic = SignUpTopic.find(topic_id) + topic.rubric_for_review(round) + end +end \ No newline at end of file diff --git a/app/models/assignment_questionnaire.rb b/app/models/assignment_questionnaire.rb index 87a55883d..eff37cb24 100644 --- a/app/models/assignment_questionnaire.rb +++ b/app/models/assignment_questionnaire.rb @@ -3,4 +3,5 @@ class AssignmentQuestionnaire < ApplicationRecord belongs_to :assignment belongs_to :questionnaire + belongs_to :topic, class_name: 'SignUpTopic', foreign_key: 'topic_id', optional: true end diff --git a/app/models/sign_up_topic.rb b/app/models/sign_up_topic.rb index 1d89b687b..a30d04933 100644 --- a/app/models/sign_up_topic.rb +++ b/app/models/sign_up_topic.rb @@ -1,9 +1,37 @@ # frozen_string_literal: true +# frozen_string_literal: true + class SignUpTopic < ApplicationRecord has_many :signed_up_teams, foreign_key: 'topic_id', dependent: :destroy - has_many :teams, through: :signed_up_teams # list all teams choose this topic, no matter in waitlist or not + has_many :teams, through: :signed_up_teams has_many :assignment_questionnaires, class_name: 'AssignmentQuestionnaire', foreign_key: 'topic_id', dependent: :destroy - has_many :due_dates, as: :parent,class_name: 'DueDate', dependent: :destroy + has_many :due_dates, as: :parent, class_name: 'DueDate', dependent: :destroy belongs_to :assignment + belongs_to :questionnaire, optional: true + + # Get the rubric/questionnaire for this topic + # Falls back to the default assignment rubric if no topic-specific rubric exists + def rubric_for_review(round = nil) + # First, try to find a topic-specific rubric + topic_questionnaire = assignment_questionnaires.find_by( + assignment_id: assignment_id, + used_in_round: round + )&.questionnaire + + return topic_questionnaire if topic_questionnaire.present? + + # Fall back to the default assignment rubric + assignment.questionnaires.find_by( + assignment_questionnaires: { used_in_round: round } + ) + end + + # Check if this topic has a specific rubric assigned + def has_specific_rubric?(round = nil) + assignment_questionnaires.exists?( + assignment_id: assignment_id, + used_in_round: round + ) + end end diff --git a/config/application.rb b/config/application.rb index 798f8702b..f78d01563 100644 --- a/config/application.rb +++ b/config/application.rb @@ -17,7 +17,7 @@ def self.preview_path=(_) module Reimplementation class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 + config.load_defaults 8.0 config.active_record.schema_format = :ruby # Configuration for the application, engines, and railties goes here. diff --git a/config/routes.rb b/config/routes.rb index b77a95f63..2dcf733c1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,12 +90,16 @@ end end - - - resources :sign_up_topics do + resources :sign_up_topics, only: [:index, :create, :update, :show, :destroy] do + member do + post :assign_rubric + delete :remove_rubric + get :show_rubric + end collection do get :filter delete '/', to: 'sign_up_topics#destroy' + get :rubric_list end end @@ -141,4 +145,6 @@ delete :delete_participants end end + + end diff --git a/db/migrate/20251029210525_add_topic_id_to_assignment_questionnaires.rb b/db/migrate/20251029210525_add_topic_id_to_assignment_questionnaires.rb new file mode 100644 index 000000000..56f033af6 --- /dev/null +++ b/db/migrate/20251029210525_add_topic_id_to_assignment_questionnaires.rb @@ -0,0 +1,7 @@ +class AddTopicIdToAssignmentQuestionnaires < ActiveRecord::Migration[8.0] + def change + add_column :assignment_questionnaires, :topic_id, :bigint + add_index :assignment_questionnaires, :topic_id + add_foreign_key :assignment_questionnaires, :sign_up_topics, column: :topic_id + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 462029322..2eec7e2c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_04_27_014225) do +ActiveRecord::Schema[8.0].define(version: 2025_10_29_210525) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -43,8 +43,10 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "used_in_round" + t.bigint "topic_id" t.index ["assignment_id"], name: "fk_aq_assignments_id" t.index ["questionnaire_id"], name: "fk_aq_questionnaire_id" + t.index ["topic_id"], name: "index_assignment_questionnaires_on_topic_id" end create_table "assignments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -408,6 +410,7 @@ add_foreign_key "account_requests", "institutions" add_foreign_key "account_requests", "roles" + add_foreign_key "assignment_questionnaires", "sign_up_topics", column: "topic_id" add_foreign_key "assignments", "courses" add_foreign_key "assignments", "users", column: "instructor_id" add_foreign_key "courses", "institutions" diff --git a/docker-compose.yml b/docker-compose.yml index f22dc27ef..1da02be58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: command: tail -f /dev/null environment: RAILS_ENV: development - DATABASE_URL: mysql2://root:expertiza@db:3306/reimplementation? + DATABASE_URL: mysql2://root:expertiza@db:3306/reimplementation CACHE_STORE: redis://redis:6380/0 ports: - "3002:3002" @@ -24,7 +24,7 @@ services: ports: - "3307:3306" volumes: - - expertiza-mysql:/var/lib/mysql2/data + - expertiza-mysql:/var/lib/mysql - ./db:/docker-entrypoint-initdb.d redis: diff --git a/spec/models/sign_up_topic_spec.rb b/spec/models/sign_up_topic_spec.rb new file mode 100644 index 000000000..84e0c77fe --- /dev/null +++ b/spec/models/sign_up_topic_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe SignUpTopic, type: :model do + let(:assignment) { create(:assignment) } + let(:questionnaire) { create(:questionnaire) } + let(:topic) { create(:sign_up_topic, assignment: assignment) } + + describe 'associations' do + it { should have_many(:assignment_questionnaires) } + it { should belong_to(:questionnaire).optional } + end + + describe '#rubric_for_review' do + context 'when topic has a specific rubric' do + before do + create(:assignment_questionnaire, + assignment: assignment, + questionnaire: questionnaire, + topic: topic) + end + + it 'returns the topic-specific rubric' do + expect(topic.rubric_for_review).to eq(questionnaire) + end + end + + context 'when topic has no specific rubric' do + before do + create(:assignment_questionnaire, + assignment: assignment, + questionnaire: questionnaire, + topic: nil) # default rubric + end + + it 'falls back to the default assignment rubric' do + expect(topic.rubric_for_review).to eq(questionnaire) + end + end + end + + describe '#has_specific_rubric?' do + it 'returns true when topic has a specific rubric' do + create(:assignment_questionnaire, + assignment: assignment, + questionnaire: questionnaire, + topic: topic) + + expect(topic.has_specific_rubric?).to be true + end + + it 'returns false when topic has no specific rubric' do + expect(topic.has_specific_rubric?).to be false + end + end +end \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9d528540b..4ca2c3910 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -77,7 +77,14 @@ end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = Rails.root.join('spec/fixtures') + # config.fixture_path = Rails.root.join('spec/fixtures') + + if config.respond_to?(:fixture_paths=) + config.fixture_paths = [Rails.root.join('spec/fixtures').to_s] + else + # Fallback for older Rails / rspec-rails + config.fixture_path = Rails.root.join('spec/fixtures') + end # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false