diff --git a/Gemfile b/Gemfile index e19ab8d..eceefaa 100644 --- a/Gemfile +++ b/Gemfile @@ -66,3 +66,4 @@ group :test do end gem "tailwindcss-rails", "~> 4.4" +gem "faker", "~> 3.8", group: :development diff --git a/Gemfile.lock b/Gemfile.lock index 4f6b1a9..0d25d35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,6 +114,8 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo + faker (3.8.0) + i18n (>= 1.8.11, < 2) ffi (1.17.4-aarch64-linux-gnu) ffi (1.17.4-aarch64-linux-musl) ffi (1.17.4-arm-linux-gnu) @@ -405,6 +407,7 @@ DEPENDENCIES bundler-audit capybara debug + faker (~> 3.8) image_processing (~> 1.2) importmap-rails jbuilder @@ -461,6 +464,7 @@ CHECKSUMS erb (6.0.3) sha256=e43685a8a0a0ea6a924871b2162e8953ef73147ce46b75b36d1f6774fd286e91 erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 diff --git a/app/controllers/students_controller.rb b/app/controllers/students_controller.rb new file mode 100644 index 0000000..9ce0047 --- /dev/null +++ b/app/controllers/students_controller.rb @@ -0,0 +1,75 @@ +class StudentsController < ApplicationController + before_action :set_school, only: %i[ index new create ] + before_action :set_student, only: %i[ show edit update destroy ] + + # GET /students or /students.json + def index + @students = @school.students.all + end + + # GET /students/1 or /students/1.json + def show + end + + # GET /students/new + def new + @student = @school.students.build + end + + # GET /students/1/edit + def edit + end + + # POST /students or /students.json + def create + @student = @school.students.build(student_params) + + respond_to do |format| + if @student.save + format.html { redirect_to @student, notice: "Student was successfully created." } + format.json { render :show, status: :created, location: @student } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @student.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /students/1 or /students/1.json + def update + respond_to do |format| + if @student.update(student_params) + format.html { redirect_to @student, notice: "Student was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @student } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @student.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /students/1 or /students/1.json + def destroy + @student.destroy! + + respond_to do |format| + format.html { redirect_to school_students_path(@student.school_id), notice: "Student 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_student + @student = Student.find(params.expect(:id)) + end + + def set_school + @school = School.find(params.expect(:school_id)) + end + + # Only allow a list of trusted parameters through. + def student_params + params.expect(student: [ :first_name, :last_name, :email, :grade_level, :gender ]) + end +end diff --git a/app/models/school.rb b/app/models/school.rb index ea2efb1..e05b6b4 100644 --- a/app/models/school.rb +++ b/app/models/school.rb @@ -1,3 +1,4 @@ class School < ApplicationRecord + has_many :students, dependent: :destroy validates :name, presence: true end diff --git a/app/models/student.rb b/app/models/student.rb new file mode 100644 index 0000000..031257f --- /dev/null +++ b/app/models/student.rb @@ -0,0 +1,7 @@ +class Student < ApplicationRecord + belongs_to :school + + enum :gender, %i[male female other].index_by(&:itself) + + validates :first_name, :last_name, :grade_level, presence: true +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2ebff44..cb63b8d 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -24,8 +24,9 @@ -
+
<%= yield %>
+ diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb index 51fe3d0..17a1e94 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/passwords/edit.html.erb @@ -1,16 +1,18 @@ <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> -
+
+
-
-

- Update your password -

- <%= form_with url: password_path(params[:token]), method: :put do |form| %> -
- <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72, class: "input w-full" %> - <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72, class: "input w-full" %> - <%= form.submit "Save", class: "btn btn-primary w-full" %> -
- <% end %> +
+

+ Update your password +

+ <%= form_with url: password_path(params[:token]), method: :put do |form| %> +
+ <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72, class: "input w-full" %> + <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72, class: "input w-full" %> + <%= form.submit "Save", class: "btn btn-primary w-full" %> +
+ <% end %> +
-
+
diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb index a4737dd..b3cdbc5 100644 --- a/app/views/passwords/new.html.erb +++ b/app/views/passwords/new.html.erb @@ -1,16 +1,18 @@ <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> -
+
+
-
-

