diff --git a/.rubocop.yml b/.rubocop.yml index 359646ed00..7ca9d71a54 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,7 @@ AllCops: Exclude: - "vendor/**/*" - "db/schema.rb" + - "db/partners_schema.rb" - "db/seeds.rb" - "db/migrate/*" - "bin/*" @@ -440,5 +441,8 @@ Rails/WhereExists: Enabled: false Rails/WhereNot: Enabled: false +Rails/HasAndBelongsToMany: + Enabled: false Lint/EnsureReturn: # The service objects return self in an ensure block. Not using an explicit return does not do correct behavior Enabled: false + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d00395f42a..b1e09a6751 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -124,6 +124,10 @@ function load_quagga() { }; $(document).on('turbolinks:load', load_quagga); +/** + * Handle loading on specified tab in the URL parameter. In case we would like + * to direct a user to a specific tab on a page rather than the default. + */ $(document).ready(function () { var hash = location.hash.replace(/^#/, ''); // ^ means starting, meaning only match the first hash diff --git a/app/controllers/partner_groups_controller.rb b/app/controllers/partner_groups_controller.rb new file mode 100644 index 0000000000..7f32d7e48a --- /dev/null +++ b/app/controllers/partner_groups_controller.rb @@ -0,0 +1,46 @@ +class PartnerGroupsController < ApplicationController + def index + @partner_groups = current_organization.partner_groups + + respond_to do |format| + format.html + end + end + + def new + @partner_group = current_organization.partner_groups.new + @item_categories = current_organization.item_categories + end + + def create + @partner_group = current_organization.partner_groups.new(partner_group_params) + if @partner_group.save + # Redirect to groups tab in Partner page. + redirect_to partners_path + "#nav-partner-groups", notice: "Partner group added!" + else + flash[:error] = "Something didn't work quite right -- try again?" + render action: :new + end + end + + def edit + @partner_group = current_organization.partner_groups.find(params[:id]) + @item_categories = current_organization.item_categories + end + + def update + @partner_group = current_organization.partner_groups.find(params[:id]) + if @partner_group.update(partner_group_params) + redirect_to partners_path + "#nav-partner-groups", notice: "Partner group edited!" + else + flash[:error] = "Something didn't work quite right -- try again?" + render action: :edit + end + end + + private + + def partner_group_params + params.require(:partner_group).permit(:name, item_category_ids: []) + end +end diff --git a/app/controllers/partners/children_controller.rb b/app/controllers/partners/children_controller.rb index c7aee0ed28..c2adcbf629 100644 --- a/app/controllers/partners/children_controller.rb +++ b/app/controllers/partners/children_controller.rb @@ -35,6 +35,11 @@ def show def new family = current_partner.families.find_by!(id: params[:family_id]) @child = family.children.new + + requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.partner.id).call + @formatted_requestable_items = requestable_items.map do |rt| + [rt.name, rt.id] + end end def active @@ -45,6 +50,10 @@ def active def edit @child = current_partner.children.find_by(id: params[:id]) + requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.partner.id).call + @formatted_requestable_items = requestable_items.map do |rt| + [rt.name, rt.id] + end end def create @@ -107,4 +116,4 @@ def fetch_valid_item_name(id) @valid_items.find { |vi| vi[:id] == id }&.fetch(:name) end end -end \ No newline at end of file +end diff --git a/app/controllers/partners/individuals_requests_controller.rb b/app/controllers/partners/individuals_requests_controller.rb index daee980ae9..04e4ba42d7 100644 --- a/app/controllers/partners/individuals_requests_controller.rb +++ b/app/controllers/partners/individuals_requests_controller.rb @@ -2,9 +2,11 @@ module Partners class IndividualsRequestsController < BaseController def new @request = FamilyRequest.new({}, initial_items: 1) - @requestable_items = Organization.find(current_partner.diaper_bank_id).valid_items.map do |item| - [item[:name], item[:id]] - end.sort + + requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.partner.id).call + @formatted_requestable_items = requestable_items.map do |rt| + [rt.name, rt.id] + end end def create diff --git a/app/controllers/partners/requests_controller.rb b/app/controllers/partners/requests_controller.rb index e20fe77a0b..5069ac05aa 100644 --- a/app/controllers/partners/requests_controller.rb +++ b/app/controllers/partners/requests_controller.rb @@ -11,10 +11,10 @@ def new @partner_request = Partners::Request.new @partner_request.item_requests.build - # Fetch the valid items - @requestable_items = Organization.find(current_partner.diaper_bank_id).valid_items.map do |item| - [item[:name], item[:id]] - end.sort + requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.partner.id).call + @formatted_requestable_items = requestable_items.map do |rt| + [rt.name, rt.id] + end end def show @@ -35,7 +35,7 @@ def create else @partner_request = create_service.partner_request @errors = create_service.errors - @requestable_items = Organization.find(current_partner.diaper_bank_id).valid_items.map do |item| + @formatted_requestable_items = Organization.find(current_partner.diaper_bank_id).valid_items.map do |item| [item[:name], item[:id]] end.sort diff --git a/app/controllers/partners_controller.rb b/app/controllers/partners_controller.rb index 771f6b1e6f..254969260f 100644 --- a/app/controllers/partners_controller.rb +++ b/app/controllers/partners_controller.rb @@ -6,7 +6,8 @@ class PartnersController < ApplicationController def index @unfiltered_partners_for_statuses = Partner.where(organization: current_organization) - @partners = Partner.where(organization: current_organization).class_filter(filter_params).alphabetized + @partners = Partner.includes(:partner_group).where(organization: current_organization).class_filter(filter_params).alphabetized + @partner_groups = PartnerGroup.includes(:partners, :item_categories).where(organization: current_organization) respond_to do |format| format.html @@ -57,10 +58,12 @@ def show def new @partner = current_organization.partners.new + @partner_groups = current_organization.partner_groups end def edit @partner = current_organization.partners.find(params[:id]) + @partner_groups = PartnerGroup.where(organization: current_organization) end def update @@ -153,7 +156,7 @@ def reactivate private def partner_params - params.require(:partner).permit(:name, :email, :send_reminders, :quota, :notes, documents: []) + params.require(:partner).permit(:name, :email, :send_reminders, :quota, :notes, :partner_group_id, documents: []) end helper_method \ diff --git a/app/models/item_category.rb b/app/models/item_category.rb index 47e669f55b..10594bcdc6 100644 --- a/app/models/item_category.rb +++ b/app/models/item_category.rb @@ -4,7 +4,7 @@ # # id :bigint not null, primary key # description :text -# name :string +# name :string not null # created_at :datetime not null # updated_at :datetime not null # organization_id :integer not null @@ -16,4 +16,5 @@ class ItemCategory < ApplicationRecord belongs_to :organization has_many :items, -> { order(name: :asc) }, inverse_of: :item_category + has_many :partner_groups, dependent: :nullify end diff --git a/app/models/organization.rb b/app/models/organization.rb index df5b2d91f8..70d567fa54 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -45,6 +45,7 @@ class Organization < ApplicationRecord has_many :donations has_many :manufacturers has_many :partners + has_many :partner_groups has_many :purchases has_many :requests has_many :storage_locations diff --git a/app/models/partner.rb b/app/models/partner.rb index ecb20faca7..b854390b12 100644 --- a/app/models/partner.rb +++ b/app/models/partner.rb @@ -2,16 +2,17 @@ # # Table name: partners # -# id :integer not null, primary key -# email :string -# name :string -# notes :text -# quota :integer -# send_reminders :boolean default(FALSE), not null -# status :integer default("uninvited") -# created_at :datetime not null -# updated_at :datetime not null -# organization_id :integer +# id :integer not null, primary key +# email :string +# name :string +# notes :text +# quota :integer +# send_reminders :boolean default(FALSE), not null +# status :integer default("uninvited") +# created_at :datetime not null +# updated_at :datetime not null +# organization_id :integer +# partner_group_id :bigint # class Partner < ApplicationRecord @@ -26,8 +27,11 @@ class Partner < ApplicationRecord enum status: { uninvited: 0, invited: 1, awaiting_review: 2, approved: 3, error: 4, recertification_required: 5, deactivated: 6 } belongs_to :organization - has_many :distributions, dependent: :destroy + belongs_to :partner_group, optional: true + has_many :item_categories, through: :partner_group + has_many :requestable_items, through: :item_categories, source: :items + has_many :distributions, dependent: :destroy has_many :requests, dependent: :destroy has_many_attached :documents diff --git a/app/models/partner_group.rb b/app/models/partner_group.rb new file mode 100644 index 0000000000..a03e98421b --- /dev/null +++ b/app/models/partner_group.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: partner_groups +# +# id :bigint not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# organization_id :bigint +# +class PartnerGroup < ApplicationRecord + belongs_to :organization + has_many :partners, dependent: :nullify + has_and_belongs_to_many :item_categories + + validates :organization, presence: true + validates :name, presence: true, uniqueness: { scope: :organization } +end diff --git a/app/services/partner_fetch_requestable_items_service.rb b/app/services/partner_fetch_requestable_items_service.rb new file mode 100644 index 0000000000..8d76403d5b --- /dev/null +++ b/app/services/partner_fetch_requestable_items_service.rb @@ -0,0 +1,23 @@ +class PartnerFetchRequestableItemsService + def initialize(partner_id:) + @partner_id = partner_id + end + + def call + return organization.items.active.visible if partner.partner_group.blank? + + partner.requestable_items.active.visible + end + + private + + attr_reader :partner_id + + def partner + @partner ||= Partner.find(partner_id) + end + + def organization + @organization ||= partner.organization + end +end diff --git a/app/views/partner_groups/_form.html.erb b/app/views/partner_groups/_form.html.erb new file mode 100644 index 0000000000..09a5f50a7b --- /dev/null +++ b/app/views/partner_groups/_form.html.erb @@ -0,0 +1,36 @@ +<%= simple_form_for @partner_group, html: {class: 'form-horizontal'} do |f| %> +
+
+
+ +
+ +
+ + +
+ <%= f.input :name, label: "Name", wrapper: :input_group do %> + + <%= f.input_field :name, class: "form-control" %> + <% end %> + +
+

