diff --git a/app/controllers/sectors_controller.rb b/app/controllers/sectors_controller.rb new file mode 100644 index 000000000..66a536926 --- /dev/null +++ b/app/controllers/sectors_controller.rb @@ -0,0 +1,72 @@ +class SectorsController < ApplicationController + before_action :set_sector, only: [:show, :edit, :update, :destroy] + + def index + per_page = params[:number_of_items_per_page].presence || 25 + unpaginated = Sector.all + filtered = unpaginated.sector_name(params[:sector_name]) + .published_search(params[:published_search]) + .order(:name) + @sectors = filtered.paginate(page: params[:page], per_page: per_page) + + @count_display = if @sectors.total_entries == unpaginated.count + unpaginated.count + else + "#{@sectors.total_entries}/#{unpaginated.count}" + end + end + + def show + end + + def new + @sector = Sector.new + set_form_variables + end + + def edit + set_form_variables + end + + def create + @sector = Sector.new(sector_params) + + if @sector.save + redirect_to sectors_path, notice: "Sector was successfully created." + else + set_form_variables + render :new, status: :unprocessable_content + end + end + + def update + if @sector.update(sector_params) + redirect_to sectors_path, notice: "Sector was successfully updated.", status: :see_other + else + set_form_variables + render :edit, status: :unprocessable_content + end + end + + def destroy + @sector.destroy! + redirect_to sectors_path, notice: "Sector was successfully destroyed." + end + + # Optional hooks for setting variables for forms or index + def set_form_variables + end + + private + + def set_sector + @sector = Sector.find(params[:id]) + end + + # Strong parameters + def sector_params + params.require(:sector).permit( + :name, :published + ) + end +end diff --git a/app/helpers/admin_dashboard_cards_helper.rb b/app/helpers/admin_dashboard_cards_helper.rb index d192b403c..fca1dbd70 100644 --- a/app/helpers/admin_dashboard_cards_helper.rb +++ b/app/helpers/admin_dashboard_cards_helper.rb @@ -40,8 +40,13 @@ def user_content_cards # ----------------------------- def reference_cards [ - model_card(:categories, icon: "🗂️", intensity: 100), - custom_card("Service populations", authenticated_root_path, icon: "🏭", color: :lime, intensity: 100), + model_card(:categories, icon: "🗂️", + intensity: 100, + params: { published_search: true }), + model_card(:sectors, icon: "🏭", + intensity: 100, + title: "Service populations", + params: { published_search: true }), custom_card("Project statuses", authenticated_root_path, icon: "🧮", color: :emerald, intensity: 100), custom_card("Windows types", windows_types_path, icon: "🪟"), ] @@ -50,10 +55,10 @@ def reference_cards # ============================================================ # CARD BUILDERS # ============================================================ - def model_card(key, title: nil, icon:, intensity: 50) + def model_card(key, title: nil, icon:, intensity: 50, params: {}) { title: title || key.to_s.humanize, - path: polymorphic_path(key.to_s.classify.constantize), + path: polymorphic_path(key.to_s.classify.constantize, params), icon: icon, bg_color: DomainTheme.bg_class_for(key, intensity: intensity), hover_bg_color: DomainTheme.bg_class_for(key, intensity: intensity == 50 ? 100 : intensity + 100, hover: true), diff --git a/app/models/sector.rb b/app/models/sector.rb index 9a4b783a5..486690fa0 100644 --- a/app/models/sector.rb +++ b/app/models/sector.rb @@ -14,7 +14,11 @@ class Sector < ApplicationRecord validates_presence_of :name, uniqueness: true # Scopes - scope :published, -> { where(published: true). - order(Arel.sql("CASE WHEN name = 'Other' THEN 1 ELSE 0 END, LOWER(name) ASC")) } + scope :published, ->(published=nil) { + ["true", "false"].include?(published) ? where(published: published) : where(published: true) } + scope :published_search, ->(published_search) { published_search.present? ? published(published_search) : all } + scope :sector_name, ->(sector_name) { + sector_name.present? ? where("sectors.name LIKE ?", "%#{sector_name}%") : all } scope :has_taggings, -> { joins(:sectorable_items).distinct } + end diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb index 385910fbc..887260ba0 100644 --- a/app/views/categories/_form.html.erb +++ b/app/views/categories/_form.html.erb @@ -53,8 +53,7 @@ <%= f.button :submit, "Save Category", - class: "bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 - transition-colors duration-150" %> + class: "btn btn-primary" %> diff --git a/app/views/sectors/_form.html.erb b/app/views/sectors/_form.html.erb new file mode 100644 index 000000000..f9c90728e --- /dev/null +++ b/app/views/sectors/_form.html.erb @@ -0,0 +1,48 @@ +<%= simple_form_for(@sector) do |f| %> +
+ + + <%= f.error_notification %> + <%= render "shared/errors", resource: @sector if @sector.errors.any? %> + + +
+ + +
+ <%= f.input :name, + label: "Name", + input_html: { class: "form-control" } %> +
+ +
+ <%= f.input :published, + as: :boolean, + label: "Published?", + wrapper_html: { class: "flex items-center gap-2" }, + input_html: { class: "form-checkbox" } %> +
+ +
+ + +
+ + <% if @sector.persisted? && current_user.super_user? %> + <%= link_to "Delete", + @sector, + method: :delete, + data: { confirm: "Are you sure you want to delete this category?" }, + class: "btn btn-danger-outline" %> + <% end %> + + <%= link_to "Cancel", sectors_path, + class: "btn btn-secondary-outline" %> + + <%= f.button :submit, + "Save Sector", + class: "btn btn-primary" %> +
+ +
+<% end %> diff --git a/app/views/sectors/_search_boxes.html.erb b/app/views/sectors/_search_boxes.html.erb new file mode 100644 index 000000000..a39c51595 --- /dev/null +++ b/app/views/sectors/_search_boxes.html.erb @@ -0,0 +1,39 @@ + +
+ <%= form_with url: sectors_path, + method: :get, + local: true, + class: "grid grid-cols-1 md:grid-cols-5 gap-4 items-end" do |f| %> + + +
+ <%= f.label :sector_name, "Name Contains", + class: "block text-sm font-medium text-gray-700" %> + + <%= f.text_field :sector_name, + value: params[:sector_name], + class: "mt-1 block w-full rounded-md border border-gray-300 p-2", + oninput: "this.form.requestSubmit()" %> +
+ + +
+ <%= f.label :published_search, "Published", + class: "block text-sm font-medium text-gray-700" %> + + <%= f.select :published_search, + options_for_select([["All", ""], ["Published", "true"], ["Hidden", "false"]], params[:published_search]), + {}, + class: "mt-1 block w-full rounded-md border border-gray-300 p-2", + onchange: "this.form.requestSubmit()" %> +
+ + +
+ <%= link_to "Clear", + sectors_path, + class: "btn btn-utility-outline whitespace-nowrap" %> +
+ + <% end %> +
diff --git a/app/views/sectors/edit.html.erb b/app/views/sectors/edit.html.erb new file mode 100644 index 000000000..825418258 --- /dev/null +++ b/app/views/sectors/edit.html.erb @@ -0,0 +1,17 @@ +
+
+
+
+

