From 86568f8b7f337df92160618941ac30c34b850abf Mon Sep 17 00:00:00 2001 From: AkaMandakeru Date: Sat, 14 Dec 2024 11:23:38 -0300 Subject: [PATCH 1/6] base fixes: create person and query performances --- Gemfile | 1 + Gemfile.lock | 14 ++++++++++++++ app/controllers/people_controller.rb | 4 ++-- .../kaminari/bootstrap3/_first_page.html.slim | 3 +++ app/views/kaminari/bootstrap3/_gap.html.slim | 2 ++ app/views/kaminari/bootstrap3/_last_page.html.slim | 3 +++ app/views/kaminari/bootstrap3/_next_page.html.slim | 3 +++ app/views/kaminari/bootstrap3/_page.html.slim | 3 +++ app/views/kaminari/bootstrap3/_paginator.html.slim | 13 +++++++++++++ app/views/kaminari/bootstrap3/_prev_page.html.slim | 3 +++ app/views/people/index.html.slim | 4 +++- 11 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 app/views/kaminari/bootstrap3/_first_page.html.slim create mode 100644 app/views/kaminari/bootstrap3/_gap.html.slim create mode 100644 app/views/kaminari/bootstrap3/_last_page.html.slim create mode 100644 app/views/kaminari/bootstrap3/_next_page.html.slim create mode 100644 app/views/kaminari/bootstrap3/_page.html.slim create mode 100644 app/views/kaminari/bootstrap3/_paginator.html.slim create mode 100644 app/views/kaminari/bootstrap3/_prev_page.html.slim diff --git a/Gemfile b/Gemfile index 716a9ba..9510608 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,7 @@ gem "jbuilder" gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] gem 'slim-rails' gem "jsbundling-rails" +gem 'kaminari' group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/Gemfile.lock b/Gemfile.lock index 8aaae9a..e78b031 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,6 +115,18 @@ GEM activesupport (>= 5.0.0) jsbundling-rails (1.3.1) railties (>= 6.0.0) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) logger (1.6.1) loofah (2.23.1) crass (~> 1.0.2) @@ -279,6 +291,7 @@ GEM PLATFORMS arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES @@ -290,6 +303,7 @@ DEPENDENCIES faker jbuilder jsbundling-rails + kaminari pry-rails puma (~> 5.0) rails (~> 7.0.1) diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 6559b9e..1d32c20 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -1,7 +1,7 @@ class PeopleController < ApplicationController def index - @people = Person.all + @people = Person.includes(:company).page(params[:page] || 1).per(10) end def new @@ -19,7 +19,7 @@ def create private def person_attributes - params.require(:person).permit(:name, :email, :phone) + params.require(:person).permit(:name, :email, :phone_number) end end diff --git a/app/views/kaminari/bootstrap3/_first_page.html.slim b/app/views/kaminari/bootstrap3/_first_page.html.slim new file mode 100644 index 0000000..eefa7cd --- /dev/null +++ b/app/views/kaminari/bootstrap3/_first_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), + url, remote: remote, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_gap.html.slim b/app/views/kaminari/bootstrap3/_gap.html.slim new file mode 100644 index 0000000..4becbc8 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_gap.html.slim @@ -0,0 +1,2 @@ +li.disabled.page-item + = link_to raw(t 'views.pagination.truncate'), '#', class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_last_page.html.slim b/app/views/kaminari/bootstrap3/_last_page.html.slim new file mode 100644 index 0000000..4907dec --- /dev/null +++ b/app/views/kaminari/bootstrap3/_last_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), + url, remote: remote, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_next_page.html.slim b/app/views/kaminari/bootstrap3/_next_page.html.slim new file mode 100644 index 0000000..9c1a744 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_next_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.last?, raw(t 'views.pagination.next'), + url, rel: 'next', remote: remote, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_page.html.slim b/app/views/kaminari/bootstrap3/_page.html.slim new file mode 100644 index 0000000..b496069 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_page.html.slim @@ -0,0 +1,3 @@ +li class="#{'active' if page.current?} page-item" + = link_to page, page.current? ? '#' : url, + remote: remote, rel: page.rel, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_paginator.html.slim b/app/views/kaminari/bootstrap3/_paginator.html.slim new file mode 100644 index 0000000..adff4bd --- /dev/null +++ b/app/views/kaminari/bootstrap3/_paginator.html.slim @@ -0,0 +1,13 @@ += paginator.render do + ul.pagination + == first_page_tag unless current_page.first? + == prev_page_tag unless current_page.first? + + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + == page_tag page + - elsif !page.was_truncated? + == gap_tag + + == next_page_tag unless current_page.last? + == last_page_tag unless current_page.last? diff --git a/app/views/kaminari/bootstrap3/_prev_page.html.slim b/app/views/kaminari/bootstrap3/_prev_page.html.slim new file mode 100644 index 0000000..185aa51 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_prev_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), + url, rel: 'prev', remote: remote, class: "page-link" diff --git a/app/views/people/index.html.slim b/app/views/people/index.html.slim index ddbf52f..f06ea5f 100644 --- a/app/views/people/index.html.slim +++ b/app/views/people/index.html.slim @@ -13,9 +13,11 @@ table.table tr th[scope="row"]= person&.id td= person.try(:name) - td= person.try(:phone) + td= person.try(:phone_number) td= person.try(:email) td= person.try(:company).try(:name) += paginate @people, theme: 'bootstrap3' += page_entries_info @people From 5c46365660ee2705e17c490df279c81521f22797 Mon Sep 17 00:00:00 2001 From: AkaMandakeru Date: Sat, 14 Dec 2024 12:05:33 -0300 Subject: [PATCH 2/6] changes on the form and people controller to display error messages --- app/controllers/people_controller.rb | 7 ++++--- app/models/person.rb | 3 ++- app/views/people/_form.html.slim | 19 +++++++++++++++++++ app/views/people/new.html.slim | 14 +------------- 4 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 app/views/people/_form.html.slim diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 1d32c20..4e60e4b 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -9,10 +9,12 @@ def new end def create - if Person.create(person_attributes) + @person = Person.new(person_attributes) + + if @person.save redirect_to people_path, notice: 'Successfully created entry' else - render :create, alert: 'Unsuccessfully created entry' + render :new, status: :unprocessable_entity end end @@ -21,6 +23,5 @@ def create def person_attributes params.require(:person).permit(:name, :email, :phone_number) end - end diff --git a/app/models/person.rb b/app/models/person.rb index f935e46..fee1c9d 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -12,6 +12,7 @@ # class Person < ApplicationRecord - belongs_to :company, optional: true + + validates :name, :phone_number, :email, presence: true end diff --git a/app/views/people/_form.html.slim b/app/views/people/_form.html.slim new file mode 100644 index 0000000..a882aa1 --- /dev/null +++ b/app/views/people/_form.html.slim @@ -0,0 +1,19 @@ += form_for person, class: 'row' do |f| + - if person.errors.any? + .alert.alert-danger + h4 There were errors with your submission: + ul + - person.errors.full_messages.each do |msg| + li = msg + + .col-auto + = f.label :name, class: 'form-label' + = f.text_field :name, class: 'form-control' + .col-auto + = f.label :phone_number, class: 'form-label' + = f.text_field :phone_number, class: 'form-control' + .col-auto + = f.label :email, class: 'form-label' + = f.text_field :email, class: 'form-control' + .col-auto.mt-4 + = f.submit person.new_record? ? 'Create Person' : 'Update Person', class: 'btn btn-primary' diff --git a/app/views/people/new.html.slim b/app/views/people/new.html.slim index 40bc446..a262d8b 100644 --- a/app/views/people/new.html.slim +++ b/app/views/people/new.html.slim @@ -1,14 +1,2 @@ h2 Creating an entry - -= form_for @person, class: 'row' do |f| - .col-auto - = f.label :name, class: 'form-label' - = f.text_field :name, class: 'form-control' - .col-auto - = f.label :phone_number, class: 'form-label' - = f.text_field :phone_number, class: 'form-control' - .col-auto - = f.label :email, class: 'form-label' - = f.text_field :email, class: 'form-control' - .col-auto.mt-4 - = f.submit class: 'btn btn-primary' += render 'form', person: @person From dd26c21e46430f177d95e230fb7ba6f97589650b Mon Sep 17 00:00:00 2001 From: AkaMandakeru Date: Sat, 14 Dec 2024 12:25:56 -0300 Subject: [PATCH 3/6] factories and specs for the index action --- Gemfile | 2 ++ Gemfile.lock | 5 +++ spec/controllers/people_controller_spec.rb | 42 ++++++++++++++++++++++ spec/factories/companies.rb | 5 +++ spec/factories/people.rb | 9 +++++ spec/rails_helper.rb | 2 ++ spec/views/people/index.html.slim_spec.rb | 31 +++++++++++++++- 7 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 spec/factories/companies.rb create mode 100644 spec/factories/people.rb diff --git a/Gemfile b/Gemfile index 9510608..e26d629 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ group :development, :test do gem "debug", platforms: %i[ mri mingw x64_mingw ] gem "rspec-rails" gem "pry-rails" + gem "factory_bot_rails" end group :development do @@ -38,6 +39,7 @@ group :test do gem "rspec-its" gem "shoulda" gem "simplecov", require: false + gem "rails-controller-testing" end # Use Redis for Action Cable diff --git a/Gemfile.lock b/Gemfile.lock index e78b031..2f1a945 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,6 +183,10 @@ GEM activesupport (= 7.0.8.6) bundler (>= 1.15.0) railties (= 7.0.8.6) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -307,6 +311,7 @@ DEPENDENCIES pry-rails puma (~> 5.0) rails (~> 7.0.1) + rails-controller-testing redis (~> 4.0) rspec-its rspec-rails diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb index 82220e8..5df8ac3 100644 --- a/spec/controllers/people_controller_spec.rb +++ b/spec/controllers/people_controller_spec.rb @@ -1,11 +1,53 @@ require 'rails_helper' +def create_people_with_companies(count) + count.times do |i| + company = Company.create!(name: "Company #{i}") + Person.create!(name: "Person #{i}", email: "person#{i}@example.com", phone_number: "123456789#{i}", company: company) + end +end + RSpec.describe PeopleController, type: :controller do subject { response } describe 'GET index' do before { get :index } it { is_expected.to have_http_status(:ok) } + + context "when there are many people to list" do + before do + create_people_with_companies(25) # Creates 25 people with associated companies + end + + it "assigns @people with paginated results" do + get :index, params: { page: 1 } + expect(assigns(:people)).to be_a_kind_of(ActiveRecord::Relation) + expect(assigns(:people).count).to eq(10) # Expect 10 items per page + end + + it "renders the index template" do + get :index, params: { page: 1 } + expect(response).to render_template(:index) + end + + it "paginates correctly for the first page" do + get :index, params: { page: 1 } + expect(assigns(:people).first.name).to eq("Person 0") + expect(assigns(:people).last.name).to eq("Person 9") + end + + it "paginates correctly for the second page" do + get :index, params: { page: 2 } + expect(assigns(:people).first.name).to eq("Person 10") + expect(assigns(:people).last.name).to eq("Person 19") + end + + it "defaults to the first page if no page param is given" do + get :index + expect(assigns(:people).first.name).to eq("Person 0") + expect(assigns(:people).last.name).to eq("Person 9") + end + end end describe 'GET new' do diff --git a/spec/factories/companies.rb b/spec/factories/companies.rb new file mode 100644 index 0000000..f8d2977 --- /dev/null +++ b/spec/factories/companies.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :company do + name { "Company 123" } + end +end diff --git a/spec/factories/people.rb b/spec/factories/people.rb new file mode 100644 index 0000000..a458391 --- /dev/null +++ b/spec/factories/people.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :person do + sequence(:name) { |n| "Person #{n}" } + phone_number { "123456" } + email { "email@gmail.com" } + + association :company + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 8b88205..2100bd9 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -63,6 +63,8 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include FactoryBot::Syntax::Methods end Shoulda::Matchers.configure do |config| config.integrate do |with| diff --git a/spec/views/people/index.html.slim_spec.rb b/spec/views/people/index.html.slim_spec.rb index 7e2d362..6f76005 100644 --- a/spec/views/people/index.html.slim_spec.rb +++ b/spec/views/people/index.html.slim_spec.rb @@ -1,5 +1,34 @@ require "rails_helper" describe "people/index.html.slim" do - it "Displays the users" + it "displays the users in a table" do + # Setup test data + company = FactoryBot.create(:company, name: "Test Company") + people = FactoryBot.create_list(:person, 12, company: company) + + # Assign instance variable used in the view + assign(:people, Kaminari.paginate_array(people).page(1).per(10)) + + # Render the view + render + + # Check for table headers + expect(rendered).to have_selector("table.table thead tr th", text: "ID") + expect(rendered).to have_selector("table.table thead tr th", text: "Name") + expect(rendered).to have_selector("table.table thead tr th", text: "Phone number") + expect(rendered).to have_selector("table.table thead tr th", text: "Email address") + expect(rendered).to have_selector("table.table thead tr th", text: "Company") + + # Check for table rows + people.first(10).each do |person| + expect(rendered).to have_selector("table.table tbody tr th", text: person.id.to_s) + expect(rendered).to have_selector("table.table tbody tr td", text: person.name) + expect(rendered).to have_selector("table.table tbody tr td", text: person.phone_number) + expect(rendered).to have_selector("table.table tbody tr td", text: person.email) + expect(rendered).to have_selector("table.table tbody tr td", text: company.name) + end + + # Check for pagination + expect(rendered).to have_selector(".pagination") + end end From b29a641419f6b11431ed81059dfd087ab9b14d0e Mon Sep 17 00:00:00 2001 From: AkaMandakeru Date: Sat, 14 Dec 2024 21:47:53 -0300 Subject: [PATCH 4/6] companies creation, edition and listing --- app/controllers/companies_controller.rb | 40 +++++++ app/views/companies/edit.html.slim | 8 ++ app/views/companies/index.html.slim | 25 +++++ app/views/companies/new.html.slim | 8 ++ app/views/layouts/application.html.slim | 2 + config/routes.rb | 1 + spec/controllers/companies_controller.spec.rb | 102 ++++++++++++++++++ spec/views/companies/index.html.slim_spec.rb | 36 +++++++ 8 files changed, 222 insertions(+) create mode 100644 app/controllers/companies_controller.rb create mode 100644 app/views/companies/edit.html.slim create mode 100644 app/views/companies/index.html.slim create mode 100644 app/views/companies/new.html.slim create mode 100644 spec/controllers/companies_controller.spec.rb create mode 100644 spec/views/companies/index.html.slim_spec.rb diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb new file mode 100644 index 0000000..1456ca4 --- /dev/null +++ b/app/controllers/companies_controller.rb @@ -0,0 +1,40 @@ +class CompaniesController < ApplicationController + before_action :load_company, only: [:edit, :update] + + def index + @companies = Company.all.page(params[:page] || 1).per(10) + end + + def new + @company = Company.new + end + + def edit; end + + def create + if Company.create(company_attributes) + redirect_to companies_path, notice: 'Successfully created entry' + else + render :create, alert: 'Unsuccessfully created entry' + end + end + + def update + if @company.update(company_attributes) + redirect_to companies_path, notice: 'Successfully updated entry' + else + render :edit, alert: 'Unsuccessfully created entry' + end + end + + private + + def company_attributes + params.require(:company).permit(:name) + end + + def load_company + @company = Company.find(params[:id]) + end +end + diff --git a/app/views/companies/edit.html.slim b/app/views/companies/edit.html.slim new file mode 100644 index 0000000..6f5e495 --- /dev/null +++ b/app/views/companies/edit.html.slim @@ -0,0 +1,8 @@ +h2 Creating an entry + += form_for @company, class: 'row' do |f| + .col-auto + = f.label :name, class: 'form-label' + = f.text_field :name, class: 'form-control' + .col-auto.mt-4 + = f.submit class: 'btn btn-primary' diff --git a/app/views/companies/index.html.slim b/app/views/companies/index.html.slim new file mode 100644 index 0000000..c3d2cca --- /dev/null +++ b/app/views/companies/index.html.slim @@ -0,0 +1,25 @@ +h2 Viewing companies + +table.table + thead + tr + th[scope="col"] ID + th[scope="col"] Name + th[scope="col"] Created At + th[scope="col"] Actions + tbody + - @companies.each do |company| + tr + th[scope="row"]= company&.id + td= company.try(:name) + td= company.try(:created_at).strftime("%m/%d/%Y") + td= link_to "Edit", edit_company_path(company), class: 'btn btn-primary' + +.row.justify-content-between + .col-4 + = paginate @companies, theme: 'bootstrap3' + = page_entries_info @companies + .col-4 + = link_to "New Company", new_company_path, class: 'btn btn-success' + + diff --git a/app/views/companies/new.html.slim b/app/views/companies/new.html.slim new file mode 100644 index 0000000..6f5e495 --- /dev/null +++ b/app/views/companies/new.html.slim @@ -0,0 +1,8 @@ +h2 Creating an entry + += form_for @company, class: 'row' do |f| + .col-auto + = f.label :name, class: 'form-label' + = f.text_field :name, class: 'form-control' + .col-auto.mt-4 + = f.submit class: 'btn btn-primary' diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 1fbdb01..e5c55a2 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -19,6 +19,8 @@ html[lang="en"] a.nav-link[aria-current="page" href="/"]All li.nav-item = link_to('Create', new_person_path, class: 'nav-link') + li.nav-item + = link_to('Companies', companies_path, class: 'nav-link') form.d-flex input.form-control.me-2[type="search" placeholder="Search" aria-label="Search"] button.btn.btn-outline-success[type="submit"] Search diff --git a/config/routes.rb b/config/routes.rb index f0685d3..8641401 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do resources :people, only: [:index, :new, :create] + resources :companies root to: 'people#index' end diff --git a/spec/controllers/companies_controller.spec.rb b/spec/controllers/companies_controller.spec.rb new file mode 100644 index 0000000..f552bbf --- /dev/null +++ b/spec/controllers/companies_controller.spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +RSpec.describe CompaniesController, type: :controller do + let!(:company) { FactoryBot.create(:company, name: "Test Company") } + + describe "GET #index" do + it "assigns @companies with paginated results" do + get :index, params: { page: 1 } + expect(assigns(:companies)).to be_a_kind_of(ActiveRecord::Relation) + expect(assigns(:companies)).to include(company) + end + + it "renders the index template" do + get :index + expect(response).to render_template(:index) + end + end + + describe "GET #new" do + it "assigns a new Company to @company" do + get :new + expect(assigns(:company)).to be_a_new(Company) + end + + it "renders the new template" do + get :new + expect(response).to render_template(:new) + end + end + + describe "GET #edit" do + it "assigns the requested company to @company" do + get :edit, params: { id: company.id } + expect(assigns(:company)).to eq(company) + end + + it "renders the edit template" do + get :edit, params: { id: company.id } + expect(response).to render_template(:edit) + end + end + + describe "POST #create" do + context "with valid attributes" do + it "creates a new company" do + expect { + post :create, params: { company: { name: "New Company" } } + }.to change(Company, :count).by(1) + end + + it "redirects to the index with a success notice" do + post :create, params: { company: { name: "New Company" } } + expect(response).to redirect_to(companies_path) + expect(flash[:notice]).to eq("Successfully created entry") + end + end + + context "with invalid attributes" do + it "does not create a new company" do + expect { + post :create, params: { company: { name: "" } } + }.not_to change(Company, :count) + end + + it "renders the create template with an alert" do + post :create, params: { company: { name: "" } } + expect(response).to render_template(:create) + expect(flash[:alert]).to eq("Unsuccessfully created entry") + end + end + end + + describe "PATCH #update" do + context "with valid attributes" do + it "updates the company" do + patch :update, params: { id: company.id, company: { name: "Updated Company" } } + company.reload + expect(company.name).to eq("Updated Company") + end + + it "redirects to the index with a success notice" do + patch :update, params: { id: company.id, company: { name: "Updated Company" } } + expect(response).to redirect_to(companies_path) + expect(flash[:notice]).to eq("Successfully updated entry") + end + end + + context "with invalid attributes" do + it "does not update the company" do + patch :update, params: { id: company.id, company: { name: "" } } + company.reload + expect(company.name).to eq("Test Company") + end + + it "renders the edit template with an alert" do + patch :update, params: { id: company.id, company: { name: "" } } + expect(response).to render_template(:edit) + expect(flash[:alert]).to eq("Unsuccessfully created entry") + end + end + end +end diff --git a/spec/views/companies/index.html.slim_spec.rb b/spec/views/companies/index.html.slim_spec.rb new file mode 100644 index 0000000..52af868 --- /dev/null +++ b/spec/views/companies/index.html.slim_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +describe "companies/index.html.slim" do + let!(:company1) { FactoryBot.create(:company, name: "Company One", created_at: Time.zone.parse("2023-01-01")) } + let!(:company2) { FactoryBot.create(:company, name: "Company Two", created_at: Time.zone.parse("2023-01-02")) } + + it "displays the companies in a table" do + assign(:companies, Kaminari.paginate_array([company1, company2]).page(1).per(10)) + + render + + # Check table headers + expect(rendered).to have_selector("table.table thead tr th", text: "ID") + expect(rendered).to have_selector("table.table thead tr th", text: "Name") + expect(rendered).to have_selector("table.table thead tr th", text: "Created At") + expect(rendered).to have_selector("table.table thead tr th", text: "Actions") + + # Check rows for companies + expect(rendered).to have_selector("table.table tbody tr th", text: company1.id.to_s) + expect(rendered).to have_selector("table.table tbody tr td", text: company1.name) + expect(rendered).to have_selector("table.table tbody tr td", text: company1.created_at.strftime("%m/%d/%Y")) + expect(rendered).to have_link("Edit", href: edit_company_path(company1)) + + expect(rendered).to have_selector("table.table tbody tr th", text: company2.id.to_s) + expect(rendered).to have_selector("table.table tbody tr td", text: company2.name) + expect(rendered).to have_selector("table.table tbody tr td", text: company2.created_at.strftime("%m/%d/%Y")) + expect(rendered).to have_link("Edit", href: edit_company_path(company2)) + end + + it "displays a link to create a new company" do + assign(:companies, Kaminari.paginate_array([company1, company2]).page(1).per(10)) + + render + expect(rendered).to have_link("New Company", href: new_company_path) + end +end From 6471f547f41e9ef82d2cbab3d191c461e8523c67 Mon Sep 17 00:00:00 2001 From: AkaMandakeru Date: Sat, 14 Dec 2024 22:27:23 -0300 Subject: [PATCH 5/6] feature to add company to a person --- app/controllers/people_controller.rb | 8 +++--- app/views/people/_form.html.slim | 7 +++++ spec/controllers/people_controller_spec.rb | 30 +++++++++++++++++++--- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 4e60e4b..4840946 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -6,22 +6,24 @@ def index def new @person = Person.new + @companies = Company.all.order(:name) end def create @person = Person.new(person_attributes) if @person.save - redirect_to people_path, notice: 'Successfully created entry' + redirect_to people_path, notice: 'Person successfully created' else - render :new, status: :unprocessable_entity + @companies = Company.page(params[:page] || 1).per(10) + render :new end end private def person_attributes - params.require(:person).permit(:name, :email, :phone_number) + params.require(:person).permit(:name, :email, :phone_number, :company_id) end end diff --git a/app/views/people/_form.html.slim b/app/views/people/_form.html.slim index a882aa1..7fda90a 100644 --- a/app/views/people/_form.html.slim +++ b/app/views/people/_form.html.slim @@ -9,11 +9,18 @@ .col-auto = f.label :name, class: 'form-label' = f.text_field :name, class: 'form-control' + .col-auto = f.label :phone_number, class: 'form-label' = f.text_field :phone_number, class: 'form-control' + .col-auto = f.label :email, class: 'form-label' = f.text_field :email, class: 'form-control' + + .col-auto + = f.label :company_id, 'Select a Company', class: 'form-label' + = f.collection_select :company_id, @companies, :id, :name, prompt: 'Choose a company', class: 'form-select' + .col-auto.mt-4 = f.submit person.new_record? ? 'Create Person' : 'Update Person', class: 'btn btn-primary' diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb index 5df8ac3..c6c7672 100644 --- a/spec/controllers/people_controller_spec.rb +++ b/spec/controllers/people_controller_spec.rb @@ -57,12 +57,34 @@ def create_people_with_companies(count) end describe 'POST create' do - it 'Creates a record' do - expect{ post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo' } } }.to change{ Person.count }.by(1) + let!(:company) { FactoryBot.create(:company) } + + context 'with valid attributes' do + it 'creates a record' do + expect { + post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo@example.com', company_id: company.id } } + }.to change { Person.count }.by(1) + end + + it 'associates the person with the correct company' do + post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo@example.com', company_id: company.id } } + person = Person.last + expect(person.company).to eq(company) + end + + it 'redirects to the index with a success notice' do + post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo@example.com', company_id: company.id } } + expect(response).to redirect_to(people_path) + expect(flash[:notice]).to eq('Person successfully created') + end end - it 'has status found' do - expect(post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo' } }).to have_http_status(:found) + context 'with invalid attributes' do + it 'does not create a record' do + expect { + post :create, params: { person: { name: '', phone_number: '', email: '', company_id: nil } } + }.not_to change { Person.count } + end end end end From a8b32023c0c3b5a230e5eb78ebcad84c7d101e74 Mon Sep 17 00:00:00 2001 From: AkaMandakeru Date: Sun, 15 Dec 2024 21:41:21 -0300 Subject: [PATCH 6/6] People and Companies API created --- Gemfile | 1 + Gemfile.lock | 26 ++++++ app/controllers/api/companies_controller.rb | 23 ++++++ app/controllers/api/people_controller.rb | 23 ++++++ app/serializers/person_serializer.rb | 8 ++ config/routes.rb | 5 ++ .../api/companies_controller_spec.rb | 42 ++++++++++ .../controllers/api/people_controller_spec.rb | 32 ++++++++ spec/controllers/companies_controller.spec.rb | 81 ++++++++++--------- spec/rails_helper.rb | 1 + 10 files changed, 202 insertions(+), 40 deletions(-) create mode 100644 app/controllers/api/companies_controller.rb create mode 100644 app/controllers/api/people_controller.rb create mode 100644 app/serializers/person_serializer.rb create mode 100644 spec/controllers/api/companies_controller_spec.rb create mode 100644 spec/controllers/api/people_controller_spec.rb diff --git a/Gemfile b/Gemfile index e26d629..8b5ba80 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,7 @@ gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] gem 'slim-rails' gem "jsbundling-rails" gem 'kaminari' +gem 'active_model_serializers' group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/Gemfile.lock b/Gemfile.lock index 2f1a945..cb1a04e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_model_serializers (0.10.15) + actionpack (>= 4.1) + activemodel (>= 4.1) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) activejob (7.0.8.6) activesupport (= 7.0.8.6) globalid (>= 0.3.6) @@ -68,6 +73,9 @@ GEM tzinfo (~> 2.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + airborne (0.0.18) + rest-client (~> 1.7, >= 1.7.2) + rspec (~> 3.1, >= 3.1.0) annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) @@ -83,6 +91,8 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + case_transform (0.2) + activesupport coderay (1.1.3) concurrent-ruby (1.3.4) crass (1.0.6) @@ -94,6 +104,7 @@ GEM reline (>= 0.3.8) diff-lcs (1.5.1) docile (1.4.1) + domain_name (0.6.20240107) erubi (1.13.0) factory_bot (6.5.0) activesupport (>= 5.0.0) @@ -104,6 +115,8 @@ GEM i18n (>= 1.8.11, < 2) globalid (1.2.1) activesupport (>= 6.1) + http-cookie (1.0.8) + domain_name (~> 0.5) i18n (1.14.6) concurrent-ruby (~> 1.0) io-console (0.7.2) @@ -115,6 +128,7 @@ GEM activesupport (>= 5.0.0) jsbundling-rails (1.3.1) railties (>= 6.0.0) + jsonapi-renderer (0.2.2) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -139,6 +153,7 @@ GEM marcel (1.0.4) matrix (0.4.2) method_source (1.1.0) + mime-types (2.99.3) mini_mime (1.1.5) minitest (5.25.2) net-imap (0.5.1) @@ -150,6 +165,7 @@ GEM timeout net-smtp (0.5.0) net-protocol + netrc (0.11.0) nio4r (2.7.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) @@ -208,7 +224,15 @@ GEM regexp_parser (2.9.2) reline (0.5.11) io-console (~> 0.5) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rexml (3.3.9) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) @@ -299,6 +323,8 @@ PLATFORMS x86_64-linux DEPENDENCIES + active_model_serializers + airborne annotate capybara cssbundling-rails diff --git a/app/controllers/api/companies_controller.rb b/app/controllers/api/companies_controller.rb new file mode 100644 index 0000000..46fa306 --- /dev/null +++ b/app/controllers/api/companies_controller.rb @@ -0,0 +1,23 @@ +class Api::CompaniesController < ActionController::API + def index + if params[:name].present? + companies = Company.where('LOWER(name) LIKE ?', "%#{params[:name]&.downcase}%").page(params[:page] || 1).per(params[:per_page] || 10) + else + companies = Company.all.page(params[:page] || 1).per(10) + end + + render json: { meta: pagination_meta(companies), data: companies }, status: 200 + end + + private + + def pagination_meta(scope) + { + current_page: scope.current_page, + next_page: scope.next_page, + prev_page: scope.prev_page, + total_pages: scope.total_pages, + total_count: scope.total_count + } + end +end diff --git a/app/controllers/api/people_controller.rb b/app/controllers/api/people_controller.rb new file mode 100644 index 0000000..88c5d36 --- /dev/null +++ b/app/controllers/api/people_controller.rb @@ -0,0 +1,23 @@ +class Api::PeopleController < ActionController::API + def index + if params[:email].present? + people = Person.where(email: params[:email]).page(params[:page] || 1).per(params[:per_page] || 10) + else + people = Person.includes(:company).page(params[:page] || 1).per(10) + end + + render json: { meta: pagination_meta(people), data: people }, status: 200 + end + + private + + def pagination_meta(scope) + { + current_page: scope.current_page, + next_page: scope.next_page, + prev_page: scope.prev_page, + total_pages: scope.total_pages, + total_count: scope.total_count + } + end +end diff --git a/app/serializers/person_serializer.rb b/app/serializers/person_serializer.rb new file mode 100644 index 0000000..939788a --- /dev/null +++ b/app/serializers/person_serializer.rb @@ -0,0 +1,8 @@ +# app/serializers/person_serializer.rb +class PersonSerializer < ActiveModel::Serializer + attributes :id, :name, :email, :phone_number, :company_name + + def company_name + object.company&.name + end +end diff --git a/config/routes.rb b/config/routes.rb index 8641401..b88d2b5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,4 +3,9 @@ resources :people, only: [:index, :new, :create] resources :companies root to: 'people#index' + + namespace :api do + resources :people, only: :index + resources :companies, only: :index + end end diff --git a/spec/controllers/api/companies_controller_spec.rb b/spec/controllers/api/companies_controller_spec.rb new file mode 100644 index 0000000..ea2de40 --- /dev/null +++ b/spec/controllers/api/companies_controller_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +RSpec.describe Api::CompaniesController, type: :controller do + describe 'GET /api/companies' do + let!(:companies) { create_list(:company, 25) } + + it 'returns a successful response' do + get :index + expect(response).to have_http_status(:ok) + end + + it 'returns paginated results' do + get :index, params: { page: 1, per_page: 10 } + json = JSON.parse(response.body) + expect(json['meta']['current_page']).to eq(1) + expect(json['meta']['total_pages']).to eq(3) + expect(json['data'].size).to eq(10) + end + + it 'returns filtered results by name' do + create(:company, name: 'Acme Corp') + get :index, params: { name: 'acme' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(1) + expect(json['data'][0]['name']).to eq('Acme Corp') + end + + it 'handles case-insensitive filtering' do + create(:company, name: 'TestCompany') + get :index, params: { name: 'testcompany' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(1) + expect(json['data'][0]['name']).to eq('TestCompany') + end + + it 'returns an empty array if no companies match the filter' do + get :index, params: { name: 'nonexistent' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(0) + end + end +end diff --git a/spec/controllers/api/people_controller_spec.rb b/spec/controllers/api/people_controller_spec.rb new file mode 100644 index 0000000..6e72527 --- /dev/null +++ b/spec/controllers/api/people_controller_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe Api::PeopleController, type: :controller do + describe 'GET /api/people' do + let!(:people) { create_list(:person, 25) } + + it 'returns a successful response' do + get :index + expect(response).to have_http_status(:ok) + end + + it 'returns paginated results' do + get :index, params: { page: 1, per_page: 10 } + json = JSON.parse(response.body) + expect(json['meta']['current_page']).to eq(1) + expect(json['meta']['total_pages']).to eq(3) + expect(json['data'].size).to eq(10) + end + + it 'returns filtered results by email' do + create(:person, email: 'test@example.com') + get :index, params: { email: 'test@example.com' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(1) + expect(json['data'][0]['email']).to eq('test@example.com') + end + + it 'returns an empty array if no people match the email filter' do + get :index, params: { email: 'nonexistent@example.com' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(0) + end + end +end diff --git a/spec/controllers/companies_controller.spec.rb b/spec/controllers/companies_controller.spec.rb index f552bbf..8cad3eb 100644 --- a/spec/controllers/companies_controller.spec.rb +++ b/spec/controllers/companies_controller.spec.rb @@ -1,101 +1,102 @@ require 'rails_helper' RSpec.describe CompaniesController, type: :controller do - let!(:company) { FactoryBot.create(:company, name: "Test Company") } + let(:valid_attributes) { { name: 'Test Company' } } + let(:invalid_attributes) { { name: '' } } + let!(:company) { create(:company, name: 'Existing Company') } - describe "GET #index" do - it "assigns @companies with paginated results" do + describe 'GET #index' do + it 'assigns @companies with paginated results' do get :index, params: { page: 1 } - expect(assigns(:companies)).to be_a_kind_of(ActiveRecord::Relation) - expect(assigns(:companies)).to include(company) + expect(assigns(:companies)).to eq([company]) end - it "renders the index template" do + it 'renders the index template' do get :index expect(response).to render_template(:index) end end - describe "GET #new" do - it "assigns a new Company to @company" do + describe 'GET #new' do + it 'assigns a new company to @company' do get :new expect(assigns(:company)).to be_a_new(Company) end - it "renders the new template" do + it 'renders the new template' do get :new expect(response).to render_template(:new) end end - describe "GET #edit" do - it "assigns the requested company to @company" do + describe 'GET #edit' do + it 'assigns the requested company to @company' do get :edit, params: { id: company.id } expect(assigns(:company)).to eq(company) end - it "renders the edit template" do + it 'renders the edit template' do get :edit, params: { id: company.id } expect(response).to render_template(:edit) end end - describe "POST #create" do - context "with valid attributes" do - it "creates a new company" do + describe 'POST #create' do + context 'with valid attributes' do + it 'creates a new company' do expect { - post :create, params: { company: { name: "New Company" } } + post :create, params: { company: valid_attributes } }.to change(Company, :count).by(1) end - it "redirects to the index with a success notice" do - post :create, params: { company: { name: "New Company" } } + it 'redirects to the index with a success notice' do + post :create, params: { company: valid_attributes } expect(response).to redirect_to(companies_path) - expect(flash[:notice]).to eq("Successfully created entry") + expect(flash[:notice]).to eq('Successfully created entry') end end - context "with invalid attributes" do - it "does not create a new company" do + context 'with invalid attributes' do + it 'does not create a new company' do expect { - post :create, params: { company: { name: "" } } + post :create, params: { company: invalid_attributes } }.not_to change(Company, :count) end - it "renders the create template with an alert" do - post :create, params: { company: { name: "" } } + it 'renders the create template with an alert' do + post :create, params: { company: invalid_attributes } expect(response).to render_template(:create) - expect(flash[:alert]).to eq("Unsuccessfully created entry") + expect(flash[:alert]).to eq('Unsuccessfully created entry') end end end - describe "PATCH #update" do - context "with valid attributes" do - it "updates the company" do - patch :update, params: { id: company.id, company: { name: "Updated Company" } } + describe 'PATCH #update' do + context 'with valid attributes' do + it 'updates the company' do + patch :update, params: { id: company.id, company: valid_attributes } company.reload - expect(company.name).to eq("Updated Company") + expect(company.name).to eq('Test Company') end - it "redirects to the index with a success notice" do - patch :update, params: { id: company.id, company: { name: "Updated Company" } } + it 'redirects to the index with a success notice' do + patch :update, params: { id: company.id, company: valid_attributes } expect(response).to redirect_to(companies_path) - expect(flash[:notice]).to eq("Successfully updated entry") + expect(flash[:notice]).to eq('Successfully updated entry') end end - context "with invalid attributes" do - it "does not update the company" do - patch :update, params: { id: company.id, company: { name: "" } } + context 'with invalid attributes' do + it 'does not update the company' do + patch :update, params: { id: company.id, company: invalid_attributes } company.reload - expect(company.name).to eq("Test Company") + expect(company.name).not_to eq('') end - it "renders the edit template with an alert" do - patch :update, params: { id: company.id, company: { name: "" } } + it 'renders the edit template with an alert' do + patch :update, params: { id: company.id, company: invalid_attributes } expect(response).to render_template(:edit) - expect(flash[:alert]).to eq("Unsuccessfully created entry") + expect(flash[:alert]).to eq('Unsuccessfully created entry') end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 2100bd9..dfca94e 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,4 +1,5 @@ require 'simplecov' + SimpleCov.start require 'spec_helper'