Which Item Categories Can They Request?

+ + <%= f.association :item_categories, collection: @item_categories, as: :check_boxes, label: '' %> +
+
+ + + +
+ +
+ +
+ +
+
+<% end %> diff --git a/app/views/partner_groups/edit.html.erb b/app/views/partner_groups/edit.html.erb new file mode 100644 index 0000000000..8fe26553a6 --- /dev/null +++ b/app/views/partner_groups/edit.html.erb @@ -0,0 +1,25 @@ +
+
+
+
+ <% content_for :title, "Edit - Partner Groups - #{@partner_group.name} - #{current_organization.name}" %> +

+ Updating Partner Group + for <%= current_organization.name %> +

+
+
+ +
+
+
+
+ +<%= render partial: "form", object: @partner_group, locals: { submit_btn_options: { text: "Update Partner Group" } } %> diff --git a/app/views/partner_groups/index.html.erb b/app/views/partner_groups/index.html.erb new file mode 100644 index 0000000000..c250a0ec1c --- /dev/null +++ b/app/views/partner_groups/index.html.erb @@ -0,0 +1,67 @@ +
+
+
+
+ <% content_for :title, "Partner Groups - #{current_organization.name}" %> +

+ Partner Groups + for <%= current_organization.name %> +

+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ + + + + + + + + <% @partner_groups.each do |partner_group| %> + + + + + <% end %> + +
Partner Group NameNumber of partners in group
<%= link_to partner_group.name, partner_group_path(partner_group) %><%= partner_group.partners.count %>
+
+
+
+
+
diff --git a/app/views/partner_groups/new.html.erb b/app/views/partner_groups/new.html.erb new file mode 100644 index 0000000000..359b4d37c3 --- /dev/null +++ b/app/views/partner_groups/new.html.erb @@ -0,0 +1,25 @@ +
+
+
+
+ <% content_for :title, "New - Partner Groups - #{current_organization.name}" %> +