- Forgot your password? -

- <%= form_with url: passwords_path do |form| %> -
- <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "input w-full" %> -
- <%= form.submit "Email reset instructions", class: "btn btn-primary w-full" %> - <% end %> +
+

+ Forgot your password? +

+ <%= form_with url: passwords_path do |form| %> +
+ <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "input w-full" %> +
+ <%= form.submit "Email reset instructions", class: "btn btn-primary w-full" %> + <% end %> +
-
+
diff --git a/app/views/schools/_form.html.erb b/app/views/schools/_form.html.erb index 5ce0e89..15b9200 100644 --- a/app/views/schools/_form.html.erb +++ b/app/views/schools/_form.html.erb @@ -1,22 +1,20 @@ <%= form_with(model: school) do |form| %> - <% if school.errors.any? %> -
-

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

+
+ <% if school.errors.any? %> +
+

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

-
    - <% school.errors.each do |error| %> -
  • <%= error.full_message %>
  • - <% end %> -
-
- <% end %> +
    + <% school.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> -
- <%= form.label :name, style: "display: block" %> - <%= form.text_field :name %> -
+ <%= form.label :name, class: "label" %> + <%= form.text_field :name, class: "input w-full" %> -
- <%= form.submit %> + <%= form.submit class: "btn btn-primary w-full" %>
<% end %> diff --git a/app/views/schools/edit.html.erb b/app/views/schools/edit.html.erb index cecb32c..1bbd16f 100644 --- a/app/views/schools/edit.html.erb +++ b/app/views/schools/edit.html.erb @@ -1,12 +1,15 @@ <% content_for :title, "Editing school" %> - -

Editing school

- -<%= render "form", school: @school %> - -
- -
- <%= link_to "Show this school", @school %> | - <%= link_to "Back to schools", schools_path %> -
+ +
+
+
+

Edit School

+ <%= render "form", school: @school %> +
+
+
diff --git a/app/views/schools/index.html.erb b/app/views/schools/index.html.erb index 4aeb8f0..20efb7e 100644 --- a/app/views/schools/index.html.erb +++ b/app/views/schools/index.html.erb @@ -9,7 +9,7 @@
    <% @schools.each do |school| %>
  • - <%= link_to school.name, school, class: "link" %> + <%= link_to school.name, school_students_path(school), class: "link" %>
  • <% end %> -
+ \ No newline at end of file diff --git a/app/views/schools/new.html.erb b/app/views/schools/new.html.erb index 6d47afe..2d8a0b1 100644 --- a/app/views/schools/new.html.erb +++ b/app/views/schools/new.html.erb @@ -1,11 +1,16 @@ <% content_for :title, "New school" %> -

New school

- -<%= render "form", school: @school %> - -
- -
- <%= link_to "Back to schools", schools_path %> -
+ +
+
+
+

New School

+ <%= render "form", school: @school %> +
+
+
\ No newline at end of file diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index fc563d9..eca0e13 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,22 +1,24 @@ <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> <%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %> -
+
+
-
-

- Login -

- <%= form_with url: session_path do |form| %> -
+
+

+ Login +

+ <%= form_with url: session_path do |form| %> +
- <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "input w-full" %> - <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "input w-full" %> -
- <%= form.submit "Sign in", class: "btn btn-primary w-full" %> - <%= link_to "Forgot password?", new_password_path, class: "btn btn-link" %> + <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "input w-full" %> + <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "input w-full" %> +
+ <%= form.submit "Sign in", class: "btn btn-primary w-full" %> + <%= link_to "Forgot password?", new_password_path, class: "btn btn-link" %> +
-
- <% end %> + <% end %> +
-
+
diff --git a/app/views/students/_form.html.erb b/app/views/students/_form.html.erb new file mode 100644 index 0000000..4c9654c --- /dev/null +++ b/app/views/students/_form.html.erb @@ -0,0 +1,32 @@ +<%= form_with(model: [@school, student]) do |form| %> +
+ <% if student.errors.any? %> +
+

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

