From 5b84548558e479716e7d1c2cb9751bc362838926 Mon Sep 17 00:00:00 2001 From: WanderWorld Date: Sat, 29 Aug 2026 14:22:28 -0400 Subject: [PATCH 1/3] Updating the view for Doctor Questions --- .../doctor_question_types_controller.rb | 70 ++++++++++++++++++ .../doctor_questions_controller.rb | 71 +++++++++++++++++++ app/helpers/doctor_question_types_helper.rb | 2 + app/helpers/doctor_questions_helper.rb | 14 ++++ app/models/doctor_question.rb | 29 ++++++++ app/models/doctor_question_type.rb | 5 ++ .../_doctor_question_type.html.erb | 3 + .../_doctor_question_type.json.jbuilder | 2 + .../doctor_question_types/_form.html.erb | 22 ++++++ app/views/doctor_question_types/edit.html.erb | 12 ++++ .../doctor_question_types/index.html.erb | 14 ++++ .../doctor_question_types/index.json.jbuilder | 1 + app/views/doctor_question_types/new.html.erb | 11 +++ app/views/doctor_question_types/show.html.erb | 10 +++ .../doctor_question_types/show.json.jbuilder | 1 + .../_doctor_question.html.erb | 3 + .../_doctor_question.json.jbuilder | 2 + app/views/doctor_questions/_form.html.erb | 32 +++++++++ app/views/doctor_questions/edit.html.erb | 12 ++++ app/views/doctor_questions/index.html.erb | 27 +++++++ .../doctor_questions/index.json.jbuilder | 1 + app/views/doctor_questions/new.html.erb | 11 +++ app/views/doctor_questions/show.html.erb | 10 +++ app/views/doctor_questions/show.json.jbuilder | 1 + app/views/layouts/application.html.erb | 3 +- config/routes.rb | 2 + ...0829160124_create_doctor_question_types.rb | 9 +++ .../20260829173203_create_doctor_questions.rb | 11 +++ ...183722_add_position_to_doctor_questions.rb | 9 +++ ...02745_remove_asked_from_doctor_question.rb | 5 ++ ...3_remove_position_from_doctor_questions.rb | 17 +++++ db/schema.rb | 18 ++++- db/seeds.rb | 64 +++-------------- db/seeds/01_tasks.rb | 7 ++ db/seeds/02_medications.rb | 48 +++++++++++++ db/seeds/03_doctor_questions.rb | 69 ++++++++++++++++++ .../doctor_question_types_controller_test.rb | 48 +++++++++++++ .../doctor_questions_controller_test.rb | 48 +++++++++++++ .../doctor_questions_index_test.rb | 36 ++++++++++ test/fixtures/doctor_question_types.yml | 15 ++++ test/fixtures/doctor_questions.yml | 13 ++++ test/models/doctor_question_test.rb | 19 +++++ test/models/doctor_question_type_test.rb | 25 +++++++ 43 files changed, 774 insertions(+), 58 deletions(-) create mode 100644 app/controllers/doctor_question_types_controller.rb create mode 100644 app/controllers/doctor_questions_controller.rb create mode 100644 app/helpers/doctor_question_types_helper.rb create mode 100644 app/helpers/doctor_questions_helper.rb create mode 100644 app/models/doctor_question.rb create mode 100644 app/models/doctor_question_type.rb create mode 100644 app/views/doctor_question_types/_doctor_question_type.html.erb create mode 100644 app/views/doctor_question_types/_doctor_question_type.json.jbuilder create mode 100644 app/views/doctor_question_types/_form.html.erb create mode 100644 app/views/doctor_question_types/edit.html.erb create mode 100644 app/views/doctor_question_types/index.html.erb create mode 100644 app/views/doctor_question_types/index.json.jbuilder create mode 100644 app/views/doctor_question_types/new.html.erb create mode 100644 app/views/doctor_question_types/show.html.erb create mode 100644 app/views/doctor_question_types/show.json.jbuilder create mode 100644 app/views/doctor_questions/_doctor_question.html.erb create mode 100644 app/views/doctor_questions/_doctor_question.json.jbuilder create mode 100644 app/views/doctor_questions/_form.html.erb create mode 100644 app/views/doctor_questions/edit.html.erb create mode 100644 app/views/doctor_questions/index.html.erb create mode 100644 app/views/doctor_questions/index.json.jbuilder create mode 100644 app/views/doctor_questions/new.html.erb create mode 100644 app/views/doctor_questions/show.html.erb create mode 100644 app/views/doctor_questions/show.json.jbuilder create mode 100644 db/migrate/20260829160124_create_doctor_question_types.rb create mode 100644 db/migrate/20260829173203_create_doctor_questions.rb create mode 100644 db/migrate/20260829183722_add_position_to_doctor_questions.rb create mode 100644 db/migrate/20260829202745_remove_asked_from_doctor_question.rb create mode 100644 db/migrate/20260829210653_remove_position_from_doctor_questions.rb create mode 100644 db/seeds/01_tasks.rb create mode 100644 db/seeds/02_medications.rb create mode 100644 db/seeds/03_doctor_questions.rb create mode 100644 test/controllers/doctor_question_types_controller_test.rb create mode 100644 test/controllers/doctor_questions_controller_test.rb create mode 100644 test/controllers/doctor_questions_index_test.rb create mode 100644 test/fixtures/doctor_question_types.yml create mode 100644 test/fixtures/doctor_questions.yml create mode 100644 test/models/doctor_question_test.rb create mode 100644 test/models/doctor_question_type_test.rb diff --git a/app/controllers/doctor_question_types_controller.rb b/app/controllers/doctor_question_types_controller.rb new file mode 100644 index 0000000..b65d278 --- /dev/null +++ b/app/controllers/doctor_question_types_controller.rb @@ -0,0 +1,70 @@ +class DoctorQuestionTypesController < ApplicationController + before_action :set_doctor_question_type, only: %i[ show edit update destroy ] + + # GET /doctor_question_types or /doctor_question_types.json + def index + @doctor_question_types = DoctorQuestionType.includes(:doctor_questions).all + end + + # GET /doctor_question_types/1 or /doctor_question_types/1.json + def show + end + + # GET /doctor_question_types/new + def new + @doctor_question_type = DoctorQuestionType.new + end + + # GET /doctor_question_types/1/edit + def edit + end + + # POST /doctor_question_types or /doctor_question_types.json + def create + @doctor_question_type = DoctorQuestionType.new(doctor_question_type_params) + + respond_to do |format| + if @doctor_question_type.save + format.html { redirect_to @doctor_question_type, notice: "Doctor question type was successfully created." } + format.json { render :show, status: :created, location: @doctor_question_type } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @doctor_question_type.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /doctor_question_types/1 or /doctor_question_types/1.json + def update + respond_to do |format| + if @doctor_question_type.update(doctor_question_type_params) + format.html { redirect_to @doctor_question_type, notice: "Doctor question type was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @doctor_question_type } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @doctor_question_type.errors, status: :unprocessable_content } + end + end + end + + # DELETE /doctor_question_types/1 or /doctor_question_types/1.json + def destroy + @doctor_question_type.destroy! + + respond_to do |format| + format.html { redirect_to doctor_question_types_path, notice: "Doctor question type was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_doctor_question_type + @doctor_question_type = DoctorQuestionType.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def doctor_question_type_params + params.expect(doctor_question_type: [ :name ]) + end +end diff --git a/app/controllers/doctor_questions_controller.rb b/app/controllers/doctor_questions_controller.rb new file mode 100644 index 0000000..eae6036 --- /dev/null +++ b/app/controllers/doctor_questions_controller.rb @@ -0,0 +1,71 @@ +class DoctorQuestionsController < ApplicationController + before_action :set_doctor_question, only: %i[ show edit update destroy ] + + # GET /doctor_questions or /doctor_questions.json + def index + @doctor_questions = DoctorQuestion.sorted_by(params[:sort], params[:direction]) + end + + + # GET /doctor_questions/1 or /doctor_questions/1.json + def show + end + + # GET /doctor_questions/new + def new + @doctor_question = DoctorQuestion.new + end + + # GET /doctor_questions/1/edit + def edit + end + + # POST /doctor_questions or /doctor_questions.json + def create + @doctor_question = DoctorQuestion.new(doctor_question_params) + + respond_to do |format| + if @doctor_question.save + format.html { redirect_to @doctor_question, notice: "Doctor question was successfully created." } + format.json { render :show, status: :created, location: @doctor_question } + else + format.html { render :new, status: :unprocessable_content } + format.json { render json: @doctor_question.errors, status: :unprocessable_content } + end + end + end + + # PATCH/PUT /doctor_questions/1 or /doctor_questions/1.json + def update + respond_to do |format| + if @doctor_question.update(doctor_question_params) + format.html { redirect_to @doctor_question, notice: "Doctor question was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @doctor_question } + else + format.html { render :edit, status: :unprocessable_content } + format.json { render json: @doctor_question.errors, status: :unprocessable_content } + end + end + end + + # DELETE /doctor_questions/1 or /doctor_questions/1.json + def destroy + @doctor_question.destroy! + + respond_to do |format| + format.html { redirect_to doctor_questions_path, notice: "Doctor question was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_doctor_question + @doctor_question = DoctorQuestion.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def doctor_question_params + params.expect(doctor_question: [ :question, :doctor_question_type_id ]) + end +end diff --git a/app/helpers/doctor_question_types_helper.rb b/app/helpers/doctor_question_types_helper.rb new file mode 100644 index 0000000..813172a --- /dev/null +++ b/app/helpers/doctor_question_types_helper.rb @@ -0,0 +1,2 @@ +module DoctorQuestionTypesHelper +end diff --git a/app/helpers/doctor_questions_helper.rb b/app/helpers/doctor_questions_helper.rb new file mode 100644 index 0000000..b1c46ed --- /dev/null +++ b/app/helpers/doctor_questions_helper.rb @@ -0,0 +1,14 @@ +module DoctorQuestionsHelper + # A column heading that links back to the index with the sort it should apply. + # Clicking the column already sorted flips the direction; clicking any other + # starts it ascending. + def doctor_question_sort_link(column, label) + sorting_by_this = params[:sort].to_s == column + descending = params[:direction].to_s == "desc" + + direction = sorting_by_this && !descending ? "desc" : "asc" + arrow = sorting_by_this ? (descending ? " ▼" : " ▲") : "" + + link_to "#{label}#{arrow}", doctor_questions_path(sort: column, direction: direction) + end +end diff --git a/app/models/doctor_question.rb b/app/models/doctor_question.rb new file mode 100644 index 0000000..2b3c213 --- /dev/null +++ b/app/models/doctor_question.rb @@ -0,0 +1,29 @@ +class DoctorQuestion < ApplicationRecord + belongs_to :doctor_question_type + + validates :question, presence: true, length: { maximum: 1000 } + + # Whitelist. Never interpolate a query parameter into an ORDER BY: anything + # not in this hash falls back to the default rather than reaching SQL, which + # is what makes the Arel.sql below safe to write. + SORT_COLUMNS = { + "type" => "doctor_question_types.name", + "question" => "doctor_questions.question" + }.freeze + + DEFAULT_SORT = "type" + + scope :sorted_by, ->(column, direction) { + column = SORT_COLUMNS.fetch(column) { SORT_COLUMNS.fetch(DEFAULT_SORT) } + direction = direction == "desc" ? "desc" : "asc" + + # references forces the LEFT JOIN that ordering on the types table needs; + # includes on its own would load them in a second query with nothing to sort + # on. created_at settles rows the chosen column ties on, so questions stay + # in the order they were added within a type rather than an arbitrary one. + includes(:doctor_question_type) + .references(:doctor_question_types) + .order(Arel.sql("#{column} #{direction}")) + .order(:created_at) + } +end diff --git a/app/models/doctor_question_type.rb b/app/models/doctor_question_type.rb new file mode 100644 index 0000000..4bc09f5 --- /dev/null +++ b/app/models/doctor_question_type.rb @@ -0,0 +1,5 @@ +class DoctorQuestionType < ApplicationRecord + has_many :doctor_questions, dependent: :restrict_with_error + + validates :name, presence: true, uniqueness: { case_sensitive: false } +end diff --git a/app/views/doctor_question_types/_doctor_question_type.html.erb b/app/views/doctor_question_types/_doctor_question_type.html.erb new file mode 100644 index 0000000..5be591f --- /dev/null +++ b/app/views/doctor_question_types/_doctor_question_type.html.erb @@ -0,0 +1,3 @@ +