+ New Partner Group + for <%= current_organization.name %> +

+
+
+ +
+
+
+
+ +<%= render partial: "form", object: @partner_group, locals: { submit_btn_options: { text: "Add Partner Group" } } %> diff --git a/app/views/partners/_form.html.erb b/app/views/partners/_form.html.erb index 714928ecba..5f8fab670a 100644 --- a/app/views/partners/_form.html.erb +++ b/app/views/partners/_form.html.erb @@ -8,15 +8,20 @@
-
+
<%= f.input :name, label: "Name", wrapper: :input_group do %> <%= f.input_field :name, class: "form-control" %> <% end %> <%= f.input :email, label: "E-mail", wrapper: :input_group do %> - <%= f.input_field :email, class: "form-control" %> + <%= f.input_field :email, class: "form-control"%> <% end %> +
+ <%= f.input :partner_group_id, label: "Group", wrapper: :input_group do %> + <%= f.input_field :partner_group_id, collection: @partner_groups, class: "form-control", include_blank: "None" %> + <% end %> +
<%= f.input :send_reminders, label: "Send Reminders?", wrapper: :input_group do %> <%= f.check_box :send_reminders, {class: "input-group-text", id: "send_reminders"}, "true", "false" %> <% end %> diff --git a/app/views/partners/_partner_groups_table.html.erb b/app/views/partners/_partner_groups_table.html.erb new file mode 100644 index 0000000000..91ccf5f08d --- /dev/null +++ b/app/views/partners/_partner_groups_table.html.erb @@ -0,0 +1,57 @@ + diff --git a/app/views/partners/_partner_row.html.erb b/app/views/partners/_partner_row.html.erb index abec030155..0b69a2ab8c 100644 --- a/app/views/partners/_partner_row.html.erb +++ b/app/views/partners/_partner_row.html.erb @@ -1,7 +1,8 @@ <% status = partner_row.status%> - <%= link_to partner_row.name, partner_path(partner_row) %> + <%= link_to partner_row.name, partner_path(partner_row) %> <%= link_to partner_row.email, "mailto:#{partner_row.email}" %> + <%= partner_row.partner_group&.name %> <%= partner_row.quota %> <% case status %> diff --git a/app/views/partners/_partners_table.html.erb b/app/views/partners/_partners_table.html.erb new file mode 100644 index 0000000000..62aefce006 --- /dev/null +++ b/app/views/partners/_partners_table.html.erb @@ -0,0 +1,47 @@ + diff --git a/app/views/partners/children/_form.html.erb b/app/views/partners/children/_form.html.erb index f4fd2ab815..58202a2861 100644 --- a/app/views/partners/children/_form.html.erb +++ b/app/views/partners/children/_form.html.erb @@ -17,7 +17,7 @@ <%= form.text_field :last_name, class: "form-control" %> <%= form.label :item_needed, "Diaper/Item Used" %> - <%= form.select :item_needed_diaperid, current_partner.organization.valid_items_for_select, + <%= form.select :item_needed_diaperid, @formatted_requestable_items, {include_blank: 'Select an item'}, {class: 'form-control'} %>
diff --git a/app/views/partners/index.html.erb b/app/views/partners/index.html.erb index 5c8bef284e..079f07a090 100644 --- a/app/views/partners/index.html.erb +++ b/app/views/partners/index.html.erb @@ -24,56 +24,34 @@
-
-
-
-

