Skip to content

Commit

Permalink
Add subscriber status counts to the lists UI.
Browse files Browse the repository at this point in the history
- Change `query-lists` query to aggregate the subscriber count by
  status (confirmed, unsubscribed etc.) and expose them under a new
  `subscriber_statuses: {}` field in the `GET /lists` API.
- Display the statuses and counts in the lists table on the UI.

Closes #616
  • Loading branch information
knadh committed Feb 2, 2022
1 parent 182795e commit da30d46
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 15 deletions.
5 changes: 5 additions & 0 deletions cmd/lists.go
Expand Up @@ -93,6 +93,11 @@ func handleGetLists(c echo.Context) error {
if v.Tags == nil {
out.Results[i].Tags = make(pq.StringArray, 0)
}

// Total counts.
for _, c := range v.SubscriberCounts {
out.Results[i].SubscriberCount += c
}
}

if single {
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/views/Lists.vue
Expand Up @@ -61,7 +61,7 @@
</b-table-column>

<b-table-column v-slot="props" field="type" :label="$t('globals.fields.type')"
header-class="cy-type" sortable>
header-class="cy-type" sortable width="15%">
<div class="tags">
<b-tag :class="props.row.type" :data-cy="`type-${props.row.type}`">
{{ $t(`lists.types.${props.row.type}`) }}
Expand Down Expand Up @@ -94,6 +94,16 @@
</router-link>
</b-table-column>

<b-table-column v-slot="props" field="subscriber_counts"
header-class="cy-subscribers" width="10%">
<div class="fields stats">
<p v-for="(count, status) in props.row.subscriberStatuses" :key="status">
<label>{{ $t(`subscribers.status.${status}`) }}</label>
<span :class="status">{{ $utils.formatNumber(count) }}</span>
</p>
</div>
</b-table-column>

<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
header-class="cy-created_at" sortable>
{{ $utils.niceDate(props.row.createdAt) }}
Expand Down
30 changes: 21 additions & 9 deletions models/models.go
Expand Up @@ -149,6 +149,9 @@ type subLists struct {
// SubscriberAttribs is the map of key:value attributes of a subscriber.
type SubscriberAttribs map[string]interface{}

// StringIntMap is used to define DB Scan()s.
type StringIntMap map[string]int

// Subscribers represents a slice of Subscriber.
type Subscribers []Subscriber

Expand All @@ -167,13 +170,14 @@ type SubscriberExport struct {
type List struct {
Base

UUID string `db:"uuid" json:"uuid"`
Name string `db:"name" json:"name"`
Type string `db:"type" json:"type"`
Optin string `db:"optin" json:"optin"`
Tags pq.StringArray `db:"tags" json:"tags"`
SubscriberCount int `db:"subscriber_count" json:"subscriber_count"`
SubscriberID int `db:"subscriber_id" json:"-"`
UUID string `db:"uuid" json:"uuid"`
Name string `db:"name" json:"name"`
Type string `db:"type" json:"type"`
Optin string `db:"optin" json:"optin"`
Tags pq.StringArray `db:"tags" json:"tags"`
SubscriberCount int `db:"-" json:"subscriber_count"`
SubscriberCounts StringIntMap `db:"subscriber_statuses" json:"subscriber_statuses"`
SubscriberID int `db:"subscriber_id" json:"-"`

// This is only relevant when querying the lists of a subscriber.
SubscriptionStatus string `db:"subscription_status" json:"subscription_status,omitempty"`
Expand Down Expand Up @@ -319,12 +323,20 @@ func (s SubscriberAttribs) Value() (driver.Value, error) {
return json.Marshal(s)
}

// Scan unmarshals JSON into SubscriberAttribs.
// Scan unmarshals JSONB from the DB.
func (s SubscriberAttribs) Scan(src interface{}) error {
if data, ok := src.([]byte); ok {
return json.Unmarshal(data, &s)
}
return fmt.Errorf("Could not not decode type %T -> %T", src, s)
return fmt.Errorf("could not not decode type %T -> %T", src, s)
}

// Scan unmarshals JSONB from the DB.
func (s StringIntMap) Scan(src interface{}) error {
if data, ok := src.([]byte); ok {
return json.Unmarshal(data, &s)
}
return fmt.Errorf("could not not decode type %T -> %T", src, s)
}

// GetIDs returns the list of campaign IDs.
Expand Down
11 changes: 6 additions & 5 deletions queries.sql
Expand Up @@ -351,12 +351,13 @@ WITH ls AS (
OFFSET $3 LIMIT (CASE WHEN $4 = 0 THEN NULL ELSE $4 END)
),
counts AS (
SELECT COUNT(*) as subscriber_count, list_id FROM subscriber_lists
WHERE status != 'unsubscribed'
AND ($1 = 0 OR list_id = $1)
GROUP BY list_id
SELECT list_id, JSON_OBJECT_AGG(status, subscriber_count) AS subscriber_statuses FROM (
SELECT COUNT(*) as subscriber_count, list_id, status FROM subscriber_lists
WHERE ($1 = 0 OR list_id = $1)
GROUP BY list_id, status
) row GROUP BY list_id
)
SELECT ls.*, COALESCE(subscriber_count, 0) AS subscriber_count FROM ls
SELECT ls.*, subscriber_statuses FROM ls
LEFT JOIN counts ON (counts.list_id = ls.id) ORDER BY %s %s;


Expand Down

0 comments on commit da30d46

Please sign in to comment.