feat: Team APIs (#1654)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose 2021-01-17 23:56:56 +05:30 committed by GitHub
parent dd90e24d02
commit a0c33254e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 523 additions and 2 deletions

View file

@ -0,0 +1,24 @@
class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseController
before_action :fetch_team
before_action :check_authorization
def index
@team_members = @team.team_members.map(&:user)
end
def create
record = @team.team_members.find_or_create_by(user_id: params[:user_id])
@team_member = record.user
end
def destroy
@team.team_members.find_by(user_id: params[:user_id])&.destroy
head :ok
end
private
def fetch_team
@team = Current.account.teams.find(params[:team_id])
end
end

View file

@ -0,0 +1,34 @@
class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
before_action :fetch_team, only: [:show, :update, :destroy]
before_action :check_authorization
def index
@teams = Current.account.teams
end
def show; end
def create
@team = Current.account.teams.new(team_params)
@team.save!
end
def update
@team.update!(team_params)
end
def destroy
@team.destroy
head :ok
end
private
def fetch_team
@team = Current.account.teams.find(params[:id])
end
def team_params
params.require(:team).permit(:name, :description, :allow_auto_assign)
end
end

View file

@ -55,6 +55,7 @@ class Account < ApplicationRecord
has_many :kbase_portals, dependent: :destroy, class_name: '::Kbase::Portal'
has_many :kbase_categories, dependent: :destroy, class_name: '::Kbase::Category'
has_many :kbase_articles, dependent: :destroy, class_name: '::Kbase::Article'
has_many :teams, dependent: :destroy
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h

View file

@ -19,16 +19,19 @@
# contact_inbox_id :bigint
# display_id :integer not null
# inbox_id :integer not null
# team_id :bigint
#
# Indexes
#
# index_conversations_on_account_id (account_id)
# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE
# index_conversations_on_contact_inbox_id (contact_inbox_id)
# index_conversations_on_team_id (team_id)
#
# Foreign Keys
#
# fk_rails_... (contact_inbox_id => contact_inboxes.id)
# fk_rails_... (team_id => teams.id)
#
class Conversation < ApplicationRecord
@ -49,6 +52,7 @@ class Conversation < ApplicationRecord
belongs_to :assignee, class_name: 'User', optional: true
belongs_to :contact
belongs_to :contact_inbox
belongs_to :team, optional: true
has_many :messages, dependent: :destroy, autosave: true

25
app/models/team.rb Normal file
View file

@ -0,0 +1,25 @@
# == Schema Information
#
# Table name: teams
#
# id :bigint not null, primary key
# allow_auto_assign :boolean default(TRUE)
# description :text
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_teams_on_account_id (account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class Team < ApplicationRecord
belongs_to :account
has_many :team_members, dependent: :destroy
has_many :conversations, dependent: :nullify
end

24
app/models/team_member.rb Normal file
View file

@ -0,0 +1,24 @@
# == Schema Information
#
# Table name: team_members
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# team_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_team_members_on_team_id (team_id)
# index_team_members_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (team_id => teams.id)
# fk_rails_... (user_id => users.id)
#
class TeamMember < ApplicationRecord
belongs_to :user
belongs_to :team
end

View file

@ -78,6 +78,8 @@ class User < ApplicationRecord
has_many :notifications, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :notification_subscriptions, dependent: :destroy
has_many :team_members, dependent: :destroy
has_many :teams, through: :team_members
before_validation :set_password_and_uid, on: :create

View file

@ -0,0 +1,13 @@
class TeamMemberPolicy < ApplicationPolicy
def index?
true
end
def create?
@account_user.administrator?
end
def destroy?
@account_user.administrator?
end
end

View file

@ -0,0 +1,21 @@
class TeamPolicy < ApplicationPolicy
def index?
true
end
def update?
@account_user.administrator?
end
def show?
true
end
def create?
@account_user.administrator?
end
def destroy?
@account_user.administrator?
end
end

View file

@ -1,5 +1,5 @@
json.payload do
json.array! @agents do |agent|
json.partial! 'api/v1/models/user.json.jbuilder', resource: agent
json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent
end
end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/agent.json.jbuilder', resource: @team_member

View file

@ -0,0 +1,3 @@
json.array! @team_members do |team_member|
json.partial! 'api/v1/models/agent.json.jbuilder', resource: team_member
end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/team.json.jbuilder', resource: @team

View file

@ -0,0 +1,3 @@
json.array! @teams do |team|
json.partial! 'api/v1/models/team.json.jbuilder', resource: team
end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/team.json.jbuilder', resource: @team

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/team.json.jbuilder', resource: @team

View file

@ -0,0 +1,5 @@
json.id resource.id
json.name resource.name
json.description resource.description
json.allow_auto_assign resource.allow_auto_assign
json.account_id resource.account.id

View file

@ -97,6 +97,14 @@ Rails.application.routes.draw do
end
resource :notification_settings, only: [:show, :update]
resources :teams do
resources :team_members, only: [:index, :create] do
collection do
delete :destroy
end
end
end
resources :webhooks, except: [:show]
namespace :integrations do
resources :apps, only: [:index, :show]

View file

@ -0,0 +1,20 @@
class CreateTeams < ActiveRecord::Migration[6.0]
def change
create_table :teams do |t|
t.string :name, null: false
t.text :description
t.boolean :allow_auto_assign, default: true
t.references :account, null: false, foreign_key: true
t.timestamps
end
create_table :team_members do |t|
t.references :team, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps
end
add_reference :conversations, :team, foreign_key: true
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_01_13_045116) do
ActiveRecord::Schema.define(version: 2021_01_14_202310) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@ -234,9 +234,11 @@ ActiveRecord::Schema.define(version: 2021_01_13_045116) do
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
t.string "identifier"
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
t.bigint "team_id"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id"], name: "index_conversations_on_account_id"
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
t.index ["team_id"], name: "index_conversations_on_team_id"
end
create_table "email_templates", force: :cascade do |t|
@ -491,6 +493,25 @@ ActiveRecord::Schema.define(version: 2021_01_13_045116) do
t.index ["name"], name: "index_tags_on_name", unique: true
end
create_table "team_members", force: :cascade do |t|
t.bigint "team_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["team_id"], name: "index_team_members_on_team_id"
t.index ["user_id"], name: "index_team_members_on_user_id"
end
create_table "teams", force: :cascade do |t|
t.string "name", null: false
t.text "description"
t.boolean "allow_auto_assign", default: true
t.bigint "account_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_teams_on_account_id"
end
create_table "telegram_bots", id: :serial, force: :cascade do |t|
t.string "name"
t.string "auth_key"
@ -561,6 +582,10 @@ ActiveRecord::Schema.define(version: 2021_01_13_045116) do
add_foreign_key "contact_inboxes", "contacts"
add_foreign_key "contact_inboxes", "inboxes"
add_foreign_key "conversations", "contact_inboxes"
add_foreign_key "conversations", "teams"
add_foreign_key "team_members", "teams"
add_foreign_key "team_members", "users"
add_foreign_key "teams", "accounts"
create_trigger("accounts_after_insert_row_tr", :generated => true, :compatibility => 1).
on("accounts").
after(:insert).

View file

@ -0,0 +1,106 @@
require 'rails_helper'
RSpec.describe 'Team Members API', type: :request do
let(:account) { create(:account) }
let!(:team) { create(:team, account: account) }
describe 'GET /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns all the teams' do
create(:team_member, team: team, user: agent)
get "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body).first['id']).to eq(agent.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unathorized for agent' do
params = { user_id: agent.id }
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'add a new team member when its administrator' do
params = { user_id: agent.id }
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['id']).to eq(agent.id)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'return unauthorized for agent' do
params = { user_id: agent.id }
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'destroys the team member when its administrator' do
params = { user_id: agent.id }
create(:team_member, team: team, user: agent)
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(team.team_members.count).to eq(0)
end
end
end
end

