Skip to content

Commit

Permalink
Add ability to export select subscriber ids.
Browse files Browse the repository at this point in the history
- Add `id=[]` query param to `/api/subscribers/export` API.
- Add UI export prompt.
- Add Cypress tests.

Closes #739
  • Loading branch information
knadh committed Mar 19, 2022
1 parent 8db8ecf commit ef643a1
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 19 deletions.
18 changes: 12 additions & 6 deletions cmd/subscribers.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func handleQuerySubscribers(c echo.Context) error {
)

// Limit the subscribers to sepcific lists?
listIDs, err := getQueryListIDs(c.QueryParams())
listIDs, err := getQueryInts("list_id", c.QueryParams())
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
Expand Down Expand Up @@ -199,7 +199,13 @@ func handleExportSubscribers(c echo.Context) error {
)

// Limit the subscribers to sepcific lists?
listIDs, err := getQueryListIDs(c.QueryParams())
listIDs, err := getQueryInts("list_id", c.QueryParams())
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}

// Export only specific subscriber IDs?
subIDs, err := getQueryInts("id", c.QueryParams())
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
Expand All @@ -222,7 +228,7 @@ func handleExportSubscribers(c echo.Context) error {
}
defer tx.Rollback()

if _, err := tx.Query(stmt, nil, 0, 1); err != nil {
if _, err := tx.Query(stmt, nil, 0, nil, 1); err != nil {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
}
Expand Down Expand Up @@ -253,7 +259,7 @@ func handleExportSubscribers(c echo.Context) error {
loop:
for {
var out []models.SubscriberExport
if err := tx.Select(&out, listIDs, id, app.constants.DBBatchSize); err != nil {
if err := tx.Select(&out, listIDs, id, subIDs, app.constants.DBBatchSize); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
Expand Down Expand Up @@ -858,9 +864,9 @@ func sanitizeSQLExp(q string) string {
return q
}

func getQueryListIDs(qp url.Values) (pq.Int64Array, error) {
func getQueryInts(param string, qp url.Values) (pq.Int64Array, error) {
out := pq.Int64Array{}
if vals, ok := qp["list_id"]; ok {
if vals, ok := qp[param]; ok {
for _, v := range vals {
if v == "" {
continue
Expand Down
61 changes: 51 additions & 10 deletions frontend/cypress/integration/subscribers.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ describe('Subscribers', () => {
});
});

it('Exports subscribers', () => {
const cases = [
{
listIDs: [], ids: [], query: '', length: 3,
},
{
listIDs: [], ids: [], query: "name ILIKE '%anon%'", length: 2,
},
{
listIDs: [], ids: [], query: "name like 'nope'", length: 1,
},
];

// listIDs[] and ids[] are unused for now as Cypress doesn't support encoding of arrays in `qs`.
cases.forEach((c) => {
cy.request({ url: `${apiUrl}/api/subscribers/export`, qs: { query: c.query, list_id: c.listIDs, id: c.ids } }).then((resp) => {
cy.expect(resp.body.trim().split('\n')).to.have.lengthOf(c.length);
});
});
});


it('Advanced searches subscribers', () => {
cy.get('[data-cy=btn-advanced-search]').click();
Expand Down Expand Up @@ -253,24 +274,36 @@ describe('Domain blocklist', () => {

// Add non-banned domain.
cy.request({
method: 'POST', url: `${apiUrl}/api/subscribers`, failOnStatusCode: true,
body: { email: '[email protected]', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'POST',
url: `${apiUrl}/api/subscribers`,
failOnStatusCode: true,
body: {
email: '[email protected]', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(200);
});

// Add banned domain.
cy.request({
method: 'POST', url: `${apiUrl}/api/subscribers`, failOnStatusCode: false,
body: { email: '[email protected]', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'POST',
url: `${apiUrl}/api/subscribers`,
failOnStatusCode: false,
body: {
email: '[email protected]', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(400);
});

// Modify an existinb subscriber to a banned domain.
cy.request({
method: 'PUT', url: `${apiUrl}/api/subscribers/1`, failOnStatusCode: false,
body: { email: '[email protected]', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'PUT',
url: `${apiUrl}/api/subscribers/1`,
failOnStatusCode: false,
body: {
email: '[email protected]', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(400);
});
Expand Down Expand Up @@ -305,16 +338,24 @@ describe('Domain blocklist', () => {

// Add banned domain.
cy.request({
method: 'POST', url: `${apiUrl}/api/subscribers`, failOnStatusCode: true,
body: { email: '[email protected]', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'POST',
url: `${apiUrl}/api/subscribers`,
failOnStatusCode: true,
body: {
email: '[email protected]', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(200);
});

// Modify an existinb subscriber to a banned domain.
cy.request({
method: 'PUT', url: `${apiUrl}/api/subscribers/1`, failOnStatusCode: true,
body: { email: '[email protected]', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'PUT',
url: `${apiUrl}/api/subscribers/1`,
failOnStatusCode: true,
body: {
email: '[email protected]', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(200);
});
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/views/Subscribers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@

<template #top-left>
<div class="actions">
<a class="a" href='' @click.prevent="exportSubscribers">
<a class="a" href='' @click.prevent="exportSubscribers"
data-cy="btn-export-subscribers">
<b-icon icon="cloud-download-outline" size="is-small" />
{{ $t('subscribers.export') }}
</a>
Expand Down Expand Up @@ -398,13 +399,22 @@ export default Vue.extend({
},
exportSubscribers() {
this.$utils.confirm(this.$t('subscribers.confirmExport', { num: this.subscribers.total }), () => {
const num = !this.bulk.all && this.bulk.checked.length > 0
? this.bulk.checked.length : this.subscribers.total;
this.$utils.confirm(this.$t('subscribers.confirmExport', { num }), () => {
const q = new URLSearchParams();
q.append('query', this.queryParams.queryExp);
if (this.queryParams.listID) {
q.append('list_id', this.queryParams.listID);
}
// Export selected subscribers.
if (!this.bulk.all && this.bulk.checked.length > 0) {
this.bulk.checked.map((s) => q.append('id', s.id));
}
document.location.href = `${uris.exportSubscribers}?${q.toString()}`;
});
},
Expand Down
3 changes: 2 additions & 1 deletion queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,9 @@ SELECT subscribers.id,
AND sl.subscriber_id = subscribers.id
)
WHERE sl.list_id = ALL($1::INT[]) AND id > $2
AND (CASE WHEN CARDINALITY($3::INT[]) > 0 THEN id=ANY($3) ELSE true END)
%s
ORDER BY subscribers.id ASC LIMIT (CASE WHEN $3 = 0 THEN NULL ELSE $3 END);
ORDER BY subscribers.id ASC LIMIT (CASE WHEN $4 = 0 THEN NULL ELSE $4 END);

-- name: query-subscribers-template
-- raw: true
Expand Down

0 comments on commit ef643a1

Please sign in to comment.