Partner Filters

+
+ +
+
+
+
-
- <%= render partial: "statuses", object: @unfiltered_partners_for_statuses, as: :partners %> -
-
- -
-
- -
- -
-
-
- -
-
- - - - - - - - - - - - - <%= render partial: "partner_row", collection: @partners %> - -
PartnerE-mailQuotaStatusAction 
-
-
+ <%= render( layout: "shared/csv_import_modal", locals: { diff --git a/app/views/partners/individuals_requests/_item_request.html.erb b/app/views/partners/individuals_requests/_item_request.html.erb index 35ae31b29e..c63830aeb9 100644 --- a/app/views/partners/individuals_requests/_item_request.html.erb +++ b/app/views/partners/individuals_requests/_item_request.html.erb @@ -1,6 +1,6 @@ <%= form.fields_for(:items) do |field| %> - <%= field.input :item_id, collection: @requestable_items, label: false, allow_blank: true, class: 'form-control' %> + <%= field.input :item_id, collection: @formatted_requestable_items, label: false, allow_blank: true, class: 'form-control' %> <%= field.number_field :person_count, as: :integer, label: false, step: 1, min: 1, class: 'form-control' %> <%= remove_item_button "Remove" %> diff --git a/app/views/partners/requests/_item_request.html.erb b/app/views/partners/requests/_item_request.html.erb index 80607ac331..a6aac1165d 100644 --- a/app/views/partners/requests/_item_request.html.erb +++ b/app/views/partners/requests/_item_request.html.erb @@ -1,6 +1,6 @@ <%= form.fields_for :item_requests, defined?(object) ? object : nil do |field| %> - <%= field.select :item_id, @requestable_items, {include_blank: 'Select an item'}, {class: 'form-control'} %> + <%= field.select :item_id, @formatted_requestable_items, {include_blank: 'Select an item'}, {class: 'form-control'} %> <%= field.number_field :quantity, label: false, step: 1, min: 1, class: 'form-control' %> <%= field.hidden_field :_destroy, as: :hidden %> diff --git a/app/views/partners/show.html.erb b/app/views/partners/show.html.erb index 3aae25f657..4d1b70eeef 100644 --- a/app/views/partners/show.html.erb +++ b/app/views/partners/show.html.erb @@ -90,6 +90,41 @@
+
+
+

Settings

+
+
+
+
+
+

Partner Group:

+
+ +
+
+ +

Requestable Item Categories:

+

You can change this through their partner group.

+ + <% if @partner.item_categories.present? %> +
    + <% @partner.item_categories.each do |ic| %> +
  • + <%= ic.name %> +
  • + <% end %> +
+ <% elsif @partner.partner_group.present? %> + No Items Requestable + <% else %> + All Items Requestable + <% end %> +
+
+
+
+

Notes

diff --git a/config/routes.rb b/config/routes.rb index fb83b60012..7cf480f655 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -158,6 +158,8 @@ def set_up_flipper end end + resources :partner_groups, only: [:new, :create, :edit, :update] + resources :diaper_drives resources :donations do # collection do diff --git a/db/migrate/20201230233305_create_partner_groups.rb b/db/migrate/20201230233305_create_partner_groups.rb new file mode 100644 index 0000000000..61f1350370 --- /dev/null +++ b/db/migrate/20201230233305_create_partner_groups.rb @@ -0,0 +1,10 @@ +class CreatePartnerGroups < ActiveRecord::Migration[6.0] + def change + create_table :partner_groups do |t| + t.references :organization, foreign_key: true + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20210729003044_add_uniquness_idx_on_name_and_organization_id_in_partner_groups.rb b/db/migrate/20210729003044_add_uniquness_idx_on_name_and_organization_id_in_partner_groups.rb new file mode 100644 index 0000000000..3c889334b4 --- /dev/null +++ b/db/migrate/20210729003044_add_uniquness_idx_on_name_and_organization_id_in_partner_groups.rb @@ -0,0 +1,7 @@ +class AddUniqunessIdxOnNameAndOrganizationIdInPartnerGroups < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :partner_groups, [:name, :organization_id], unique: true, algorithm: :concurrently + end +end diff --git a/db/migrate/20210729003314_add_reference_between_partners_and_partner_groups.rb b/db/migrate/20210729003314_add_reference_between_partners_and_partner_groups.rb new file mode 100644 index 0000000000..48b07bdc65 --- /dev/null +++ b/db/migrate/20210729003314_add_reference_between_partners_and_partner_groups.rb @@ -0,0 +1,7 @@ +class AddReferenceBetweenPartnersAndPartnerGroups < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_reference :partners, :partner_group, index: { algorithm: :concurrently } + end +end diff --git a/db/migrate/20210729003516_add_join_table_between_partner_groups_to_item_categories.rb b/db/migrate/20210729003516_add_join_table_between_partner_groups_to_item_categories.rb new file mode 100644 index 0000000000..784875135b --- /dev/null +++ b/db/migrate/20210729003516_add_join_table_between_partner_groups_to_item_categories.rb @@ -0,0 +1,10 @@ +class AddJoinTableBetweenPartnerGroupsToItemCategories < ActiveRecord::Migration[6.1] + def change + create_table :item_categories_partner_groups do |t| + t.references :partner_group, foreign_key: true, null: false + t.references :item_category, foreign_key: true, null: false + + t.timestamps + end + end +end diff --git a/db/partners_schema.rb b/db/partners_schema.rb index ace7af9116..bef15ad466 100644 --- a/db/partners_schema.rb +++ b/db/partners_schema.rb @@ -10,7 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_200_518_010_905) do +ActiveRecord::Schema.define(version: 2020_05_18_010905) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -21,7 +22,7 @@ t.bigint "blob_id", null: false t.datetime "created_at", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" - t.index %w(record_type record_id name blob_id), name: "index_active_storage_attachments_uniqueness", unique: true + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end create_table "active_storage_blobs", force: :cascade do |t| @@ -115,7 +116,7 @@ t.string "value" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index %w(feature_key key value), name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true + t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true end create_table "item_requests", force: :cascade do |t| @@ -261,7 +262,7 @@ t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true t.index ["invitations_count"], name: "index_users_on_invitations_count" t.index ["invited_by_id"], name: "index_users_on_invited_by_id" - t.index %w(invited_by_type invited_by_id), name: "index_users_on_invited_by_type_and_invited_by_id" + t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by" t.index ["partner_id"], name: "index_users_on_partner_id" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end diff --git a/db/schema.rb b/db/schema.rb index 38a02f735d..c56b6423ac 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.define(version: 2021_06_26_170605) do +ActiveRecord::Schema.define(version: 2021_07_29_003516) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -160,8 +160,8 @@ t.integer "organization_id" t.datetime "issued_at" t.string "agency_rep" - t.boolean "reminder_email_enabled", default: false, null: false t.integer "state", default: 5, null: false + t.boolean "reminder_email_enabled", default: false, null: false t.integer "delivery_method", default: 0, null: false t.index ["organization_id"], name: "index_distributions_on_organization_id" t.index ["partner_id"], name: "index_distributions_on_partner_id" @@ -225,7 +225,7 @@ end create_table "item_categories", force: :cascade do |t| - t.string "name" + t.string "name", null: false t.text "description" t.integer "organization_id", null: false t.datetime "created_at", precision: 6, null: false @@ -233,6 +233,15 @@ t.index ["name", "organization_id"], name: "index_item_categories_on_name_and_organization_id", unique: true end + create_table "item_categories_partner_groups", force: :cascade do |t| + t.bigint "partner_group_id", null: false + t.bigint "item_category_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["item_category_id"], name: "index_item_categories_partner_groups_on_item_category_id" + t.index ["partner_group_id"], name: "index_item_categories_partner_groups_on_partner_group_id" + end + create_table "items", id: :serial, force: :cascade do |t| t.string "name" t.string "category" @@ -310,6 +319,15 @@ t.index ["short_name"], name: "index_organizations_on_short_name" end + create_table "partner_groups", force: :cascade do |t| + t.bigint "organization_id" + t.string "name" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["name", "organization_id"], name: "index_partner_groups_on_name_and_organization_id", unique: true + t.index ["organization_id"], name: "index_partner_groups_on_organization_id" + end + create_table "partners", id: :serial, force: :cascade do |t| t.string "name" t.string "email" @@ -320,7 +338,9 @@ t.boolean "send_reminders", default: false, null: false t.text "notes" t.integer "quota" + t.bigint "partner_group_id" t.index ["organization_id"], name: "index_partners_on_organization_id" + t.index ["partner_group_id"], name: "index_partners_on_partner_group_id" end create_table "purchases", force: :cascade do |t| @@ -438,7 +458,7 @@ t.jsonb "object" t.datetime "created_at" t.jsonb "object_changes" - t.index %w(item_type item_id), name: "index_versions_on_item_type_and_item_id" + t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" end add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" @@ -452,11 +472,14 @@ add_foreign_key "donations", "manufacturers" add_foreign_key "donations", "storage_locations" add_foreign_key "item_categories", "organizations" + add_foreign_key "item_categories_partner_groups", "item_categories" + add_foreign_key "item_categories_partner_groups", "partner_groups" add_foreign_key "items", "item_categories" add_foreign_key "items", "kits" add_foreign_key "kits", "organizations" add_foreign_key "manufacturers", "organizations" add_foreign_key "organizations", "account_requests" + add_foreign_key "partner_groups", "organizations" add_foreign_key "requests", "distributions" add_foreign_key "requests", "organizations" add_foreign_key "requests", "partners" diff --git a/db/seeds.rb b/db/seeds.rb index 0fecce99ee..c16482ad1b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -100,6 +100,19 @@ def random_record_for_org(org, klass) end end +# ---------------------------------------------------------------------------- +# Partner Group & Item Categories +# ---------------------------------------------------------------------------- +Organization.all.each do |org| + # Setup the Partner Group & their item categories + partner_group = FactoryBot.create(:partner_group, organization: org) + + total_item_categories_to_add = Faker::Number.between(from: 0, to: 2) + org.item_categories.sample(total_item_categories_to_add).each do |item_category| + partner_group.item_categories << item_category + end +end + # ---------------------------------------------------------------------------- # Users # ---------------------------------------------------------------------------- @@ -182,8 +195,11 @@ def random_record_for_org(org, klass) ].each do |partner_option| p = Partner.find_or_create_by!(partner_option) do |partner| partner.organization = pdx_org + partner.partner_group = pdx_org.partner_groups.first end + + # ---------------------------------------------------------------------------- # Creating associated records within the Partnerbase database # diff --git a/spec/factories/item_categories.rb b/spec/factories/item_categories.rb index b34d2037e8..9cd3743c3c 100644 --- a/spec/factories/item_categories.rb +++ b/spec/factories/item_categories.rb @@ -4,7 +4,7 @@ # # id :bigint not null, primary key # description :text -# name :string +# name :string not null # created_at :datetime not null # updated_at :datetime not null # organization_id :integer not null diff --git a/spec/factories/partner_groups.rb b/spec/factories/partner_groups.rb new file mode 100644 index 0000000000..bdf52b9fad --- /dev/null +++ b/spec/factories/partner_groups.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: partner_groups +# +# id :bigint not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# organization_id :bigint +# + +FactoryBot.define do + factory :partner_group do + sequence(:name) { |n| "Group #{n}" } + organization { Organization.try(:first) || create(:organization) } + end +end diff --git a/spec/factories/partners.rb b/spec/factories/partners.rb index ea0607044d..24d5f8f718 100644 --- a/spec/factories/partners.rb +++ b/spec/factories/partners.rb @@ -2,16 +2,17 @@ # # Table name: partners # -# id :integer not null, primary key -# email :string -# name :string -# notes :text -# quota :integer -# send_reminders :boolean default(FALSE), not null -# status :integer default("uninvited") -# created_at :datetime not null -# updated_at :datetime not null -# organization_id :integer +# id :integer not null, primary key +# email :string +# name :string +# notes :text +# quota :integer +# send_reminders :boolean default(FALSE), not null +# status :integer default("uninvited") +# created_at :datetime not null +# updated_at :datetime not null +# organization_id :integer +# partner_group_id :bigint # FactoryBot.define do diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 72cb9f9183..4f226616ff 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -2,9 +2,10 @@ # # Table name: users # -# id :bigint not null, primary key +# id :integer not null, primary key # current_sign_in_at :datetime # current_sign_in_ip :inet +# discarded_at :datetime # email :string default(""), not null # encrypted_password :string default(""), not null # invitation_accepted_at :datetime @@ -14,17 +15,20 @@ # invitation_token :string # invitations_count :integer default(0) # invited_by_type :string +# last_request_at :datetime # last_sign_in_at :datetime # last_sign_in_ip :inet -# name :string +# name :string default("CHANGEME"), not null +# organization_admin :boolean # remember_created_at :datetime # reset_password_sent_at :datetime # reset_password_token :string # sign_in_count :integer default(0), not null +# super_admin :boolean default(FALSE) # created_at :datetime not null # updated_at :datetime not null -# invited_by_id :bigint -# partner_id :bigint +# invited_by_id :integer +# organization_id :integer # FactoryBot.define do diff --git a/spec/models/item_category_spec.rb b/spec/models/item_category_spec.rb index 3455552740..2ed46cafae 100644 --- a/spec/models/item_category_spec.rb +++ b/spec/models/item_category_spec.rb @@ -4,7 +4,7 @@ # # id :bigint not null, primary key # description :text -# name :string +# name :string not null # created_at :datetime not null # updated_at :datetime not null # organization_id :integer not null @@ -24,5 +24,6 @@ describe 'assocations' do it { should belong_to(:organization) } it { should have_many(:items) } + it { should have_and_belong_to_many(:partner_groups) } end end diff --git a/spec/models/partner_group_spec.rb b/spec/models/partner_group_spec.rb new file mode 100644 index 0000000000..d8dd6923ad --- /dev/null +++ b/spec/models/partner_group_spec.rb @@ -0,0 +1,33 @@ +# == Schema Information +# +# Table name: partner_groups +# +# id :bigint not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# organization_id :bigint +# +RSpec.describe PartnerGroup, type: :model do + describe 'associations' do + it { should belong_to(:organization) } + it { should have_many(:partners) } + it { should have_and_belong_to_many(:item_categories) } + end + context "Validations >" do + it "must belong to an organization" do + expect(build(:partner_group, organization_id: nil)).not_to be_valid + end + + it "requires a unique name within an organization" do + expect(build(:partner_group, name: nil)).not_to be_valid + create(:partner_group, name: "Foo") + expect(build(:partner_group, name: "Foo")).not_to be_valid + end + + it "does not require a unique name between organizations" do + create(:partner, name: "Foo") + expect(build(:partner, name: "Foo", organization: build(:organization))).to be_valid + end + end +end diff --git a/spec/models/partner_spec.rb b/spec/models/partner_spec.rb index 868ca0fd84..c367564948 100644 --- a/spec/models/partner_spec.rb +++ b/spec/models/partner_spec.rb @@ -2,19 +2,30 @@ # # Table name: partners # -# id :integer not null, primary key -# email :string -# name :string -# notes :text -# quota :integer -# send_reminders :boolean default(FALSE), not null -# status :integer default("uninvited") -# created_at :datetime not null -# updated_at :datetime not null -# organization_id :integer +# id :integer not null, primary key +# email :string +# name :string +# notes :text +# quota :integer +# send_reminders :boolean default(FALSE), not null +# status :integer default("uninvited") +# created_at :datetime not null +# updated_at :datetime not null +# organization_id :integer +# partner_group_id :bigint # RSpec.describe Partner, type: :model do + describe 'associations' do + it { should belong_to(:organization) } + it { should belong_to(:partner_group).optional } + it { should have_many(:item_categories).through(:partner_group) } + it { should have_many(:requestable_items).through(:item_categories).source(:items) } + it { should have_many(:requests) } + it { should have_many(:distributions) } + it { should have_many(:requests) } + end + context "Validations >" do it "must belong to an organization" do expect(build(:partner, organization_id: nil)).not_to be_valid diff --git a/spec/services/partner_fetch_requestable_items_service_spec.rb b/spec/services/partner_fetch_requestable_items_service_spec.rb new file mode 100644 index 0000000000..472bab2549 --- /dev/null +++ b/spec/services/partner_fetch_requestable_items_service_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +describe PartnerFetchRequestableItemsService do + describe '#call' do + subject { described_class.new(partner_id: partner_id).call } + let(:partner_id) { partner.id } + let(:partner) { create(:partner) } + let(:organization) { partner.organization } + + context 'when the partner id does not match any Partner' do + let(:partner_id) { 0 } + + it 'raise an error indiciating the partner does not exist' do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when the partner is not in any partner group' do + before do + expect(partner.partner_group).to be_nil + end + + it 'should return all active and visible items' do + expect(subject).to eq(organization.items.active.visible) + end + end + + context 'when the partner is in a partner group' do + before do + pg = create(:partner_group) + pg.item_categories << create(:item_category, organization: organization) + partner.update(partner_group: pg) + end + + it 'should return all active and visible items specified by the item associated with' do + expect(subject).to eq(partner.requestable_items.active.visible) + end + end + end +end diff --git a/spec/system/partner_system_spec.rb b/spec/system/partner_system_spec.rb index 853cf52d6d..2d0a1a314d 100644 --- a/spec/system/partner_system_spec.rb +++ b/spec/system/partner_system_spec.rb @@ -152,7 +152,8 @@ visit url_prefix + "/partners" - within("table > tbody > tr:nth-child(4) > td:nth-child(5)") { click_on "Invite" } + ele = find('tr', text: partner.name) + within(ele) { click_on "Invite" } invite_alert = page.driver.browser.switch_to.alert expect(invite_alert.text).to eq("Send an invitation to #{partner.name} to begin using the partner application?") @@ -163,9 +164,9 @@ end it "shows invite button only for unapproved partners" do - expect(page.find(:xpath, "//table/tbody/tr[1]/td[5]")).to have_no_content('Invite') - expect(page.find(:xpath, "//table/tbody/tr[2]/td[5]")).to have_content('Invite') - expect(page.find(:xpath, "//table/tbody/tr[3]/td[5]")).to have_no_content('Invite') + expect(page.find('tr', text: 'Abc')).to have_content('Invited') + expect(page.find('tr', text: 'Bcd')).to have_content('Invite') + expect(page.find('tr', text: 'Cde')).to have_no_content('Invite') end context "when filtering" do @@ -326,7 +327,7 @@ let!(:awaiting_review_partner) { create(:partner, name: "Beau Brummel", status: :awaiting_review) } context "when partner has :invited status" do - before { visit_approval_page(1) } + before { visit_approval_page(partner_name: invited_partner.name) } it { expect(page).to have_selector(:link_or_button, 'Approve Partner') } it { expect(page).to have_selector('span#pending-approval-request-tooltip > a.btn.btn-success.btn-md.disabled') } @@ -338,7 +339,7 @@ end context "when partner has :awaiting_review status" do - before { visit_approval_page(2) } + before { visit_approval_page(partner_name: awaiting_review_partner.name) } it { expect(page).to have_selector(:link_or_button, 'Approve Partner') } it { expect(page).not_to have_selector('span#pending-approval-request-tooltip > a.btn.btn-success.btn-md.disabled') } @@ -349,9 +350,131 @@ end end end + + describe 'changing partner group association' do + before do + sign_in(@user) + visit url_prefix + "/partners/#{@partner.id}" + end + let!(:existing_partner_group) { create(:partner_group) } + + context 'when the partner has no partner group' do + before do + expect(@partner.partner_group).to be_nil + end + + it 'it should say they can request every item' do + assert page.has_content? 'All Items Requestable' + assert page.has_content? 'Settings' + expect(PartnerFetchRequestableItemsService.new(partner_id: @partner.id).call).to eq(@organization.items.active.visible) + end + end + + context 'when a partner is assigned to partner group' do + before do + assert page.has_content? 'All Items Requestable' + expect(PartnerFetchRequestableItemsService.new(partner_id: @partner.id).call).to eq(@organization.items.active.visible) + end + + context 'that has requestable item categories' do + let!(:item_category) do + ic = create(:item_category, organization: @organization) + existing_partner_group.item_categories << ic + ic + end + let!(:items_in_category) { create_list(:item, 3, item_category_id: item_category.id) } + + before do + click_on 'Edit' + select existing_partner_group.name + click_on 'Update Partner' + end + + it 'should properly indicate the requestable items and adjust the partners requestable items' do + assert page.has_content? item_category.name + expect(PartnerFetchRequestableItemsService.new(partner_id: @partner.id).call).to eq(items_in_category) + end + end + + context 'that has no requestable item categories' do + before do + expect(existing_partner_group.item_categories).to be_empty + click_on 'Edit' + select existing_partner_group.name + click_on 'Update Partner' + end + + it 'should properly indicate the requestable items and adjust the partners requestable items' do + assert page.has_content? 'No Items Requestable' + expect(PartnerFetchRequestableItemsService.new(partner_id: @partner.id).call).to eq([]) + end + end + end + end + + describe "partner group management", type: :system, js: true do + before do + sign_in(@user) + end + + let!(:url_prefix) { "/#{@organization.to_param}" } + let!(:item_category_1) { create(:item_category, organization: @organization) } + let!(:item_category_2) { create(:item_category, organization: @organization) } + let!(:items_in_category_1) { create_list(:item, 3, item_category_id: item_category_1.id) } + let!(:items_in_category_2) { create_list(:item, 3, item_category_id: item_category_2.id) } + + describe 'creating a new partner group' do + it 'should allow creating a new partner group with item categories' do + visit url_prefix + "/partners" + + click_on 'Groups' + click_on 'New Partner Group' + fill_in 'Name *', with: 'Test Group' + + # Click on the second item category + find("input#partner_group_item_category_ids_#{item_category_2.id}").click + + find_button('Add Partner Group').click + + assert page.has_content? 'Group Name' + assert page.has_content? 'Test Group' + assert page.has_content? item_category_2.name + end + end + + describe 'editing a existing partner group' do + let!(:existing_partner_group) { create(:partner_group, organization: @organization) } + before do + existing_partner_group.item_categories << item_category_1 + end + + it 'should allow updating the partner name' do + visit url_prefix + "/partners" + + click_on 'Groups' + assert page.has_content? existing_partner_group.name + assert page.has_content? item_category_1.name + + click_on 'Edit' + fill_in 'Name *', with: 'New Group Name' + + # Unset the existing category + find("input#partner_group_item_category_ids_#{item_category_1.id}").click + # Set a new one on the category + find("input#partner_group_item_category_ids_#{item_category_2.id}").click + + find_button('Update Partner Group').click + + assert page.has_content? 'New Group Name' + refute page.has_content? item_category_1.name + assert page.has_content? item_category_2.name + end + end + end end -def visit_approval_page(table_row) +def visit_approval_page(partner_name:) visit url_prefix + "/partners" - within("table > tbody > tr:nth-child(#{table_row}) > td:nth-child(5)") { click_on "Review Application" } + ele = find('tr', text: partner_name) + within(ele) { click_on "Review Application" } end