+ +
    + <% student.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + + <%= form.label :first_name, class: "label" %> + <%= form.text_field :first_name, class: "input w-full" %> + + <%= form.label :last_name, class: "label" %> + <%= form.text_field :last_name, class: "input w-full" %> + + <%= form.label :email, class: "label" %> + <%= form.text_field :email, class: "input w-full" %> + + <%= form.label :grade_level, class: "label" %> + <%= form.number_field :grade_level, class: "input w-full" %> + + <%= form.label :gender, class: "label" %> + <%= form.text_field :gender, class: "input w-full" %> + + <%= form.submit class: "btn btn-primary" %> +
+<% end %> diff --git a/app/views/students/_student.html.erb b/app/views/students/_student.html.erb new file mode 100644 index 0000000..f106eba --- /dev/null +++ b/app/views/students/_student.html.erb @@ -0,0 +1,27 @@ +
+
+ First name: + <%= student.first_name %> +
+ +
+ Last name: + <%= student.last_name %> +
+ +
+ Email: + <%= student.email %> +
+ +
+ Grade level: + <%= student.grade_level %> +
+ +
+ Gender: + <%= student.gender %> +
+ +
diff --git a/app/views/students/edit.html.erb b/app/views/students/edit.html.erb new file mode 100644 index 0000000..76317ac --- /dev/null +++ b/app/views/students/edit.html.erb @@ -0,0 +1,16 @@ +<% content_for :title, "Editing student" %> + +
+
+
+

Editing Student

+ <%= render "form", student: @student %> +
+
+
diff --git a/app/views/students/index.html.erb b/app/views/students/index.html.erb new file mode 100644 index 0000000..3fa0122 --- /dev/null +++ b/app/views/students/index.html.erb @@ -0,0 +1,45 @@ +

<%= notice %>

+ +<% content_for :title, [ @school.name, "Students" ].join(" - ") %> + + + +
+

Students for <%= @school.name %>

+ <%= link_to "New student", new_school_student_path(@school), class: "btn btn-primary" %> +
+
+ + + + + + + + + + + + + + <% @students.each do |student| %> + + + + + + + +
First nameLast nameEmailGrade levelGenderActions
<%= student.first_name %><%= student.last_name %><%= student.email %><%= student.grade_level %><%= student.gender %> +
+ <%= link_to "Edit", edit_student_path(student), class: "btn btn-primary" %> + <%= button_to "Delete", student, method: :delete, data: { 'turbo-confirm': "Are you sure?" }, class: "btn btn-error" %> +
+ <% end %> +
+
diff --git a/app/views/students/new.html.erb b/app/views/students/new.html.erb new file mode 100644 index 0000000..a250adf --- /dev/null +++ b/app/views/students/new.html.erb @@ -0,0 +1,17 @@ +<% content_for :title, "New student" %> + + +
+
+
+

New Student

