mirror of
https://github.com/knadh/listmonk.git
synced 2025-10-16 10:17:13 +08:00
Refactor subsbscription status option on the import page.
- Refactor subimporter New*() funcs to take opt structs. - Refactor and simplify Vue code. - Remove redundant i18n entries and use existing ones. - Remove redundant subimporter constants and use existing ones. - Consider 'overwrite' option for subscription status as well. - Write Cypress integration tests for the new feature.
This commit is contained in:
parent
7ca08f0a36
commit
868fae6ac2
16 changed files with 579 additions and 534 deletions
|
@ -8,18 +8,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/knadh/listmonk/internal/subimporter"
|
"github.com/knadh/listmonk/internal/subimporter"
|
||||||
|
"github.com/knadh/listmonk/models"
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// reqImport represents file upload import params.
|
|
||||||
type reqImport struct {
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
SubscriptionStatus string `json:"subscriptionStatus"`
|
|
||||||
Overwrite bool `json:"overwrite"`
|
|
||||||
Delim string `json:"delim"`
|
|
||||||
ListIDs []int `json:"lists"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleImportSubscribers handles the uploading and bulk importing of
|
// handleImportSubscribers handles the uploading and bulk importing of
|
||||||
// a ZIP file of one or more CSV files.
|
// a ZIP file of one or more CSV files.
|
||||||
func handleImportSubscribers(c echo.Context) error {
|
func handleImportSubscribers(c echo.Context) error {
|
||||||
|
@ -31,21 +23,34 @@ func handleImportSubscribers(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarsal the JSON params.
|
// Unmarsal the JSON params.
|
||||||
var r reqImport
|
var opt subimporter.SessionOpt
|
||||||
if err := json.Unmarshal([]byte(c.FormValue("params")), &r); err != nil {
|
if err := json.Unmarshal([]byte(c.FormValue("params")), &opt); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
app.i18n.Ts("import.invalidParams", "error", err.Error()))
|
app.i18n.Ts("import.invalidParams", "error", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Mode != subimporter.ModeSubscribe && r.Mode != subimporter.ModeBlocklist {
|
// Validate mode.
|
||||||
|
if opt.Mode != subimporter.ModeSubscribe && opt.Mode != subimporter.ModeBlocklist {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidMode"))
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidMode"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.SubscriptionStatus != subimporter.SubscriptionStatusUnconfirmed && r.SubscriptionStatus != subimporter.SubscriptionStatusConfirmed {
|
// If no status is specified, pick a default one.
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidSubscriptionStatus"))
|
if opt.SubStatus == "" {
|
||||||
|
switch opt.Mode {
|
||||||
|
case subimporter.ModeSubscribe:
|
||||||
|
opt.SubStatus = models.SubscriptionStatusUnconfirmed
|
||||||
|
case subimporter.ModeBlocklist:
|
||||||
|
opt.SubStatus = models.SubscriptionStatusUnsubscribed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(r.Delim) != 1 {
|
if opt.SubStatus != models.SubscriptionStatusUnconfirmed &&
|
||||||
|
opt.SubStatus != models.SubscriptionStatusConfirmed &&
|
||||||
|
opt.SubStatus != models.SubscriptionStatusUnsubscribed {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidSubStatus"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opt.Delim) != 1 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidDelim"))
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidDelim"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +79,8 @@ func handleImportSubscribers(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the importer session.
|
// Start the importer session.
|
||||||
impSess, err := app.importer.NewSession(file.Filename, r.Mode, r.SubscriptionStatus, r.Overwrite, r.ListIDs)
|
opt.Filename = file.Filename
|
||||||
|
impSess, err := app.importer.NewSession(opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
app.i18n.Ts("import.errorStarting", "error", err.Error()))
|
app.i18n.Ts("import.errorStarting", "error", err.Error()))
|
||||||
|
@ -82,7 +88,7 @@ func handleImportSubscribers(c echo.Context) error {
|
||||||
go impSess.Start()
|
go impSess.Start()
|
||||||
|
|
||||||
if strings.HasSuffix(strings.ToLower(file.Filename), ".csv") {
|
if strings.HasSuffix(strings.ToLower(file.Filename), ".csv") {
|
||||||
go impSess.LoadCSV(out.Name(), rune(r.Delim[0]))
|
go impSess.LoadCSV(out.Name(), rune(opt.Delim[0]))
|
||||||
} else {
|
} else {
|
||||||
// Only 1 CSV from the ZIP is considered. If multiple files have
|
// Only 1 CSV from the ZIP is considered. If multiple files have
|
||||||
// to be processed, counting the net number of lines (to track progress),
|
// to be processed, counting the net number of lines (to track progress),
|
||||||
|
@ -95,7 +101,7 @@ func handleImportSubscribers(c echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
app.i18n.Ts("import.errorProcessingZIP", "error", err.Error()))
|
app.i18n.Ts("import.errorProcessingZIP", "error", err.Error()))
|
||||||
}
|
}
|
||||||
go impSess.LoadCSV(dir+"/"+files[0], rune(r.Delim[0]))
|
go impSess.LoadCSV(dir+"/"+files[0], rune(opt.Delim[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
|
return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
|
||||||
|
|
|
@ -7,12 +7,19 @@ describe('Import', () => {
|
||||||
|
|
||||||
it('Imports subscribers', () => {
|
it('Imports subscribers', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{ mode: 'check-subscribe', status: 'enabled', count: 102 },
|
{ chkMode: 'subscribe', status: 'enabled', chkSubStatus: 'unconfirmed', subStatus: 'unconfirmed', overwrite: true, count: 102 },
|
||||||
{ mode: 'check-blocklist', status: 'blocklisted', count: 102 },
|
{ chkMode: 'subscribe', status: 'enabled', chkSubStatus: 'confirmed', subStatus: 'confirmed', overwrite: true, count: 102 },
|
||||||
|
{ chkMode: 'subscribe', status: 'enabled', chkSubStatus: 'unconfirmed', subStatus: 'confirmed', overwrite: false, count: 102 },
|
||||||
|
{ chkMode: 'blocklist', status: 'blocklisted', chkSubStatus: 'unsubscribed', subStatus: 'unsubscribed', overwrite: true, count: 102 },
|
||||||
];
|
];
|
||||||
|
|
||||||
cases.forEach((c) => {
|
cases.forEach((c) => {
|
||||||
cy.get(`[data-cy=${c.mode}] .check`).click();
|
cy.get(`[data-cy=check-${c.chkMode}] .check`).click();
|
||||||
|
cy.get(`[data-cy=check-${c.chkSubStatus}] .check`).click();
|
||||||
|
|
||||||
|
if (!c.overwrite) {
|
||||||
|
cy.get(`[data-cy=overwrite]`).click();
|
||||||
|
}
|
||||||
|
|
||||||
if (c.status === 'enabled') {
|
if (c.status === 'enabled') {
|
||||||
cy.get('.list-selector input').click();
|
cy.get('.list-selector input').click();
|
||||||
|
@ -39,12 +46,39 @@ describe('Import', () => {
|
||||||
cy.expect(parseInt($el.text().trim())).to.equal(c.count);
|
cy.expect(parseInt($el.text().trim())).to.equal(c.count);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscriber status.
|
||||||
cy.get('tbody td[data-label=Status]').each(($el) => {
|
cy.get('tbody td[data-label=Status]').each(($el) => {
|
||||||
cy.wrap($el).find(`.tag.${c.status}`);
|
cy.wrap($el).find(`.tag.${c.status}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscription status.
|
||||||
|
cy.get('tbody td[data-label=E-mail]').each(($el) => {
|
||||||
|
cy.wrap($el).find(`.tag.${c.subStatus}`);
|
||||||
|
});
|
||||||
|
|
||||||
cy.loginAndVisit('/subscribers/import');
|
cy.loginAndVisit('/subscribers/import');
|
||||||
cy.wait(100);
|
cy.wait(100);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Imports subscribers incorrectly', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/subscribers/import');
|
||||||
|
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').first().click();
|
||||||
|
cy.get('input[name=delim]').clear().type('|');
|
||||||
|
|
||||||
|
cy.fixture('subs.csv').then((data) => {
|
||||||
|
cy.get('input[type="file"]').attachFile({
|
||||||
|
fileContent: data.toString(),
|
||||||
|
fileName: 'subs.csv',
|
||||||
|
mimeType: 'text/csv',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get('section.wrap .has-text-danger');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<b-field :label="$t('import.mode')">
|
<b-field :label="$t('import.mode')" :addons="false">
|
||||||
<div>
|
<div>
|
||||||
<b-radio v-model="form.mode" name="mode"
|
<b-radio v-model="form.mode" name="mode"
|
||||||
native-value="subscribe"
|
native-value="subscribe"
|
||||||
|
@ -21,52 +21,47 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<b-field :label="$t('import.subscriptionStatus')">
|
<b-field :label="$t('globals.fields.status')" :addons="false">
|
||||||
<div>
|
<template v-if="form.mode === 'subscribe'">
|
||||||
<div v-if="form.mode === 'subscribe'" style="display:block">
|
<b-radio
|
||||||
<b-radio
|
v-model="form.subStatus"
|
||||||
v-model="form.subscriptionStatus"
|
name="subStatus"
|
||||||
name="subscriptionStatus"
|
native-value="unconfirmed"
|
||||||
native-value="unconfirmed"
|
data-cy="check-unconfirmed">
|
||||||
data-cy="check-unconfirmed">
|
{{ $t('subscribers.status.unconfirmed') }}
|
||||||
{{ $t('import.unconfirmed') }}
|
</b-radio>
|
||||||
</b-radio>
|
<b-radio
|
||||||
</div>
|
v-model="form.subStatus"
|
||||||
<div v-if="form.mode === 'subscribe'" style="display:block">
|
name="subStatus"
|
||||||
<b-radio
|
native-value="confirmed"
|
||||||
v-model="form.subscriptionStatus"
|
data-cy="check-confirmed">
|
||||||
name="subscriptionStatus"
|
{{ $t('subscribers.status.confirmed') }}
|
||||||
native-value="confirmed"
|
</b-radio>
|
||||||
data-cy="check-confirmed">
|
</template>
|
||||||
{{ $t('import.confirmed') }}
|
|
||||||
</b-radio>
|
<b-radio v-else
|
||||||
</div>
|
v-model="form.subStatus"
|
||||||
<div v-if="form.mode === 'blocklist'" style="display:block">
|
name="subStatus"
|
||||||
<b-radio
|
native-value="unsubscribed"
|
||||||
v-model="form.subscriptionStatus"
|
data-cy="check-unsubscribed">
|
||||||
name="subscriptionStatus"
|
{{ $t('subscribers.status.unsubscribed') }}
|
||||||
native-value="unsubscribed"
|
</b-radio>
|
||||||
data-cy="check-unsubscribed">
|
|
||||||
{{ $t('import.unsubscribed') }}
|
|
||||||
</b-radio>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<b-field v-if="form.mode === 'subscribe'"
|
<b-field v-if="form.mode === 'subscribe'"
|
||||||
:label="$t('import.overwrite')"
|
:label="$t('import.overwrite')"
|
||||||
:message="$t('import.overwriteHelp')">
|
:message="$t('import.overwriteHelp')">
|
||||||
<div>
|
<div>
|
||||||
<b-switch v-model="form.overwrite" name="overwrite" />
|
<b-switch v-model="form.overwrite" name="overwrite" data-cy="overwrite" />
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<b-field :label="$t('import.csvDelim')" :message="$t('import.csvDelimHelp')"
|
<b-field :label="$t('import.csvDelim')" :message="$t('import.csvDelimHelp')" class="delimiter">
|
||||||
class="delimiter">
|
<b-input v-model="form.delim" name="delim" placeholder="," maxlength="1" required />
|
||||||
<b-input v-model="form.delim" name="delim"
|
|
||||||
placeholder="," maxlength="1" required />
|
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,7 +182,7 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
mode: 'subscribe',
|
mode: 'subscribe',
|
||||||
subscriptionStatus: 'unconfirmed',
|
subStatus: 'unconfirmed',
|
||||||
delim: ',',
|
delim: ',',
|
||||||
lists: [],
|
lists: [],
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
@ -206,15 +201,15 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
form: {
|
'form.mode': function formMode() {
|
||||||
handler(val) {
|
// Select the appropriate status radio whenever mode changes.
|
||||||
if (val.mode === 'subscribe') {
|
this.$nextTick(() => {
|
||||||
this.form.subscriptionStatus = 'unconfirmed';
|
if (this.form.mode === 'subscribe') {
|
||||||
} else if (val.mode === 'blocklist') {
|
this.form.subStatus = 'unconfirmed';
|
||||||
this.form.subscriptionStatus = 'unsubscribed';
|
} else {
|
||||||
|
this.form.subStatus = 'unsubscribed';
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
deep: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -317,7 +312,7 @@ export default Vue.extend({
|
||||||
const params = new FormData();
|
const params = new FormData();
|
||||||
params.set('params', JSON.stringify({
|
params.set('params', JSON.stringify({
|
||||||
mode: this.form.mode,
|
mode: this.form.mode,
|
||||||
subscriptionStatus: this.form.subscriptionStatus,
|
subscription_status: this.form.subStatus,
|
||||||
delim: this.form.delim,
|
delim: this.form.delim,
|
||||||
lists: this.form.lists.map((l) => l.id),
|
lists: this.form.lists.map((l) => l.id),
|
||||||
overwrite: this.form.overwrite,
|
overwrite: this.form.overwrite,
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Ungültige Datei: {error}",
|
"import.invalidFile": "Ungültige Datei: {error}",
|
||||||
"import.invalidMode": "Ungültiger Modus",
|
"import.invalidMode": "Ungültiger Modus",
|
||||||
"import.invalidParams": "Ungültiger Parameter: {error}",
|
"import.invalidParams": "Ungültiger Parameter: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Listen die abonniert werden.",
|
"import.listSubHelp": "Listen die abonniert werden.",
|
||||||
"import.mode": "Modus",
|
"import.mode": "Modus",
|
||||||
"import.overwrite": "Überschreiben?",
|
"import.overwrite": "Überschreiben?",
|
||||||
|
|
|
@ -185,16 +185,12 @@
|
||||||
"import.invalidDelim": "Delimiter should be a single character.",
|
"import.invalidDelim": "Delimiter should be a single character.",
|
||||||
"import.invalidFile": "Invalid file: {error}",
|
"import.invalidFile": "Invalid file: {error}",
|
||||||
"import.invalidMode": "Invalid mode",
|
"import.invalidMode": "Invalid mode",
|
||||||
"import.invalidSubscriptionStatus": "Invalid subscription status",
|
|
||||||
"import.invalidParams": "Invalid params: {error}",
|
"import.invalidParams": "Invalid params: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Lists to subscribe to.",
|
"import.listSubHelp": "Lists to subscribe to.",
|
||||||
"import.mode": "Mode",
|
"import.mode": "Mode",
|
||||||
"import.subscriptionStatus": "Subscription Status",
|
|
||||||
"import.confirmed": "Confirmed",
|
|
||||||
"import.unconfirmed": "Unconfirmed",
|
|
||||||
"import.unsubscribed": "Unsubscribed",
|
|
||||||
"import.overwrite": "Overwrite?",
|
"import.overwrite": "Overwrite?",
|
||||||
"import.overwriteHelp": "Overwrite name and attribs of existing subscribers?",
|
"import.overwriteHelp": "Overwrite name, attribs, subscription status of existing subscribers?",
|
||||||
"import.recordsCount": "{num} / {total} records",
|
"import.recordsCount": "{num} / {total} records",
|
||||||
"import.stopImport": "Stop import",
|
"import.stopImport": "Stop import",
|
||||||
"import.subscribe": "Subscribe",
|
"import.subscribe": "Subscribe",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Archivo inválido: {error}",
|
"import.invalidFile": "Archivo inválido: {error}",
|
||||||
"import.invalidMode": "Modo inválido",
|
"import.invalidMode": "Modo inválido",
|
||||||
"import.invalidParams": "Paramétros inválidos: {error}",
|
"import.invalidParams": "Paramétros inválidos: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Listas a subscribir",
|
"import.listSubHelp": "Listas a subscribir",
|
||||||
"import.mode": "Modo",
|
"import.mode": "Modo",
|
||||||
"import.overwrite": "¿Sobre-escribir?",
|
"import.overwrite": "¿Sobre-escribir?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Fichier non valide : {error}",
|
"import.invalidFile": "Fichier non valide : {error}",
|
||||||
"import.invalidMode": "Mode invalide",
|
"import.invalidMode": "Mode invalide",
|
||||||
"import.invalidParams": "Paramètres non valides : {error}",
|
"import.invalidParams": "Paramètres non valides : {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Abonner aux listes",
|
"import.listSubHelp": "Abonner aux listes",
|
||||||
"import.mode": "Mode",
|
"import.mode": "Mode",
|
||||||
"import.overwrite": "Écraser ?",
|
"import.overwrite": "Écraser ?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "File non valido: {error}",
|
"import.invalidFile": "File non valido: {error}",
|
||||||
"import.invalidMode": "Modalità non valida",
|
"import.invalidMode": "Modalità non valida",
|
||||||
"import.invalidParams": "Parametri non validi: {error}",
|
"import.invalidParams": "Parametri non validi: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Liste a cui iscriversi.",
|
"import.listSubHelp": "Liste a cui iscriversi.",
|
||||||
"import.mode": "Modalità",
|
"import.mode": "Modalità",
|
||||||
"import.overwrite": "Sovrascrivere?",
|
"import.overwrite": "Sovrascrivere?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": " ഫയൽ അസാധുവാണ് : {error}",
|
"import.invalidFile": " ഫയൽ അസാധുവാണ് : {error}",
|
||||||
"import.invalidMode": "ശൈലി അസാധുവാണ്",
|
"import.invalidMode": "ശൈലി അസാധുവാണ്",
|
||||||
"import.invalidParams": "പരാമുകൾ അസാധുവാണ്: {error}",
|
"import.invalidParams": "പരാമുകൾ അസാധുവാണ്: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "വരിക്കാരനാകാനുള്ള ലിസ്റ്റുകൾ.",
|
"import.listSubHelp": "വരിക്കാരനാകാനുള്ള ലിസ്റ്റുകൾ.",
|
||||||
"import.mode": "ശൈലി",
|
"import.mode": "ശൈലി",
|
||||||
"import.overwrite": "തിരുത്തിയെഴുതട്ടേ?",
|
"import.overwrite": "തിരുത്തിയെഴുതട്ടേ?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Nieprawidłowy plik: {error}",
|
"import.invalidFile": "Nieprawidłowy plik: {error}",
|
||||||
"import.invalidMode": "Nieprawidłowy tryp",
|
"import.invalidMode": "Nieprawidłowy tryp",
|
||||||
"import.invalidParams": "Nieprawidłowe parametry: {error}",
|
"import.invalidParams": "Nieprawidłowe parametry: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Listy do subskrybowania.",
|
"import.listSubHelp": "Listy do subskrybowania.",
|
||||||
"import.mode": "Tryb",
|
"import.mode": "Tryb",
|
||||||
"import.overwrite": "Nadpisać?",
|
"import.overwrite": "Nadpisać?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Arquivo inválido: {error}",
|
"import.invalidFile": "Arquivo inválido: {error}",
|
||||||
"import.invalidMode": "Modo inválido",
|
"import.invalidMode": "Modo inválido",
|
||||||
"import.invalidParams": "Parâmetros inválidos: {error}",
|
"import.invalidParams": "Parâmetros inválidos: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Listas para inscrever.",
|
"import.listSubHelp": "Listas para inscrever.",
|
||||||
"import.mode": "Modo",
|
"import.mode": "Modo",
|
||||||
"import.overwrite": "Sobrescrever?",
|
"import.overwrite": "Sobrescrever?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Ficheiro inválido: {error}",
|
"import.invalidFile": "Ficheiro inválido: {error}",
|
||||||
"import.invalidMode": "Modo inválido",
|
"import.invalidMode": "Modo inválido",
|
||||||
"import.invalidParams": "Parâmetros inválidos: {error}",
|
"import.invalidParams": "Parâmetros inválidos: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Listas a subscrever.",
|
"import.listSubHelp": "Listas a subscrever.",
|
||||||
"import.mode": "Modo",
|
"import.mode": "Modo",
|
||||||
"import.overwrite": "Sobrescrever?",
|
"import.overwrite": "Sobrescrever?",
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
"import.invalidFile": "Неверный файл: {error}",
|
"import.invalidFile": "Неверный файл: {error}",
|
||||||
"import.invalidMode": "Неверный режим",
|
"import.invalidMode": "Неверный режим",
|
||||||
"import.invalidParams": "Неверные параметры: {error}",
|
"import.invalidParams": "Неверные параметры: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Списки для подписки.",
|
"import.listSubHelp": "Списки для подписки.",
|
||||||
"import.mode": "Режим",
|
"import.mode": "Режим",
|
||||||
"import.overwrite": "Перезаписать?",
|
"import.overwrite": "Перезаписать?",
|
||||||
|
|
12
i18n/tr.json
12
i18n/tr.json
|
@ -25,6 +25,7 @@
|
||||||
"campaigns.fromAddress": "Gelen adres",
|
"campaigns.fromAddress": "Gelen adres",
|
||||||
"campaigns.fromAddressPlaceholder": "isminiz <cevap-verme@siteniz.com>",
|
"campaigns.fromAddressPlaceholder": "isminiz <cevap-verme@siteniz.com>",
|
||||||
"campaigns.invalid": "Yanlış tanımlı kapmanya",
|
"campaigns.invalid": "Yanlış tanımlı kapmanya",
|
||||||
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Kampanya için tanımlanmış bir tarih gerekli.",
|
"campaigns.needsSendAt": "Kampanya için tanımlanmış bir tarih gerekli.",
|
||||||
"campaigns.newCampaign": "Yeni kampanya",
|
"campaigns.newCampaign": "Yeni kampanya",
|
||||||
"campaigns.noKnownSubsToTest": "Test için bilinen üye yok.",
|
"campaigns.noKnownSubsToTest": "Test için bilinen üye yok.",
|
||||||
|
@ -185,6 +186,7 @@
|
||||||
"import.invalidFile": "Hatalı dosya: {error}",
|
"import.invalidFile": "Hatalı dosya: {error}",
|
||||||
"import.invalidMode": "Hatalı mod",
|
"import.invalidMode": "Hatalı mod",
|
||||||
"import.invalidParams": "Hatalı parametre: {error}",
|
"import.invalidParams": "Hatalı parametre: {error}",
|
||||||
|
"import.invalidSubStatus": "Invalid subscription status",
|
||||||
"import.listSubHelp": "Üye olunacak listeler.",
|
"import.listSubHelp": "Üye olunacak listeler.",
|
||||||
"import.mode": "Mod",
|
"import.mode": "Mod",
|
||||||
"import.overwrite": "Üzerine yaz?",
|
"import.overwrite": "Üzerine yaz?",
|
||||||
|
@ -259,10 +261,10 @@
|
||||||
"public.privacyWipeHelp": "Tüm üyeliklerinizi ve ilişkili verilerinizi veritabanından silin.",
|
"public.privacyWipeHelp": "Tüm üyeliklerinizi ve ilişkili verilerinizi veritabanından silin.",
|
||||||
"public.sub": "Üyelik",
|
"public.sub": "Üyelik",
|
||||||
"public.subConfirmed": "Başarıyla üye olundu.",
|
"public.subConfirmed": "Başarıyla üye olundu.",
|
||||||
"public.subOptinPending": "Üyelik doğrulaması için bir e-posta gönderilmiştir.",
|
|
||||||
"public.subConfirmedTitle": "Doğrulanmıştır",
|
"public.subConfirmedTitle": "Doğrulanmıştır",
|
||||||
"public.subName": "İsim (opsiyonel)",
|
"public.subName": "İsim (opsiyonel)",
|
||||||
"public.subNotFound": "Üyelik bulunamadı.",
|
"public.subNotFound": "Üyelik bulunamadı.",
|
||||||
|
"public.subOptinPending": "Üyelik doğrulaması için bir e-posta gönderilmiştir.",
|
||||||
"public.subPrivateList": "Kişisel liste",
|
"public.subPrivateList": "Kişisel liste",
|
||||||
"public.subTitle": "Üye ol",
|
"public.subTitle": "Üye ol",
|
||||||
"public.unsub": "Üyelikten ayrıl",
|
"public.unsub": "Üyelikten ayrıl",
|
||||||
|
@ -272,15 +274,14 @@
|
||||||
"public.unsubbedInfo": "Başarı ile üyeliğinizi bitirdiniz.",
|
"public.unsubbedInfo": "Başarı ile üyeliğinizi bitirdiniz.",
|
||||||
"public.unsubbedTitle": "Üyelik bitirildi.",
|
"public.unsubbedTitle": "Üyelik bitirildi.",
|
||||||
"public.unsubscribeTitle": "e-posta listesi üyeliğini bitir",
|
"public.unsubscribeTitle": "e-posta listesi üyeliğini bitir",
|
||||||
"settings.needsRestart": "Ayarlar değişti. Çalışan tüm kampanyaları durdur ve uygulamayı yeniden başlat.",
|
|
||||||
"settings.confirmRestart": "Çalışan kampanyaların duraklatıldığından emin ol. Yeniden başlat?",
|
"settings.confirmRestart": "Çalışan kampanyaların duraklatıldığından emin ol. Yeniden başlat?",
|
||||||
"settings.updateAvailable": "Yeni bir güncel sürüm {version} mevcuttur.",
|
|
||||||
"settings.restart": "Yeniden başlat",
|
|
||||||
"settings.duplicateMessengerName": "Çoklanmış messenger ismi: {name}",
|
"settings.duplicateMessengerName": "Çoklanmış messenger ismi: {name}",
|
||||||
"settings.errorEncoding": "Hatalı kodlama ayarları: {error}",
|
"settings.errorEncoding": "Hatalı kodlama ayarları: {error}",
|
||||||
"settings.errorNoSMTP": "En azından bir SMTP bloğu etkin olmalı",
|
"settings.errorNoSMTP": "En azından bir SMTP bloğu etkin olmalı",
|
||||||
"settings.general.adminNotifEmails": "Yönetici e-posta bildirimleri",
|
"settings.general.adminNotifEmails": "Yönetici e-posta bildirimleri",
|
||||||
"settings.general.adminNotifEmailsHelp": "İçe aktarma güncellemeleri, kampanya tamamlama, başarısızlık gibi yönetici bildirimlerinin gönderilmesi gereken e-posta adreslerinin virgülle ayrılmış listesi.",
|
"settings.general.adminNotifEmailsHelp": "İçe aktarma güncellemeleri, kampanya tamamlama, başarısızlık gibi yönetici bildirimlerinin gönderilmesi gereken e-posta adreslerinin virgülle ayrılmış listesi.",
|
||||||
|
"settings.general.checkUpdates": "Check for updates",
|
||||||
|
"settings.general.checkUpdatesHelp": "Periodically check for new app releases and notify.",
|
||||||
"settings.general.enablePublicSubPage": "Erişime açık üyelik sayfasını etkinleştir",
|
"settings.general.enablePublicSubPage": "Erişime açık üyelik sayfasını etkinleştir",
|
||||||
"settings.general.enablePublicSubPageHelp": "Kişilerin abone olması için tüm genel listeleri içeren genel bir abonelik sayfası gösterin.",
|
"settings.general.enablePublicSubPageHelp": "Kişilerin abone olması için tüm genel listeleri içeren genel bir abonelik sayfası gösterin.",
|
||||||
"settings.general.faviconURL": "Favicon URL",
|
"settings.general.faviconURL": "Favicon URL",
|
||||||
|
@ -326,6 +327,7 @@
|
||||||
"settings.messengers.url": "URL",
|
"settings.messengers.url": "URL",
|
||||||
"settings.messengers.urlHelp": "Postback sunusucu için kök URL.",
|
"settings.messengers.urlHelp": "Postback sunusucu için kök URL.",
|
||||||
"settings.messengers.username": "Kullanıcı adı",
|
"settings.messengers.username": "Kullanıcı adı",
|
||||||
|
"settings.needsRestart": "Ayarlar değişti. Çalışan tüm kampanyaları durdur ve uygulamayı yeniden başlat.",
|
||||||
"settings.performance.batchSize": "Batch büyüklüğü",
|
"settings.performance.batchSize": "Batch büyüklüğü",
|
||||||
"settings.performance.batchSizeHelp": "Veritabanından tek bir yinelemede çekilecek abone sayısı. Her yineleme, aboneleri veritabanından çeker, onlara mesajlar gönderir ve ardından bir sonraki grubu çekmek için bir sonraki yinelemeye geçer. Bu, ideal olarak elde edilebilecek maksimum iş hacminden (eşzamanlılık * ileti_ hızı) daha yüksek olmalıdır.",
|
"settings.performance.batchSizeHelp": "Veritabanından tek bir yinelemede çekilecek abone sayısı. Her yineleme, aboneleri veritabanından çeker, onlara mesajlar gönderir ve ardından bir sonraki grubu çekmek için bir sonraki yinelemeye geçer. Bu, ideal olarak elde edilebilecek maksimum iş hacminden (eşzamanlılık * ileti_ hızı) daha yüksek olmalıdır.",
|
||||||
"settings.performance.concurrency": "Çoklu bağlantı",
|
"settings.performance.concurrency": "Çoklu bağlantı",
|
||||||
|
@ -352,6 +354,7 @@
|
||||||
"settings.privacy.listUnsubHeader": " `List-Unsubscribe` Başlık bilgisini ekle",
|
"settings.privacy.listUnsubHeader": " `List-Unsubscribe` Başlık bilgisini ekle",
|
||||||
"settings.privacy.listUnsubHeaderHelp": "E-posta istemcilerinin kullanıcıların tek bir tıklamayla abonelikten çıkmalarına olanak tanıyan abonelik iptal başlıklarını ekleyin.",
|
"settings.privacy.listUnsubHeaderHelp": "E-posta istemcilerinin kullanıcıların tek bir tıklamayla abonelikten çıkmalarına olanak tanıyan abonelik iptal başlıklarını ekleyin.",
|
||||||
"settings.privacy.name": "Gizlilik",
|
"settings.privacy.name": "Gizlilik",
|
||||||
|
"settings.restart": "Yeniden başlat",
|
||||||
"settings.smtp.authProtocol": "Protokol",
|
"settings.smtp.authProtocol": "Protokol",
|
||||||
"settings.smtp.customHeaders": "Özel başlık bilgisi",
|
"settings.smtp.customHeaders": "Özel başlık bilgisi",
|
||||||
"settings.smtp.customHeadersHelp": "Bu sunucudan gönderilen tüm iletilere eklenecek isteğe bağlı e-posta başlıkları dizisi. Örnek: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
"settings.smtp.customHeadersHelp": "Bu sunucudan gönderilen tüm iletilere eklenecek isteğe bağlı e-posta başlıkları dizisi. Örnek: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
|
@ -380,6 +383,7 @@
|
||||||
"settings.smtp.waitTimeout": "Bekleme süresi aşımı",
|
"settings.smtp.waitTimeout": "Bekleme süresi aşımı",
|
||||||
"settings.smtp.waitTimeoutHelp": "Bir bağlantıdaki yeni etkinliği kapatmadan ve havuzdan kaldırmadan önce bekleme süresi (saniye için s, dakika için m). ",
|
"settings.smtp.waitTimeoutHelp": "Bir bağlantıdaki yeni etkinliği kapatmadan ve havuzdan kaldırmadan önce bekleme süresi (saniye için s, dakika için m). ",
|
||||||
"settings.title": "Ayarlar",
|
"settings.title": "Ayarlar",
|
||||||
|
"settings.updateAvailable": "Yeni bir güncel sürüm {version} mevcuttur.",
|
||||||
"subscribers.advancedQuery": "İleri düzey",
|
"subscribers.advancedQuery": "İleri düzey",
|
||||||
"subscribers.advancedQueryHelp": "Üye attributes verisini görüntülemek için SQL verisi",
|
"subscribers.advancedQueryHelp": "Üye attributes verisini görüntülemek için SQL verisi",
|
||||||
"subscribers.attribs": "Attributes",
|
"subscribers.attribs": "Attributes",
|
||||||
|
|
|
@ -46,9 +46,6 @@ const (
|
||||||
|
|
||||||
ModeSubscribe = "subscribe"
|
ModeSubscribe = "subscribe"
|
||||||
ModeBlocklist = "blocklist"
|
ModeBlocklist = "blocklist"
|
||||||
|
|
||||||
SubscriptionStatusUnconfirmed = "unconfirmed"
|
|
||||||
SubscriptionStatusConfirmed = "confirmed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Importer represents the bulk CSV subscriber import system.
|
// Importer represents the bulk CSV subscriber import system.
|
||||||
|
@ -75,10 +72,17 @@ type Session struct {
|
||||||
subQueue chan SubReq
|
subQueue chan SubReq
|
||||||
log *log.Logger
|
log *log.Logger
|
||||||
|
|
||||||
mode string
|
opt SessionOpt
|
||||||
subscriptionStatus string
|
}
|
||||||
overwrite bool
|
|
||||||
listIDs []int
|
// SessionOpt represents the options for an importer session.
|
||||||
|
type SessionOpt struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
SubStatus string `json:"subscription_status"`
|
||||||
|
Overwrite bool `json:"overwrite"`
|
||||||
|
Delim string `json:"delim"`
|
||||||
|
ListIDs []int `json:"lists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status reporesents statistics from an ongoing import session.
|
// Status reporesents statistics from an ongoing import session.
|
||||||
|
@ -131,28 +135,25 @@ func New(opt Options, db *sql.DB) *Importer {
|
||||||
|
|
||||||
// NewSession returns an new instance of Session. It takes the name
|
// NewSession returns an new instance of Session. It takes the name
|
||||||
// of the uploaded file, but doesn't do anything with it but retains it for stats.
|
// of the uploaded file, but doesn't do anything with it but retains it for stats.
|
||||||
func (im *Importer) NewSession(fName, mode string, subscriptionStatus string, overWrite bool, listIDs []int) (*Session, error) {
|
func (im *Importer) NewSession(opt SessionOpt) (*Session, error) {
|
||||||
if im.getStatus() != StatusNone {
|
if im.getStatus() != StatusNone {
|
||||||
return nil, errors.New("an import is already running")
|
return nil, errors.New("an import is already running")
|
||||||
}
|
}
|
||||||
|
|
||||||
im.Lock()
|
im.Lock()
|
||||||
im.status = Status{Status: StatusImporting,
|
im.status = Status{Status: StatusImporting,
|
||||||
Name: fName,
|
Name: opt.Filename,
|
||||||
logBuf: bytes.NewBuffer(nil)}
|
logBuf: bytes.NewBuffer(nil)}
|
||||||
im.Unlock()
|
im.Unlock()
|
||||||
|
|
||||||
s := &Session{
|
s := &Session{
|
||||||
im: im,
|
im: im,
|
||||||
log: log.New(im.status.logBuf, "", log.Ldate|log.Ltime|log.Lshortfile),
|
log: log.New(im.status.logBuf, "", log.Ldate|log.Ltime|log.Lshortfile),
|
||||||
subQueue: make(chan SubReq, commitBatchSize),
|
subQueue: make(chan SubReq, commitBatchSize),
|
||||||
mode: mode,
|
opt: opt,
|
||||||
subscriptionStatus: subscriptionStatus,
|
|
||||||
overwrite: overWrite,
|
|
||||||
listIDs: listIDs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Printf("processing '%s'", fName)
|
s.log.Printf("processing '%s'", opt.Filename)
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,10 +241,10 @@ func (s *Session) Start() {
|
||||||
total = 0
|
total = 0
|
||||||
cur = 0
|
cur = 0
|
||||||
|
|
||||||
listIDs = make(pq.Int64Array, len(s.listIDs))
|
listIDs = make(pq.Int64Array, len(s.opt.ListIDs))
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, v := range s.listIDs {
|
for i, v := range s.opt.ListIDs {
|
||||||
listIDs[i] = int64(v)
|
listIDs[i] = int64(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +257,7 @@ func (s *Session) Start() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.mode == ModeSubscribe {
|
if s.opt.Mode == ModeSubscribe {
|
||||||
stmt = tx.Stmt(s.im.opt.UpsertStmt)
|
stmt = tx.Stmt(s.im.opt.UpsertStmt)
|
||||||
} else {
|
} else {
|
||||||
stmt = tx.Stmt(s.im.opt.BlocklistStmt)
|
stmt = tx.Stmt(s.im.opt.BlocklistStmt)
|
||||||
|
@ -270,9 +271,9 @@ func (s *Session) Start() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.mode == ModeSubscribe {
|
if s.opt.Mode == ModeSubscribe {
|
||||||
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, listIDs, s.subscriptionStatus, s.overwrite)
|
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, listIDs, s.opt.SubStatus, s.opt.Overwrite)
|
||||||
} else if s.mode == ModeBlocklist {
|
} else if s.opt.Mode == ModeBlocklist {
|
||||||
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs)
|
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -95,7 +95,7 @@ subs AS (
|
||||||
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
|
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
|
||||||
VALUES((SELECT id FROM sub), UNNEST($5::INT[]), $6)
|
VALUES((SELECT id FROM sub), UNNEST($5::INT[]), $6)
|
||||||
ON CONFLICT (subscriber_id, list_id) DO UPDATE
|
ON CONFLICT (subscriber_id, list_id) DO UPDATE
|
||||||
SET updated_at=NOW()
|
SET updated_at=NOW(), status=(CASE WHEN $7 THEN $6 ELSE subscriber_lists.status END)
|
||||||
)
|
)
|
||||||
SELECT uuid, id from sub;
|
SELECT uuid, id from sub;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue