feat: Add filter APIs for Contacts and Conversations (#3264)

This commit is contained in:
Tejaswini Chile 2021-11-01 13:57:04 +05:30 committed by GitHub
parent 68fa694268
commit 372bd75028
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 470 additions and 19 deletions

View file

@ -51,7 +51,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def show; end
def filter
@contacts = Current.account.contacts.limit(10)
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts_with_conversation_count(contacts)
end
def contactable_inboxes

View file

@ -32,7 +32,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def show; end
def filter
@conversations = Current.account.conversations.limit(10)
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@conversations_count = result[:count]
end
def mute

View file

@ -0,0 +1,44 @@
class Contacts::FilterService < FilterService
def perform
@contacts = contact_query_builder
{
contacts: @contacts,
count: {
all_count: @contacts.count
}
}
end
def contact_query_builder
contact_filters = @filters['contacts']
@params[:payload].each_with_index do |query_hash, current_index|
current_filter = contact_filters[query_hash['attribute_key']]
@query_string += contact_query_string(current_filter, query_hash, current_index)
end
base_relation.where(@query_string, @filter_values.with_indifferent_access)
end
def contact_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash[:attribute_key]
query_operator = query_hash[:query_operator]
filter_operator_value = filter_operation(query_hash, current_index)
case current_filter['attribute_type']
when 'additional_attributes'
" contacts.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
when 'standard'
if attribute_key == 'labels'
" tags.id #{filter_operator_value} #{query_operator} "
else
" contacts.#{attribute_key} #{filter_operator_value} #{query_operator} "
end
end
end
def base_relation
Current.account.contacts.left_outer_joins(:labels)
end
end

View file

@ -0,0 +1,59 @@
class Conversations::FilterService < FilterService
def perform
@conversations = conversation_query_builder
mine_count, unassigned_count, all_count, = set_count_for_all_conversations
assigned_count = all_count - unassigned_count
{
conversations: conversations,
count: {
mine_count: mine_count,
assigned_count: assigned_count,
unassigned_count: unassigned_count,
all_count: all_count
}
}
end
def conversation_query_builder
conversation_filters = @filters['conversations']
@params[:payload].each_with_index do |query_hash, current_index|
current_filter = conversation_filters[query_hash['attribute_key']]
@query_string += conversation_query_string(current_filter, query_hash, current_index)
end
base_relation.where(@query_string, @filter_values.with_indifferent_access)
end
def conversation_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash[:attribute_key]
query_operator = query_hash[:query_operator]
filter_operator_value = filter_operation(query_hash, current_index)
case current_filter['attribute_type']
when 'additional_attributes'
" conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
when 'standard'
if attribute_key == 'labels'
" tags.id #{filter_operator_value} #{query_operator} "
else
" conversations.#{attribute_key} #{filter_operator_value} #{query_operator} "
end
end
end
def base_relation
Current.account.conversations.left_outer_joins(:labels)
end
def current_page
@params[:page] || 1
end
def conversations
@conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
)
@conversations.latest.page(current_page)
end
end

View file

@ -0,0 +1,50 @@
require 'json'
class FilterService
def initialize(params, user)
@params = params
@user = user
file = File.read('./lib/filters/filter_keys.json')
@filters = JSON.parse(file)
@query_string = ''
@filter_values = {}
end
def perform; end
def filter_operation(query_hash, current_index)
case query_hash[:filter_operator]
when 'equal_to'
@filter_values["value_#{current_index}"] = filter_values(query_hash)
"IN (:value_#{current_index})"
when 'not_equal_to'
@filter_values["value_#{current_index}"] = filter_values(query_hash)
"NOT IN (:value_#{current_index})"
when 'contains'
@filter_values["value_#{current_index}"] = "%#{filter_values(query_hash)}%"
"LIKE :value_#{current_index}"
when 'does_not_contain'
@filter_values["value_#{current_index}"] = "%#{filter_values(query_hash)}%"
"NOT LIKE :value_#{current_index}"
else
@filter_values["value_#{current_index}"] = filter_values(query_hash).to_s
"= :value_#{current_index}"
end
end
def filter_values(query_hash)
if query_hash['attribute_key'] == 'status'
query_hash['values'].map { |x| Conversation.statuses[x.to_sym] }
else
query_hash['values']
end
end
def set_count_for_all_conversations
[
@conversations.assigned_to(@user).count,
@conversations.unassigned.count,
@conversations.count
]
end
end

View file

@ -1,3 +1,10 @@
json.array! @conversations do |conversation|
json.partial! 'api/v1/models/conversation.json.jbuilder', conversation: conversation
json.meta do
json.mine_count @conversations_count[:mine_count]
json.unassigned_count @conversations_count[:unassigned_count]
json.all_count @conversations_count[:all_count]
end
json.payload do
json.array! @conversations do |conversation|
json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation
end
end

View file

@ -61,7 +61,7 @@ Rails.application.routes.draw do
collection do
get :meta
get :search
get :filter
post :filter
end
scope module: :conversations do
resources :messages, only: [:index, :create, :destroy]
@ -83,7 +83,7 @@ Rails.application.routes.draw do
collection do
get :active
get :search
get :filter
post :filter
post :import
end
member do

View file

@ -65,8 +65,8 @@
"attribute_type": "standard"
},
{
"attribute_key": "browser",
"attribute_name": "browser",
"attribute_key": "browser_language",
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],

