Add ability to export select subscriber ids.

- Add `id=[]` query param to `/api/subscribers/export` API.
- Add UI export prompt.
- Add Cypress tests.

Closes #739
This commit is contained in:
Kailash Nadh 2022-03-19 13:44:23 +05:30
parent 8db8ecfccd
commit ef643a14a3
4 changed files with 77 additions and 19 deletions

View file

@ -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"))
}
@ -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"))
}
@ -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)))
}
@ -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)))
@ -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

View file

@ -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();
@ -253,24 +274,36 @@ describe('Domain blocklist', () => {
// Add non-banned domain.
cy.request({
method: 'POST', url: `${apiUrl}/api/subscribers`, failOnStatusCode: true,
body: { email: 'test1@noban.net', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'POST',
url: `${apiUrl}/api/subscribers`,
failOnStatusCode: true,
body: {
email: 'test1@noban.net', 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: 'test1@ban.com', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'POST',
url: `${apiUrl}/api/subscribers`,
failOnStatusCode: false,
body: {
email: 'test1@ban.com', 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: 'test3@ban.org', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'PUT',
url: `${apiUrl}/api/subscribers/1`,
failOnStatusCode: false,
body: {
email: 'test3@ban.org', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(400);
});
@ -305,16 +338,24 @@ describe('Domain blocklist', () => {
// Add banned domain.
cy.request({
method: 'POST', url: `${apiUrl}/api/subscribers`, failOnStatusCode: true,
body: { email: 'test4@BAN.com', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'POST',
url: `${apiUrl}/api/subscribers`,
failOnStatusCode: true,
body: {
email: 'test4@BAN.com', 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: 'test4@BAN.org', 'name': 'test', 'lists': [1], 'status': 'enabled' }
method: 'PUT',
url: `${apiUrl}/api/subscribers/1`,
failOnStatusCode: true,
body: {
email: 'test4@BAN.org', name: 'test', lists: [1], status: 'enabled',
},
}).should((response) => {
expect(response.status).to.equal(200);
});

View file

@ -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>
@ -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()}`;
});
},

View file

@ -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