-
Notifications
You must be signed in to change notification settings - Fork 194
E2608. Hierarchical list display #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
639f7da
cfe8d88
4e4110a
fcbbd4b
f60611b
6fa372e
d2d0e41
36d5383
c2f5e42
96c940a
6418df3
fd90cff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,3 +32,8 @@ | |
| coverage/ | ||
| rsa_keys.yml | ||
| pg_data/ | ||
|
|
||
| # Ignore ruby version | ||
| .ruby-version | ||
| Gemfile | ||
| Gemfile.lock | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| ruby-3.4.5 | ||
| 3.4.5 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,4 +21,4 @@ RUN bundle install | |
| EXPOSE 3002 | ||
|
|
||
| # Set the entry point | ||
| ENTRYPOINT ["/app/setup.sh"] | ||
| ENTRYPOINT ["/app/setup.sh"] | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,64 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| class QuestionnairesController < ApplicationController | ||||||||||||||||||||||||||||||||||||||||||||||||||
| DISPLAY_TYPES = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Review', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Metareview', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Author feedback', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Teammate Review', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Assignment survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Global survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Course survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Bookmark rating', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Quiz' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ].freeze | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| TYPE_DISPLAY_MAP = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'ReviewQuestionnaire' => 'Review', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'MetareviewQuestionnaire' => 'Metareview', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Author FeedbackQuestionnaire' => 'Author feedback', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'AuthorFeedbackQuestionnaire' => 'Author feedback', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Teammate ReviewQuestionnaire' => 'Teammate Review', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'TeammateReviewQuestionnaire' => 'Teammate Review', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'SurveyQuestionnaire' => 'Survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'AssignmentSurveyQuestionnaire' => 'Assignment survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Assignment SurveyQuestionnaire' => 'Assignment survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Global SurveyQuestionnaire' => 'Global survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'GlobalSurveyQuestionnaire' => 'Global survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Course SurveyQuestionnaire' => 'Course survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'CourseSurveyQuestionnaire' => 'Course survey', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Bookmark RatingQuestionnaire' => 'Bookmark rating', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'BookmarkRatingQuestionnaire' => 'Bookmark rating', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'QuizQuestionnaire' => 'Quiz' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }.freeze | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # Index method returns the list of JSON objects of the questionnaire | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # GET on /questionnaires | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def index | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaires = Questionnaire.order(:id) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: @questionnaires, status: :ok and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # Hierarchical list of questionnaire types and questionnaires available to the current user. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # GET on /questionnaires/hierarchical | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def hierarchical | ||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaires = Questionnaire | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .includes(:instructor) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .where(private: false) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .or(Questionnaire.where(instructor_id: current_user.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .order(:name) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .distinct | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| grouped_questionnaires = questionnaires.group_by do |questionnaire| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| display_type_for(questionnaire.questionnaire_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: DISPLAY_TYPES.map { |display_type| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: display_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaires: (grouped_questionnaires[display_type] || []).map(&:as_json) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, status: :ok and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # Show method returns the JSON object of questionnaire with id = {:id} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # GET on /questionnaires/:id | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,18 +70,30 @@ def show | |||||||||||||||||||||||||||||||||||||||||||||||||
| render json: $ERROR_INFO.to_s, status: :not_found and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # GET /questionnaires/:id/items | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def items | ||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaire = Questionnaire.find(params[:id]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: questionnaire.items.order(:seq), status: :ok and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue ActiveRecord::RecordNotFound | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: { error: "Questionnaire not found" }, status: :not_found and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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_attributes, item_attributes = split_questionnaire_params | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire = Questionnaire.new(questionnaire_attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire.display_type = sanitize_display_type(@questionnaire.questionnaire_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire.save! | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Questionnaire.transaction do | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire.save! | ||||||||||||||||||||||||||||||||||||||||||||||||||
| sync_items!(@questionnaire, item_attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: @questionnaire, status: :created and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue ActiveRecord::RecordInvalid | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: $ERROR_INFO.to_s, status: :unprocessable_entity | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: { errors: $ERROR_INFO.record.errors.full_messages }, status: :unprocessable_entity | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -37,9 +102,12 @@ def create | |||||||||||||||||||||||||||||||||||||||||||||||||
| def destroy | ||||||||||||||||||||||||||||||||||||||||||||||||||
| begin | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire = Questionnaire.find(params[:id]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire.delete | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire.destroy! | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: { message: 'Questionnaire deleted successfully' }, status: :ok and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue ActiveRecord::RecordNotFound | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: $ERROR_INFO.to_s, status: :not_found and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue ActiveRecord::RecordNotDestroyed, ActiveRecord::InvalidForeignKey | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: { error: $ERROR_INFO.message }, status: :unprocessable_entity and return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -48,11 +116,16 @@ def destroy | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaire_attributes, item_attributes = split_questionnaire_params | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Questionnaire.transaction do | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @questionnaire.update!(questionnaire_attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| sync_items!(@questionnaire, item_attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: @questionnaire, status: :ok | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue ActiveRecord::RecordInvalid | ||||||||||||||||||||||||||||||||||||||||||||||||||
| render json: { errors: $ERROR_INFO.record.errors.full_messages }, status: :unprocessable_entity | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # Copy method creates a copy of questionnaire with id - {:id} and return its JSON object | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # POST on /questionnaires/copy/:id | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -87,7 +160,31 @@ 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: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+163
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not permit client-controlled Permitting 🛡️ Proposed fix def questionnaire_params
params.require(:questionnaire).permit(
:name,
:questionnaire_type,
:private,
:min_question_score,
:max_question_score,
- :instructor_id,
items_attributes: [
:id,
:txt,
:question_type,
@@# additionally in create/update assignment path
questionnaire_attributes[:instructor_id] = current_user.id🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| :id, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :txt, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :question_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :weight, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :alternatives, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :min_label, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :max_label, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :seq, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :break_before, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :textarea_width, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :textarea_height, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :textbox_width, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :row_names, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :col_names, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :_destroy | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def sanitize_display_type(type) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,4 +195,78 @@ def sanitize_display_type(type) | |||||||||||||||||||||||||||||||||||||||||||||||||
| display_type | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def display_type_for(questionnaire_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| TYPE_DISPLAY_MAP.fetch(questionnaire_type, questionnaire_type.to_s.delete_suffix('Questionnaire')) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def split_questionnaire_params | ||||||||||||||||||||||||||||||||||||||||||||||||||
| permitted_params = questionnaire_params.to_h.deep_symbolize_keys | ||||||||||||||||||||||||||||||||||||||||||||||||||
| items_attributes = permitted_params.delete(:items_attributes) || [] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| [permitted_params, items_attributes] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def sync_items!(questionnaire, item_attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| item_attributes.each_with_index do |item_data, index| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| destroy_item = ActiveModel::Type::Boolean.new.cast(item_data[:_destroy]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if destroy_item && item_data[:id].present? | ||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaire.items.find(item_data[:id]).destroy! | ||||||||||||||||||||||||||||||||||||||||||||||||||
| next | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| next if destroy_item | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if item_data[:id].present? | ||||||||||||||||||||||||||||||||||||||||||||||||||
| existing_item = questionnaire.items.find(item_data[:id]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes = build_item_attributes(item_data, index, existing_item) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| existing_item.update!(attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes = build_item_attributes(item_data, index) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaire.items.create!(attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def build_item_attributes(item_data, index, existing_item = nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| question_type = canonical_question_type(item_data[:question_type]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| txt: item_data[:txt].presence || existing_item&.txt, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| question_type: question_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| weight: item_data[:weight].presence || existing_item&.weight, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| seq: item_data[:seq].presence || index + 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| alternatives: normalize_alternatives(item_data[:alternatives]) || existing_item&.alternatives, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| min_label: item_data[:min_label].presence || existing_item&.min_label, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| max_label: item_data[:max_label].presence || existing_item&.max_label, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| break_before: item_data.key?(:break_before) ? ActiveModel::Type::Boolean.new.cast(item_data[:break_before]) : true, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| size: build_item_size(question_type, item_data, existing_item) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+233
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update path currently overwrites existing item values when fields are omitted. Line 236 defaults missing 🐛 Proposed fix def build_item_attributes(item_data, index, existing_item = nil)
question_type = canonical_question_type(item_data[:question_type])
{
txt: item_data[:txt].presence || existing_item&.txt,
question_type: question_type,
weight: item_data[:weight].presence || existing_item&.weight,
- seq: item_data[:seq].presence || index + 1,
+ seq: item_data[:seq].presence || existing_item&.seq || index + 1,
alternatives: normalize_alternatives(item_data[:alternatives]) || existing_item&.alternatives,
min_label: item_data[:min_label].presence || existing_item&.min_label,
max_label: item_data[:max_label].presence || existing_item&.max_label,
- break_before: item_data.key?(:break_before) ? ActiveModel::Type::Boolean.new.cast(item_data[:break_before]) : true,
+ break_before: if item_data.key?(:break_before)
+ ActiveModel::Type::Boolean.new.cast(item_data[:break_before])
+ elsif existing_item
+ existing_item.break_before
+ else
+ true
+ end,
size: build_item_size(question_type, item_data, existing_item)
}.compact
end📝 Committable suggestion
Suggested change
🧰 Tools🪛 RuboCop (1.86.1)[convention] 240-240: Line is too long. [121/120] (Layout/LineLength) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| }.compact | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def canonical_question_type(question_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Text area' => 'TextArea', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Text field' => 'TextField', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Multiple choice' => 'MultipleChoiceRadio' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }.fetch(question_type, question_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def normalize_alternatives(alternatives) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil if alternatives.blank? | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| alternatives.to_s.split(',').map(&:strip).reject(&:empty?).join('|') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def build_item_size(question_type, item_data, existing_item = nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case question_type | ||||||||||||||||||||||||||||||||||||||||||||||||||
| when 'Criterion', 'TextArea' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| width = item_data[:textarea_width].presence | ||||||||||||||||||||||||||||||||||||||||||||||||||
| height = item_data[:textarea_height].presence | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return "#{width},#{height}" if width && height | ||||||||||||||||||||||||||||||||||||||||||||||||||
| when 'TextField' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return item_data[:textbox_width].to_s if item_data[:textbox_width].present? | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| existing_item&.size | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,2 @@ | ||||||||||||
| class QuestionType < ApplicationRecord | ||||||||||||
| end | ||||||||||||
|
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check the current state of the QuestionType model
cat -n app/models/question_type.rbRepository: expertiza/reimplementation-back-end Length of output: 136 🏁 Script executed: # Find the migration file for question_types table
fd -e rb -path "*db/migrate*" | xargs grep -l "QuestionType\|question_type" | head -5Repository: expertiza/reimplementation-back-end Length of output: 248 🏁 Script executed: # Search for QuestionsController and the types method
fd -e rb -path "*controllers*" | xargs grep -l "QuestionsController" | head -3Repository: expertiza/reimplementation-back-end Length of output: 248 🏁 Script executed: # Search for usage of QuestionType in the codebase
rg "QuestionType" --type rb -A 3 -B 3 | head -100Repository: expertiza/reimplementation-back-end Length of output: 106 🏁 Script executed: # Find migration files related to question_type
git ls-files | grep -i "migrate" | xargs grep -l "question_type" 2>/dev/null || echo "Checking db/migrate directly..."
ls -la db/migrate/ | grep -i questionRepository: expertiza/reimplementation-back-end Length of output: 968 🏁 Script executed: # Find QuestionsController
git ls-files | grep -i "controller" | xargs grep -l "QuestionsController\|questions_controller" 2>/dev/null | head -5Repository: expertiza/reimplementation-back-end Length of output: 119 🏁 Script executed: # Search for all references to QuestionType and types method
rg "QuestionType" --multiline -A 5 -B 2Repository: expertiza/reimplementation-back-end Length of output: 800 🏁 Script executed: # Look for any validates statements in question_type model
rg "validates" app/models/question_type.rbRepository: expertiza/reimplementation-back-end Length of output: 61 🏁 Script executed: # Read the migration file that creates question_tables
cat -n db/migrate/20250214224716_create_question_tables.rbRepository: expertiza/reimplementation-back-end Length of output: 1267 🏁 Script executed: # Also check the other migration file
cat -n db/migrate/20230401213404_create_questions.rbRepository: expertiza/reimplementation-back-end Length of output: 777 Add presence and uniqueness constraints to the
Add model validations and update the migration to enforce constraints at the database level: 🛠️ Suggested fixesModel validation: class QuestionType < ApplicationRecord
+ validates :name, presence: true, uniqueness: true
endMigration constraint (add to create_table :question_types, options: "CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci" do |t|
- t.string :name
+ t.string :name, null: false
t.timestamps
end
+add_index :question_types, :name, unique: true📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add access control in
itemsto prevent private questionnaire data exposure.Line 76 fetches by ID only; any authenticated user can request items for another instructor’s private questionnaire if they know the ID.
🔒 Proposed fix
🤖 Prompt for AI Agents