Skip to content

Commit

Permalink
Merge pull request rapidpro#486 from nyaruka/topics
Browse files Browse the repository at this point in the history
Ticket Topics
  • Loading branch information
rowanseymour authored Aug 25, 2021
2 parents 90b5ccc + 878ce9c commit 60f299a
Show file tree
Hide file tree
Showing 17 changed files with 230 additions and 25 deletions.
10 changes: 10 additions & 0 deletions core/handlers/ticket_opened.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ func handleTicketOpened(ctx context.Context, tx *sqlx.Tx, rp *redis.Pool, oa *mo
return errors.Errorf("unable to find ticketer with UUID: %s", event.Ticket.Ticketer.UUID)
}

var topicID models.TopicID
if event.Ticket.Topic != nil {
topic := oa.TopicByUUID(event.Ticket.Topic.UUID)
if topic == nil {
return errors.Errorf("unable to find topic with UUID: %s", event.Ticket.Topic.UUID)
}
topicID = topic.ID()
}

var assigneeID models.UserID
if event.Ticket.Assignee != nil {
assignee := oa.UserByEmail(event.Ticket.Assignee.Email)
Expand All @@ -43,6 +52,7 @@ func handleTicketOpened(ctx context.Context, tx *sqlx.Tx, rp *redis.Pool, oa *mo
scene.ContactID(),
ticketer.ID(),
event.Ticket.ExternalID,
topicID,
event.Ticket.Subject,
event.Ticket.Body,
assigneeID,
Expand Down
6 changes: 3 additions & 3 deletions core/handlers/ticket_opened_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestTicketOpened(t *testing.T) {
}))

// an existing ticket
cathyTicket := models.NewTicket(flows.TicketUUID(uuids.New()), testdata.Org1.ID, testdata.Cathy.ID, testdata.Mailgun.ID, "748363", "Old Question", "Who?", models.NilUserID, nil)
cathyTicket := models.NewTicket(flows.TicketUUID(uuids.New()), testdata.Org1.ID, testdata.Cathy.ID, testdata.Mailgun.ID, "748363", testdata.DefaultTopic.ID, "Old Question", "Who?", models.NilUserID, nil)
err := models.InsertTickets(ctx, db, []*models.Ticket{cathyTicket})
require.NoError(t, err)