+ <%= link_to doctor_question_type.name, doctor_question_type %> +

diff --git a/app/views/doctor_question_types/_doctor_question_type.json.jbuilder b/app/views/doctor_question_types/_doctor_question_type.json.jbuilder new file mode 100644 index 0000000..0e6a538 --- /dev/null +++ b/app/views/doctor_question_types/_doctor_question_type.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! doctor_question_type, :id, :name, :created_at, :updated_at +json.url doctor_question_type_url(doctor_question_type, format: :json) diff --git a/app/views/doctor_question_types/_form.html.erb b/app/views/doctor_question_types/_form.html.erb new file mode 100644 index 0000000..9d0d8ea --- /dev/null +++ b/app/views/doctor_question_types/_form.html.erb @@ -0,0 +1,22 @@ +<%= form_with(model: doctor_question_type) do |form| %> + <% if doctor_question_type.errors.any? %> +
+

<%= pluralize(doctor_question_type.errors.count, "error") %> prohibited this doctor_question_type from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/doctor_question_types/edit.html.erb b/app/views/doctor_question_types/edit.html.erb new file mode 100644 index 0000000..a4c163f --- /dev/null +++ b/app/views/doctor_question_types/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing doctor question type" %> + +

Editing doctor question type

+ +<%= render "form", doctor_question_type: @doctor_question_type %> + +
+ +
+ <%= link_to "Show this doctor question type", @doctor_question_type %> | + <%= link_to "Back to doctor question types", doctor_question_types_path %> +
diff --git a/app/views/doctor_question_types/index.html.erb b/app/views/doctor_question_types/index.html.erb new file mode 100644 index 0000000..7adc5ae --- /dev/null +++ b/app/views/doctor_question_types/index.html.erb @@ -0,0 +1,14 @@ +

