diff --git a/app/controllers/item_types_controller.rb b/app/controllers/item_types_controller.rb new file mode 100644 index 000000000..a4ef7432c --- /dev/null +++ b/app/controllers/item_types_controller.rb @@ -0,0 +1,7 @@ +class ItemTypesController < ApplicationController + # GET /item_types + def index + item_types = ItemType.all + render json: item_types + end +end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb new file mode 100644 index 000000000..632545208 --- /dev/null +++ b/app/controllers/items_controller.rb @@ -0,0 +1,16 @@ +# app/controllers/items_controller.rb +class ItemsController < ApplicationController + def index + questionnaire = Questionnaire.find(params[:questionnaire_id]) + + Rails.logger.info "Items for Questionnaire #{questionnaire.id}:" + questionnaire.items.each do |item| + Rails.logger.info item.attributes.inspect + end + + items_json = questionnaire.items.as_json + Rails.logger.info "JSON being rendered: #{items_json.inspect}" + + render json: items_json + end +end diff --git a/app/controllers/questionnaire_types_controller.rb b/app/controllers/questionnaire_types_controller.rb new file mode 100644 index 000000000..9321b51a9 --- /dev/null +++ b/app/controllers/questionnaire_types_controller.rb @@ -0,0 +1,7 @@ +class QuestionnaireTypesController < ApplicationController + # GET /questionnaire_types + def index + questionnaire_types = QuestionnaireType.all + render json: questionnaire_types + end +end diff --git a/app/controllers/questionnaires_controller.rb b/app/controllers/questionnaires_controller.rb index 278f70c07..30b2fb354 100644 --- a/app/controllers/questionnaires_controller.rb +++ b/app/controllers/questionnaires_controller.rb @@ -21,39 +21,46 @@ def show # Create method creates a questionnaire and returns the JSON object of the created questionnaire # POST on /questionnaires # Instructor Id statically defined since implementation of Instructor model is out of scope of E2345. - def create - begin - @questionnaire = Questionnaire.new(questionnaire_params) - @questionnaire.display_type = sanitize_display_type(@questionnaire.questionnaire_type) - @questionnaire.save! - render json: @questionnaire, status: :created and return - rescue ActiveRecord::RecordInvalid - render json: $ERROR_INFO.to_s, status: :unprocessable_entity - end + def create + begin + @questionnaire = Questionnaire.new(questionnaire_params) + @questionnaire.display_type = sanitize_display_type(@questionnaire.questionnaire_type) + @questionnaire.save! + render json: @questionnaire, status: :created and return + rescue ActiveRecord::RecordInvalid => e + render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity + + rescue => e + render json: { error: e.message, backtrace: e.backtrace.take(5) }, status: :internal_server_error end +end # Destroy method deletes the questionnaire object with id- {:id} # DELETE on /questionnaires/:id def destroy - begin - @questionnaire = Questionnaire.find(params[:id]) - @questionnaire.delete - rescue ActiveRecord::RecordNotFound + begin + @questionnaire = Questionnaire.find(params[:id]) + @questionnaire.destroy! # ensures dependent items are removed + rescue ActiveRecord::RecordNotFound render json: $ERROR_INFO.to_s, status: :not_found and return end - end +end # Update method updates the questionnaire object with id - {:id} and returns the updated questionnaire JSON object # PUT on /questionnaires/:id - def update - @questionnaire = Questionnaire.find(params[:id]) - if @questionnaire.update(questionnaire_params) - render json: @questionnaire, status: :ok - else - render json: @questionnaire.errors.full_messages, status: :unprocessable_entity - end +def update + @questionnaire = Questionnaire.find(params[:id]) + + if @questionnaire.update(questionnaire_params) + render json: @questionnaire, status: :ok + else + render json: { errors: @questionnaire.errors.full_messages }, status: :unprocessable_entity end +rescue ActiveRecord::RecordNotFound + render json: { error: 'Questionnaire not found' }, status: :not_found +end + # Copy method creates a copy of questionnaire with id - {:id} and return its JSON object # POST on /questionnaires/copy/:id def copy @@ -87,9 +94,16 @@ def toggle_access private def questionnaire_params - params.require(:questionnaire).permit(:name, :questionnaire_type, :private, :min_question_score, :max_question_score, :instructor_id) + params.require(:questionnaire).permit(:name, :questionnaire_type, :private, + :min_question_score, :max_question_score, :instructor_id, + items_attributes: [ + :id, :txt, :question_type, :seq, :weight, + :size, :alternatives, :min_label, :max_label, :textarea_width, :textarea_height, :textbox_width, :col_names, :row_names, + :break_before, :_destroy, :max_value + ]) end + # To match the expected format, replace a space by a for questionnaire types with 2 or more words def sanitize_display_type(type) display_type = type.split('Questionnaire')[0] if %w[AuthorFeedback CourseSurvey TeammateReview GlobalSurvey AssignmentSurvey BookmarkRating].include?(display_type) @@ -98,4 +112,4 @@ def sanitize_display_type(type) display_type end -end \ No newline at end of file +end diff --git a/app/models/Item.rb b/app/models/Item.rb index 7d8cf2ed5..8c4ae8b48 100644 --- a/app/models/Item.rb +++ b/app/models/Item.rb @@ -2,7 +2,7 @@ class Item < ApplicationRecord before_create :set_seq - belongs_to :questionnaire # each item belongs to a specific questionnaire + belongs_to :questionnaire, optional: true has_many :answers, dependent: :destroy, foreign_key: 'item_id' attr_accessor :choice_strategy @@ -18,14 +18,18 @@ def scorable? def scored? question_type.in?(%w[ScaleItem CriterionItem]) end - + def set_seq self.seq = questionnaire.items.size + 1 end def as_json(options = {}) super(options.merge({ - only: %i[txt weight seq question_type size alternatives break_before min_label max_label created_at updated_at], + only: %i[ + id txt weight seq question_type size alternatives break_before + min_label max_label created_at updated_at textarea_width + textarea_height textbox_width col_names row_names + ], include: { questionnaire: { only: %i[name id] } } @@ -73,4 +77,4 @@ def self.for(record) # Cast the existing record to the desired subclass klass.new(record.attributes) end -end \ No newline at end of file +end diff --git a/app/models/item_type.rb b/app/models/item_type.rb new file mode 100644 index 000000000..736a6b268 --- /dev/null +++ b/app/models/item_type.rb @@ -0,0 +1,4 @@ +class ItemType < ApplicationRecord + # Validations + validates :name, presence: true, uniqueness: true +end diff --git a/app/models/questionnaire.rb b/app/models/questionnaire.rb index 82c950ca2..6bd9d5100 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -2,13 +2,14 @@ class Questionnaire < ApplicationRecord belongs_to :instructor - has_many :items, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire + has_many :items, -> { order(:seq) }, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire + accepts_nested_attributes_for :items, allow_destroy: true before_destroy :check_for_question_associations validate :validate_questionnaire validates :name, presence: true validates :max_question_score, :min_question_score, numericality: true - + # after_initialize :post_initialization # @print_name = 'Review Rubric' @@ -28,7 +29,7 @@ def symbol def get_assessments_for(participant) participant.reviews end - + # validate the entries for this questionnaire def validate_questionnaire errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score < 1 @@ -64,9 +65,9 @@ def self.copy_questionnaire_details(params) questionnaire end - # Check_for_question_associations checks if questionnaire has associated questions or not + # Check_for_question_associations checks if questionnaire has associated items or not def check_for_question_associations - if questions.any? + if items.any? raise ActiveRecord::DeleteRestrictionError.new( "Cannot delete record because dependent questions exist") end end @@ -82,4 +83,4 @@ def as_json(options = {}) hash['instructor'] ||= { id: nil, name: nil } end end -end \ No newline at end of file +end diff --git a/app/models/questionnaire_type.rb b/app/models/questionnaire_type.rb new file mode 100644 index 000000000..30e62eea9 --- /dev/null +++ b/app/models/questionnaire_type.rb @@ -0,0 +1,7 @@ +class QuestionnaireType < ApplicationRecord + # Validations + validates :name, presence: true, uniqueness: true + + # Associations (if any later) + # has_many :questionnaires, foreign_key: :questionnaire_type, primary_key: :name +end diff --git a/config/routes.rb b/config/routes.rb index 25642363c..591f010f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,6 +69,13 @@ end end + resources :questionnaire_types, only: [:index] do + end + + resources :item_types, only: [:index] do + end + + resources :questions do collection do get :types @@ -77,6 +84,12 @@ end end + # config/routes.rb + resources :questionnaires do + resources :items, only: [:index] + end + + resources :signed_up_teams do collection do post '/sign_up', to: 'signed_up_teams#sign_up' @@ -149,4 +162,4 @@ get '/:participant_id/instructor_review', to: 'grades#instructor_review' end end -end \ No newline at end of file +end diff --git a/db/migrate/20251013123456_add_textarea_width_to_items.rb b/db/migrate/20251013123456_add_textarea_width_to_items.rb new file mode 100644 index 000000000..38be859b2 --- /dev/null +++ b/db/migrate/20251013123456_add_textarea_width_to_items.rb @@ -0,0 +1,7 @@ +class AddTextareaWidthToItems < ActiveRecord::Migration[8.0] + def change + add_column :items, :textarea_width, :integer + add_column :items, :textarea_height, :integer + add_column :items, :textbox_width, :integer + end +end diff --git a/db/migrate/20251013123457_add_cols_rows_to_items.rb b/db/migrate/20251013123457_add_cols_rows_to_items.rb new file mode 100644 index 000000000..cee2fcd52 --- /dev/null +++ b/db/migrate/20251013123457_add_cols_rows_to_items.rb @@ -0,0 +1,6 @@ +class AddColsRowsToItems < ActiveRecord::Migration[8.0] + def change + add_column :items, :col_names, :string + add_column :items, :row_names, :string + end +end diff --git a/db/migrate/20251230123456_rename_question_types_to_item_types.rb.rb b/db/migrate/20251230123456_rename_question_types_to_item_types.rb.rb new file mode 100644 index 000000000..5a0329015 --- /dev/null +++ b/db/migrate/20251230123456_rename_question_types_to_item_types.rb.rb @@ -0,0 +1,5 @@ +class RenameQuestionTypesToItemTypes < ActiveRecord::Migration[8.0] + def change + rename_table :question_types, :item_types + end +end diff --git a/db/schema.rb b/db/schema.rb index d3a15fcfa..4a7b2a84f 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_10_29_071649) do +ActiveRecord::Schema[8.0].define(version: 2025_12_30_123456) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -188,6 +188,12 @@ t.index ["to_id"], name: "index_invitations_on_to_id" end + create_table "item_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.text "txt" t.integer "weight" @@ -201,6 +207,11 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "questionnaire_id", null: false + t.integer "textarea_width" + t.integer "textarea_height" + t.integer "textbox_width" + t.string "col_names" + t.string "row_names" t.index ["questionnaire_id"], name: "fk_question_questionnaires" t.index ["questionnaire_id"], name: "index_items_on_questionnaire_id" end @@ -256,12 +267,6 @@ t.index ["question_id"], name: "index_question_advices_on_question_id" end - create_table "question_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "questionnaire_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false diff --git a/db/seeds.rb b/db/seeds.rb index 8c11894f0..c3ae81729 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,27 +1,32 @@ # frozen_string_literal: true -begin - # Create an instritution - inst_id = Institution.create!( - name: 'North Carolina State University' - ).id - - Role.create!(id: 1, name: 'Super Administrator') - Role.create!(id: 2, name: 'Administrator') - Role.create!(id: 3, name: 'Instructor') - Role.create!(id: 4, name: 'Teaching Assistant') - Role.create!(id: 5, name: 'Student') - - # Create an admin user - User.create!( - name: 'admin', - email: 'admin2@example.com', - password: 'password123', - full_name: 'admin admin', - institution_id: 1, - role_id: 1 - ) +# Create an institution +inst = Institution.find_or_create_by!( + name: 'North Carolina State University' +) +inst_id = inst.id +# Create Roles +Role.find_or_create_by!(id: 1, name: 'Super Administrator') +Role.find_or_create_by!(id: 2, name: 'Administrator') +Role.find_or_create_by!(id: 3, name: 'Instructor') +Role.find_or_create_by!(id: 4, name: 'Teaching Assistant') +Role.find_or_create_by!(id: 5, name: 'Student') + +# Create an admin user +User.find_or_create_by!(name: 'admin') do |user| + user.email = 'admin2@example.com' + user.password = 'password123' + user.full_name = 'admin admin' + user.institution_id = 1 + user.role_id = 1 +end + +# Check if we should generate random data +# We assume if instructors exist, we've already seeded random data +if User.where(role_id: 3).exists? + puts "Random data already seeded (Instructors found). Skipping..." +else # Generate Random Users num_students = 48 num_assignments = 8 @@ -122,7 +127,44 @@ team_id: team_ids[i%num_teams], ).id end +end + +questionnaire_type_names = [ + 'Review', + 'Author feedback', + 'Teammate review', + 'Survey', + 'Quiz', + 'Bookmark rating', + 'Teammate review', + 'Assignment survey', + 'Course evaluation', + 'Global survey' +] + +questionnaire_types = {} +questionnaire_type_names.each do |type_name| + questionnaire_types[type_name] = QuestionnaireType.find_or_create_by!(name: type_name) +end +puts "Created questionnaire types: #{questionnaire_types.keys.join(', ')}" + +item_type_names = [ + 'Section header', + 'Table header', + 'Column header', + 'Criterion', + 'Text field', + 'Text area', + 'Dropdown', + 'Multiple choice', + 'Scale', + 'Grid', + 'Checkbox', + 'Upload', +] -rescue ActiveRecord::RecordInvalid => e - puts e, 'The db has already been seeded' +item_types = {} +item_type_names.each do |type_name| + item_types[type_name] = ItemType.find_or_create_by!(name: type_name) end +puts "Created item types: #{item_types.keys.join(', ')}"