+ <%= render "form", student: @student %> +
+
+
\ No newline at end of file diff --git a/app/views/students/show.html.erb b/app/views/students/show.html.erb new file mode 100644 index 0000000..89f99f4 --- /dev/null +++ b/app/views/students/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @student %> + +
+ <%= link_to "Edit this student", edit_student_path(@student) %> | + <%= link_to "Back to students", school_students_path(@student.school_id) %> + + <%= button_to "Destroy this student", @student, method: :delete %> +
diff --git a/config/ci.rb b/config/ci.rb index 1712cc1..8b43ccf 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -9,7 +9,6 @@ step "Security: Importmap vulnerability audit", "bin/importmap audit" step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" step "Tests: Rails", "bin/rails test" - step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" # Optional: Run system tests # step "Tests: System", "bin/rails test:system" diff --git a/config/routes.rb b/config/routes.rb index 0282416..608cfc5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,9 @@ resource :session resources :passwords, param: :token scope :admin do - resources :schools + resources :schools do + resources :students, shallow: true + end end root to: "schools#index" diff --git a/db/migrate/20260421234927_create_students.rb b/db/migrate/20260421234927_create_students.rb new file mode 100644 index 0000000..aa5baa7 --- /dev/null +++ b/db/migrate/20260421234927_create_students.rb @@ -0,0 +1,14 @@ +class CreateStudents < ActiveRecord::Migration[8.1] + def change + create_table :students do |t| + t.string :first_name, null: false + t.string :last_name, null: false + t.string :email + t.integer :grade_level, null: false + t.string :gender + t.belongs_to :school, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1736a01..e8cf50e 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_04_18_182356) do +ActiveRecord::Schema[8.1].define(version: 2026_04_21_234927) do create_table "schools", force: :cascade do |t| t.datetime "created_at", null: false t.string "name" @@ -26,6 +26,18 @@ t.index ["user_id"], name: "index_sessions_on_user_id" end + create_table "students", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "email" + t.string "first_name", null: false + t.string "gender" + t.integer "grade_level", null: false + t.string "last_name", null: false + t.integer "school_id", null: false + t.datetime "updated_at", null: false + t.index ["school_id"], name: "index_students_on_school_id" + end + create_table "users", force: :cascade do |t| t.datetime "created_at", null: false t.string "email_address", null: false @@ -36,4 +48,5 @@ end add_foreign_key "sessions", "users" + add_foreign_key "students", "schools" end diff --git a/db/seeds.rb b/db/seeds.rb index 1611db1..9c601be 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,4 +5,26 @@ user.password = "password" end -School.find_or_create_by!(name: "Example School") +school = School.find_or_create_by!(name: "Example School") + +def maybe(&block) + return unless Faker::Boolean.boolean + + block.call +end + +def build_student_attrs(overrides = {}) + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + email: maybe { Faker::Internet.email(domain: 'example.com') }, + gender: maybe { Student.genders.keys.sample } + }.merge(overrides) +end + +# randomly sample 3 grade levels and create 10 students for each grade level +grades = (1..12).to_a.sample(3) +grades.each do |grade| + students = 10.times.map { build_student_attrs(grade_level: grade) } + school.students.create!(students) +end diff --git a/test/controllers/students_controller_test.rb b/test/controllers/students_controller_test.rb new file mode 100644 index 0000000..49c3fa0 --- /dev/null +++ b/test/controllers/students_controller_test.rb @@ -0,0 +1,50 @@ +require "test_helper" + +class StudentsControllerTest < ActionDispatch::IntegrationTest + setup do + @school = schools(:one) + @student = students(:ada) + sign_in_as users(:admin) + end + + test "should get index" do + get school_students_url(@school) + assert_response :success + end + + test "should get new" do + get new_school_student_url(@school) + assert_response :success + end + + test "should create student" do + assert_difference("Student.count") do + post school_students_url(@school), params: { student: { email: @student.email, first_name: @student.first_name, gender: @student.gender, grade_level: @student.grade_level, last_name: @student.last_name, school_id: @student.school_id } } + end + + assert_redirected_to student_url(Student.last) + end + + test "should show student" do + get student_url(@student) + assert_response :success + end + + test "should get edit" do + get edit_student_url(@student) + assert_response :success + end + + test "should update student" do + patch student_url(@student), params: { student: { email: @student.email, first_name: @student.first_name, gender: @student.gender, grade_level: @student.grade_level, last_name: @student.last_name, school_id: @student.school_id } } + assert_redirected_to student_url(@student) + end + + test "should destroy student" do + assert_difference("Student.count", -1) do + delete student_url(@student) + end + + assert_redirected_to school_students_url(@school) + end +end diff --git a/test/fixtures/students.yml b/test/fixtures/students.yml new file mode 100644 index 0000000..b6b8a60 --- /dev/null +++ b/test/fixtures/students.yml @@ -0,0 +1,15 @@ +ada: + first_name: Ada + last_name: Lovelace + email: + grade_level: 5 + gender: + school: one + +grace: + first_name: Grace + last_name: Hopper + email: ghopper@example.com + grade_level: 6 + gender: female + school: two diff --git a/test/models/student_test.rb b/test/models/student_test.rb new file mode 100644 index 0000000..88b348f --- /dev/null +++ b/test/models/student_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class StudentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end