feat: Category API to handle linked categories and parent-sub categories (#4879)

This commit is contained in:
Tejaswini Chile 2022-06-28 11:23:20 +05:30 committed by GitHub
parent c0249a1b5b
commit df1bf112ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 286 additions and 29 deletions

View file

@ -8,10 +8,20 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def create def create
@category = @portal.categories.create!(category_params) @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 end
def show; end
def update def update
@category.update!(category_params) @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 end
def destroy 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]) @portal ||= Current.account.portals.find_by(slug: params[:portal_id])
end end
def related_categories_records
@portal.categories.where(id: params[:category][:related_category_ids])
end
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position, :slug, :locale :name, :description, :position, :slug, :locale, :parent_category_id, :linked_category_id
) )
end end
end end

View file

@ -2,28 +2,56 @@
# #
# Table name: categories # Table name: categories
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# description :text # description :text
# locale :string default("en") # locale :string default("en")
# name :string # name :string
# position :integer # position :integer
# slug :string not null # slug :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :integer not null # account_id :integer not null
# portal_id :integer not null # linked_category_id :bigint
# parent_category_id :bigint
# portal_id :integer not null
# #
# Indexes # Indexes
# #
# index_categories_on_linked_category_id (linked_category_id)
# index_categories_on_locale (locale) # index_categories_on_locale (locale)
# index_categories_on_locale_and_account_id (locale,account_id) # 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 # 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 class Category < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :portal belongs_to :portal
has_many :folders, dependent: :destroy_async has_many :folders, dependent: :destroy_async
has_many :articles, dependent: :nullify 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 before_validation :ensure_account_id
validates :account_id, presence: true validates :account_id, presence: true
@ -31,6 +59,7 @@ class Category < ApplicationRecord
validates :name, presence: true validates :name, presence: true
validates :locale, uniqueness: { scope: %i[slug portal_id], validates :locale, uniqueness: { scope: %i[slug portal_id],
message: 'should be unique in the category and portal' } 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? } scope :search_by_locale, ->(locale) { where(locale: locale) if locale.present? }

View file

@ -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

View file

@ -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

View file

@ -5,3 +5,23 @@ json.locale category.locale
json.description category.description json.description category.description
json.position category.position json.position category.position
json.account_id category.account_id 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

View file

@ -0,0 +1,3 @@
json.payload do
json.partial! 'category', category: @category
end

View file

@ -159,9 +159,7 @@ Rails.application.routes.draw do
member do member do
post :archive post :archive
end end
resources :categories do resources :categories
resources :folders
end
resources :articles resources :articles
end end
end end

View file

@ -0,0 +1,5 @@
class AddParentIdToCategory < ActiveRecord::Migration[6.1]
def change
add_reference :categories, :parent_category, foreign_key: { to_table: :categories }
end
end

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddLinkedCategoryIdToCategories < ActiveRecord::Migration[6.1]
def change
add_reference :categories, :linked_category, foreign_key: { to_table: :categories }
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" 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.datetime "updated_at", precision: 6, null: false
t.string "locale", default: "en" t.string "locale", default: "en"
t.string "slug", null: false 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", "account_id"], name: "index_categories_on_locale_and_account_id"
t.index ["locale"], name: "index_categories_on_locale" 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 t.index ["slug", "locale", "portal_id"], name: "index_categories_on_slug_and_locale_and_portal_id", unique: true
end 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" t.index ["user_id"], name: "index_portals_members_on_user_id"
end 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| create_table "reporting_events", force: :cascade do |t|
t.string "name" t.string "name"
t.float "value" 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 "articles", "users", column: "author_id"
add_foreign_key "campaigns", "accounts", on_delete: :cascade add_foreign_key "campaigns", "accounts", on_delete: :cascade
add_foreign_key "campaigns", "inboxes", 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", "contacts", on_delete: :cascade
add_foreign_key "contact_inboxes", "inboxes", on_delete: :cascade add_foreign_key "contact_inboxes", "inboxes", on_delete: :cascade
add_foreign_key "conversations", "campaigns", on_delete: :cascade add_foreign_key "conversations", "campaigns", on_delete: :cascade

View file

@ -5,6 +5,9 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) } 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) { 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 describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
@ -15,23 +18,76 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
category_params = { let!(:category_params) do
category: { {
name: 'test_category', category: {
description: 'test_description', name: 'test_category',
position: 1, description: 'test_description',
locale: 'es', position: 1,
slug: 'test_category_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 it 'creates category' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params, params: category_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) 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 end
it 'will throw an error on locale, category_id uniqueness' do 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: { category: {
name: 'test_category_2', name: 'test_category_2',
description: 'test_description', 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.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}", put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
params: category_params, params: category_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
expect(json_response['payload']['name']).to eql(category_params[:category][:name]) 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 end
end end
@ -125,7 +237,9 @@ RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
end end
context 'when it is an authenticated user' do 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') category2 = create(:category, name: 'test_category_2', portal: portal, locale: 'es', slug: 'category_slug_2')
expect(category2.id).not_to be nil 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 headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) 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 end
end end

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :related_category do
category
related_category
end
end

View file

@ -9,8 +9,10 @@ RSpec.describe Category, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:account) } it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:portal) } 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(: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 end
describe 'search' do describe 'search' do

View file

@ -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