Expand All @@ -56,8 +56,8 @@ func TestTicketOpened(t *testing.T) {
actions.NewOpenTicket(
handlers.NewActionUUID(),
assets.NewTicketerReference(testdata.Mailgun.UUID, "Mailgun (IT Support)"),
nil,
"Need help",
assets.NewTopicReference(testdata.SupportTopic.UUID, "Support"),
"",
"Where are my cookies?",
assets.NewUserReference(testdata.Admin.Email, "Admin"),
"Email Ticket",
Expand Down
31 changes: 30 additions & 1 deletion core/models/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type OrgAssets struct {
ticketersByID map[TicketerID]*Ticketer
ticketersByUUID map[assets.TicketerUUID]*Ticketer

topics []assets.Topic
topicsByID map[TopicID]*Topic
topicsByUUID map[assets.TopicUUID]*Topic

resthooks []assets.Resthook
templates []assets.Template
triggers []*Trigger
Expand Down Expand Up @@ -320,6 +324,23 @@ func NewOrgAssets(ctx context.Context, db *sqlx.DB, orgID OrgID, prev *OrgAssets
oa.ticketersByUUID = prev.ticketersByUUID
}

if prev == nil || refresh&RefreshTopics > 0 {
oa.topics, err = loadTopics(ctx, db, orgID)
if err != nil {
return nil, errors.Wrapf(err, "error loading topic assets for org %d", orgID)
}
oa.topicsByID = make(map[TopicID]*Topic, len(oa.topics))
oa.topicsByUUID = make(map[assets.TopicUUID]*Topic, len(oa.topics))
for _, t := range oa.topics {
oa.topicsByID[t.(*Topic).ID()] = t.(*Topic)
oa.topicsByUUID[t.UUID()] = t.(*Topic)
}
} else {
oa.topics = prev.topics
oa.topicsByID = prev.topicsByID
oa.topicsByUUID = prev.topicsByUUID
}

if prev == nil || refresh&RefreshUsers > 0 {
oa.users, err = loadUsers(ctx, db, orgID)
if err != nil {
Expand Down Expand Up @@ -631,7 +652,15 @@ func (a *OrgAssets) TicketerByUUID(uuid assets.TicketerUUID) *Ticketer {
}

func (a *OrgAssets) Topics() ([]assets.Topic, error) {
return nil, nil // TODO
return a.topics, nil
}

func (a *OrgAssets) TopicByID(id TopicID) *Topic {
return a.topicsByID[id]
}

func (a *OrgAssets) TopicByUUID(uuid assets.TopicUUID) *Topic {
return a.topicsByUUID[uuid]
}

func (a *OrgAssets) Users() ([]assets.User, error) {
Expand Down
3 changes: 2 additions & 1 deletion core/models/contacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ func LoadContacts(ctx context.Context, db Queryer, org *OrgAssets, ids []Contact
for _, t := range e.Tickets {
ticketer := org.TicketerByID(t.TicketerID)
if ticketer != nil {
tickets = append(tickets, NewTicket(t.UUID, org.OrgID(), contact.ID(), ticketer.ID(), t.ExternalID, t.Subject, t.Body, t.AssigneeID, nil))
tickets = append(tickets, NewTicket(t.UUID, org.OrgID(), contact.ID(), ticketer.ID(), t.ExternalID, t.TopicID, t.Subject, t.Body, t.AssigneeID, nil))
}
}
contact.tickets = tickets
Expand Down Expand Up @@ -466,6 +466,7 @@ type contactEnvelope struct {
UUID flows.TicketUUID `json:"uuid"`
TicketerID TicketerID `json:"ticketer_id"`
ExternalID string `json:"external_id"`
TopicID TopicID `json:"topic_id"`
Subject string `json:"subject"`
Body string `json:"body"`
AssigneeID UserID `json:"assignee_id"`
Expand Down
21 changes: 16 additions & 5 deletions core/models/tickets.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type Ticket struct {
TicketerID TicketerID `db:"ticketer_id"`
ExternalID null.String `db:"external_id"`
Status TicketStatus `db:"status"`
TopicID TopicID `db:"topic_id"`
Subject string `db:"subject"`
Body string `db:"body"`
AssigneeID UserID `db:"assignee_id"`
Expand All @@ -86,14 +87,15 @@ type Ticket struct {
}

// NewTicket creates a new open ticket
func NewTicket(uuid flows.TicketUUID, orgID OrgID, contactID ContactID, ticketerID TicketerID, externalID, subject, body string, assigneeID UserID, config map[string]interface{}) *Ticket {
func NewTicket(uuid flows.TicketUUID, orgID OrgID, contactID ContactID, ticketerID TicketerID, externalID string, topicID TopicID, subject, body string, assigneeID UserID, config map[string]interface{}) *Ticket {
t := &Ticket{}
t.t.UUID = uuid
t.t.OrgID = orgID
t.t.ContactID = contactID
t.t.TicketerID = ticketerID
t.t.ExternalID = null.String(externalID)
t.t.Status = TicketStatusOpen
t.t.TopicID = topicID
t.t.Subject = subject
t.t.Body = body
t.t.AssigneeID = assigneeID
Expand All @@ -108,6 +110,7 @@ func (t *Ticket) ContactID() ContactID { return t.t.ContactID }
func (t *Ticket) TicketerID() TicketerID { return t.t.TicketerID }
func (t *Ticket) ExternalID() null.String { return t.t.ExternalID }
func (t *Ticket) Status() TicketStatus { return t.t.Status }
func (t *Ticket) TopicID() TopicID { return t.t.TopicID }
func (t *Ticket) Subject() string { return t.t.Subject }
func (t *Ticket) Body() string { return t.t.Body }
func (t *Ticket) AssigneeID() UserID { return t.t.AssigneeID }
Expand All @@ -122,22 +125,30 @@ func (t *Ticket) FlowTicket(oa *OrgAssets) (*flows.Ticket, error) {
return nil, errors.New("unable to load ticketer with id %d")
}

var flowUser *flows.User
var topic *flows.Topic
if t.TopicID() != NilTopicID {
dbTopic := oa.TopicByID(t.TopicID())
if dbTopic != nil {
topic = oa.SessionAssets().Topics().Get(dbTopic.UUID())
}
}

var assignee *flows.User
if t.AssigneeID() != NilUserID {
user := oa.UserByID(t.AssigneeID())
if user != nil {
flowUser = oa.SessionAssets().Users().Get(user.Email())
assignee = oa.SessionAssets().Users().Get(user.Email())
}
}

return flows.NewTicket(
t.UUID(),
oa.SessionAssets().Ticketers().Get(modelTicketer.UUID()),
nil, // TODO
topic,
t.Subject(),
t.Body(),
string(t.ExternalID()),
flowUser,
assignee,
), nil
}

Expand Down
4 changes: 4 additions & 0 deletions core/models/tickets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func TestTickets(t *testing.T) {
testdata.Cathy.ID,
testdata.Mailgun.ID,
"EX12345",
testdata.DefaultTopic.ID,
"New Ticket",
"Where are my cookies?",
testdata.Admin.ID,
Expand All @@ -81,6 +82,7 @@ func TestTickets(t *testing.T) {
testdata.Bob.ID,
testdata.Zendesk.ID,
"EX7869",
testdata.SalesTopic.ID,
"New Zen Ticket",
"Where are my trousers?",
models.NilUserID,
Expand All @@ -92,6 +94,7 @@ func TestTickets(t *testing.T) {
testdata.Alexandria.ID,
testdata.Zendesk.ID,
"EX6677",
models.NilTopicID,
"Other Org Ticket",
"Where are my pants?",
testdata.Org2Admin.ID,
Expand All @@ -103,6 +106,7 @@ func TestTickets(t *testing.T) {
assert.Equal(t, testdata.Cathy.ID, ticket1.ContactID())
assert.Equal(t, testdata.Mailgun.ID, ticket1.TicketerID())
assert.Equal(t, null.String("EX12345"), ticket1.ExternalID())
assert.Equal(t, testdata.DefaultTopic.ID, ticket1.TopicID())
assert.Equal(t, "New Ticket", ticket1.Subject())
assert.Equal(t, "Cathy", ticket1.Config("contact-display"))
assert.Equal(t, testdata.Admin.ID, ticket1.AssigneeID())
Expand Down
107 changes: 107 additions & 0 deletions core/models/topics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package models

import (
"context"
"database/sql"
"database/sql/driver"
"time"

"github.com/jmoiron/sqlx"
"github.com/nyaruka/gocommon/dates"
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/mailroom/utils/dbutil"
"github.com/nyaruka/null"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

type TopicID null.Int

const NilTopicID = TopicID(0)

type Topic struct {
t struct {
ID TopicID `json:"id"`
UUID assets.TopicUUID `json:"uuid"`
OrgID OrgID `json:"org_id"`
Name string `json:"name"`
IsDefault bool `json:"is_default"`
}
}

// ID returns the ID
func (t *Topic) ID() TopicID { return t.t.ID }

// UUID returns the UUID
func (t *Topic) UUID() assets.TopicUUID { return t.t.UUID }

// OrgID returns the org ID
func (t *Topic) OrgID() OrgID { return t.t.OrgID }

// Name returns the name
func (t *Topic) Name() string { return t.t.Name }

// Type returns the type
func (t *Topic) IsDefault() bool { return t.t.IsDefault }

const selectOrgTopicsSQL = `
SELECT ROW_TO_JSON(r) FROM (SELECT
t.id as id,
t.uuid as uuid,
t.org_id as org_id,
t.name as name,
t.is_default as is_default
FROM
tickets_topic t
WHERE
t.org_id = $1 AND
t.is_active = TRUE
ORDER BY
t.is_default DESC, t.created_on ASC
) r;
`

// loadTopics loads all the topics for the passed in org
func loadTopics(ctx context.Context, db sqlx.Queryer, orgID OrgID) ([]assets.Topic, error) {
start := dates.Now()

rows, err := db.Queryx(selectOrgTopicsSQL, orgID)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "error querying topics for org: %d", orgID)
}
defer rows.Close()

topics := make([]assets.Topic, 0, 2)
for rows.Next() {
topic := &Topic{}
err := dbutil.ReadJSONRow(rows, &topic.t)
if err != nil {
return nil, errors.Wrapf(err, "error unmarshalling topic")
}
topics = append(topics, topic)
}

logrus.WithField("elapsed", time.Since(start)).WithField("org_id", orgID).WithField("count", len(topics)).Debug("loaded topics")

return topics, nil
}

// MarshalJSON marshals into JSON. 0 values will become null
func (i TopicID) MarshalJSON() ([]byte, error) {
return null.Int(i).MarshalJSON()
}

// UnmarshalJSON unmarshals from JSON. null values become 0
func (i *TopicID) UnmarshalJSON(b []byte) error {
return null.UnmarshalInt(b, (*null.Int)(i))
}

// Value returns the db value, null is returned for 0
func (i TopicID) Value() (driver.Value, error) {
return null.Int(i).Value()
}

// Scan scans from the db value. null values become 0
func (i *TopicID) Scan(value interface{}) error {
return null.ScanInt(value, (*null.Int)(i))
}
33 changes: 33 additions & 0 deletions core/models/topics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package models_test

import (
"testing"

"github.com/nyaruka/mailroom/core/models"
"github.com/nyaruka/mailroom/testsuite"
"github.com/nyaruka/mailroom/testsuite/testdata"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTopics(t *testing.T) {
ctx, _, db, _ := testsuite.Get()

oa, err := models.GetOrgAssetsWithRefresh(ctx, db, testdata.Org1.ID, models.RefreshTopics)
require.NoError(t, err)

topics, err := oa.Topics()
require.NoError(t, err)

assert.Equal(t, 3, len(topics))
assert.Equal(t, testdata.DefaultTopic.UUID, topics[0].UUID())
assert.Equal(t, "General", topics[0].Name())
assert.Equal(t, testdata.SalesTopic.UUID, topics[1].UUID())
assert.Equal(t, "Sales", topics[1].Name())
assert.Equal(t, testdata.SupportTopic.UUID, topics[2].UUID())
assert.Equal(t, "Support", topics[2].Name())

assert.Equal(t, topics[1], oa.TopicByID(testdata.SalesTopic.ID))
assert.Equal(t, topics[2], oa.TopicByUUID(testdata.SupportTopic.UUID))
}
Binary file modified mailroom_test.dump
Binary file not shown.
6 changes: 3 additions & 3 deletions services/tickets/intern/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestOpenAndForward(t *testing.T) {
assert.Equal(t, "", ticket.ExternalID())
assert.Equal(t, 0, len(logger.Logs))

dbTicket := models.NewTicket(ticket.UUID(), testdata.Org1.ID, testdata.Cathy.ID, testdata.Internal.ID, "", "Need help", "Where are my cookies?", models.NilUserID, nil)
dbTicket := models.NewTicket(ticket.UUID(), testdata.Org1.ID, testdata.Cathy.ID, testdata.Internal.ID, "", testdata.DefaultTopic.ID, "Need help", "Where are my cookies?", models.NilUserID, nil)

logger = &flows.HTTPLogger{}
err = svc.Forward(
Expand All @@ -77,8 +77,8 @@ func TestCloseAndReopen(t *testing.T) {
require.NoError(t, err)

logger := &flows.HTTPLogger{}
ticket1 := models.NewTicket("88bfa1dc-be33-45c2-b469-294ecb0eba90", testdata.Org1.ID, testdata.Cathy.ID, testdata.Internal.ID, "12", "New ticket", "Where my cookies?", models.NilUserID, nil)
ticket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Bob.ID, testdata.Internal.ID, "14", "Second ticket", "Where my shoes?", models.NilUserID, nil)
ticket1 := models.NewTicket("88bfa1dc-be33-45c2-b469-294ecb0eba90", testdata.Org1.ID, testdata.Cathy.ID, testdata.Internal.ID, "12", testdata.DefaultTopic.ID, "New ticket", "Where my cookies?", models.NilUserID, nil)
ticket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Bob.ID, testdata.Internal.ID, "14", testdata.DefaultTopic.ID, "Second ticket", "Where my shoes?", models.NilUserID, nil)

err = svc.Close([]*models.Ticket{ticket1, ticket2}, logger.Log)

Expand Down
6 changes: 3 additions & 3 deletions services/tickets/mailgun/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestOpenAndForward(t *testing.T) {
assert.Equal(t, 1, len(logger.Logs))
test.AssertSnapshot(t, "open_ticket", logger.Logs[0].Request)

dbTicket := models.NewTicket(ticket.UUID(), testdata.Org1.ID, testdata.Cathy.ID, testdata.Mailgun.ID, "", "Need help", "Where are my cookies?", models.NilUserID, map[string]interface{}{
dbTicket := models.NewTicket(ticket.UUID(), testdata.Org1.ID, testdata.Cathy.ID, testdata.Mailgun.ID, "", testdata.DefaultTopic.ID, "Need help", "Where are my cookies?", models.NilUserID, map[string]interface{}{
"contact-uuid": string(testdata.Cathy.UUID),
"contact-display": "Cathy",
})
Expand Down Expand Up @@ -154,8 +154,8 @@ func TestCloseAndReopen(t *testing.T) {
require.NoError(t, err)

logger := &flows.HTTPLogger{}
ticket1 := models.NewTicket("88bfa1dc-be33-45c2-b469-294ecb0eba90", testdata.Org1.ID, testdata.Cathy.ID, testdata.Zendesk.ID, "12", "New ticket", "Where my cookies?", models.NilUserID, nil)
ticket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Bob.ID, testdata.Zendesk.ID, "14", "Second ticket", "Where my shoes?", models.NilUserID, nil)
ticket1 := models.NewTicket("88bfa1dc-be33-45c2-b469-294ecb0eba90", testdata.Org1.ID, testdata.Cathy.ID, testdata.Zendesk.ID, "12", testdata.DefaultTopic.ID, "New ticket", "Where my cookies?", models.NilUserID, nil)
ticket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Bob.ID, testdata.Zendesk.ID, "14", testdata.DefaultTopic.ID, "Second ticket", "Where my shoes?", models.NilUserID, nil)

err = svc.Close([]*models.Ticket{ticket1, ticket2}, logger.Log)

Expand Down
Loading

0 comments on commit 60f299a

Please sign in to comment.