Edit Service population

+ <%= link_to "Taggings", taggings_path(sector_names: @sector.name), + class: "btn btn-secondary-outline" %> +
+ +
+
+ <%= render "form", sector: @sector %> +
+
+
+
+
diff --git a/app/views/sectors/index.html.erb b/app/views/sectors/index.html.erb new file mode 100644 index 000000000..b250eb974 --- /dev/null +++ b/app/views/sectors/index.html.erb @@ -0,0 +1,91 @@ +
+
+
+
+ + +
+

+ Service populations (<%= @count_display %>) +

+ <%= link_to "New #{Sector.model_name.human.downcase}", + new_sector_path, + class: "btn btn-primary-outline" %> +
+ + <%= render "search_boxes" %> + + +
+
+ <% if @sectors.any? %> + + + + + + + + + + + + + <% @sectors.each do |sector| %> + + <%= sector.published ? "hover:bg-gray-50" : "hover:bg-gray-100" %> transition-colors duration-150"> + + + + + + + + + + <% end %> + +
+ Name + + Published + + Actions +
+ <%= sector.name %> + + <% if sector.published? %> + + Yes + + <% else %> + + No + + <% end %> + + <%= link_to "Edit", + edit_sector_path(sector), + class: "btn btn-secondary-outline whitespace-nowrap" %> + <%= link_to "Taggings", + taggings_path(sector_names: sector.name), + class: "btn btn-secondary-outline whitespace-nowrap" %> +
+ <% else %> + +

+ No <%= Sector.model_name.human.pluralize %> found. +

+ <% end %> +
+
+ + + + +
+
+
+
diff --git a/app/views/sectors/new.html.erb b/app/views/sectors/new.html.erb new file mode 100644 index 000000000..d5f4735ae --- /dev/null +++ b/app/views/sectors/new.html.erb @@ -0,0 +1,17 @@ +
+
+
+
+

