From 4fcc521f57abdd599c34bf96e7e45e545d11102b Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 24 Aug 2021 16:52:10 -0500 Subject: [PATCH] Add ticket topics (WIP) --- core/models/assets.go | 22 +++++++- core/models/topics.go | 105 +++++++++++++++++++++++++++++++++++++ core/models/topics_test.go | 24 +++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 core/models/topics.go create mode 100644 core/models/topics_test.go diff --git a/core/models/assets.go b/core/models/assets.go index 0e0d0f302..45a989a2e 100644 --- a/core/models/assets.go +++ b/core/models/assets.go @@ -60,6 +60,8 @@ type OrgAssets struct { ticketers []assets.Ticketer ticketersByID map[TicketerID]*Ticketer ticketersByUUID map[assets.TicketerUUID]*Ticketer + topics []assets.Topic + topicsByUUID map[assets.TopicUUID]*Topic resthooks []assets.Resthook templates []assets.Template @@ -320,6 +322,20 @@ 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.topicsByUUID = make(map[assets.TopicUUID]*Topic) + for _, t := range oa.topics { + oa.topicsByUUID[t.UUID()] = t.(*Topic) + } + } else { + oa.topics = prev.topics + oa.topicsByUUID = prev.topicsByUUID + } + if prev == nil || refresh&RefreshUsers > 0 { oa.users, err = loadUsers(ctx, db, orgID) if err != nil { @@ -631,7 +647,11 @@ 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) TopicByUUID(uuid assets.TopicUUID) *Topic { + return a.topicsByUUID[uuid] } func (a *OrgAssets) Users() ([]assets.User, error) { diff --git a/core/models/topics.go b/core/models/topics.go new file mode 100644 index 000000000..bcc1e77e0 --- /dev/null +++ b/core/models/topics.go @@ -0,0 +1,105 @@ +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 + +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)) +} diff --git a/core/models/topics_test.go b/core/models/topics_test.go new file mode 100644 index 000000000..cd1e4b65e --- /dev/null +++ b/core/models/topics_test.go @@ -0,0 +1,24 @@ +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, 1, len(topics)) + assert.Equal(t, "General", topics[0].Name()) +}