View file

@ -0,0 +1,160 @@
require 'rails_helper'
RSpec.describe 'Teams API', type: :request do
let(:account) { create(:account) }
let!(:team) { create(:team, account: account) }
describe 'GET /api/v1/accounts/{account.id}/teams' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/teams"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns all the teams' do
get "/api/v1/accounts/#{account.id}/teams",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body).first['id']).to eq(account.teams.first.id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/teams/{team_id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/teams/#{team.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns all the teams' do
get "/api/v1/accounts/#{account.id}/teams/#{team.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['id']).to eq(team.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/teams' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/teams"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unathorized for agent' do
params = { name: 'Test Team' }
post "/api/v1/accounts/#{account.id}/teams",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new team when its administrator' do
params = { name: 'Test Team' }
post "/api/v1/accounts/#{account.id}/teams",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Team.count).to eq(2)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/teams/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/teams/#{team.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unauthorized for agent' do
params = { name: 'New Team' }
put "/api/v1/accounts/#{account.id}/teams/#{team.id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates an existing team when its an administrator' do
params = { name: 'New Team' }
put "/api/v1/accounts/#{account.id}/teams/#{team.id}",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(team.reload.name).to eq('New Team')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/teams/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'return unauthorized for agent' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'destroys the team when its administrator' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Team.count).to eq(0)
end
end
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
FactoryBot.define do
factory :team_member do
user
team
end
end

8
spec/factories/teams.rb Normal file
View file

@ -0,0 +1,8 @@
FactoryBot.define do
factory :team do
name { 'MyString' }
description { 'MyText' }
allow_auto_assign { true }
account
end
end

View file

@ -20,4 +20,5 @@ RSpec.describe Account do
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:kbase_portals).dependent(:destroy) }
it { is_expected.to have_many(:kbase_categories).dependent(:destroy) }
it { is_expected.to have_many(:teams).dependent(:destroy) }
end

View file

@ -3,6 +3,10 @@
require 'rails_helper'
RSpec.describe Conversation, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:account) }
end
describe '.before_create' do
let(:conversation) { build(:conversation, display_id: nil) }

View file

@ -0,0 +1,8 @@
require 'rails_helper'
RSpec.describe TeamMember, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:team) }
it { is_expected.to belong_to(:user) }
end
end

9
spec/models/team_spec.rb Normal file
View file

@ -0,0 +1,9 @@
require 'rails_helper'
RSpec.describe Team, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to have_many(:conversations) }
it { is_expected.to have_many(:team_members) }
end
end

View file

@ -19,6 +19,7 @@ RSpec.describe User do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:messages) }
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:teams) }
end
describe 'concerns' do