mirror of
https://github.com/knadh/listmonk.git
synced 2025-12-05 16:00:03 +01:00
Add bulk deletion (by id or query) to lists and campaigns.
- Like subscribers, select one-or more or 'all' items and delete them on the lists and campaigns UIs. - New `DELETE /api/lists` and `DELETE /api/campaigns` endpoints that take one or more `id` params or a single `query` param.
This commit is contained in:
@@ -423,6 +423,54 @@ func (a *App) DeleteCampaign(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// DeleteCampaigns deletes multiple campaigns by IDs or by query.
|
||||
func (a *App) DeleteCampaigns(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
var (
|
||||
hasAllPerm = user.HasPerm(auth.PermCampaignsManageAll)
|
||||
permittedLists []int
|
||||
)
|
||||
|
||||
if !hasAllPerm {
|
||||
// Either the user has campaigns:manage_all permissions and can manage all campaigns,
|
||||
// or the campaigns are filtered by the lists the user has get|manage access to.
|
||||
hasAllPerm, permittedLists = user.GetPermittedLists(auth.PermTypeGet | auth.PermTypeManage)
|
||||
}
|
||||
|
||||
var (
|
||||
ids []int
|
||||
query string
|
||||
)
|
||||
|
||||
// Check for IDs in query params.
|
||||
if len(c.Request().URL.Query()["id"]) > 0 {
|
||||
var err error
|
||||
ids, err = parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
} else {
|
||||
// Check for query param.
|
||||
query = strings.TrimSpace(c.FormValue("query"))
|
||||
}
|
||||
|
||||
// Validate that either IDs or query is provided.
|
||||
if len(ids) == 0 && query == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", "id or query required"))
|
||||
}
|
||||
|
||||
// Delete the campaigns from the DB.
|
||||
if err := a.core.DeleteCampaigns(ids, query, hasAllPerm, permittedLists); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// GetRunningCampaignStats returns stats of a given set of campaign IDs.
|
||||
func (a *App) GetRunningCampaignStats(c echo.Context) error {
|
||||
// Get the running campaign stats from the DB.
|
||||
|
||||
@@ -153,7 +153,8 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
|
||||
g.GET("/api/lists/:id", hasID(a.GetList))
|
||||
g.POST("/api/lists", pm(a.CreateList, "lists:manage_all"))
|
||||
g.PUT("/api/lists/:id", hasID(a.UpdateList))
|
||||
g.DELETE("/api/lists/:id", hasID(a.DeleteLists))
|
||||
g.DELETE("/api/lists", a.DeleteLists)
|
||||
g.DELETE("/api/lists/:id", hasID(a.DeleteList))
|
||||
|
||||
g.GET("/api/campaigns", pm(a.GetCampaigns, "campaigns:get_all", "campaigns:get"))
|
||||
g.GET("/api/campaigns/running/stats", pm(a.GetRunningCampaignStats, "campaigns:get_all", "campaigns:get"))
|
||||
@@ -169,6 +170,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
|
||||
g.PUT("/api/campaigns/:id", pm(hasID(a.UpdateCampaign), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id/status", pm(hasID(a.UpdateCampaignStatus), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id/archive", pm(hasID(a.UpdateCampaignArchive), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.DELETE("/api/campaigns", pm(a.DeleteCampaigns, "campaigns:manage", "campaigns:manage_all"))
|
||||
g.DELETE("/api/campaigns/:id", pm(hasID(a.DeleteCampaign), "campaigns:manage_all", "campaigns:manage"))
|
||||
|
||||
g.GET("/api/media", pm(a.GetAllMedia, "media:get"))
|
||||
|
||||
82
cmd/lists.go
82
cmd/lists.go
@@ -143,33 +143,73 @@ func (a *App) UpdateList(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// DeleteLists handles list deletion, either a single one (ID in the URI), or a list.
|
||||
// It's permission checked by the listPerm middleware.
|
||||
func (a *App) DeleteLists(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
// DeleteList deletes a single list by ID.
|
||||
func (a *App) DeleteList(c echo.Context) error {
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has manage permission for the list.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
var (
|
||||
id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
ids []int
|
||||
)
|
||||
if id < 1 && len(ids) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
ids = append(ids, int(id))
|
||||
}
|
||||
|
||||
// Check if the user has access to the list.
|
||||
if err := user.HasListPerm(auth.PermTypeManage, ids...); err != nil {
|
||||
if err := user.HasListPerm(auth.PermTypeManage, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the lists from the DB.
|
||||
if err := a.core.DeleteLists(ids); err != nil {
|
||||
// Delete the list from the DB.
|
||||
// Pass getAll=true since we've already verified permissions above.
|
||||
if err := a.core.DeleteLists([]int{id}, "", true, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// DeleteLists deletes multiple lists by IDs or by query.
|
||||
func (a *App) DeleteLists(c echo.Context) error {
|
||||
user := auth.GetUser(c)
|
||||
|
||||
var (
|
||||
ids []int
|
||||
query string
|
||||
)
|
||||
|
||||
// Check for IDs in query params.
|
||||
if len(c.Request().URL.Query()["id"]) > 0 {
|
||||
var err error
|
||||
ids, err = parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
} else {
|
||||
// Check for query param.
|
||||
query = strings.TrimSpace(c.FormValue("query"))
|
||||
}
|
||||
|
||||
// Validate that either IDs or query is provided.
|
||||
if len(ids) == 0 && query == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", "id or query required"))
|
||||
}
|
||||
|
||||
// For ID deletion, check if the user has manage permission for the specific lists.
|
||||
if len(ids) > 0 {
|
||||
if err := user.HasListPerm(auth.PermTypeManage, ids...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the lists from the DB.
|
||||
// Pass getAll=true since we've already verified permissions above.
|
||||
if err := a.core.DeleteLists(ids, "", true, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// For query deletion, get the list IDs the user has manage permission for.
|
||||
hasAllPerm, permittedIDs := user.GetPermittedLists(auth.PermTypeManage)
|
||||
|
||||
// Delete the lists from the DB with permission filtering.
|
||||
if err := a.core.DeleteLists(nil, query, hasAllPerm, permittedIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
| PUT | [/api/campaigns/{campaign_id}/status](#put-apicampaignscampaign_idstatus) | Change status of a campaign. |
|
||||
| PUT | [/api/campaigns/{campaign_id}/archive](#put-apicampaignscampaign_idarchive) | Publish campaign to public archive. |
|
||||
| DELETE | [/api/campaigns/{campaign_id}](#delete-apicampaignscampaign_id) | Delete a campaign. |
|
||||
| DELETE | [/api/campaigns](#delete-apicampaigns) | Delete multiple campaigns. |
|
||||
|
||||
____________________________________________________________________________________________________________________________________
|
||||
|
||||
@@ -506,3 +507,36 @@ curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/campaigns/34'
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
#### DELETE /api/campaigns
|
||||
|
||||
Delete multiple campaigns by IDs or by a search query.
|
||||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
| :---- | :--------- | :---------------------------- | :--------------------------------------------------------------------------- |
|
||||
| id | number\[\] | Yes (if `query` not provided) | Onr or more campaign IDs to delete. |
|
||||
| query | string | Yes (if `id` not provided) | Fulltext search query to filter campaigns for deletion (same as GET query). |
|
||||
|
||||
##### Example Request (by IDs)
|
||||
|
||||
```shell
|
||||
curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/campaigns?id=10&id=11&id=12'
|
||||
```
|
||||
|
||||
##### Example Request (by search query)
|
||||
|
||||
```shell
|
||||
curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/campaigns?query=test%20campaign'
|
||||
```
|
||||
|
||||
##### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
| POST | [/api/lists](#post-apilists) | Create a new list. |
|
||||
| PUT | [/api/lists/{list_id}](#put-apilistslist_id) | Update a list. |
|
||||
| DELETE | [/api/lists/{list_id}](#delete-apilistslist_id) | Delete a list. |
|
||||
| DELETE | [/api/lists](#delete-apilists) | Delete multiple lists. |
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
@@ -258,3 +259,38 @@ curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/lists/1
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
#### DELETE /api/lists
|
||||
|
||||
Delete multiple lists by IDs or by a search query.
|
||||
|
||||
> **Note:** Users can only delete lists they have `manage` permission for. Any lists in the query that the user doesn't have permission to manage is ignored.
|
||||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
| :---- | :--------- | :---------------------------- | :----------------------------------------------------------------- |
|
||||
| id | number\[\] | Yes (if `query` not provided) | One or more list IDs to delete. |
|
||||
| query | string | Yes (if `id` not provided) | Search query to filter lists for deletion (same as the GET query). |
|
||||
|
||||
##### Example Request (by IDs)
|
||||
|
||||
```shell
|
||||
curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/lists?id=10&id=11&id=12'
|
||||
```
|
||||
|
||||
##### Example Request (by search query)
|
||||
|
||||
```shell
|
||||
curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/lists?query=test%20list'
|
||||
```
|
||||
|
||||
##### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -153,6 +153,11 @@ export const deleteList = (id) => http.delete(
|
||||
{ loading: models.lists },
|
||||
);
|
||||
|
||||
export const deleteLists = (params) => http.delete(
|
||||
'/api/lists',
|
||||
{ params, loading: models.lists },
|
||||
);
|
||||
|
||||
// Subscribers.
|
||||
export const getSubscribers = async (params) => http.get(
|
||||
'/api/subscribers',
|
||||
@@ -353,6 +358,11 @@ export const deleteCampaign = async (id) => http.delete(
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const deleteCampaigns = (params) => http.delete(
|
||||
'/api/campaigns',
|
||||
{ params, loading: models.campaigns },
|
||||
);
|
||||
|
||||
// Media.
|
||||
export const getMedia = async (params) => http.get(
|
||||
'/api/media',
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<b-table :data="campaigns.results" :loading="loading.campaigns" :row-class="highlightedRow" paginated
|
||||
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
|
||||
:per-page="campaigns.perPage" :total="campaigns.total" hoverable backend-sorting @sort="onSort">
|
||||
<b-table :data="campaigns.results" :loading="loading.campaigns" :row-class="highlightedRow"
|
||||
@check-all="onTableCheck" @check="onTableCheck" :checked-rows.sync="bulk.checked" paginated backend-pagination
|
||||
pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
|
||||
:per-page="campaigns.perPage" :total="campaigns.total" hoverable checkable backend-sorting @sort="onSort">
|
||||
<template #top-left>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
@@ -36,6 +37,20 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" v-if="bulk.checked.length > 0">
|
||||
<a class="a" href="#" @click.prevent="deleteCampaigns" data-cy="btn-delete-campaigns">
|
||||
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
||||
</a>
|
||||
<span class="a">
|
||||
{{ $tc('globals.messages.numSelected', numSelectedCampaigns, { num: numSelectedCampaigns }) }}
|
||||
<span v-if="!bulk.all && campaigns.total > campaigns.perPage">
|
||||
—
|
||||
<a href="#" @click.prevent="onSelectAll">
|
||||
{{ $tc('globals.messages.selectAll', campaigns.total, { num: campaigns.total }) }}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="status" field="status" :label="$t('globals.fields.status')" width="10%"
|
||||
@@ -285,6 +300,12 @@ export default Vue.extend({
|
||||
},
|
||||
pollID: null,
|
||||
campaignStatsData: {},
|
||||
|
||||
// Table bulk row selection states.
|
||||
bulk: {
|
||||
checked: [],
|
||||
all: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -456,10 +477,57 @@ export default Vue.extend({
|
||||
this.$utils.toast(this.$t('globals.messages.deleted', { name: c.name }));
|
||||
});
|
||||
},
|
||||
|
||||
// Mark all campaigns in the query as selected.
|
||||
onSelectAll() {
|
||||
this.bulk.all = true;
|
||||
},
|
||||
|
||||
onTableCheck() {
|
||||
// Disable bulk.all selection if there are no rows checked in the table.
|
||||
if (this.bulk.checked.length !== this.campaigns.total) {
|
||||
this.bulk.all = false;
|
||||
}
|
||||
},
|
||||
|
||||
deleteCampaigns() {
|
||||
const name = this.$tc('globals.terms.campaign', this.numSelectedCampaigns);
|
||||
|
||||
const fn = () => {
|
||||
const params = {};
|
||||
if (!this.bulk.all && this.bulk.checked.length > 0) {
|
||||
// If 'all' is not selected, delete campaigns by IDs.
|
||||
params.id = this.bulk.checked.map((c) => c.id);
|
||||
} else {
|
||||
// 'All' is selected, delete by query.
|
||||
params.query = this.queryParams.query.replace(/[^\p{L}\p{N}\s]/gu, ' ');
|
||||
}
|
||||
|
||||
this.$api.deleteCampaigns(params)
|
||||
.then(() => {
|
||||
this.getCampaigns();
|
||||
this.$utils.toast(this.$tc(
|
||||
'globals.messages.deletedCount',
|
||||
this.numSelectedCampaigns,
|
||||
{ num: this.numSelectedCampaigns, name },
|
||||
));
|
||||
});
|
||||
};
|
||||
|
||||
this.$utils.confirm(this.$tc(
|
||||
'globals.messages.confirmDelete',
|
||||
this.numSelectedCampaigns,
|
||||
{ num: this.numSelectedCampaigns, name: name.toLowerCase() },
|
||||
), fn);
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['campaigns', 'loading']),
|
||||
|
||||
numSelectedCampaigns() {
|
||||
return this.bulk.all ? this.campaigns.total : this.bulk.checked.length;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<b-table :data="lists.results" :loading="loading.listsFull" hoverable default-sort="createdAt" paginated
|
||||
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
|
||||
:per-page="lists.perPage" :total="lists.total" backend-sorting @sort="onSort">
|
||||
<b-table :data="lists.results" :loading="loading.listsFull" @check-all="onTableCheck" @check="onTableCheck"
|
||||
:checked-rows.sync="bulk.checked" hoverable default-sort="createdAt" paginated backend-pagination
|
||||
pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page" :per-page="lists.perPage"
|
||||
:total="lists.total" checkable backend-sorting @sort="onSort">
|
||||
<template #top-left>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
@@ -42,6 +43,20 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" v-if="bulk.checked.length > 0">
|
||||
<a class="a" href="#" @click.prevent="deleteLists" data-cy="btn-delete-lists">
|
||||
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
||||
</a>
|
||||
<span class="a">
|
||||
{{ $tc('globals.messages.numSelected', numSelectedLists, { num: numSelectedLists }) }}
|
||||
<span v-if="!bulk.all && lists.total > lists.perPage">
|
||||
—
|
||||
<a href="#" @click.prevent="onSelectAll">
|
||||
{{ $tc('globals.messages.selectAll', lists.total, { num: lists.total }) }}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" header-class="cy-name" sortable
|
||||
@@ -195,6 +210,12 @@ export default Vue.extend({
|
||||
order: 'asc',
|
||||
status: this.$route.query.status || 'active',
|
||||
},
|
||||
|
||||
// Table bulk row selection states.
|
||||
bulk: {
|
||||
checked: [],
|
||||
all: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -272,6 +293,49 @@ export default Vue.extend({
|
||||
);
|
||||
},
|
||||
|
||||
// Mark all lists in the query as selected.
|
||||
onSelectAll() {
|
||||
this.bulk.all = true;
|
||||
},
|
||||
|
||||
onTableCheck() {
|
||||
// Disable bulk.all selection if there are no rows checked in the table.
|
||||
if (this.bulk.checked.length !== this.lists.total) {
|
||||
this.bulk.all = false;
|
||||
}
|
||||
},
|
||||
|
||||
deleteLists() {
|
||||
const name = this.$tc('globals.terms.list', this.numSelectedCampaigns);
|
||||
|
||||
const fn = () => {
|
||||
const params = {};
|
||||
if (!this.bulk.all && this.bulk.checked.length > 0) {
|
||||
// If 'all' is not selected, delete lists by IDs.
|
||||
params.id = this.bulk.checked.map((l) => l.id);
|
||||
} else {
|
||||
// 'All' is selected, delete by query.
|
||||
params.query = this.queryParams.query.replace(/[^\p{L}\p{N}\s]/gu, ' ');
|
||||
}
|
||||
|
||||
this.$api.deleteLists(params)
|
||||
.then(() => {
|
||||
this.getLists();
|
||||
this.$utils.toast(this.$tc(
|
||||
'globals.messages.deletedCount',
|
||||
this.numSelectedLists,
|
||||
{ num: this.numSelectedLists, name },
|
||||
));
|
||||
});
|
||||
};
|
||||
|
||||
this.$utils.confirm(this.$tc(
|
||||
'globals.messages.confirmDelete',
|
||||
this.numSelectedLists,
|
||||
{ num: this.numSelectedLists, name: name.toLowerCase() },
|
||||
), fn);
|
||||
},
|
||||
|
||||
createOptinCampaign(list) {
|
||||
const data = {
|
||||
name: this.$t('lists.optinTo', { name: list.name }),
|
||||
@@ -292,6 +356,10 @@ export default Vue.extend({
|
||||
|
||||
computed: {
|
||||
...mapState(['loading', 'settings']),
|
||||
|
||||
numSelectedLists() {
|
||||
return this.bulk.all ? this.lists.total : this.bulk.checked.length;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
</span>
|
||||
<span v-if="currentList">
|
||||
» {{ currentList.name }}
|
||||
<span v-if="queryParams.subStatus" class="has-text-grey has-text-weight-normal is-capitalized">({{ queryParams.subStatus }})</span>
|
||||
<span v-if="queryParams.subStatus" class="has-text-grey has-text-weight-normal is-capitalized">({{
|
||||
queryParams.subStatus }})</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -92,11 +93,11 @@
|
||||
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
|
||||
</a>
|
||||
<span class="a">
|
||||
{{ $t('subscribers.numSelected', { num: numSelectedSubscribers }) }}
|
||||
{{ $t('globals.messages.numSelected', { num: numSelectedSubscribers }) }}
|
||||
<span v-if="!bulk.all && subscribers.total > subscribers.perPage">
|
||||
—
|
||||
<a href="#" @click.prevent="selectAllSubscribers">
|
||||
{{ $t('subscribers.selectAll', { num: subscribers.total }) }}
|
||||
{{ $t('globals.messages.selectAll', { num: subscribers.total }) }}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"globals.fields.type": "Type",
|
||||
"globals.fields.updatedAt": "Updated",
|
||||
"globals.fields.uuid": "UUID",
|
||||
"globals.messages.selectAll": "Select all {num}",
|
||||
"globals.messages.confirmDelete": "Delete {num} {name}?",
|
||||
"globals.messages.numSelected": "{num} selected",
|
||||
"globals.messages.confirm": "Are you sure?",
|
||||
"globals.messages.confirmDiscard": "Discard changes?",
|
||||
|
||||
@@ -330,6 +330,27 @@ func (c *Core) DeleteCampaign(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCampaigns deletes multiple campaigns by IDs or by query.
|
||||
func (c *Core) DeleteCampaigns(ids []int, query string, hasAllPerm bool, permittedLists []int) error {
|
||||
var queryStr string
|
||||
|
||||
if len(ids) > 0 {
|
||||
queryStr = ""
|
||||
} else if query != "" {
|
||||
queryStr = makeSearchString(query)
|
||||
} else {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
if _, err := c.q.DeleteCampaigns.Exec(pq.Array(ids), queryStr, hasAllPerm, pq.Array(permittedLists)); err != nil {
|
||||
c.log.Printf("error deleting campaigns: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.campaigns}", "error", pqErrMsg(err)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CampaignHasLists checks if a campaign has any of the given list IDs.
|
||||
func (c *Core) CampaignHasLists(id int, listIDs []int) (bool, error) {
|
||||
has := false
|
||||
|
||||
@@ -136,9 +136,7 @@ func pqErrMsg(err error) string {
|
||||
// query SQL statement (string interpolated) and returns the
|
||||
// search query string along with the SQL expression.
|
||||
func makeSearchQuery(searchStr, orderBy, order, query string, querySortFields []string) (string, string) {
|
||||
if searchStr != "" {
|
||||
searchStr = `%` + string(regexFullTextQuery.ReplaceAll([]byte(searchStr), []byte("&"))) + `%`
|
||||
}
|
||||
searchStr = makeSearchString(searchStr)
|
||||
|
||||
// Sort params.
|
||||
if !strSliceContains(orderBy, querySortFields) {
|
||||
@@ -153,6 +151,14 @@ func makeSearchQuery(searchStr, orderBy, order, query string, querySortFields []
|
||||
return searchStr, query
|
||||
}
|
||||
|
||||
// makeSearchString prepares a search string for use in both tsquery and ILIKE queries.
|
||||
func makeSearchString(searchStr string) string {
|
||||
if searchStr == "" {
|
||||
return ""
|
||||
}
|
||||
return `%` + string(regexFullTextQuery.ReplaceAll([]byte(searchStr), []byte("&"))) + `%`
|
||||
}
|
||||
|
||||
// strSliceContains checks if a string is present in the string slice.
|
||||
func strSliceContains(str string, sl []string) bool {
|
||||
for _, s := range sl {
|
||||
|
||||
@@ -195,15 +195,25 @@ func (c *Core) UpdateList(id int, l models.List) (models.List, error) {
|
||||
|
||||
// DeleteList deletes a list.
|
||||
func (c *Core) DeleteList(id int) error {
|
||||
return c.DeleteLists([]int{id})
|
||||
return c.DeleteLists([]int{id}, "", true, nil)
|
||||
}
|
||||
|
||||
// DeleteLists deletes multiple lists.
|
||||
func (c *Core) DeleteLists(ids []int) error {
|
||||
if _, err := c.q.DeleteLists.Exec(pq.Array(ids)); err != nil {
|
||||
func (c *Core) DeleteLists(ids []int, query string, getAll bool, permittedIDs []int) error {
|
||||
var queryStr string
|
||||
|
||||
if len(ids) > 0 {
|
||||
queryStr = ""
|
||||
} else if query != "" {
|
||||
queryStr = makeSearchString(query)
|
||||
} else {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
if _, err := c.q.DeleteLists.Exec(pq.Array(ids), queryStr, getAll, pq.Array(permittedIDs)); err != nil {
|
||||
c.log.Printf("error deleting lists: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.list}", "error", pqErrMsg(err)))
|
||||
c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ type Queries struct {
|
||||
UpdateCampaignArchive *sqlx.Stmt `query:"update-campaign-archive"`
|
||||
RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"`
|
||||
DeleteCampaign *sqlx.Stmt `query:"delete-campaign"`
|
||||
DeleteCampaigns *sqlx.Stmt `query:"delete-campaigns"`
|
||||
|
||||
InsertMedia *sqlx.Stmt `query:"insert-media"`
|
||||
GetMedia *sqlx.Stmt `query:"get-media"`
|
||||
|
||||
@@ -431,6 +431,21 @@ UPDATE campaigns SET
|
||||
-- name: delete-campaign
|
||||
DELETE FROM campaigns WHERE id=$1;
|
||||
|
||||
-- name: delete-campaigns
|
||||
DELETE FROM campaigns c
|
||||
WHERE (
|
||||
CASE
|
||||
WHEN CARDINALITY($1::INT[]) > 0 THEN id = ANY($1)
|
||||
ELSE $2 = '' OR TO_TSVECTOR(CONCAT(name, ' ', subject)) @@ TO_TSQUERY($2) OR CONCAT(c.name, ' ', c.subject) ILIKE $2
|
||||
END
|
||||
)
|
||||
-- Get all campaigns or filter by permitted list IDs.
|
||||
AND (
|
||||
$3 OR EXISTS (
|
||||
SELECT 1 FROM campaign_lists WHERE campaign_id = c.id AND list_id = ANY($4::INT[])
|
||||
)
|
||||
);
|
||||
|
||||
-- name: register-campaign-view
|
||||
WITH view AS (
|
||||
SELECT campaigns.id as campaign_id, subscribers.id AS subscriber_id FROM campaigns
|
||||
|
||||
@@ -77,5 +77,13 @@ SELECT COUNT(*) FROM l, c;
|
||||
UPDATE lists SET updated_at=NOW() WHERE id = ANY($1);
|
||||
|
||||
-- name: delete-lists
|
||||
DELETE FROM lists WHERE id = ALL($1);
|
||||
DELETE FROM lists
|
||||
WHERE CASE
|
||||
WHEN CARDINALITY($1::INT[]) > 0 THEN id = ANY($1)
|
||||
ELSE ($2 = '' OR to_tsvector(name) @@ to_tsquery($2))
|
||||
END
|
||||
AND CASE
|
||||
-- Optional list IDs based on user permission.
|
||||
WHEN $3 = TRUE THEN TRUE ELSE id = ANY($4::INT[])
|
||||
END;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user