diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index a77f1fb2a..13917701e 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -8,10 +8,20 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle def create @category = @portal.categories.create!(category_params) + @category.related_categories << related_categories_records + render json: { error: @category.errors.messages }, status: :unprocessable_entity and return unless @category.valid? + + @category.save! end + def show; end + def update @category.update!(category_params) + @category.related_categories = related_categories_records if related_categories_records.any? + render json: { error: @category.errors.messages }, status: :unprocessable_entity and return unless @category.valid? + + @category.save! end def destroy @@ -29,9 +39,13 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle @portal ||= Current.account.portals.find_by(slug: params[:portal_id]) end + def related_categories_records + @portal.categories.where(id: params[:category][:related_category_ids]) + end + def category_params params.require(:category).permit( - :name, :description, :position, :slug, :locale + :name, :description, :position, :slug, :locale, :parent_category_id, :linked_category_id ) end end diff --git a/app/models/category.rb b/app/models/category.rb index 8c5989c0f..6e9b30ae0 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -2,28 +2,56 @@ # # Table name: categories # -# id :bigint not null, primary key -# description :text -# locale :string default("en") -# name :string -# position :integer -# slug :string not null -# created_at :datetime not null -# updated_at :datetime not null -# account_id :integer not null -# portal_id :integer not null +# id :bigint not null, primary key +# description :text +# locale :string default("en") +# name :string +# position :integer +# slug :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# linked_category_id :bigint +# parent_category_id :bigint +# portal_id :integer not null # # Indexes # +# index_categories_on_linked_category_id (linked_category_id) # index_categories_on_locale (locale) # index_categories_on_locale_and_account_id (locale,account_id) +# index_categories_on_parent_category_id (parent_category_id) # index_categories_on_slug_and_locale_and_portal_id (slug,locale,portal_id) UNIQUE # +# Foreign Keys +# +# fk_rails_... (linked_category_id => categories.id) +# fk_rails_... (parent_category_id => categories.id) +# class Category < ApplicationRecord belongs_to :account belongs_to :portal has_many :folders, dependent: :destroy_async has_many :articles, dependent: :nullify + has_many :category_related_categories, + class_name: :RelatedCategory, + dependent: :destroy_async + has_many :related_categories, + through: :category_related_categories, + class_name: :Category, + dependent: :nullify + has_many :sub_categories, + class_name: :Category, + foreign_key: :parent_category_id, + dependent: :nullify, + inverse_of: 'parent_category' + has_many :linked_categories, + class_name: :Category, + foreign_key: :linked_category_id, + dependent: :nullify, + inverse_of: 'linked_category' + belongs_to :parent_category, class_name: :Category, optional: true + belongs_to :linked_category, class_name: :Category, optional: true before_validation :ensure_account_id validates :account_id, presence: true @@ -31,6 +59,7 @@ class Category < ApplicationRecord validates :name, presence: true validates :locale, uniqueness: { scope: %i[slug portal_id], message: 'should be unique in the category and portal' } + accepts_nested_attributes_for :related_categories scope :search_by_locale, ->(locale) { where(locale: locale) if locale.present? } diff --git a/app/models/related_category.rb b/app/models/related_category.rb new file mode 100644 index 000000000..018a13418 --- /dev/null +++ b/app/models/related_category.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: related_categories +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# category_id :bigint +# related_category_id :bigint +# +# Indexes +# +# index_related_categories_on_category_id_and_related_category_id (category_id,related_category_id) UNIQUE +# index_related_categories_on_related_category_id_and_category_id (related_category_id,category_id) UNIQUE +# +class RelatedCategory < ApplicationRecord + belongs_to :related_category, class_name: 'Category' + belongs_to :category, class_name: 'Category' +end diff --git a/app/views/api/v1/accounts/categories/_associated_category.json.jbuilder b/app/views/api/v1/accounts/categories/_associated_category.json.jbuilder new file mode 100644 index 000000000..4b4915da9 --- /dev/null +++ b/app/views/api/v1/accounts/categories/_associated_category.json.jbuilder @@ -0,0 +1,7 @@ +json.id category.id +json.name category.name +json.slug category.slug +json.locale category.locale +json.description category.description +json.position category.position +json.account_id category.account_id diff --git a/app/views/api/v1/accounts/categories/_category.json.jbuilder b/app/views/api/v1/accounts/categories/_category.json.jbuilder index 4b4915da9..d299a50d0 100644 --- a/app/views/api/v1/accounts/categories/_category.json.jbuilder +++ b/app/views/api/v1/accounts/categories/_category.json.jbuilder @@ -5,3 +5,23 @@ json.locale category.locale json.description category.description json.position category.position json.account_id category.account_id + +json.related_categories do + if category.related_categories.any? + json.array! category.related_categories.each do |related_category| + json.partial! 'api/v1/accounts/categories/associated_category.json.jbuilder', category: related_category + end + end +end + +if category.parent_category.present? + json.parent_category do + json.partial! 'api/v1/accounts/categories/associated_category.json.jbuilder', category: category.parent_category + end +end + +if category.linked_category.present? + json.linked_category do + json.partial! 'api/v1/accounts/categories/associated_category.json.jbuilder', category: category.linked_category + end +end diff --git a/app/views/api/v1/accounts/categories/show.json.jbuilder b/app/views/api/v1/accounts/categories/show.json.jbuilder new file mode 100644 index 000000000..932cd6a18 --- /dev/null +++ b/app/views/api/v1/accounts/categories/show.json.jbuilder @@ -0,0 +1,3 @@ +json.payload do + json.partial! 'category', category: @category +end diff --git a/config/routes.rb b/config/routes.rb index 81538c007..809f6a76b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,9 +159,7 @@ Rails.application.routes.draw do member do post :archive end - resources :categories do - resources :folders - end + resources :categories resources :articles end end diff --git a/db/migrate/20220623113405_add_parent_id_to_category.rb b/db/migrate/20220623113405_add_parent_id_to_category.rb new file mode 100644 index 000000000..57a694bac --- /dev/null +++ b/db/migrate/20220623113405_add_parent_id_to_category.rb @@ -0,0 +1,5 @@ +class AddParentIdToCategory < ActiveRecord::Migration[6.1] + def change + add_reference :categories, :parent_category, foreign_key: { to_table: :categories } + end +end diff --git a/db/migrate/20220623113604_create_related_categories.rb b/db/migrate/20220623113604_create_related_categories.rb new file mode 100644 index 000000000..c9633c61e --- /dev/null +++ b/db/migrate/20220623113604_create_related_categories.rb @@ -0,0 +1,12 @@ +class CreateRelatedCategories < ActiveRecord::Migration[6.1] + def change + create_table :related_categories do |t| + t.bigint :category_id + t.bigint :related_category_id + t.timestamps + end + + add_index :related_categories, [:category_id, :related_category_id], unique: true + add_index :related_categories, [:related_category_id, :category_id], unique: true + end +end diff --git a/db/migrate/20220627135753_add_linked_category_id_to_categories.rb b/db/migrate/20220627135753_add_linked_category_id_to_categories.rb new file mode 100644 index 000000000..662a55879 --- /dev/null +++ b/db/migrate/20220627135753_add_linked_category_id_to_categories.rb @@ -0,0 +1,5 @@ +class AddLinkedCategoryIdToCategories < ActiveRecord::Migration[6.1] + def change + add_reference :categories, :linked_category, foreign_key: { to_table: :categories } + end +end diff --git a/db/schema.rb b/db/schema.rb index f9557e535..fefbe449a 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: 2022_06_22_090344) do +ActiveRecord::Schema.define(version: 2022_06_27_135753) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -197,8 +197,12 @@ ActiveRecord::Schema.define(version: 2022_06_22_090344) do t.datetime "updated_at", precision: 6, null: false t.string "locale", default: "en" t.string "slug", null: false + t.bigint "parent_category_id" + t.bigint "linked_category_id" + t.index ["linked_category_id"], name: "index_categories_on_linked_category_id" t.index ["locale", "account_id"], name: "index_categories_on_locale_and_account_id" t.index ["locale"], name: "index_categories_on_locale" + t.index ["parent_category_id"], name: "index_categories_on_parent_category_id" t.index ["slug", "locale", "portal_id"], name: "index_categories_on_slug_and_locale_and_portal_id", unique: true end @@ -683,6 +687,15 @@ ActiveRecord::Schema.define(version: 2022_06_22_090344) do t.index ["user_id"], name: "index_portals_members_on_user_id" end + create_table "related_categories", force: :cascade do |t| + t.bigint "category_id" + t.bigint "related_category_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["category_id", "related_category_id"], name: "index_related_categories_on_category_id_and_related_category_id", unique: true + t.index ["related_category_id", "category_id"], name: "index_related_categories_on_related_category_id_and_category_id", unique: true + end + create_table "reporting_events", force: :cascade do |t| t.string "name" t.float "value" @@ -826,6 +839,8 @@ ActiveRecord::Schema.define(version: 2022_06_22_090344) do add_foreign_key "articles", "users", column: "author_id" add_foreign_key "campaigns", "accounts", on_delete: :cascade add_foreign_key "campaigns", "inboxes", on_delete: :cascade + add_foreign_key "categories", "categories", column: "linked_category_id" + add_foreign_key "categories", "categories", column: "parent_category_id" add_foreign_key "contact_inboxes", "contacts", on_delete: :cascade add_foreign_key "contact_inboxes", "inboxes", on_delete: :cascade add_foreign_key "conversations", "campaigns", on_delete: :cascade diff --git a/spec/controllers/api/v1/accounts/categories_controller_spec.rb b/spec/controllers/api/v1/accounts/categories_controller_spec.rb index 053fe9d5e..4ebdb54d5 100644 --- a/spec/controllers/api/v1/accounts/categories_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/categories_controller_spec.rb @@ -5,6 +5,9 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do let(:agent) { create(:user, account: account, role: :agent) } let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) } let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, slug: 'category_slug') } + let!(:category_to_link) { create(:category, name: 'linked category', portal: portal, account_id: account.id, slug: 'linked_category_slug') } + let!(:related_category_1) { create(:category, name: 'related category 1', portal: portal, account_id: account.id, slug: 'category_slug_1') } + let!(:related_category_2) { create(:category, name: 'related category 2', portal: portal, account_id: account.id, slug: 'category_slug_2') } describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do context 'when it is an unauthenticated user' do @@ -15,23 +18,76 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do end context 'when it is an authenticated user' do - category_params = { - category: { - name: 'test_category', - description: 'test_description', - position: 1, - locale: 'es', - slug: 'test_category_1' + let!(:category_params) do + { + category: { + name: 'test_category', + description: 'test_description', + position: 1, + locale: 'es', + slug: 'test_category_1', + parent_category_id: category.id, + linked_category_id: category_to_link.id, + related_category_ids: [related_category_1.id, related_category_2.id] + } } - } + end + + let!(:category_params_2) do + { + category: { + name: 'test_category_2', + description: 'test_description_2', + position: 1, + locale: 'es', + slug: 'test_category_2', + parent_category_id: category.id, + linked_category_id: category_to_link.id, + related_category_ids: [related_category_1.id, related_category_2.id] + } + } + end it 'creates category' do post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", params: category_params, headers: agent.create_new_auth_token expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) - expect(json_response['payload']['name']).to eql('test_category') + + expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id) + expect(json_response['payload']['related_categories'][1]['id']).to eql(related_category_2.id) + expect(json_response['payload']['parent_category']['id']).to eql(category.id) + expect(json_response['payload']['linked_category']['id']).to eql(category_to_link.id) + expect(category.reload.sub_category_ids).to eql([Category.last.id]) + expect(category_to_link.reload.linked_category_ids).to eql([Category.last.id]) + end + + it 'creates multiple sub_categories under one parent_category' do + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params, + headers: agent.create_new_auth_token + + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params_2, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:success) + expect(category.reload.sub_category_ids).to eql(Category.last(2).pluck(:id)) + end + + it 'creates multiple linked_categories with one category' do + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params, + headers: agent.create_new_auth_token + + post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", + params: category_params_2, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:success) + expect(category_to_link.reload.linked_category_ids).to eql(Category.last(2).pluck(:id)) end it 'will throw an error on locale, category_id uniqueness' do @@ -81,18 +137,74 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do category: { name: 'test_category_2', description: 'test_description', - position: 1 + position: 1, + related_category_ids: [related_category_1.id], + parent_category_id: related_category_2.id } } expect(category.name).not_to eql(category_params[:category][:name]) + expect(category.related_categories).to be_empty + expect(category.parent_category).to be_nil put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}", params: category_params, headers: agent.create_new_auth_token - expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response['payload']['name']).to eql(category_params[:category][:name]) + expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id) + expect(json_response['payload']['parent_category']['id']).to eql(related_category_2.id) + expect(related_category_2.reload.sub_category_ids).to eql([category.id]) + end + + it 'updates related categories' do + category_params = { + category: { + related_category_ids: [related_category_1.id] + } + } + category.related_categories << related_category_2 + category.save! + + expect(category.related_category_ids).to eq([related_category_2.id]) + + put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}", + params: category_params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:success) + + json_response = JSON.parse(response.body) + + expect(json_response['payload']['name']).to eql(category.name) + expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id) + expect(category.reload.related_category_ids).to eq([related_category_1.id]) + expect(related_category_1.reload.related_category_ids).to be_empty + end + + # [category_1, category_2] !== [category_2, category_1] + it 'update reverse associations for related categories' do + category.related_categories << related_category_2 + category.save! + + expect(category.related_category_ids).to eq([related_category_2.id]) + + category_params = { + category: { + related_category_ids: [category.id] + } + } + + put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{related_category_2.id}", + params: category_params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:success) + + expect(category.reload.related_category_ids).to eq([related_category_2.id]) + expect(related_category_2.reload.related_category_ids).to eq([category.id]) end end end @@ -125,7 +237,9 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do end context 'when it is an authenticated user' do - it 'get all portals' do + it 'get all categories in portal' do + category_count = Category.all.count + category2 = create(:category, name: 'test_category_2', portal: portal, locale: 'es', slug: 'category_slug_2') expect(category2.id).not_to be nil @@ -133,7 +247,7 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do headers: agent.create_new_auth_token expect(response).to have_http_status(:success) json_response = JSON.parse(response.body) - expect(json_response['payload'].count).to be 2 + expect(json_response['payload'].count).to be(category_count + 1) end end end diff --git a/spec/factories/related_categories.rb b/spec/factories/related_categories.rb new file mode 100644 index 000000000..492cfa0b7 --- /dev/null +++ b/spec/factories/related_categories.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :related_category do + category + related_category + end +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index 37b17284a..565abba8c 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -9,8 +9,10 @@ RSpec.describe Category, type: :model do describe 'associations' do it { is_expected.to belong_to(:account) } it { is_expected.to belong_to(:portal) } - it { is_expected.to have_many(:folders) } it { is_expected.to have_many(:articles) } + it { is_expected.to have_many(:sub_categories) } + it { is_expected.to have_many(:linked_categories) } + it { is_expected.to have_many(:related_categories) } end describe 'search' do diff --git a/spec/models/related_category_spec.rb b/spec/models/related_category_spec.rb new file mode 100644 index 000000000..5a500d53b --- /dev/null +++ b/spec/models/related_category_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +RSpec.describe RelatedCategory, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:category) } + it { is_expected.to belong_to(:related_category) } + end +end