mirror of
https://github.com/knadh/listmonk.git
synced 2024-12-29 18:33:43 +08:00
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:
parent
8db8ecfccd
commit
ef643a14a3
4 changed files with 77 additions and 19 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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()}`;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue