Add 'slug' (permalink) support for campaign archives. Closes #1394.

This commit is contained in:
Kailash Nadh 2024-01-09 23:34:08 +05:30
parent 3335171960
commit 0d319ad9fd
43 changed files with 235 additions and 76 deletions

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"html/template"
"net/http"
"net/url"
"github.com/gorilla/feeds"
"github.com/knadh/listmonk/internal/manager"
@ -120,10 +121,19 @@ func handleCampaignArchivesPage(c echo.Context) error {
func handleCampaignArchivePage(c echo.Context) error {
var (
app = c.Get("app").(*App)
uuid = c.Param("uuid")
id = c.Param("id")
uuid = ""
slug = ""
)
pubCamp, err := app.core.GetArchivedCampaign(0, uuid)
// ID can be the UUID or slug.
if reUUID.MatchString(id) {
uuid = id
} else {
slug = id
}
pubCamp, err := app.core.GetArchivedCampaign(0, uuid, slug)
if err != nil || pubCamp.Type != models.CampaignTypeRegular {
notFound := false
if er, ok := err.(*echo.HTTPError); ok {
@ -202,7 +212,12 @@ func getCampaignArchives(offset, limit int, renderBody bool, app *App) ([]campAr
Subject: camp.Subject,
CreatedAt: camp.CreatedAt,
SendAt: camp.SendAt,
URL: app.constants.ArchiveURL + "/" + camp.UUID,
}
if camp.ArchiveSlug.Valid {
archive.URL, _ = url.JoinPath(app.constants.ArchiveURL, camp.ArchiveSlug.String)
} else {
archive.URL, _ = url.JoinPath(app.constants.ArchiveURL, camp.UUID)
}
if renderBody {

View file

@ -16,6 +16,7 @@ import (
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
"gopkg.in/volatiletech/null.v6"
)
// campaignReq is a wrapper over the Campaign model for receiving
@ -48,6 +49,7 @@ type campaignContentReq struct {
var (
regexFromAddress = regexp.MustCompile(`((.+?)\s)?<(.+?)@(.+?)>`)
regexSlug = regexp.MustCompile(`[^\p{L}\p{M}\p{N}]`)
)
// handleGetCampaigns handles retrieval of campaigns.
@ -99,7 +101,7 @@ func handleGetCampaign(c echo.Context) error {
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
)
out, err := app.core.GetCampaign(id, "")
out, err := app.core.GetCampaign(id, "", "")
if err != nil {
return err
}
@ -244,7 +246,7 @@ func handleUpdateCampaign(c echo.Context) error {
}
cm, err := app.core.GetCampaign(id, "")
cm, err := app.core.GetCampaign(id, "", "")
if err != nil {
return err
}
@ -314,9 +316,10 @@ func handleUpdateCampaignArchive(c echo.Context) error {
)
req := struct {
Archive bool `json:"archive"`
TemplateID int `json:"archive_template_id"`
Meta models.JSON `json:"archive_meta"`
Archive bool `json:"archive"`
TemplateID int `json:"archive_template_id"`
Meta models.JSON `json:"archive_meta"`
ArchiveSlug string `json:"archive_slug"`
}{}
// Get and validate fields.
@ -324,11 +327,19 @@ func handleUpdateCampaignArchive(c echo.Context) error {
return err
}
if err := app.core.UpdateCampaignArchive(id, req.Archive, req.TemplateID, req.Meta); err != nil {
if req.ArchiveSlug != "" {
// Format the slug to be alpha-numeric-dash.
s := strings.ToLower(req.ArchiveSlug)
s = strings.TrimSpace(regexSlug.ReplaceAllString(s, " "))
s = regexpSpaces.ReplaceAllString(s, "-")
req.ArchiveSlug = s
}
if err := app.core.UpdateCampaignArchive(id, req.Archive, req.TemplateID, req.Meta, req.ArchiveSlug); err != nil {
return err
}
return c.JSON(http.StatusOK, okResp{true})
return c.JSON(http.StatusOK, okResp{req})
}
// handleDeleteCampaign handles campaign deletion.
@ -571,6 +582,18 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
c.ArchiveMeta = json.RawMessage("{}")
}
if c.ArchiveSlug.String != "" {
// Format the slug to be alpha-numeric-dash.
s := strings.ToLower(c.ArchiveSlug.String)
s = strings.TrimSpace(regexSlug.ReplaceAllString(s, " "))
s = regexpSpaces.ReplaceAllString(s, "-")
c.ArchiveSlug = null.NewString(s, true)
} else {
// If there's no slug set, set it to NULL in the DB.
c.ArchiveSlug.Valid = false
}
return c, nil
}

View file

@ -206,7 +206,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
if app.constants.EnablePublicArchive {
e.GET("/archive", handleCampaignArchivesPage)
e.GET("/archive.xml", handleGetCampaignArchivesFeed)
e.GET("/archive/:uuid", handleCampaignArchivePage)
e.GET("/archive/:id", handleCampaignArchivePage)
e.GET("/archive/latest", handleCampaignArchivePageLatest)
}

View file

@ -158,6 +158,7 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempo
campTplID,
pq.Int64Array{1},
false,
"welcome-to-listmonk",
archiveTplID,
`{"name": "Subscriber"}`,
nil,

View file

@ -50,7 +50,7 @@ func (s *store) NextSubscribers(campID, limit int) ([]models.Subscriber, error)
// GetCampaign fetches a campaign from the database.
func (s *store) GetCampaign(campID int) (*models.Campaign, error) {
var out = &models.Campaign{}
err := s.queries.GetCampaign.Get(out, campID, nil, "default")
err := s.queries.GetCampaign.Get(out, campID, nil, nil, "default")
return out, err
}

View file

@ -143,7 +143,7 @@ func handleViewCampaignMessage(c echo.Context) error {
)
// Get the campaign.
camp, err := app.core.GetCampaign(0, campUUID)
camp, err := app.core.GetCampaign(0, campUUID, "")
if err != nil {
if er, ok := err.(*echo.HTTPError); ok {
if er.Code == http.StatusBadRequest {

View file

@ -13,16 +13,43 @@ describe('Archive', () => {
cy.get('.modal input').clear().type('clone').click();
cy.get('.modal button.is-primary').click();
cy.wait(250);
cy.loginAndVisit('/campaigns');
cy.get('[data-cy=btn-clone]').first().click();
cy.get('.modal input').clear().type('clone2').click();
cy.get('.modal button.is-primary').click();
cy.wait(250);
cy.clickMenu('all-campaigns');
});
it('Starts un-archived campaign', () => {
it('Starts campaigns', () => {
cy.get('td[data-label=Status] a').eq(0).click();
cy.get('[data-cy=btn-start]').click();
cy.get('.modal button.is-primary').click();
cy.get('td[data-label=Status] a').eq(1).click();
cy.get('[data-cy=btn-start]').click();
cy.get('.modal button.is-primary').click();
cy.wait(1000);
});
it('Enables archive on one campaign (no slug)', () => {
cy.loginAndVisit('/campaigns');
cy.wait(250);
cy.get('td[data-label=Status] a').eq(0).click();
// Switch to archive tab and enable archive.
cy.get('.b-tabs nav a').eq(2).click();
cy.wait(500);
cy.get('[data-cy=btn-archive] .check').click();
cy.get('[data-cy=archive-slug]').clear();
cy.get('[data-cy=archive-meta]').clear()
.type('{"email": "archive@domain.com", "name": "Archive", "attribs": { "city": "Bengaluru"}}', { parseSpecialCharSequences: false });
cy.get('[data-cy=btn-save]').click();
cy.wait(250);
});
it('Enables archive on one campaign', () => {
cy.loginAndVisit('/campaigns');
cy.wait(250);
@ -32,21 +59,22 @@ describe('Archive', () => {
cy.get('.b-tabs nav a').eq(2).click();
cy.wait(500);
cy.get('[data-cy=btn-archive] .check').click();
cy.get('[data-cy=archive-meta]').clear()
.type('{"email": "archive@domain.com", "name": "Archive", "attribs": { "city": "Bengaluru"}}', { 'parseSpecialCharSequences': false });
// Start the campaign.
cy.get('[data-cy=archive-slug]').clear().type('my-archived-campaign');
cy.get('[data-cy=btn-save]').click();
cy.wait(500);
cy.get('[data-cy=btn-start]').click();
cy.get('.modal button.is-primary').click();
cy.wait(1000);
cy.wait(250);
});
it('Opens campaign archive page', () => {
cy.loginAndVisit(`${apiUrl}/archive`);
cy.get('li a').click();
cy.get('h3').contains('Hi Archive!');
cy.get('p').eq(0).contains('Bengaluru');
for (let i = 0; i < 2; i++) {
cy.loginAndVisit(`${apiUrl}/archive`);
cy.get('li a').eq(i).click();
cy.wait(250);
if (i === 0) {
cy.get('h3').contains('Hi Archive!');
cy.get('p').eq(0).contains('Bengaluru');
} else {
cy.get('h3').contains('Hi Subscriber!');
}
}
});
});

View file

@ -59,7 +59,7 @@
<form @submit.prevent="() => onSubmit(isNew ? 'create' : 'update')">
<b-field :label="$t('globals.fields.name')" label-position="on-border">
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" name="name" :disabled="!canEdit"
:placeholder="$t('globals.fields.name')" required />
:placeholder="$t('globals.fields.name')" required autofocus />
</b-field>
<b-field :label="$t('campaigns.subject')" label-position="on-border">
@ -201,19 +201,32 @@
<b-tab-item :label="$t('campaigns.archive')" icon="newspaper-variant-outline" value="archive" :disabled="isNew">
<section class="wrap">
<b-field :label="$t('campaigns.archiveEnable')" data-cy="btn-archive" :message="$t('campaigns.archiveHelp')">
<div class="columns">
<div class="column">
<b-switch data-cy="btn-archive" v-model="form.archive" :disabled="!canArchive" />
</div>
<div class="column is-12">
<a :href="`${settings['app.root_url']}/archive/${data.uuid}`" target="_blank" rel="noopener noreferer"
:class="{ 'has-text-grey-light': !form.archive }" aria-label="$t('campaigns.archive')">
<b-icon icon="link-variant" />
</a>
</div>
<div class="columns">
<div class="column is-4">
<b-field :label="$t('campaigns.archiveEnable')" data-cy="btn-archive"
:message="$t('campaigns.archiveHelp')">
<div class="columns">
<div class="column">
<b-switch data-cy="btn-archive" v-model="form.archive" :disabled="!canArchive" />
</div>
<div class="column is-12">
<a :href="`${settings['app.root_url']}/archive/${data.uuid}`" target="_blank" rel="noopener noreferer"
:class="{ 'has-text-grey-light': !form.archive }" aria-label="$t('campaigns.archive')">
<b-icon icon="link-variant" />
</a>
</div>
</div>
</b-field>
</div>
</b-field>
<div class="column is-8 has-text-right">
<b-field v-if="!canEdit && canArchive">
<b-button @click="onUpdateCampaignArchive" :loading="loading.campaigns" type="is-primary"
icon-left="content-save-outline" data-cy="btn-save">
{{ $t('globals.buttons.saveChanges') }}
</b-button>
</b-field>
</div>
</div>
<div class="columns">
<div class="column is-8">
@ -234,18 +247,18 @@
@click.prevent="onFillArchiveMeta">{}</a>
</div>
</div>
<b-field>
<b-field :label="$t('campaigns.archiveSlug')" label-position="on-border"
:message="$t('campaigns.archiveSlugHelp')">
<b-input :maxlength="200" :ref="'focus'" v-model="form.archiveSlug" name="archive_slug"
data-cy="archive-slug" :disabled="!canArchive || !form.archive" />
</b-field>
</b-field>
<b-field :label="$t('campaigns.archiveMeta')" :message="$t('campaigns.archiveMetaHelp')"
label-position="on-border">
<b-input v-model="form.archiveMetaStr" name="archive_meta" type="textarea" data-cy="archive-meta"
:disabled="!canArchive || !form.archive" rows="20" />
</b-field>
<b-field v-if="!canEdit && canArchive">
<b-button @click="onUpdateCampaignArchive" :loading="loading.campaigns" type="is-primary"
icon-left="content-save-outline" data-cy="btn-archive-save">
{{ $t('globals.buttons.saveChanges') }}
</b-button>
</b-field>
</section>
</b-tab-item><!-- archive -->
</b-tabs>
@ -295,6 +308,7 @@ export default Vue.extend({
// Binds form input values.
form: {
archiveSlug: null,
name: '',
subject: '',
fromEmail: '',
@ -470,6 +484,7 @@ export default Vue.extend({
createCampaign() {
const data = {
archiveSlug: this.form.subject,
name: this.form.name,
subject: this.form.subject,
lists: this.form.lists.map((l) => l.id),
@ -494,6 +509,7 @@ export default Vue.extend({
async updateCampaign(typ) {
const data = {
archive_slug: this.form.archiveSlug,
name: this.form.name,
subject: this.form.subject,
lists: this.form.lists.map((l) => l.id),
@ -523,6 +539,7 @@ export default Vue.extend({
return new Promise((resolve) => {
this.$api.updateCampaign(this.data.id, data).then((d) => {
this.data = d;
this.form.archiveSlug = d.archiveSlug;
this.$utils.toast(this.$t(typMsg, { name: d.name }));
resolve();
});
@ -538,9 +555,12 @@ export default Vue.extend({
archive: this.form.archive,
archive_template_id: this.form.archiveTemplateId,
archive_meta: JSON.parse(this.form.archiveMetaStr),
archive_slug: this.form.archiveSlug,
};
this.$api.updateCampaignArchive(this.data.id, data);
this.$api.updateCampaignArchive(this.data.id, data).then((d) => {
this.form.archiveSlug = d.archiveSlug;
});
},
// Starts or schedule a campaign.

View file

@ -145,8 +145,10 @@
<p v-if="stats.rate">
<label for="#"><b-icon icon="speedometer" size="is-small" /></label>
<span class="send-rate">
<b-tooltip :label="`${stats.netRate} / ${$t('campaigns.rateMinuteShort')} @
${$utils.duration(stats.startedAt, stats.updatedAt)}`" type="is-dark">
<b-tooltip
:label="`${stats.netRate} / ${$t('campaigns.rateMinuteShort')} @
${$utils.duration(stats.startedAt, stats.updatedAt)}`"
type="is-dark">
{{ stats.rate.toFixed(0) }} / {{ $t('campaigns.rateMinuteShort') }}
</b-tooltip>
</span>
@ -225,8 +227,7 @@
placeholder: $t('globals.fields.name'),
value: $t('campaigns.copyOf', { name: props.row.name }),
},
(name) => cloneCampaign(name, props.row))" data-cy="btn-clone"
:aria-label="$t('globals.buttons.clone')">
(name) => cloneCampaign(name, props.row))" data-cy="btn-clone" :aria-label="$t('globals.buttons.clone')">
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
<b-icon icon="file-multiple-outline" size="is-small" />
</b-tooltip>
@ -405,6 +406,7 @@ export default Vue.extend({
const data = {
name,
archive_slug: `${c.name}-2`,
subject: c.subject,
lists: c.lists.map((l) => l.id),
type: c.type,

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publica (en curs, aturada, finalitzada) el missatge de campanya a l'arxiu públic ",
"campaigns.archiveMeta": "Metadades de la campanya",
"campaigns.archiveMetaHelp": "Dades del subscriptor de prova per ser usat en el missatge públic que inclou nom, correu electrònic i qualsevol atribut opcional emprat en el missatge de campanya o plantilla.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Adjunts",
"campaigns.cantUpdate": "No es pot actualitzar una campanya en curs o ja finalitzada.",
"campaigns.clicks": "Clics",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Zveřejnit (bežící, pozastavenou, dokončenou) zprávu kampaně ve veřejném archivu",
"campaigns.archiveMeta": "Metadata kampaně",
"campaigns.archiveMetaHelp": "Použít prázdná data přihlášených ve veřejné zpráve včetně jména, emailu a jiných volitelných atributů použitých ve zprávách kampaně nebo šablonách.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Přílohy",
"campaigns.cantUpdate": "Nelze aktualizovat spuštěnou nebo dokončenou kampaň.",
"campaigns.clicks": "Klepnutí",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Cyhoeddi neges yr ymgyrch (wrthi'n rhedeg",
"campaigns.archiveMeta": "Ymgyrch metaddata",
"campaigns.archiveMetaHelp": "Data tanysgrifiwr ffug i'w defnyddio yn y neges gyhoeddus",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Atodiadau",
"campaigns.cantUpdate": "Does dim modd diweddaru ymgyrch fyw neu ymgyrch sydd wedi dod i ben.",
"campaigns.clicks": "Cliciau",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Udgiv (kør, hold pause, afslut) kampagnebesked til det offentlige arkiv.",
"campaigns.archiveMeta": "Kampagne metadata",
"campaigns.archiveMetaHelp": "Dummy abonnent-data til brug i den offebntlige besked herunder navn, e-mail og enhver valgfri egenskab, der bruges i kampagebeskeden eller skabelonen.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Vedhæftninger",
"campaigns.cantUpdate": "Kan ike opdatere en kørende eller afsluttet kampagne.",
"campaigns.clicks": "Klik",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Veröffentliche die Nachricht (laufende, pausierte, beendete) der Kampagne im öffentlichen Archiv.",
"campaigns.archiveMeta": "Kampagne Metadaten",
"campaigns.archiveMetaHelp": "Dummy-Abonnentendaten, die in der öffentlichen Nachricht verwendet werden sollen, einschließlich Name, E-Mail und alle optionalen Attribute, die in der Kampagnennachricht oder -vorlage verwendet werden.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Anhänge",
"campaigns.cantUpdate": "Eine laufende oder abgeschlossene Kampagne kann nicht geändert werden.",
"campaigns.clicks": "Klicks",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Δημοσιεύστε το μήνυμα της (σε εξέλιξη, σε παύση, ολοκληρωμένης) εκστρατείας στο δημόσιο αρχείο.",
"campaigns.archiveMeta": "Μεταδεδομένα εκστρατείας",
"campaigns.archiveMetaHelp": "Εικονικά δεδομένα συνδρομητή που χρησιμοποιούνται στο δημόσιο μήνυμα, συμπεριλαμβανομένου του ονόματος, της διεύθυνσης email και οποιωνδήποτε προαιρετικών χαρακτηριστικών που χρησιμοποιούνται στο μήνυμα ή το πρότυπο της εκστρατείας.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Συνημμένα",
"campaigns.cantUpdate": "Δεν είναι δυνατή η ενημέρωση μιας εκστρατείας που βρίσκεται σε εξέλιξη ή έχει ολοκληρωθεί.",
"campaigns.clicks": "Κλικ",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
"campaigns.archiveMeta": "Campaign metadata",
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Attachments",
"campaigns.cantUpdate": "Cannot update a running or a finished campaign.",
"campaigns.clicks": "Clicks",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publicar los mensajes de las campañas (en marcha, pausadas y terminadas) en el archivo público.",
"campaigns.archiveMeta": "Metadata de la campaña",
"campaigns.archiveMetaHelp": "Información de suscripción de ejemplo (por defecto) para ser usada en el mensaje público incluido nombre, correo electrónico, o cualquier valor accesible mediante atributos `{}` opcionales tanto en el mensaje de la campaña como en la plantilla.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Archivos adjuntos",
"campaigns.cantUpdate": "No es posible actualizar una campaña iniciada o finalizada.",
"campaigns.clicks": "Clics",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Julkaise (käynnissä, pausessa, valmis) kampanjaviesti julkisessa arkistossa.",
"campaigns.archiveMeta": "Kampanjan metatiedot",
"campaigns.archiveMetaHelp": "Tietuekuvioita voidaan käyttää julkisessa viestissä, joissa on mukana nimi, sähköposti ja kampanjaviestissä tai mallipohjassa käytetyt valinnaiset attribuutit.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Liitteet",
"campaigns.cantUpdate": "Käynnissä olevaa tai päättynyttä kampanjaa ei voi päivittää.",
"campaigns.clicks": "Klikkaukset",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publier (en cours, en pause, terminé) le message de la campagne sur l'archive publique.",
"campaigns.archiveMeta": "Métadonnées de la campagne",
"campaigns.archiveMetaHelp": "Données d'abonné fictives à utiliser dans le message public, notamment le nom, l'adresse électronique et tout attribut facultatif utilisé dans le message ou le modèle de la campagne.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Pièces jointes",
"campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.",
"campaigns.clicks": "Clics",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "פרסם (פועל, מושהה, הושלם) את הודעת הקמפיין בארכיון הציבורי.",
"campaigns.archiveMeta": "מטא-נתונים של קמפיין",
"campaigns.archiveMetaHelp": "נתוני חבוי של המנויים לשימוש בהודעה ציבורית כולל שם, דואר אלקטרוני, וכל מאפיינים אופציונליים שבשימוש בהודעת הקמפיין או התבנית.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "קבצים מצורפים",
"campaigns.cantUpdate": "לא ניתן לעדכן קמפיין בריצה או שהושלם.",
"campaigns.clicks": "לחיצות",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "A kampány nyilvános archívumba mentése, közzététele.",
"campaigns.archiveMeta": "Kapány metaadat",
"campaigns.archiveMetaHelp": "A nyilvánosan közzétett kampányüzenetbe helyettesítendő adatok (pl. név, e-mail cím, és amiket a sablon használ).",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Mellékletek",
"campaigns.cantUpdate": "Nem lehet frissíteni futó vagy befejezett kampányt.",
"campaigns.clicks": "Kattintások",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Pubblicare i messaggi delle campagne (avviate, pausate, finite) nel archivio pubblico.",
"campaigns.archiveMeta": "Metadati della campagna",
"campaigns.archiveMetaHelp": "Dati fittizi dell'iscritto da utilizzare nel messaggio pubblico, inclusi nome, e-mail ed eventuali attributi facoltativi utilizzati nel messaggio o nel modello della campagna.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Allegati",
"campaigns.cantUpdate": "Impossibile aggiornare una campagna in corso o già effettuata.",
"campaigns.clicks": "Click",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "公開アーカイブにキャンペーンメッセージを発行(実行中, 停止された, 終わりましたキャンペーン全部含めて)。",
"campaigns.archiveMeta": "キャンペーンメタデータ",
"campaigns.archiveMetaHelp": "キャンペーンのメッセージやテンプレートに使う偽データ(名やメールアドレスや設定)。",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "添付ファイル",
"campaigns.cantUpdate": "実行中又は終了しているキャンペーンの更新はできません。",
"campaigns.clicks": "クリック",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "പ്രചാരണ സന്ദേശം (റൺ ചെയ്യുന്ന, താൽക്കാലികമായി നിർത്തിയ, പൂർത്തിയായ) പൊതു ആർക്കൈവിൽ പ്രസിദ്ധീകരിക്കുക.",
"campaigns.archiveMeta": "കാമ്പെയ്‌ൻ മെറ്റാഡാറ്റ",
"campaigns.archiveMetaHelp": "പേര്, ഇമെയിൽ, പ്രചാരണ സന്ദേശത്തിലോ ടെംപ്ലേറ്റിലോ ഉപയോഗിക്കുന്ന ഏതെങ്കിലും ഓപ്ഷണൽ ആട്രിബ്യൂട്ടുകൾ എന്നിവയുൾപ്പെടെ പൊതു സന്ദേശത്തിൽ ഉപയോഗിക്കാനുള്ള ഡമ്മി സബ്സ്ക്രൈബർ ഡാറ്റ.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "അറ്റാച്ച്മെന്റ്സ്",
"campaigns.cantUpdate": "ഇപ്പോൾ നടന്നുകൊണ്ടിരിയ്ക്കുന്നതോ, അവസാനിച്ചതോ ആയ ക്യാമ്പേയ്ൻ പുതുക്കാനാകില്ല.",
"campaigns.clicks": "ക്ലീക്കുകൾ",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publiceer (lopende, gepauzeerde, afgeronde) het campange bericht naar het publiek archief.",
"campaigns.archiveMeta": "Campagne metadata",
"campaigns.archiveMetaHelp": "Dummy-abonneegegevens om te gebruiken in het openbare bericht, inclusief naam, e-mail en eventuele optionele attributen die worden gebruikt in het campagnebericht of de sjabloon.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Bijlagen",
"campaigns.cantUpdate": "Kan een lopende of afgelopen campagne niet updaten.",
"campaigns.clicks": "Kliks",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Opublikuj (w trakcie, zatrzymane, zakończone) treść kampanii do publicznego archiwum.",
"campaigns.archiveMeta": "Metadane kampanii",
"campaigns.archiveMetaHelp": "Dane podstawione subskrybenta do użycia w publicznym archiwum. W tym nazwa, email, i dowolne opcjonalne atrybuty użyte w szablonie kampanii.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Załączniki",
"campaigns.cantUpdate": "Nie można aktualizować aktywnej ani zakończonej kampanii",
"campaigns.clicks": "Kliknięcia",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publicar (executando, pausada, finalizada) a mensagem da campanha no arquivo publico.",
"campaigns.archiveMeta": "Metadados da campanha",
"campaigns.archiveMetaHelp": "Dados de assinante fictício para utilizar na mensagem publica incluindo nome, email e qualquer atributo opcional usado na mensagem ou template da campanha.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Anexos",
"campaigns.cantUpdate": "Não é possível atualizar uma campanha em execução ou finalizada.",
"campaigns.clicks": "Cliques",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publicar (em execução, em pausa e terminadas) as mensagens da campanha no arquivo público.",
"campaigns.archiveMeta": "Metadados da campanha",
"campaigns.archiveMetaHelp": "Dados do subscritor modelo a usar em mensagens públicas, tais como nome, email e quais quer outros atributos opcionais usados na mensagem ou template da campanha.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Anexos",
"campaigns.cantUpdate": "Não é possível atualizar uma campanha em curso ou terminada.",
"campaigns.clicks": "Cliques",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publicați (rulând, întrerupt, terminat) mesajul campaniei în arhiva publică.",
"campaigns.archiveMeta": "Metadatele campaniei",
"campaigns.archiveMetaHelp": "Datele abonaților inactivi de utilizat în mesajul public, inclusiv numele, e-mailul și orice atribute opționale utilizate în mesajul sau șablonul campaniei.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Fișiere atașate",
"campaigns.cantUpdate": "Nu se poate actualiza o campanie care rulează sau s-a terminat.",
"campaigns.clicks": "Click-uri",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Опубликовать (запущено, на паузе, завершено) сообщение кампании в общедоступном архиве.",
"campaigns.archiveMeta": "Метаданные кампании",
"campaigns.archiveMetaHelp": "Данные фиктивных подписчиков для использования в публичном сообщении, включая имя, электронную почту и любые дополнительные атрибуты, используемые в сообщении или шаблоне кампании.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Вложения",
"campaigns.cantUpdate": "Не возможно обновить запущенную или завершённую кампанию.",
"campaigns.clicks": "Клики",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
"campaigns.archiveMeta": "Campaign metadata",
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Attachments",
"campaigns.cantUpdate": "Cannot update a running or a finished campaign.",
"campaigns.clicks": "Clicks",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Zverejniť (prebiehajúcu, pozastavenú, dokončenú) správu kampane vo verejnom archíve",
"campaigns.archiveMeta": "Metadáta kampane",
"campaigns.archiveMetaHelp": "Použíť prázdne dáta prihlásených vo verejnom archíve vrátane mena, emailu a iných voliteľných atribútov použitých v správach kampane aleebo šablónach.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Prílohy",
"campaigns.cantUpdate": "Nedá sa aktualizovať spustená alebo dokončená kampaň.",
"campaigns.clicks": "Kliknutia",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Kampanya mesajını genel arşivde yayınlayın (çalışıyor, duraklatıldı, bitti).",
"campaigns.archiveMeta": "Kampanya meta verisi",
"campaigns.archiveMetaHelp": "Ad, e-posta ve kampanya mesajında veya şablonunda kullanılan tüm isteğe bağlı öznitelikler dahil olmak üzere genel mesajda kullanılacak kukla abone verileri.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Ekler",
"campaigns.cantUpdate": "Gönderilmekte olan veya gönderilmiş kampaynalar güncellenemez.",
"campaigns.clicks": "Tıklama",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Розмістити лист кампанії (запущеної, призупиненої, завершеної) в загальнодоступному архіві.",
"campaigns.archiveMeta": "Метадані кампанії",
"campaigns.archiveMetaHelp": "Дані вигаданої підписни_ці для використання в загальнодоступному листі, зокрема ім'я (name), е-пошта (email) та будь-які необов'язкові атрибути, використані в листі чи шаблоні кампанії.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Вкладення",
"campaigns.cantUpdate": "Неможливо оновити запущену чи завершену кампанію.",
"campaigns.clicks": "Переходи",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "Xuất bản (đang chạy, tạm dừng, hoàn thành) tin nhắn chiến dịch vào lưu trữ công khai.",
"campaigns.archiveMeta": "Dữ liệu siêu của chiến dịch",
"campaigns.archiveMetaHelp": "Dữ liệu giả của người đăng ký để sử dụng trong tin nhắn công khai bao gồm tên, email và bất kỳ thuộc tính tùy chọn nào được sử dụng trong tin nhắn chiến dịch hoặc mẫu.",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "Tệp đính kèm",
"campaigns.cantUpdate": "Không thể cập nhật chiến dịch đang chạy hoặc đã kết thúc.",
"campaigns.clicks": "Số lần nhấp chuột",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "在公共档案中发布(运行、暂停、完成)活动消息。",
"campaigns.archiveMeta": "活动元数据",
"campaigns.archiveMetaHelp": "在公共消息中使用的模拟订阅者数据,包括姓名、电子邮件以及活动消息或模板中使用的任何可选属性。",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "附件",
"campaigns.cantUpdate": "无法更新正在运行或已完成的广告系列。",
"campaigns.clicks": "点击次数",

View file

@ -23,6 +23,8 @@
"campaigns.archiveHelp": "在公開歸檔中發送(運行中、暫停、已完成)的活動訊息。",
"campaigns.archiveMeta": "活動中繼資料",
"campaigns.archiveMetaHelp": "用於公開訊息的虛擬訂閱者資料,包括姓名、電子郵件和任何在活動訊息或範本中使用的選擇性屬性。",
"campaigns.archiveSlug": "URL Slug",
"campaigns.archiveSlugHelp": "A short name for the page to be used in the public URL. eg: my-newsletter-edition-2",
"campaigns.attachments": "附件",
"campaigns.cantUpdate": "無法更新正在運行或已完成的廣告系列。",
"campaigns.clicks": "點擊次數",

View file

@ -65,13 +65,13 @@ func (c *Core) QueryCampaigns(searchStr string, statuses, tags []string, orderBy
}
// GetCampaign retrieves a campaign.
func (c *Core) GetCampaign(id int, uuid string) (models.Campaign, error) {
return c.getCampaign(id, uuid, campaignTplDefault)
func (c *Core) GetCampaign(id int, uuid, archiveSlug string) (models.Campaign, error) {
return c.getCampaign(id, uuid, archiveSlug, campaignTplDefault)
}
// GetArchivedCampaign retrieves a campaign with the archive template body.
func (c *Core) GetArchivedCampaign(id int, uuid string) (models.Campaign, error) {
out, err := c.getCampaign(id, uuid, campaignTplArchive)
func (c *Core) GetArchivedCampaign(id int, uuid, archiveSlug string) (models.Campaign, error) {
out, err := c.getCampaign(id, uuid, archiveSlug, campaignTplArchive)
if err != nil {
return out, err
}
@ -87,7 +87,7 @@ func (c *Core) GetArchivedCampaign(id int, uuid string) (models.Campaign, error)
// getCampaign retrieves a campaign. If typlType=default, then the campaign's
// template body is returned as "template_body". If tplType="archive",
// the archive template is returned.
func (c *Core) getCampaign(id int, uuid string, tplType string) (models.Campaign, error) {
func (c *Core) getCampaign(id int, uuid, archiveSlug string, tplType string) (models.Campaign, error) {
// Unsafe to ignore scanning fields not present in models.Campaigns.
var uu interface{}
if uuid != "" {
@ -95,7 +95,7 @@ func (c *Core) getCampaign(id int, uuid string, tplType string) (models.Campaign
}
var out models.Campaigns
if err := c.q.GetCampaign.Select(&out, id, uu, tplType); err != nil {
if err := c.q.GetCampaign.Select(&out, id, uu, archiveSlug, tplType); err != nil {
// if err := c.db.Select(&out, stmt, 0, pq.Array([]string{}), queryStr, 0, 1); err != nil {
c.log.Printf("error fetching campaign: %v", err)
return models.Campaign{}, echo.NewHTTPError(http.StatusInternalServerError,
@ -185,6 +185,7 @@ func (c *Core) CreateCampaign(o models.Campaign, listIDs []int, mediaIDs []int)
o.TemplateID,
pq.Array(listIDs),
o.Archive,
o.ArchiveSlug,
o.ArchiveTemplateID,
o.ArchiveMeta,
pq.Array(mediaIDs),
@ -198,7 +199,7 @@ func (c *Core) CreateCampaign(o models.Campaign, listIDs []int, mediaIDs []int)
c.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
}
out, err := c.GetCampaign(newID, "")
out, err := c.GetCampaign(newID, "", "")
if err != nil {
return models.Campaign{}, err
}
@ -223,6 +224,7 @@ func (c *Core) UpdateCampaign(id int, o models.Campaign, listIDs []int, mediaIDs
o.TemplateID,
pq.Array(listIDs),
o.Archive,
o.ArchiveSlug,
o.ArchiveTemplateID,
o.ArchiveMeta,
pq.Array(mediaIDs))
@ -232,7 +234,7 @@ func (c *Core) UpdateCampaign(id int, o models.Campaign, listIDs []int, mediaIDs
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
}
out, err := c.GetCampaign(id, "")
out, err := c.GetCampaign(id, "", "")
if err != nil {
return models.Campaign{}, err
}
@ -242,7 +244,7 @@ func (c *Core) UpdateCampaign(id int, o models.Campaign, listIDs []int, mediaIDs
// UpdateCampaignStatus updates a campaign's status, eg: draft to running.
func (c *Core) UpdateCampaignStatus(id int, status string) (models.Campaign, error) {
cm, err := c.GetCampaign(id, "")
cm, err := c.GetCampaign(id, "", "")
if err != nil {
return models.Campaign{}, err
}
@ -297,8 +299,8 @@ func (c *Core) UpdateCampaignStatus(id int, status string) (models.Campaign, err
}
// UpdateCampaignArchive updates a campaign's archive properties.
func (c *Core) UpdateCampaignArchive(id int, enabled bool, tplID int, meta models.JSON) error {
if _, err := c.q.UpdateCampaignArchive.Exec(id, enabled, tplID, meta); err != nil {
func (c *Core) UpdateCampaignArchive(id int, enabled bool, tplID int, meta models.JSON, archiveSlug string) error {
if _, err := c.q.UpdateCampaignArchive.Exec(id, enabled, archiveSlug, tplID, meta); err != nil {
c.log.Printf("error updating campaign: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,

View file

@ -204,7 +204,7 @@ func (p *pipe) cleanup() {
// Fetch the up-to-date campaign status from the DB.
c, err := p.m.store.GetCampaign(p.camp.ID)
if err != nil {
p.m.log.Printf("error fetching campaign (%s) for ending", p.camp.Name)
p.m.log.Printf("error fetching campaign (%s) for ending: %v", p.camp.Name, err)
return
}

View file

@ -18,5 +18,9 @@ func V3_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
return err
}
if _, err := db.Exec(`ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS archive_slug TEXT NULL UNIQUE`); err != nil {
return err
}
return nil
}

View file

@ -252,6 +252,7 @@ type Campaign struct {
TemplateID int `db:"template_id" json:"template_id"`
Messenger string `db:"messenger" json:"messenger"`
Archive bool `db:"archive" json:"archive"`
ArchiveSlug null.String `db:"archive_slug" json:"archive_slug"`
ArchiveTemplateID int `db:"archive_template_id" json:"archive_template_id"`
ArchiveMeta json.RawMessage `db:"archive_meta" json:"archive_meta"`

View file

@ -492,16 +492,16 @@ counts AS (
AND subscribers.status='enabled'
),
camp AS (
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, headers, tags, messenger, template_id, to_send, max_subscriber_id, archive, archive_template_id, archive_meta)
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, headers, tags, messenger, template_id, to_send, max_subscriber_id, archive, archive_slug, archive_template_id, archive_meta)
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12,
(SELECT id FROM tpl), (SELECT to_send FROM counts),
(SELECT max_sub_id FROM counts), $15,
(CASE WHEN $16 = 0 THEN (SELECT id FROM tpl) ELSE $16 END), $17
(SELECT max_sub_id FROM counts), $15, $16,
(CASE WHEN $17 = 0 THEN (SELECT id FROM tpl) ELSE $17 END), $18
RETURNING id
),
med AS (
INSERT INTO campaign_media (campaign_id, media_id, filename)
(SELECT (SELECT id FROM camp), id, filename FROM media WHERE id=ANY($18::INT[]))
(SELECT (SELECT id FROM camp), id, filename FROM media WHERE id=ANY($19::INT[]))
)
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
(SELECT (SELECT id FROM camp), id, name FROM lists WHERE id=ANY($14::INT[]))
@ -517,7 +517,7 @@ INSERT INTO campaign_lists (campaign_id, list_id, list_name)
SELECT c.id, c.uuid, c.name, c.subject, c.from_email,
c.messenger, c.started_at, c.to_send, c.sent, c.type,
c.body, c.altbody, c.send_at, c.headers, c.status, c.content_type, c.tags,
c.template_id, c.archive, c.archive_template_id, c.archive_meta,
c.template_id, c.archive, c.archive_slug, c.archive_template_id, c.archive_meta,
c.created_at, c.updated_at,
COUNT(*) OVER () AS total,
(
@ -539,10 +539,14 @@ SELECT campaigns.*,
COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
FROM campaigns
LEFT JOIN templates ON (
CASE WHEN $3 = 'default' THEN templates.id = campaigns.template_id
CASE WHEN $4 = 'default' THEN templates.id = campaigns.template_id
ELSE templates.id = campaigns.archive_template_id END
)
WHERE CASE WHEN $1 > 0 THEN campaigns.id = $1 ELSE uuid = $2 END;
WHERE CASE
WHEN $1 > 0 THEN campaigns.id = $1
WHEN $3 != '' THEN campaigns.archive_slug = $3
ELSE uuid = $2
END;
-- name: get-archived-campaigns
SELECT COUNT(*) OVER () AS total, campaigns.*,
@ -808,8 +812,9 @@ WITH camp AS (
messenger=$12,
template_id=$13,
archive=$15,
archive_template_id=$16,
archive_meta=$17,
archive_slug=$16,
archive_template_id=$17,
archive_meta=$18,
updated_at=NOW()
WHERE id = $1 RETURNING id
),
@ -819,11 +824,11 @@ clists AS (
),
med AS (
DELETE FROM campaign_media WHERE campaign_id = $1
AND media_id IS NULL or NOT(media_id = ANY($18)) RETURNING media_id
AND media_id IS NULL or NOT(media_id = ANY($19)) RETURNING media_id
),
medi AS (
INSERT INTO campaign_media (campaign_id, media_id, filename)
(SELECT $1 AS campaign_id, id, filename FROM media WHERE id=ANY($18::INT[]))
(SELECT $1 AS campaign_id, id, filename FROM media WHERE id=ANY($19::INT[]))
ON CONFLICT (campaign_id, media_id) DO NOTHING
)
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
@ -844,8 +849,9 @@ UPDATE campaigns SET status=$2, updated_at=NOW() WHERE id = $1;
-- name: update-campaign-archive
UPDATE campaigns SET
archive=$2,
archive_template_id=(CASE WHEN $3 > 0 THEN $3 ELSE archive_template_id END),
archive_meta=(CASE WHEN $4::TEXT != '' THEN $4::JSONB ELSE archive_meta END),
archive_slug=(CASE WHEN $3::TEXT = '' THEN NULL ELSE $3 END),
archive_template_id=(CASE WHEN $4 > 0 THEN $4 ELSE archive_template_id END),
archive_meta=(CASE WHEN $5::TEXT != '' THEN $5::JSONB ELSE archive_meta END),
updated_at=NOW()
WHERE id=$1;

View file

@ -103,6 +103,7 @@ CREATE TABLE campaigns (
-- Publishing.
archive BOOLEAN NOT NULL DEFAULT false,
archive_slug TEXT NULL UNIQUE,
archive_template_id INTEGER REFERENCES templates(id) ON DELETE SET DEFAULT DEFAULT 1,
archive_meta JSONB NOT NULL DEFAULT '{}',