<%= notice %>

+ +<% content_for :title, "Doctor question types" %> + +

Doctor question types

+ +
+ <% @doctor_question_types.each do |doctor_question_type| %> + <%= render doctor_question_type %> + + <% end %> +
diff --git a/app/views/doctor_question_types/index.json.jbuilder b/app/views/doctor_question_types/index.json.jbuilder new file mode 100644 index 0000000..bb75649 --- /dev/null +++ b/app/views/doctor_question_types/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @doctor_question_types, partial: "doctor_question_types/doctor_question_type", as: :doctor_question_type diff --git a/app/views/doctor_question_types/new.html.erb b/app/views/doctor_question_types/new.html.erb new file mode 100644 index 0000000..3511167 --- /dev/null +++ b/app/views/doctor_question_types/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New doctor question type" %> + +

New doctor question type

+ +<%= render "form", doctor_question_type: @doctor_question_type %> + +
+ +
+ <%= link_to "Back to doctor question types", doctor_question_types_path %> +
diff --git a/app/views/doctor_question_types/show.html.erb b/app/views/doctor_question_types/show.html.erb new file mode 100644 index 0000000..ca5e599 --- /dev/null +++ b/app/views/doctor_question_types/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @doctor_question_type %> + +
+ <%= link_to "Edit this doctor question type", edit_doctor_question_type_path(@doctor_question_type) %> | + <%= link_to "Back to doctor question types", doctor_question_types_path %> + + <%= button_to "Destroy this doctor question type", @doctor_question_type, method: :delete %> +
diff --git a/app/views/doctor_question_types/show.json.jbuilder b/app/views/doctor_question_types/show.json.jbuilder new file mode 100644 index 0000000..134f619 --- /dev/null +++ b/app/views/doctor_question_types/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "doctor_question_types/doctor_question_type", doctor_question_type: @doctor_question_type diff --git a/app/views/doctor_questions/_doctor_question.html.erb b/app/views/doctor_questions/_doctor_question.html.erb new file mode 100644 index 0000000..159f4f0 --- /dev/null +++ b/app/views/doctor_questions/_doctor_question.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to doctor_question.question, doctor_question %> +
  • diff --git a/app/views/doctor_questions/_doctor_question.json.jbuilder b/app/views/doctor_questions/_doctor_question.json.jbuilder new file mode 100644 index 0000000..20e05fa --- /dev/null +++ b/app/views/doctor_questions/_doctor_question.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! doctor_question, :id, :question, :doctor_question_type_id, :created_at, :updated_at +json.url doctor_question_url(doctor_question, format: :json) diff --git a/app/views/doctor_questions/_form.html.erb b/app/views/doctor_questions/_form.html.erb new file mode 100644 index 0000000..0f56e37 --- /dev/null +++ b/app/views/doctor_questions/_form.html.erb @@ -0,0 +1,32 @@ +<%= form_with(model: doctor_question) do |form| %> + <% if doctor_question.errors.any? %> +
    +

    <%= pluralize(doctor_question.errors.count, "error") %> prohibited this doctor_question from being saved:

    + + +
    + <% end %> + +
    + <%= form.label :question, style: "display: block" %> + <%= form.textarea :question %> +
    + +
    + <%= form.label :doctor_question_type_id, "Type", style: "display: block" %> + <%= form.collection_select :doctor_question_type_id, + DoctorQuestionType.order(:name), + :id, + :name, + { prompt: "Choose a type" } %> +
    + + +
    + <%= form.submit %> +
    +<% end %> diff --git a/app/views/doctor_questions/edit.html.erb b/app/views/doctor_questions/edit.html.erb new file mode 100644 index 0000000..d98c1e8 --- /dev/null +++ b/app/views/doctor_questions/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing doctor question" %> + +

    Editing doctor question

    + +<%= render "form", doctor_question: @doctor_question %> + +
    + +
    + <%= link_to "Show this doctor question", @doctor_question %> | + <%= link_to "Back to doctor questions", doctor_questions_path %> +
    diff --git a/app/views/doctor_questions/index.html.erb b/app/views/doctor_questions/index.html.erb new file mode 100644 index 0000000..d7a9d2a --- /dev/null +++ b/app/views/doctor_questions/index.html.erb @@ -0,0 +1,27 @@ +

    <%= notice %>

    + +<% content_for :title, "Questions to be asked" %> + +

    Questions to be asked

    + + + + + + + + + + + + <% @doctor_questions.each do |doctor_question| %> + + + + + + <% end %> + +
    <%= doctor_question_sort_link "type", "Type" %><%= doctor_question_sort_link "question", "Question" %>
    <%= doctor_question.doctor_question_type.name %><%= doctor_question.question %><%= link_to "Show this doctor question", doctor_question %>
    + +<%= link_to "New doctor question", new_doctor_question_path %> diff --git a/app/views/doctor_questions/index.json.jbuilder b/app/views/doctor_questions/index.json.jbuilder new file mode 100644 index 0000000..0e4e6c0 --- /dev/null +++ b/app/views/doctor_questions/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @doctor_questions, partial: "doctor_questions/doctor_question", as: :doctor_question diff --git a/app/views/doctor_questions/new.html.erb b/app/views/doctor_questions/new.html.erb new file mode 100644 index 0000000..8004bd0 --- /dev/null +++ b/app/views/doctor_questions/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New doctor question" %> + +

    New doctor question

    + +<%= render "form", doctor_question: @doctor_question %> + +
    + +
    + <%= link_to "Back to doctor questions", doctor_questions_path %> +
    diff --git a/app/views/doctor_questions/show.html.erb b/app/views/doctor_questions/show.html.erb new file mode 100644 index 0000000..09fe8c4 --- /dev/null +++ b/app/views/doctor_questions/show.html.erb @@ -0,0 +1,10 @@ +

    <%= notice %>

    + +<%= render @doctor_question %> + +
    + <%= link_to "Edit this doctor question", edit_doctor_question_path(@doctor_question) %> | + <%= link_to "Back to doctor questions", doctor_questions_path %> + + <%= button_to "Destroy this doctor question", @doctor_question, method: :delete %> +
    diff --git a/app/views/doctor_questions/show.json.jbuilder b/app/views/doctor_questions/show.json.jbuilder new file mode 100644 index 0000000..89a12e5 --- /dev/null +++ b/app/views/doctor_questions/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "doctor_questions/doctor_question", doctor_question: @doctor_question diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ec70fcb..912c55e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -63,7 +63,8 @@ equal billing with People and Prescriptions. %> <%= link_to "Admin", admin_path, aria: { current: ("page" if admin_area?) } %> - + <%= link_to "Questions", doctor_question_types_path, + aria: { current: aria_current_section(doctor_question_types_path) } %> <%# Devise signs out via DELETE, so this needs Turbo to issue the verb. button_to would work without it, but it would land in the nav as a filled button - and, being a delete, painted in the diff --git a/config/routes.rb b/config/routes.rb index c305b40..51533f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do resources :appointment_requirements + resources :doctor_questions + resources :doctor_question_types devise_for :users resources :medications resources :medication_forms diff --git a/db/migrate/20260829160124_create_doctor_question_types.rb b/db/migrate/20260829160124_create_doctor_question_types.rb new file mode 100644 index 0000000..2031e59 --- /dev/null +++ b/db/migrate/20260829160124_create_doctor_question_types.rb @@ -0,0 +1,9 @@ +class CreateDoctorQuestionTypes < ActiveRecord::Migration[8.1] + def change + create_table :doctor_question_types do |t| + t.string :name, null: false, index: { unique: true } + + t.timestamps + end + end +end diff --git a/db/migrate/20260829173203_create_doctor_questions.rb b/db/migrate/20260829173203_create_doctor_questions.rb new file mode 100644 index 0000000..952ed46 --- /dev/null +++ b/db/migrate/20260829173203_create_doctor_questions.rb @@ -0,0 +1,11 @@ +class CreateDoctorQuestions < ActiveRecord::Migration[8.1] + def change + create_table :doctor_questions do |t| + t.text :question, null: false + t.boolean :asked, null: false, default: false + t.belongs_to :doctor_question_type, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20260829183722_add_position_to_doctor_questions.rb b/db/migrate/20260829183722_add_position_to_doctor_questions.rb new file mode 100644 index 0000000..562b62f --- /dev/null +++ b/db/migrate/20260829183722_add_position_to_doctor_questions.rb @@ -0,0 +1,9 @@ +class AddPositionToDoctorQuestions < ActiveRecord::Migration[8.1] + def change + # Position is per type, not global: the first question of every type is 0. + # Existing rows all land on the default, and DoctorQuestion.in_order breaks + # that tie on created_at until someone drags something. + add_column :doctor_questions, :position, :integer, null: false, default: 0 + add_index :doctor_questions, [ :doctor_question_type_id, :position ] + end +end diff --git a/db/migrate/20260829202745_remove_asked_from_doctor_question.rb b/db/migrate/20260829202745_remove_asked_from_doctor_question.rb new file mode 100644 index 0000000..8f65b17 --- /dev/null +++ b/db/migrate/20260829202745_remove_asked_from_doctor_question.rb @@ -0,0 +1,5 @@ +class RemoveAskedFromDoctorQuestion < ActiveRecord::Migration[8.1] + def change + remove_column :doctor_questions, :asked, :boolean + end +end diff --git a/db/migrate/20260829210653_remove_position_from_doctor_questions.rb b/db/migrate/20260829210653_remove_position_from_doctor_questions.rb new file mode 100644 index 0000000..4db481b --- /dev/null +++ b/db/migrate/20260829210653_remove_position_from_doctor_questions.rb @@ -0,0 +1,17 @@ +class RemovePositionFromDoctorQuestions < ActiveRecord::Migration[8.1] + # up/down rather than change: the index is removed by name, and Rails cannot + # infer from a name alone how to put it back. + def up + # By name, not by column list. Dropping the column on its own leaves SQLite + # holding this index with position pruned out of it - a duplicate of + # index_doctor_questions_on_doctor_question_type_id, under a name that still + # advertises a column that no longer exists. + remove_index :doctor_questions, name: "index_doctor_questions_on_doctor_question_type_id_and_position" + remove_column :doctor_questions, :position + end + + def down + add_column :doctor_questions, :position, :integer, null: false, default: 0 + add_index :doctor_questions, [ :doctor_question_type_id, :position ] + end +end diff --git a/db/schema.rb b/db/schema.rb index 4727c87..192fddf 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.1].define(version: 2026_08_29_203900) do +ActiveRecord::Schema[8.1].define(version: 2026_08_29_210653) do create_table "addresses", force: :cascade do |t| t.string "city" t.datetime "created_at", null: false @@ -26,6 +26,21 @@ t.datetime "updated_at", null: false end + create_table "doctor_question_types", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "name", null: false + t.datetime "updated_at", null: false + t.index ["name"], name: "index_doctor_question_types_on_name", unique: true + end + + create_table "doctor_questions", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "doctor_question_type_id", null: false + t.text "question", null: false + t.datetime "updated_at", null: false + t.index ["doctor_question_type_id"], name: "index_doctor_questions_on_doctor_question_type_id" + end + create_table "medication_forms", force: :cascade do |t| t.datetime "created_at", null: false t.string "name" @@ -117,6 +132,7 @@ end add_foreign_key "addresses", "people" + add_foreign_key "doctor_questions", "doctor_question_types" add_foreign_key "medications", "medication_types" add_foreign_key "people", "relationships" add_foreign_key "prescriptions", "medication_forms" diff --git a/db/seeds.rb b/db/seeds.rb index f7802de..d35cace 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,60 +1,12 @@ # This file should ensure the existence of records required to run the application in every environment (production, # development, test). The code here should be idempotent so that it can be executed at any point in every environment. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). - -if Task.none? - Task.create!([ - { title: "Open mobile/src/app/tasks.tsx and edit this list", completed: false }, - { title: "Point EXPO_PUBLIC_API_URL at the Rails server", completed: true }, - { title: "Replace Task with a real model", completed: false } - ]) -end - -# The medication catalog from issue #164. Looked up by name so re-seeding an -# existing database tops it up instead of duplicating it. -[ - "Prescription", - "Over-the-counter", - "Vitamin or supplement", - "Herbal or natural" -].each { |name| MedicationType.find_or_create_by!(name: name) } - -[ - "Capsule", - "Liquid", - "Suspension", - "Syrup", - "Chewable tablet", - "Dissolvable tablet", - "Powder", - "Injection", - "Inhaler", - "Nebulizer solution", - "Patch", - "Cream", - "Ointment", - "Gel", - "Drops", - "Suppository", - "Spray" -].each { |name| MedicationForm.find_or_create_by!(name: name) } - -{ - "Lisinopril" => "Dry cough, dizziness, headache", - "Atorvastatin" => "Muscle aches, nausea, joint pain", - "Metformin" => "Nausea, diarrhea, stomach upset", - "Levothyroxine" => "Weight changes, insomnia, tremor", - "Amlodipine" => "Swollen ankles, flushing, fatigue", - "Metoprolol" => "Fatigue, dizziness, slow heartbeat", - "Omeprazole" => "Headache, gas, constipation", - "Albuterol" => "Jitteriness, rapid heartbeat, headache", - "Gabapentin" => "Drowsiness, dizziness, coordination problems", - "Sertraline" => "Nausea, insomnia, dry mouth", - "Ibuprofen" => "Stomach upset, heartburn, dizziness", - "Acetaminophen" => "Nausea, rash", - "Amoxicillin" => "Diarrhea, nausea, rash", - "Prednisone" => "Increased appetite, insomnia, mood changes", - "Warfarin" => "Easy bruising, bleeding, nausea" -}.each do |name, side_effects| - Medication.find_or_create_by!(name: name) { |medication| medication.side_effects = side_effects } +# +# The data itself lives in db/seeds/, one file per concern, loaded in filename +# order - the numeric prefixes are there for when one file depends on another +# having run first. + +Dir[Rails.root.join("db/seeds/*.rb")].sort.each do |seed_file| + puts "Seeding #{File.basename(seed_file)}" + load seed_file end diff --git a/db/seeds/01_tasks.rb b/db/seeds/01_tasks.rb new file mode 100644 index 0000000..665962a --- /dev/null +++ b/db/seeds/01_tasks.rb @@ -0,0 +1,7 @@ +if Task.none? + Task.create!([ + { title: "Open mobile/src/app/tasks.tsx and edit this list", completed: false }, + { title: "Point EXPO_PUBLIC_API_URL at the Rails server", completed: true }, + { title: "Replace Task with a real model", completed: false } + ]) +end diff --git a/db/seeds/02_medications.rb b/db/seeds/02_medications.rb new file mode 100644 index 0000000..61e9640 --- /dev/null +++ b/db/seeds/02_medications.rb @@ -0,0 +1,48 @@ +# The medication catalog from issue #164. Looked up by name so re-seeding an +# existing database tops it up instead of duplicating it. +[ + "Prescription", + "Over-the-counter", + "Vitamin or supplement", + "Herbal or natural" +].each { |name| MedicationType.find_or_create_by!(name: name) } + +[ + "Capsule", + "Liquid", + "Suspension", + "Syrup", + "Chewable tablet", + "Dissolvable tablet", + "Powder", + "Injection", + "Inhaler", + "Nebulizer solution", + "Patch", + "Cream", + "Ointment", + "Gel", + "Drops", + "Suppository", + "Spray" +].each { |name| MedicationForm.find_or_create_by!(name: name) } + +{ + "Lisinopril" => "Dry cough, dizziness, headache", + "Atorvastatin" => "Muscle aches, nausea, joint pain", + "Metformin" => "Nausea, diarrhea, stomach upset", + "Levothyroxine" => "Weight changes, insomnia, tremor", + "Amlodipine" => "Swollen ankles, flushing, fatigue", + "Metoprolol" => "Fatigue, dizziness, slow heartbeat", + "Omeprazole" => "Headache, gas, constipation", + "Albuterol" => "Jitteriness, rapid heartbeat, headache", + "Gabapentin" => "Drowsiness, dizziness, coordination problems", + "Sertraline" => "Nausea, insomnia, dry mouth", + "Ibuprofen" => "Stomach upset, heartburn, dizziness", + "Acetaminophen" => "Nausea, rash", + "Amoxicillin" => "Diarrhea, nausea, rash", + "Prednisone" => "Increased appetite, insomnia, mood changes", + "Warfarin" => "Easy bruising, bleeding, nausea" +}.each do |name, side_effects| + Medication.find_or_create_by!(name: name) { |medication| medication.side_effects = side_effects } +end diff --git a/db/seeds/03_doctor_questions.rb b/db/seeds/03_doctor_questions.rb new file mode 100644 index 0000000..42a65ff --- /dev/null +++ b/db/seeds/03_doctor_questions.rb @@ -0,0 +1,69 @@ +# Questions a patient might want to take into an appointment, grouped by the +# type they belong to. The hash defines the types too - there is no separate +# list to keep in step with this one. +# +# Order matters: questions are created in the order written here, and the index +# falls back to created_at within a type, so this is the order a patient sees. +questions_by_type = { + "Health concern" => [ + "What is my diagnosis?", + "Do I need any more tests to confirm the diagnosis?", + "What is changing?", + "What are my treatment options? What are the benefits of each option? What are the side effects or complications of each option?", + "What if I choose to do nothing?", + "How soon do I need to make a decision?", + "What will the medicine you are prescribing do? How do I take it? Are there any side effects? How long will I need to take the medicine?", + "Why do I need surgery? Are there other ways to treat my condition? How often do you perform this surgery?", + "What are the costs involved in the treatment?", + "Do I need to change my daily routine? Do I need special help at home? Can I drive?", + "What is the outlook for my future?" + ], + + "Diagnostic tests" => [ + "What is the test for?", + "How is the test done?", + "How accurate is the test?", + "What are the possible complications?", + "Is the test the only way to find out that information?", + "What do I need to do to prepare for the test?", + "Will I be able to drive myself home after the test?", + "When will I get the results?", + "What will the results tell me?", + "What is the next step after the tests?" + ], + + "Surgery" => [ + "Why do I need the surgery?", + "What does the surgery involve?", + "What are the potential complications?", + "Is there some other way to treat my condition?", + "Who will do the surgery? How many times have they done this surgery?", + "Which hospital is best for the surgery?", + "Will I need anesthesia?", + "How long will it take me to recover?", + "How long will I be in the hospital?", + "What will happen after the surgery? Will I need special help at home? Will I be able to drive?", + "What will happen if I wait or do not have the surgery?" + ], + + "Insurance" => [ + "Is this visit/procedure/test covered by my insurance plan? Do you anticipate any problems with my insurance?", + "Is this provider in-network for both the health care provider and the facility/hospital?", + "Do I need a referral or prior authorization before this visit/test/procedure? If authorization is needed, will your office handle it, or do I need to contact my insurer first?" + ], + + "Costs" => [ + "What is the estimated total cost for the visit/procedure/test?", + "What portion is typically deductible, copay or coinsurance?", + "What is my estimated out-of-pocket cost specifically?", + "Is there anything else about cost that patients are usually surprised by in this situation?" + ] +} + +questions_by_type.each do |type_name, questions| + type = DoctorQuestionType.find_or_create_by!(name: type_name) + + questions.each do |question| + type.doctor_questions.find_or_create_by!(question: question) + end +end diff --git a/test/controllers/doctor_question_types_controller_test.rb b/test/controllers/doctor_question_types_controller_test.rb new file mode 100644 index 0000000..05b997f --- /dev/null +++ b/test/controllers/doctor_question_types_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class DoctorQuestionTypesControllerTest < ActionDispatch::IntegrationTest + setup do + @doctor_question_type = doctor_question_types(:health_concern) + end + + test "should get index" do + get doctor_question_types_url + assert_response :success + end + + test "should get new" do + get new_doctor_question_type_url + assert_response :success + end + + test "should create doctor_question_type" do + assert_difference("DoctorQuestionType.count") do + post doctor_question_types_url, params: { doctor_question_type: { name: "Insurance" } } + end + + assert_redirected_to doctor_question_type_url(DoctorQuestionType.last) + end + + test "should show doctor_question_type" do + get doctor_question_type_url(@doctor_question_type) + assert_response :success + end + + test "should get edit" do + get edit_doctor_question_type_url(@doctor_question_type) + assert_response :success + end + + test "should update doctor_question_type" do + patch doctor_question_type_url(@doctor_question_type), params: { doctor_question_type: { name: @doctor_question_type.name } } + assert_redirected_to doctor_question_type_url(@doctor_question_type) + end + + test "should destroy doctor_question_type" do + assert_difference("DoctorQuestionType.count", -1) do + delete doctor_question_type_url(doctor_question_types(:unused)) + end + + assert_redirected_to doctor_question_types_url + end +end diff --git a/test/controllers/doctor_questions_controller_test.rb b/test/controllers/doctor_questions_controller_test.rb new file mode 100644 index 0000000..5738cc2 --- /dev/null +++ b/test/controllers/doctor_questions_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class DoctorQuestionsControllerTest < ActionDispatch::IntegrationTest + setup do + @doctor_question = doctor_questions(:diagnosis) + end + + test "should get index" do + get doctor_questions_url + assert_response :success + end + + test "should get new" do + get new_doctor_question_url + assert_response :success + end + + test "should create doctor_question" do + assert_difference("DoctorQuestion.count") do + post doctor_questions_url, params: { doctor_question: { doctor_question_type_id: @doctor_question.doctor_question_type_id, question: @doctor_question.question } } + end + + assert_redirected_to doctor_question_url(DoctorQuestion.last) + end + + test "should show doctor_question" do + get doctor_question_url(@doctor_question) + assert_response :success + end + + test "should get edit" do + get edit_doctor_question_url(@doctor_question) + assert_response :success + end + + test "should update doctor_question" do + patch doctor_question_url(@doctor_question), params: { doctor_question: { doctor_question_type_id: @doctor_question.doctor_question_type_id, question: @doctor_question.question } } + assert_redirected_to doctor_question_url(@doctor_question) + end + + test "should destroy doctor_question" do + assert_difference("DoctorQuestion.count", -1) do + delete doctor_question_url(@doctor_question) + end + + assert_redirected_to doctor_questions_url + end +end diff --git a/test/controllers/doctor_questions_index_test.rb b/test/controllers/doctor_questions_index_test.rb new file mode 100644 index 0000000..d0fa5e3 --- /dev/null +++ b/test/controllers/doctor_questions_index_test.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class DoctorQuestionsIndexTest < ActionDispatch::IntegrationTest + test "index lists questions in a table under one heading" do + get doctor_questions_url + + assert_response :success + assert_select "h1", "Questions to be asked" + assert_select "table tbody tr", count: DoctorQuestion.count + assert_select "td", text: "Health Concern" + end + + test "index sorts by type name by default" do + get doctor_questions_url + + types = css_select("tbody tr td:first-child").map(&:text) + assert_equal types.sort, types + end + + test "sort links reverse the order" do + get doctor_questions_url(sort: "type", direction: "desc") + + types = css_select("tbody tr td:first-child").map(&:text) + assert_equal types.sort.reverse, types + end + + # The sort parameter reaches Arel.sql, so it must never reach it as SQL: an + # unknown column has to fall back to the default rather than be interpolated. + test "an unknown sort column falls back to the default" do + get doctor_questions_url(sort: "question; drop table doctor_questions", direction: "asc") + + assert_response :success + types = css_select("tbody tr td:first-child").map(&:text) + assert_equal types.sort, types + end +end diff --git a/test/fixtures/doctor_question_types.yml b/test/fixtures/doctor_question_types.yml new file mode 100644 index 0000000..8f4e116 --- /dev/null +++ b/test/fixtures/doctor_question_types.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# Names have to differ from each other: fixtures are inserted straight into the +# database, so they skip the model's validations but not its unique index. + +health_concern: + name: Health Concern + +surgery: + name: Surgery + +# Deliberately referenced by no question, so a test has something it is allowed +# to destroy. dependent: :restrict_with_error blocks deleting the other two. +unused: + name: Costs diff --git a/test/fixtures/doctor_questions.yml b/test/fixtures/doctor_questions.yml new file mode 100644 index 0000000..d326deb --- /dev/null +++ b/test/fixtures/doctor_questions.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# Two questions under different types, so the index has more than one type to +# group and sort by. doctor_question_type names a label in +# doctor_question_types.yml, which Rails resolves to that row's id. + +diagnosis: + question: Should I take this with food? + doctor_question_type: health_concern + +recovery: + question: What did the scan show? + doctor_question_type: surgery diff --git a/test/models/doctor_question_test.rb b/test/models/doctor_question_test.rb new file mode 100644 index 0000000..d1cdb3b --- /dev/null +++ b/test/models/doctor_question_test.rb @@ -0,0 +1,19 @@ +require "test_helper" + +class DoctorQuestionTest < ActiveSupport::TestCase + test "requires a question" do + question = DoctorQuestion.new(doctor_question_type: doctor_question_types(:health_concern)) + + assert_not question.valid? + assert_includes question.errors[:question], "can't be blank" + end + + # belongs_to is required by default, so this passes with no validation of our + # own - and matches the null: false on the column. + test "requires a type" do + question = DoctorQuestion.new(question: "Anything I should watch for?") + + assert_not question.valid? + assert_includes question.errors[:doctor_question_type], "must exist" + end +end diff --git a/test/models/doctor_question_type_test.rb b/test/models/doctor_question_type_test.rb new file mode 100644 index 0000000..4860ff9 --- /dev/null +++ b/test/models/doctor_question_type_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class DoctorQuestionTypeTest < ActiveSupport::TestCase + # Lowercase on purpose: this is what proves case_sensitive: false is doing + # something. "Health Concern" alone would pass with or without it. + test "requires a unique name regardless of case" do + duplicate = DoctorQuestionType.new(name: "health concern") + + assert_not duplicate.valid? + assert_includes duplicate.errors[:name], "has already been taken" + end + + test "refuses to be destroyed while questions still use it" do + type = doctor_question_types(:health_concern) + + assert_not type.destroy + assert_includes type.errors[:base], "Cannot delete record because dependent doctor questions exist" + end + + test "can be destroyed when nothing uses it" do + assert_difference("DoctorQuestionType.count", -1) do + doctor_question_types(:unused).destroy + end + end +end From 8a28169ccc524616181a7eedb172475a2b691ec3 Mon Sep 17 00:00:00 2001 From: WanderWorld Date: Sat, 29 Aug 2026 18:36:22 -0400 Subject: [PATCH 2/3] Add test cases --- .../doctor_question_types_controller_test.rb | 2 +- .../doctor_questions_index_test.rb | 19 +++++++++ test/fixtures/doctor_question_types.yml | 5 ++- test/fixtures/doctor_questions.yml | 17 ++++++-- test/helpers/doctor_questions_helper_test.rb | 42 +++++++++++++++++++ test/models/doctor_question_test.rb | 19 +++++++++ 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 test/helpers/doctor_questions_helper_test.rb diff --git a/test/controllers/doctor_question_types_controller_test.rb b/test/controllers/doctor_question_types_controller_test.rb index 05b997f..4abcac2 100644 --- a/test/controllers/doctor_question_types_controller_test.rb +++ b/test/controllers/doctor_question_types_controller_test.rb @@ -17,7 +17,7 @@ class DoctorQuestionTypesControllerTest < ActionDispatch::IntegrationTest test "should create doctor_question_type" do assert_difference("DoctorQuestionType.count") do - post doctor_question_types_url, params: { doctor_question_type: { name: "Insurance" } } + post doctor_question_types_url, params: { doctor_question_type: { name: "Medication" } } end assert_redirected_to doctor_question_type_url(DoctorQuestionType.last) diff --git a/test/controllers/doctor_questions_index_test.rb b/test/controllers/doctor_questions_index_test.rb index d0fa5e3..28de17f 100644 --- a/test/controllers/doctor_questions_index_test.rb +++ b/test/controllers/doctor_questions_index_test.rb @@ -33,4 +33,23 @@ class DoctorQuestionsIndexTest < ActionDispatch::IntegrationTest types = css_select("tbody tr td:first-child").map(&:text) assert_equal types.sort, types end + + test "sorts by the question column" do + get doctor_questions_url(sort: "question", direction: "asc") + + questions = css_select("tbody tr td:nth-child(2)").map(&:text) + assert_equal questions.sort, questions + + # The fixtures sort differently by question than by type, so this also + # proves the question sort is not quietly falling back to the default. + types = css_select("tbody tr td:first-child").map(&:text) + assert_not_equal types.sort, types + end + + test "reverses the question column" do + get doctor_questions_url(sort: "question", direction: "desc") + + questions = css_select("tbody tr td:nth-child(2)").map(&:text) + assert_equal questions.sort.reverse, questions + end end diff --git a/test/fixtures/doctor_question_types.yml b/test/fixtures/doctor_question_types.yml index 8f4e116..0bb2441 100644 --- a/test/fixtures/doctor_question_types.yml +++ b/test/fixtures/doctor_question_types.yml @@ -9,7 +9,10 @@ health_concern: surgery: name: Surgery +insurance: + name: Insurance + # Deliberately referenced by no question, so a test has something it is allowed -# to destroy. dependent: :restrict_with_error blocks deleting the other two. +# to destroy. dependent: :restrict_with_error blocks deleting the others. unused: name: Costs diff --git a/test/fixtures/doctor_questions.yml b/test/fixtures/doctor_questions.yml index d326deb..0c403b3 100644 --- a/test/fixtures/doctor_questions.yml +++ b/test/fixtures/doctor_questions.yml @@ -1,8 +1,15 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -# Two questions under different types, so the index has more than one type to -# group and sort by. doctor_question_type names a label in -# doctor_question_types.yml, which Rails resolves to that row's id. +# Three questions under three types. Three rather than two on purpose: with two +# rows a sort assertion passes even if the sort merely reverses, and the middle +# element is what catches an ordering that is subtly wrong. +# +# The question text deliberately sorts into a different order than the type +# names do (Does/Should/What against Health Concern/Insurance/Surgery), so a +# test sorting by one column cannot pass while reading the other. +# +# doctor_question_type names a label in doctor_question_types.yml, which Rails +# resolves to that row's id. diagnosis: question: Should I take this with food? @@ -11,3 +18,7 @@ diagnosis: recovery: question: What did the scan show? doctor_question_type: surgery + +coverage: + question: Does my plan cover this? + doctor_question_type: insurance diff --git a/test/helpers/doctor_questions_helper_test.rb b/test/helpers/doctor_questions_helper_test.rb new file mode 100644 index 0000000..61e26e8 --- /dev/null +++ b/test/helpers/doctor_questions_helper_test.rb @@ -0,0 +1,42 @@ +require "test_helper" + +class DoctorQuestionsHelperTest < ActionView::TestCase + test "a column that is not the current sort links to it ascending, with no arrow" do + link = doctor_question_sort_link("type", "Type") + + assert_includes link, "sort=type" + assert_includes link, "direction=asc" + assert_includes link, ">Type" + end + + test "the column sorted ascending offers descending, and shows an up arrow" do + params[:sort] = "type" + params[:direction] = "asc" + + link = doctor_question_sort_link("type", "Type") + + assert_includes link, "direction=desc" + assert_includes link, "Type ▲" + end + + test "the column sorted descending offers ascending again, and shows a down arrow" do + params[:sort] = "type" + params[:direction] = "desc" + + link = doctor_question_sort_link("type", "Type") + + assert_includes link, "direction=asc" + assert_includes link, "Type ▼" + end + + # The arrow marks which column is sorted, so it must not appear on the others. + test "other columns show no arrow while one column is sorted" do + params[:sort] = "type" + params[:direction] = "desc" + + link = doctor_question_sort_link("question", "Question") + + assert_includes link, ">Question" + assert_includes link, "direction=asc" + end +end diff --git a/test/models/doctor_question_test.rb b/test/models/doctor_question_test.rb index d1cdb3b..4c86a20 100644 --- a/test/models/doctor_question_test.rb +++ b/test/models/doctor_question_test.rb @@ -16,4 +16,23 @@ class DoctorQuestionTest < ActiveSupport::TestCase assert_not question.valid? assert_includes question.errors[:doctor_question_type], "must exist" end + + test "rejects a question longer than 1000 characters" do + question = DoctorQuestion.new( + question: "a" * 1001, + doctor_question_type: doctor_question_types(:health_concern) + ) + + assert_not question.valid? + assert_includes question.errors[:question], "is too long (maximum is 1000 characters)" + end + + test "accepts a question of exactly 1000 characters" do + question = DoctorQuestion.new( + question: "a" * 1000, + doctor_question_type: doctor_question_types(:health_concern) + ) + + assert question.valid? + end end From 894c23d337835190c93fe369f80189b295d5b27a Mon Sep 17 00:00:00 2001 From: Amjad Al-Rikabi Date: Sun, 30 Aug 2026 10:17:55 -0400 Subject: [PATCH 3/3] Updated for comments --- .../doctor_questions_controller.rb | 1 - app/helpers/doctor_question_types_helper.rb | 2 - app/models/doctor_question.rb | 4 +- .../20260829173203_create_doctor_questions.rb | 1 - ...183722_add_position_to_doctor_questions.rb | 9 --- ...02745_remove_asked_from_doctor_question.rb | 5 -- ...3_remove_position_from_doctor_questions.rb | 17 ----- db/seeds/02_doctor_questions.rb | 69 +++++++++++++++++++ 8 files changed, 70 insertions(+), 38 deletions(-) delete mode 100644 app/helpers/doctor_question_types_helper.rb delete mode 100644 db/migrate/20260829183722_add_position_to_doctor_questions.rb delete mode 100644 db/migrate/20260829202745_remove_asked_from_doctor_question.rb delete mode 100644 db/migrate/20260829210653_remove_position_from_doctor_questions.rb create mode 100644 db/seeds/02_doctor_questions.rb diff --git a/app/controllers/doctor_questions_controller.rb b/app/controllers/doctor_questions_controller.rb index eae6036..f622738 100644 --- a/app/controllers/doctor_questions_controller.rb +++ b/app/controllers/doctor_questions_controller.rb @@ -6,7 +6,6 @@ def index @doctor_questions = DoctorQuestion.sorted_by(params[:sort], params[:direction]) end - # GET /doctor_questions/1 or /doctor_questions/1.json def show end diff --git a/app/helpers/doctor_question_types_helper.rb b/app/helpers/doctor_question_types_helper.rb deleted file mode 100644 index 813172a..0000000 --- a/app/helpers/doctor_question_types_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module DoctorQuestionTypesHelper -end diff --git a/app/models/doctor_question.rb b/app/models/doctor_question.rb index 2b3c213..0b574ca 100644 --- a/app/models/doctor_question.rb +++ b/app/models/doctor_question.rb @@ -3,9 +3,7 @@ class DoctorQuestion < ApplicationRecord validates :question, presence: true, length: { maximum: 1000 } - # Whitelist. Never interpolate a query parameter into an ORDER BY: anything - # not in this hash falls back to the default rather than reaching SQL, which - # is what makes the Arel.sql below safe to write. + # A whitelist, not a lookup table: params never reach the Arel.sql below. SORT_COLUMNS = { "type" => "doctor_question_types.name", "question" => "doctor_questions.question" diff --git a/db/migrate/20260829173203_create_doctor_questions.rb b/db/migrate/20260829173203_create_doctor_questions.rb index 952ed46..7702c9c 100644 --- a/db/migrate/20260829173203_create_doctor_questions.rb +++ b/db/migrate/20260829173203_create_doctor_questions.rb @@ -2,7 +2,6 @@ class CreateDoctorQuestions < ActiveRecord::Migration[8.1] def change create_table :doctor_questions do |t| t.text :question, null: false - t.boolean :asked, null: false, default: false t.belongs_to :doctor_question_type, null: false, foreign_key: true t.timestamps diff --git a/db/migrate/20260829183722_add_position_to_doctor_questions.rb b/db/migrate/20260829183722_add_position_to_doctor_questions.rb deleted file mode 100644 index 562b62f..0000000 --- a/db/migrate/20260829183722_add_position_to_doctor_questions.rb +++ /dev/null @@ -1,9 +0,0 @@ -class AddPositionToDoctorQuestions < ActiveRecord::Migration[8.1] - def change - # Position is per type, not global: the first question of every type is 0. - # Existing rows all land on the default, and DoctorQuestion.in_order breaks - # that tie on created_at until someone drags something. - add_column :doctor_questions, :position, :integer, null: false, default: 0 - add_index :doctor_questions, [ :doctor_question_type_id, :position ] - end -end diff --git a/db/migrate/20260829202745_remove_asked_from_doctor_question.rb b/db/migrate/20260829202745_remove_asked_from_doctor_question.rb deleted file mode 100644 index 8f65b17..0000000 --- a/db/migrate/20260829202745_remove_asked_from_doctor_question.rb +++ /dev/null @@ -1,5 +0,0 @@ -class RemoveAskedFromDoctorQuestion < ActiveRecord::Migration[8.1] - def change - remove_column :doctor_questions, :asked, :boolean - end -end diff --git a/db/migrate/20260829210653_remove_position_from_doctor_questions.rb b/db/migrate/20260829210653_remove_position_from_doctor_questions.rb deleted file mode 100644 index 4db481b..0000000 --- a/db/migrate/20260829210653_remove_position_from_doctor_questions.rb +++ /dev/null @@ -1,17 +0,0 @@ -class RemovePositionFromDoctorQuestions < ActiveRecord::Migration[8.1] - # up/down rather than change: the index is removed by name, and Rails cannot - # infer from a name alone how to put it back. - def up - # By name, not by column list. Dropping the column on its own leaves SQLite - # holding this index with position pruned out of it - a duplicate of - # index_doctor_questions_on_doctor_question_type_id, under a name that still - # advertises a column that no longer exists. - remove_index :doctor_questions, name: "index_doctor_questions_on_doctor_question_type_id_and_position" - remove_column :doctor_questions, :position - end - - def down - add_column :doctor_questions, :position, :integer, null: false, default: 0 - add_index :doctor_questions, [ :doctor_question_type_id, :position ] - end -end diff --git a/db/seeds/02_doctor_questions.rb b/db/seeds/02_doctor_questions.rb new file mode 100644 index 0000000..42a65ff --- /dev/null +++ b/db/seeds/02_doctor_questions.rb @@ -0,0 +1,69 @@ +# Questions a patient might want to take into an appointment, grouped by the +# type they belong to. The hash defines the types too - there is no separate +# list to keep in step with this one. +# +# Order matters: questions are created in the order written here, and the index +# falls back to created_at within a type, so this is the order a patient sees. +questions_by_type = { + "Health concern" => [ + "What is my diagnosis?", + "Do I need any more tests to confirm the diagnosis?", + "What is changing?", + "What are my treatment options? What are the benefits of each option? What are the side effects or complications of each option?", + "What if I choose to do nothing?", + "How soon do I need to make a decision?", + "What will the medicine you are prescribing do? How do I take it? Are there any side effects? How long will I need to take the medicine?", + "Why do I need surgery? Are there other ways to treat my condition? How often do you perform this surgery?", + "What are the costs involved in the treatment?", + "Do I need to change my daily routine? Do I need special help at home? Can I drive?", + "What is the outlook for my future?" + ], + + "Diagnostic tests" => [ + "What is the test for?", + "How is the test done?", + "How accurate is the test?", + "What are the possible complications?", + "Is the test the only way to find out that information?", + "What do I need to do to prepare for the test?", + "Will I be able to drive myself home after the test?", + "When will I get the results?", + "What will the results tell me?", + "What is the next step after the tests?" + ], + + "Surgery" => [ + "Why do I need the surgery?", + "What does the surgery involve?", + "What are the potential complications?", + "Is there some other way to treat my condition?", + "Who will do the surgery? How many times have they done this surgery?", + "Which hospital is best for the surgery?", + "Will I need anesthesia?", + "How long will it take me to recover?", + "How long will I be in the hospital?", + "What will happen after the surgery? Will I need special help at home? Will I be able to drive?", + "What will happen if I wait or do not have the surgery?" + ], + + "Insurance" => [ + "Is this visit/procedure/test covered by my insurance plan? Do you anticipate any problems with my insurance?", + "Is this provider in-network for both the health care provider and the facility/hospital?", + "Do I need a referral or prior authorization before this visit/test/procedure? If authorization is needed, will your office handle it, or do I need to contact my insurer first?" + ], + + "Costs" => [ + "What is the estimated total cost for the visit/procedure/test?", + "What portion is typically deductible, copay or coinsurance?", + "What is my estimated out-of-pocket cost specifically?", + "Is there anything else about cost that patients are usually surprised by in this situation?" + ] +} + +questions_by_type.each do |type_name, questions| + type = DoctorQuestionType.find_or_create_by!(name: type_name) + + questions.each do |question| + type.doctor_questions.find_or_create_by!(question: question) + end +end