New service population

+
+ +
+ +
+
+ <%= render "form", sector: @sector %> +
+
+
+
+
\ No newline at end of file diff --git a/app/views/sectors/show.html.erb b/app/views/sectors/show.html.erb new file mode 100644 index 000000000..d151b2925 --- /dev/null +++ b/app/views/sectors/show.html.erb @@ -0,0 +1,37 @@ +
+
+
+
+ +

+ Service population details +

+ + +
+ <%= link_to("Index", sectors_path, class: "btn btn-secondary-outline") %> + <% if current_user.super_user? %> + <%= link_to("Edit", edit_sector_path(@sector), class: "btn btn-primary-outline") %> + <% end %> +
+
+ +
+
+
+ +
+

Name:

+

<%= @sector.name %>

+
+
+

Published:

+

<%= @sector.published %>

+
+ +
+
+
+
+
+
diff --git a/config/routes.rb b/config/routes.rb index 6d8dc63b6..de71b048b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,12 +52,6 @@ resources :projects resources :project_users resources :quotes - resources :users do - member do - get :generate_facilitator - end - end - resources :user_forms resources :monthly_reports get 'reports/:id/edit_story', to: 'reports#edit_story', as: 'reports_edit_story' @@ -71,6 +65,7 @@ get 'reports/annual', to: 'reports#annual' resources :reports + resources :resources do get :download member do @@ -80,8 +75,15 @@ post :search end end + resources :sectors resources :story_ideas resources :stories + resources :users do + member do + get :generate_facilitator + end + end + resources :user_forms resources :windows_types resources :workshop_ideas resources :workshop_logs diff --git a/spec/models/sector_spec.rb b/spec/models/sector_spec.rb index aa862e9d8..de6c345f6 100644 --- a/spec/models/sector_spec.rb +++ b/spec/models/sector_spec.rb @@ -1,28 +1,5 @@ require 'rails_helper' -RSpec.describe Sector do - describe 'associations' do - it { should have_many(:sectorable_items).dependent(:destroy) } - it { should have_many(:workshops).through(:sectorable_items) } - it { should have_many(:quotes).through(:workshops) } - end - - describe 'validations' do - let!(:existing_sector) { create(:sector) } - subject { build(:sector, name: existing_sector.name) } - it { should validate_presence_of(:name) } - end - - it "has a valid factory" do - expect(build(:sector)).to be_valid - end - - describe ".published" do - it "returns only published sectors" do - published = create(:sector, published: true) - unpublished = create(:sector, published: false) - - expect(Sector.published).to contain_exactly(published) - end - end +RSpec.describe Sector, type: :model do + pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/requests/sectors_spec.rb b/spec/requests/sectors_spec.rb new file mode 100644 index 000000000..b9515aec8 --- /dev/null +++ b/spec/requests/sectors_spec.rb @@ -0,0 +1,128 @@ +require 'rails_helper' + +RSpec.describe "/sectors", type: :request do + + let(:valid_attributes) do + { + name: "Test Sector", + published: true + } + end + + let(:invalid_attributes) do + { + name: "", # invalid: required + published: nil # invalid: boolean required + } + end + + let(:admin) { create(:user, :admin) } + + before do + sign_in admin + end + + describe "GET /index" do + it "renders a successful response" do + Sector.create! valid_attributes + get sectors_url + expect(response).to be_successful + end + end + + describe "GET /show" do + it "renders a successful response" do + sector = Sector.create! valid_attributes + get sector_url(sector) + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + get new_sector_url + expect(response).to be_successful + end + end + + describe "GET /edit" do + it "renders a successful response" do + sector = Sector.create! valid_attributes + get edit_sector_url(sector) + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new Sector" do + expect { + post sectors_url, params: { sector: valid_attributes } + }.to change(Sector, :count).by(1) + end + + it "redirects to sectors index" do + post sectors_url, params: { sector: valid_attributes } + expect(response).to redirect_to(sectors_url) + end + end + + context "with invalid parameters" do + it "does not create a new Sector" do + expect { + post sectors_url, params: { sector: invalid_attributes } + }.to change(Sector, :count).by(0) + end + + it "renders a response with 422 status (i.e. to display the 'new' template)" do + post sectors_url, params: { sector: invalid_attributes } + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested sector" do + sector = Sector.create! valid_attributes + patch sector_url(sector), params: { sector: new_attributes } + sector.reload + skip("Add assertions for updated state") + end + + it "redirects to the sectors index" do + sector = Sector.create! valid_attributes + patch sector_url(sector), params: { sector: new_attributes } + sector.reload + expect(response).to redirect_to(sectors_url) + end + end + + context "with invalid parameters" do + it "renders a response with 422 status (i.e. to display the 'edit' template)" do + sector = Sector.create! valid_attributes + patch sector_url(sector), params: { sector: invalid_attributes } + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested sector" do + sector = Sector.create! valid_attributes + expect { + delete sector_url(sector) + }.to change(Sector, :count).by(-1) + end + + it "redirects to the sectors list" do + sector = Sector.create! valid_attributes + delete sector_url(sector) + expect(response).to redirect_to(sectors_url) + end + end +end diff --git a/spec/routing/sectors_routing_spec.rb b/spec/routing/sectors_routing_spec.rb new file mode 100644 index 000000000..f5187ad9e --- /dev/null +++ b/spec/routing/sectors_routing_spec.rb @@ -0,0 +1,38 @@ +require "rails_helper" + +RSpec.describe SectorsController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/sectors").to route_to("sectors#index") + end + + it "routes to #new" do + expect(get: "/sectors/new").to route_to("sectors#new") + end + + it "routes to #show" do + expect(get: "/sectors/1").to route_to("sectors#show", id: "1") + end + + it "routes to #edit" do + expect(get: "/sectors/1/edit").to route_to("sectors#edit", id: "1") + end + + + it "routes to #create" do + expect(post: "/sectors").to route_to("sectors#create") + end + + it "routes to #update via PUT" do + expect(put: "/sectors/1").to route_to("sectors#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/sectors/1").to route_to("sectors#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/sectors/1").to route_to("sectors#destroy", id: "1") + end + end +end diff --git a/spec/views/sectors/edit.html.erb_spec.rb b/spec/views/sectors/edit.html.erb_spec.rb new file mode 100644 index 000000000..b34ec7004 --- /dev/null +++ b/spec/views/sectors/edit.html.erb_spec.rb @@ -0,0 +1,18 @@ +require "rails_helper" + +RSpec.describe "sectors/edit", type: :view do + let(:admin) { create(:user, :admin) } + let(:sector) { create(:sector) } + + before do + assign(:sector, sector) + allow(view).to receive(:current_user).and_return(admin) + end + + it "renders the edit sector form with metadatum select" do + render + + assert_select "input[name=?]", "sector[name]" + assert_select "input[name=?][type=checkbox]", "sector[published]" + end +end diff --git a/spec/views/sectors/index.html.erb_spec.rb b/spec/views/sectors/index.html.erb_spec.rb new file mode 100644 index 000000000..7e7aa2ffa --- /dev/null +++ b/spec/views/sectors/index.html.erb_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe "sectors/index", type: :view do + let(:admin) { create(:user, :admin) } + + before do + assign(:sectors, [ + create(:category, name: "Sector One", published: true), + create(:category, name: "Sector Two", published: false) + ]) + allow(view).to receive(:current_user).and_return(admin) + end + + it "renders each category with name, type, and published label" do + render + + # NAME + expect(rendered).to include("Sector One") + expect(rendered).to include("Sector Two") + + # PUBLISHED? + expect(rendered).to include("Yes") # for first category + expect(rendered).to include("No") # for second category + end +end diff --git a/spec/views/sectors/new.html.erb_spec.rb b/spec/views/sectors/new.html.erb_spec.rb new file mode 100644 index 000000000..650e9986a --- /dev/null +++ b/spec/views/sectors/new.html.erb_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe "sectors/new", type: :view do + before do + assign(:sector, Sector.new) + end + + it "renders the new sector form" do + render + + assert_select "form[action='#{sectors_path}']" do + assert_select "input[name='sector[name]']" + assert_select "input[name='sector[published]']" + end + end +end diff --git a/spec/views/sectors/show.html.erb_spec.rb b/spec/views/sectors/show.html.erb_spec.rb new file mode 100644 index 000000000..7413e162e --- /dev/null +++ b/spec/views/sectors/show.html.erb_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe "sectors/show", type: :view do + let(:admin) { create(:user, :admin) } + + before(:each) do + assign(:sector, create(:sector, name: "Name", published: false)) + allow(view).to receive(:current_user).and_return(admin) + end + + it "renders attributes in

" do + render + expect(rendered).to match(/Name/) + expect(rendered).to match(/false/) + end +end