From edd2dae441b390f8a270172ef1b7d009bfe2859f Mon Sep 17 00:00:00 2001 From: tulasi210900-cpu Date: Sun, 7 Dec 2025 23:52:12 -0500 Subject: [PATCH 1/2] CRUD for Questionnaire --- app/controllers/items_controller.rb | 16 ++++ app/controllers/question_types_controller.rb | 7 ++ .../questionnaire_types_controller.rb | 7 ++ app/controllers/questionnaires_controller.rb | 66 ++++++++++---- app/models/Item.rb | 12 ++- app/models/question_type.rb | 7 ++ app/models/questionnaire.rb | 11 +-- app/models/questionnaire_type.rb | 7 ++ config/routes.rb | 15 +++- ...51013123456_add_textarea_width_to_items.rb | 7 ++ .../20251013123457_add_cols_rows_to_items.rb | 6 ++ db/schema.rb | 5 ++ db/seeds.rb | 88 ++++++++++++++----- 13 files changed, 203 insertions(+), 51 deletions(-) create mode 100644 app/controllers/items_controller.rb create mode 100644 app/controllers/question_types_controller.rb create mode 100644 app/controllers/questionnaire_types_controller.rb create mode 100644 app/models/question_type.rb create mode 100644 app/models/questionnaire_type.rb create mode 100644 db/migrate/20251013123456_add_textarea_width_to_items.rb create mode 100644 db/migrate/20251013123457_add_cols_rows_to_items.rb 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/question_types_controller.rb b/app/controllers/question_types_controller.rb new file mode 100644 index 000000000..e02544d59 --- /dev/null +++ b/app/controllers/question_types_controller.rb @@ -0,0 +1,7 @@ +class QuestionTypesController < ApplicationController + # GET /item_types + def index + question_types = QuestionType.all + render json: question_types + 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..74d9679c3 100644 --- a/app/controllers/questionnaires_controller.rb +++ b/app/controllers/questionnaires_controller.rb @@ -21,39 +21,62 @@ 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) + @questionnaire = Questionnaire.find(params[:id]) + + ActiveRecord::Base.transaction do + # Delete all existing items to avoid duplicate items + @questionnaire.items.destroy_all + + # Update questionnaire attributes (excluding items) + if @questionnaire.update(questionnaire_params.except(:items_attributes)) + + # Re-create (already existing ones are deleted) items from submitted params + if questionnaire_params[:items_attributes].present? + questionnaire_params[:items_attributes].each do |item_param| + @questionnaire.items.create!(item_param.except(:id, :_destroy)) + end + end + render json: @questionnaire, status: :ok else render json: @questionnaire.errors.full_messages, status: :unprocessable_entity end end +rescue ActiveRecord::RecordInvalid => e + render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity +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 +110,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 +128,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..754986996 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[ + 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/question_type.rb b/app/models/question_type.rb new file mode 100644 index 000000000..09bd11cd0 --- /dev/null +++ b/app/models/question_type.rb @@ -0,0 +1,7 @@ +class QuestionType < 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/app/models/questionnaire.rb b/app/models/questionnaire.rb index 82c950ca2..7ec76a65d 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -3,12 +3,13 @@ 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 + 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..25c955cdf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,6 +69,13 @@ end end + resources :questionnaire_types, only: [:index] do + end + + resources :question_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/schema.rb b/db/schema.rb index d3a15fcfa..3fdecc489 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -201,6 +201,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 diff --git a/db/seeds.rb b/db/seeds.rb index 8c11894f0..7b9659e94 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(', ')}" + +question_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' +question_types = {} +question_type_names.each do |type_name| + question_types[type_name] = QuestionType.find_or_create_by!(name: type_name) end +puts "Created item types: #{question_types.keys.join(', ')}" From 02c6e3ec092835aa7e10d1dc76abb90b948f4531 Mon Sep 17 00:00:00 2001 From: tulasi210900-cpu Date: Wed, 31 Dec 2025 19:57:46 -0500 Subject: [PATCH 2/2] fixed the update for questionnaire and also the naming conventions for item_types --- app/controllers/item_types_controller.rb | 7 +++++ app/controllers/question_types_controller.rb | 7 ----- app/controllers/questionnaires_controller.rb | 26 ++++--------------- app/models/Item.rb | 2 +- app/models/item_type.rb | 4 +++ app/models/question_type.rb | 7 ----- app/models/questionnaire.rb | 2 +- config/routes.rb | 2 +- ..._rename_question_types_to_item_types.rb.rb | 5 ++++ db/schema.rb | 14 +++++----- db/seeds.rb | 10 +++---- 11 files changed, 36 insertions(+), 50 deletions(-) create mode 100644 app/controllers/item_types_controller.rb delete mode 100644 app/controllers/question_types_controller.rb create mode 100644 app/models/item_type.rb delete mode 100644 app/models/question_type.rb create mode 100644 db/migrate/20251230123456_rename_question_types_to_item_types.rb.rb 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/question_types_controller.rb b/app/controllers/question_types_controller.rb deleted file mode 100644 index e02544d59..000000000 --- a/app/controllers/question_types_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class QuestionTypesController < ApplicationController - # GET /item_types - def index - question_types = QuestionType.all - render json: question_types - end -end diff --git a/app/controllers/questionnaires_controller.rb b/app/controllers/questionnaires_controller.rb index 74d9679c3..30b2fb354 100644 --- a/app/controllers/questionnaires_controller.rb +++ b/app/controllers/questionnaires_controller.rb @@ -49,30 +49,14 @@ def destroy # Update method updates the questionnaire object with id - {:id} and returns the updated questionnaire JSON object # PUT on /questionnaires/:id - def update +def update @questionnaire = Questionnaire.find(params[:id]) - ActiveRecord::Base.transaction do - # Delete all existing items to avoid duplicate items - @questionnaire.items.destroy_all - - # Update questionnaire attributes (excluding items) - if @questionnaire.update(questionnaire_params.except(:items_attributes)) - - # Re-create (already existing ones are deleted) items from submitted params - if questionnaire_params[:items_attributes].present? - questionnaire_params[:items_attributes].each do |item_param| - @questionnaire.items.create!(item_param.except(:id, :_destroy)) - end - end - - render json: @questionnaire, status: :ok - else - render json: @questionnaire.errors.full_messages, status: :unprocessable_entity - end + if @questionnaire.update(questionnaire_params) + render json: @questionnaire, status: :ok + else + render json: { errors: @questionnaire.errors.full_messages }, status: :unprocessable_entity end -rescue ActiveRecord::RecordInvalid => e - render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity rescue ActiveRecord::RecordNotFound render json: { error: 'Questionnaire not found' }, status: :not_found end diff --git a/app/models/Item.rb b/app/models/Item.rb index 754986996..8c4ae8b48 100644 --- a/app/models/Item.rb +++ b/app/models/Item.rb @@ -26,7 +26,7 @@ def set_seq def as_json(options = {}) super(options.merge({ only: %i[ - txt weight seq question_type size alternatives break_before + 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 ], 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/question_type.rb b/app/models/question_type.rb deleted file mode 100644 index 09bd11cd0..000000000 --- a/app/models/question_type.rb +++ /dev/null @@ -1,7 +0,0 @@ -class QuestionType < 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/app/models/questionnaire.rb b/app/models/questionnaire.rb index 7ec76a65d..6bd9d5100 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -2,7 +2,7 @@ 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 diff --git a/config/routes.rb b/config/routes.rb index 25c955cdf..591f010f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,7 +72,7 @@ resources :questionnaire_types, only: [:index] do end - resources :question_types, only: [:index] do + resources :item_types, only: [:index] do 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 3fdecc489..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" @@ -261,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 7b9659e94..c3ae81729 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -148,7 +148,7 @@ end puts "Created questionnaire types: #{questionnaire_types.keys.join(', ')}" -question_type_names = [ +item_type_names = [ 'Section header', 'Table header', 'Column header', @@ -163,8 +163,8 @@ 'Upload', ] -question_types = {} -question_type_names.each do |type_name| - question_types[type_name] = QuestionType.find_or_create_by!(name: type_name) +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: #{question_types.keys.join(', ')}" +puts "Created item types: #{item_types.keys.join(', ')}"