View file

@ -0,0 +1,153 @@
{
"conversations": {
"status": {
"attribute_name": "Status",
"input_type": "multi_select",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to" ],
"attribute_type": "standard"
},
"assignee_id": {
"attribute_name": "Assignee Name",
"input_type": "search_box with name tags/plain text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"contact_id": {
"attribute_name": "Contact Name",
"input_type": "plain_text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"inbox_id": {
"attribute_name": "Inbox Name",
"input_type": "search_box",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"team_id": {
"attribute_name": "Team Name",
"input_type": "search_box",
"data_type": "number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"id": {
"attribute_name": "Conversation Identifier",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"campaign_id": {
"attribute_name": "Campaign Name",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
"data_type": "text",
"filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ],
"attribute_type": "standard"
},
"browser_language": {
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
"country_code": {
"attribute_name": "Country Name",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"referer": {
"attribute_name": "Referer link",
"input_type": "textbox",
"data_type": "link",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
}
},
"contacts": {
"assignee_id": {
"attribute_name": "Assignee Name",
"input_type": "search_box with name tags/plain text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"contact_id": {
"attribute_name": "Contact Name",
"input_type": "plain_text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"inbox_id": {
"attribute_name": "Inbox Name",
"input_type": "search_box",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"team_id": {
"attribute_name": "Team Name",
"input_type": "search_box",
"data_type": "number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"id": {
"attribute_name": "Conversation Identifier",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"campaign_id": {
"attribute_name": "Campaign Name",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
"data_type": "text",
"filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ],
"attribute_type": "standard"
},
"browser_language": {
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
"country_code": {
"attribute_name": "Country Name",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"referer": {
"attribute_name": "Referer link",
"input_type": "textbox",
"data_type": "link",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
}
}
}

View file

@ -233,10 +233,12 @@ RSpec.describe 'Contacts API', type: :request do
let!(:contact2) { create(:contact, :with_email, name: 'testcontact', account: account, email: 'test@test.com') }
it 'returns all contacts when query is empty' do
get "/api/v1/accounts/#{account.id}/contacts/filter",
params: { q: [] },
headers: admin.create_new_auth_token,
as: :json
post "/api/v1/accounts/#{account.id}/contacts/filter",
params: {
payload: []
},
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)

View file

@ -112,7 +112,7 @@ RSpec.describe 'Conversations API', type: :request do
describe 'GET /api/v1/accounts/{account.id}/conversations/filter' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/filter", params: { q: 'test' }
post "/api/v1/accounts/#{account.id}/conversations/filter", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
@ -129,16 +129,15 @@ RSpec.describe 'Conversations API', type: :request do
end
it 'returns all conversations with empty query' do
get "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: { q: 'test1' },
as: :json
post "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: { payload: [] },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data.count).to eq(1)
expect(response_data[0][:messages][0][:content]).to include(Message.first.content)
expect(response_data.count).to eq(2)
end
end
end

View file

@ -0,0 +1,56 @@
require 'rails_helper'
describe ::Contacts::FilterService do
subject(:filter_service) { described_class }
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account) }
let!(:user_2) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
let!(:contact) { create(:contact, account: account, additional_attributes: { 'browser_language': 'en' }) }
before do
create(:inbox_member, user: user_1, inbox: inbox)
create(:inbox_member, user: user_2, inbox: inbox)
create(:conversation, account: account, inbox: inbox, assignee: user_1, contact: contact)
create(:conversation, account: account, inbox: inbox)
Current.account = account
end
describe '#perform' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
let(:payload) do
[
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['en'],
query_operator: nil
}.with_indifferent_access
]
end
it 'filter contacts by additional_attributes' do
params[:payload] = payload
result = filter_service.new(params, user_1).perform
expect(result.length).to be 2
end
it 'filter conversations by tags' do
Contact.last.update_labels('support')
params[:payload] = [
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: [1],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
expect(result.length).to be 2
end
end
end
end

View file

@ -0,0 +1,76 @@
require 'rails_helper'
describe ::Conversations::FilterService do
subject(:filter_service) { described_class }
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account) }
let!(:user_2) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
before do
create(:inbox_member, user: user_1, inbox: inbox)
create(:inbox_member, user: user_2, inbox: inbox)
create(:conversation, account: account, inbox: inbox, assignee: user_1)
create(:conversation, account: account, inbox: inbox, assignee: user_1,
status: 'pending', additional_attributes: { 'browser_language': 'en' })
create(:conversation, account: account, inbox: inbox, assignee: user_1,
status: 'pending', additional_attributes: { 'browser_language': 'en' })
create(:conversation, account: account, inbox: inbox, assignee: user_2)
# unassigned conversation
create(:conversation, account: account, inbox: inbox)
Current.account = account
end
describe '#perform' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
let(:payload) do
[
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['en'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: %w[open pending],
query_operator: nil
}.with_indifferent_access
]
end
it 'filter conversations by custom_attributes and status' do
params[:payload] = payload
result = filter_service.new(params, user_1).perform
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
expect(result.length).to be conversations.count
end
it 'filter conversations by tags' do
Conversation.last.update_labels('support')
params[:payload] = [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [
user_1.id,
user_2.id
],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: [1],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
expect(result.length).to be 2
end
end
end
end