Add 'send campaign test' feature
This commit is contained in:
parent
3a1faf0faa
commit
d89b22e757
100
campaigns.go
100
campaigns.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -24,6 +25,9 @@ type campaignReq struct {
|
||||||
models.Campaign
|
models.Campaign
|
||||||
MessengerID string `json:"messenger"`
|
MessengerID string `json:"messenger"`
|
||||||
Lists pq.Int64Array `json:"lists"`
|
Lists pq.Int64Array `json:"lists"`
|
||||||
|
|
||||||
|
// This is only relevant to campaign test requests.
|
||||||
|
SubscriberEmails pq.StringArray `json:"subscribers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type campaignStats struct {
|
type campaignStats struct {
|
||||||
|
@ -131,7 +135,8 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||||
}
|
}
|
||||||
tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, body)
|
tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Error compiling template: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the message body.
|
// Render the message body.
|
||||||
|
@ -139,7 +144,8 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||||
if err := tpl.ExecuteTemplate(&out,
|
if err := tpl.ExecuteTemplate(&out,
|
||||||
runner.BaseTPL,
|
runner.BaseTPL,
|
||||||
runner.Message{Campaign: &camp, Subscriber: &sub, UnsubscribeURL: "#dummy"}); err != nil {
|
runner.Message{Campaign: &camp, Subscriber: &sub, UnsubscribeURL: "#dummy"}); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error executing template: %v", err))
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Error executing template: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.HTML(http.StatusOK, out.String())
|
return c.HTML(http.StatusOK, out.String())
|
||||||
|
@ -408,6 +414,91 @@ func handleGetCampaignMessengers(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
|
return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleTestCampaign handles the sending of a campaign message to
|
||||||
|
// arbitrary subscribers for testing.
|
||||||
|
func handleTestCampaign(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
campID, _ = strconv.Atoi(c.Param("id"))
|
||||||
|
req campaignReq
|
||||||
|
)
|
||||||
|
|
||||||
|
if campID < 1 {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid campaign ID.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate fields.
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Validate.
|
||||||
|
if err := validateCampaignFields(req); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if len(req.SubscriberEmails) == 0 {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "No subscribers to target.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the subscribers.
|
||||||
|
for i := 0; i < len(req.SubscriberEmails); i++ {
|
||||||
|
req.SubscriberEmails[i] = strings.ToLower(strings.TrimSpace(req.SubscriberEmails[i]))
|
||||||
|
}
|
||||||
|
var subs models.Subscribers
|
||||||
|
if err := app.Queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err)))
|
||||||
|
} else if len(subs) == 0 {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "No known subscribers given.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The campaign.
|
||||||
|
var camp models.Campaign
|
||||||
|
if err := app.Queries.GetCampaignForPreview.Get(&camp, campID); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override certain values in the DB with incoming values.
|
||||||
|
camp.Name = req.Name
|
||||||
|
camp.Subject = req.Subject
|
||||||
|
camp.FromEmail = req.FromEmail
|
||||||
|
camp.Body = req.Body
|
||||||
|
|
||||||
|
// Send the test messages.
|
||||||
|
for _, s := range subs {
|
||||||
|
if err := sendTestMessage(&s, &camp, app); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error sending test: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, okResp{true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendTestMessage takes a campaign and a subsriber and sends out a sample campain message.
|
||||||
|
func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) error {
|
||||||
|
tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, camp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error compiling template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the message body.
|
||||||
|
var out = bytes.Buffer{}
|
||||||
|
if err := tpl.ExecuteTemplate(&out,
|
||||||
|
runner.BaseTPL,
|
||||||
|
runner.Message{Campaign: camp, Subscriber: sub, UnsubscribeURL: "#dummy"}); err != nil {
|
||||||
|
return fmt.Errorf("Error executing template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Messenger.Push(camp.FromEmail, sub.Email, camp.Subject, []byte(out.Bytes())); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateCampaignFields validates incoming campaign field values.
|
// validateCampaignFields validates incoming campaign field values.
|
||||||
func validateCampaignFields(c campaignReq) error {
|
func validateCampaignFields(c campaignReq) error {
|
||||||
if !regexFromAddress.Match([]byte(c.FromEmail)) {
|
if !regexFromAddress.Match([]byte(c.FromEmail)) {
|
||||||
|
@ -434,6 +525,11 @@ func validateCampaignFields(c campaignReq) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := runner.CompileMessageTemplate(tplTag, c.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error compiling campaign body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Editor extends React.PureComponent {
|
||||||
rawInput: null,
|
rawInput: null,
|
||||||
selContentType: "richtext",
|
selContentType: "richtext",
|
||||||
contentType: "richtext",
|
contentType: "richtext",
|
||||||
body: "",
|
body: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
quillModules = {
|
quillModules = {
|
||||||
|
@ -129,7 +129,9 @@ class Editor extends React.PureComponent {
|
||||||
modules={ this.quillModules }
|
modules={ this.quillModules }
|
||||||
defaultValue={ this.props.record.body }
|
defaultValue={ this.props.record.body }
|
||||||
ref={ (o) => {
|
ref={ (o) => {
|
||||||
|
if(o) {
|
||||||
this.setState({ quill: o })
|
this.setState({ quill: o })
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onChange={ () => {
|
onChange={ () => {
|
||||||
if(!this.state.quill) {
|
if(!this.state.quill) {
|
||||||
|
@ -167,7 +169,8 @@ class Editor extends React.PureComponent {
|
||||||
class TheFormDef extends React.PureComponent {
|
class TheFormDef extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
editorVisible: false,
|
editorVisible: false,
|
||||||
sendLater: false
|
sendLater: false,
|
||||||
|
loading: false
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
|
@ -209,6 +212,7 @@ class TheFormDef extends React.PureComponent {
|
||||||
values.content_type = this.props.contentType
|
values.content_type = this.props.contentType
|
||||||
|
|
||||||
// Create a new campaign.
|
// Create a new campaign.
|
||||||
|
this.setState({ loading: true })
|
||||||
if(!this.props.isSingle) {
|
if(!this.props.isSingle) {
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, values).then((resp) => {
|
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, values).then((resp) => {
|
||||||
notification["success"]({ placement: "topRight",
|
notification["success"]({ placement: "topRight",
|
||||||
|
@ -218,6 +222,7 @@ class TheFormDef extends React.PureComponent {
|
||||||
this.props.route.history.push(cs.Routes.ViewCampaign.replace(":id", resp.data.data.id))
|
this.props.route.history.push(cs.Routes.ViewCampaign.replace(":id", resp.data.data.id))
|
||||||
this.props.fetchRecord(resp.data.data.id)
|
this.props.fetchRecord(resp.data.data.id)
|
||||||
this.props.setCurrentTab("content")
|
this.props.setCurrentTab("content")
|
||||||
|
this.setState({ loading: false })
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
|
@ -228,11 +233,41 @@ class TheFormDef extends React.PureComponent {
|
||||||
description: `"${values["name"]}" updated` })
|
description: `"${values["name"]}" updated` })
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
|
this.setState({ loading: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleTestCampaign = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
this.props.form.validateFields((err, values) => {
|
||||||
|
if (err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!values.tags) {
|
||||||
|
values.tags = []
|
||||||
|
}
|
||||||
|
|
||||||
|
values.id = this.props.record.id
|
||||||
|
values.body = this.props.body
|
||||||
|
values.content_type = this.props.contentType
|
||||||
|
|
||||||
|
this.setState({ loading: true })
|
||||||
|
this.props.request(cs.Routes.TestCampaign, cs.MethodPost, values).then((resp) => {
|
||||||
|
this.setState({ loading: false })
|
||||||
|
notification["success"]({ placement: "topRight",
|
||||||
|
message: "Test sent",
|
||||||
|
description: `Test messages sent` })
|
||||||
|
}).catch(e => {
|
||||||
|
this.setState({ loading: false })
|
||||||
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { record } = this.props;
|
const { record } = this.props;
|
||||||
const { getFieldDecorator } = this.props.form
|
const { getFieldDecorator } = this.props.form
|
||||||
|
@ -244,6 +279,7 @@ class TheFormDef extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Spin spinning={ this.state.loading }>
|
||||||
<Form onSubmit={ this.handleSubmit }>
|
<Form onSubmit={ this.handleSubmit }>
|
||||||
<Form.Item {...formItemLayout} label="Campaign name">
|
<Form.Item {...formItemLayout} label="Campaign name">
|
||||||
{getFieldDecorator("name", {
|
{getFieldDecorator("name", {
|
||||||
|
@ -328,17 +364,22 @@ class TheFormDef extends React.PureComponent {
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
}
|
||||||
</Form>
|
|
||||||
|
|
||||||
{ this.props.isSingle &&
|
{ this.props.isSingle &&
|
||||||
<div>
|
<div>
|
||||||
<hr />
|
<hr />
|
||||||
<Form.Item {...formItemLayout} label="Send test e-mails" extra="Hit Enter after typing an e-mail to add multiple emails">
|
<Form.Item {...formItemLayout} label="Send test messages" extra="Hit Enter after typing an address to add multiple recipients. The addresses must belong to existing subscribers.">
|
||||||
<Select mode="tags" style={{ minWidth: 320 }}></Select>
|
{getFieldDecorator("subscribers")(
|
||||||
<div><Button htmlType="submit"><Icon type="mail" /> Send test</Button></div>
|
<Select mode="tags" style={{ width: "100%" }}></Select>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item {...formItemLayout} label=" " colon={ false }>
|
||||||
|
<Button onClick={ this.handleTestCampaign }><Icon type="mail" /> Send test</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
@ -381,7 +422,7 @@ class Campaign extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRecord = (id) => {
|
fetchRecord = (id) => {
|
||||||
this.props.request(cs.Routes.GetCampaigns, cs.MethodGet, { id: id }).then((r) => {
|
this.props.request(cs.Routes.GetCampaign, cs.MethodGet, { id: id }).then((r) => {
|
||||||
const record = r.data.data
|
const record = r.data.data
|
||||||
this.setState({ record: record, loading: false })
|
this.setState({ record: record, loading: false })
|
||||||
|
|
||||||
|
@ -440,7 +481,7 @@ class Campaign extends React.PureComponent {
|
||||||
record={ this.state.record }
|
record={ this.state.record }
|
||||||
isSingle={ this.state.record.id ? true : false }
|
isSingle={ this.state.record.id ? true : false }
|
||||||
messengers={ this.state.messengers }
|
messengers={ this.state.messengers }
|
||||||
body={ this.state.body }
|
body={ this.state.body ? this.state.body : this.state.record.body }
|
||||||
contentType={ this.state.contentType }
|
contentType={ this.state.contentType }
|
||||||
formDisabled={ this.state.formDisabled }
|
formDisabled={ this.state.formDisabled }
|
||||||
fetchRecord={ this.fetchRecord }
|
fetchRecord={ this.fetchRecord }
|
||||||
|
@ -476,8 +517,7 @@ class Campaign extends React.PureComponent {
|
||||||
<Media { ...{ ...this.props,
|
<Media { ...{ ...this.props,
|
||||||
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
|
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
|
||||||
onCancel: this.toggleMedia,
|
onCancel: this.toggleMedia,
|
||||||
onOk: this.toggleMedia
|
onOk: this.toggleMedia }} />
|
||||||
}} />
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{ this.state.previewRecord &&
|
{ this.state.previewRecord &&
|
||||||
|
|
|
@ -70,8 +70,10 @@ export const Routes = {
|
||||||
ViewCampaign: "/campaigns/:id",
|
ViewCampaign: "/campaigns/:id",
|
||||||
GetCampaignMessengers: "/api/campaigns/messengers",
|
GetCampaignMessengers: "/api/campaigns/messengers",
|
||||||
GetCampaigns: "/api/campaigns",
|
GetCampaigns: "/api/campaigns",
|
||||||
|
GetCampaign: "/api/campaigns/:id",
|
||||||
GetRunningCampaignStats: "/api/campaigns/running/stats",
|
GetRunningCampaignStats: "/api/campaigns/running/stats",
|
||||||
CreateCampaign: "/api/campaigns",
|
CreateCampaign: "/api/campaigns",
|
||||||
|
TestCampaign: "/api/campaigns/:id/test",
|
||||||
UpdateCampaign: "/api/campaigns/:id",
|
UpdateCampaign: "/api/campaigns/:id",
|
||||||
UpdateCampaignStatus: "/api/campaigns/:id/status",
|
UpdateCampaignStatus: "/api/campaigns/:id/status",
|
||||||
DeleteCampaign: "/api/campaigns/:id",
|
DeleteCampaign: "/api/campaigns/:id",
|
||||||
|
|
13
main.go
13
main.go
|
@ -37,6 +37,8 @@ type App struct {
|
||||||
Importer *subimporter.Importer
|
Importer *subimporter.Importer
|
||||||
Runner *runner.Runner
|
Runner *runner.Runner
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
|
|
||||||
|
Messenger messenger.Messenger
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -104,6 +106,7 @@ func registerHandlers(e *echo.Echo) {
|
||||||
e.GET("/api/campaigns/:id", handleGetCampaigns)
|
e.GET("/api/campaigns/:id", handleGetCampaigns)
|
||||||
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
|
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
|
||||||
e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
|
e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
|
||||||
|
e.POST("/api/campaigns/:id/test", handleTestCampaign)
|
||||||
e.POST("/api/campaigns", handleCreateCampaign)
|
e.POST("/api/campaigns", handleCreateCampaign)
|
||||||
e.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
e.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
||||||
e.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
e.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
||||||
|
@ -139,7 +142,7 @@ func registerHandlers(e *echo.Echo) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// initMessengers initializes various messaging backends.
|
// initMessengers initializes various messaging backends.
|
||||||
func initMessengers(r *runner.Runner) {
|
func initMessengers(r *runner.Runner) messenger.Messenger {
|
||||||
// Load SMTP configurations for the default e-mail Messenger.
|
// Load SMTP configurations for the default e-mail Messenger.
|
||||||
var srv []messenger.Server
|
var srv []messenger.Server
|
||||||
for name := range viper.GetStringMapString("smtp") {
|
for name := range viper.GetStringMapString("smtp") {
|
||||||
|
@ -158,14 +161,16 @@ func initMessengers(r *runner.Runner) {
|
||||||
logger.Printf("loaded SMTP config %s (%s@%s)", s.Name, s.Username, s.Host)
|
logger.Printf("loaded SMTP config %s (%s@%s)", s.Name, s.Username, s.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
e, err := messenger.NewEmailer(srv...)
|
msgr, err := messenger.NewEmailer(srv...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("error loading e-mail messenger: %v", err)
|
logger.Fatalf("error loading e-mail messenger: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.AddMessenger(e); err != nil {
|
if err := r.AddMessenger(msgr); err != nil {
|
||||||
logger.Printf("error registering messenger %s", err)
|
logger.Printf("error registering messenger %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return msgr
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -222,7 +227,7 @@ func main() {
|
||||||
app.Runner = r
|
app.Runner = r
|
||||||
|
|
||||||
// Add messengers.
|
// Add messengers.
|
||||||
initMessengers(app.Runner)
|
app.Messenger = initMessengers(app.Runner)
|
||||||
|
|
||||||
go r.Run(time.Duration(time.Second * 2))
|
go r.Run(time.Duration(time.Second * 2))
|
||||||
r.SpawnWorkers()
|
r.SpawnWorkers()
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
type Queries struct {
|
type Queries struct {
|
||||||
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
|
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
|
||||||
GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
|
GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
|
||||||
|
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
|
||||||
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
|
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
|
||||||
QuerySubscribers string `query:"query-subscribers"`
|
QuerySubscribers string `query:"query-subscribers"`
|
||||||
QuerySubscribersCount string `query:"query-subscribers-count"`
|
QuerySubscribersCount string `query:"query-subscribers-count"`
|
||||||
|
|
14
queries.sql
14
queries.sql
|
@ -3,6 +3,11 @@
|
||||||
-- Get a single subscriber by id or UUID.
|
-- Get a single subscriber by id or UUID.
|
||||||
SELECT * FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END;
|
SELECT * FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END;
|
||||||
|
|
||||||
|
-- subscribers
|
||||||
|
-- name: get-subscribers-by-emails
|
||||||
|
-- Get subscribers by emails.
|
||||||
|
SELECT * FROM subscribers WHERE email=ANY($1);
|
||||||
|
|
||||||
-- name: get-subscriber-lists
|
-- name: get-subscriber-lists
|
||||||
-- Get lists belonging to subscribers.
|
-- Get lists belonging to subscribers.
|
||||||
SELECT lists.*, subscriber_lists.subscriber_id, subscriber_lists.status AS subscription_status FROM lists
|
SELECT lists.*, subscriber_lists.subscriber_id, subscriber_lists.status AS subscription_status FROM lists
|
||||||
|
@ -158,7 +163,14 @@ WHERE ($1 = 0 OR id = $1) AND status=(CASE WHEN $2 != '' THEN $2::campaign_statu
|
||||||
ORDER BY created_at DESC OFFSET $3 LIMIT $4;
|
ORDER BY created_at DESC OFFSET $3 LIMIT $4;
|
||||||
|
|
||||||
-- name: get-campaign-for-preview
|
-- name: get-campaign-for-preview
|
||||||
SELECT campaigns.*, COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
|
SELECT campaigns.*, COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body,
|
||||||
|
(
|
||||||
|
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(l)), '[]') FROM (
|
||||||
|
SELECT COALESCE(campaign_lists.list_id, 0) AS id,
|
||||||
|
campaign_lists.list_name AS name
|
||||||
|
FROM campaign_lists WHERE campaign_lists.campaign_id = campaigns.id
|
||||||
|
) l
|
||||||
|
) AS lists
|
||||||
FROM campaigns
|
FROM campaigns
|
||||||
LEFT JOIN templates ON (templates.id = campaigns.template_id)
|
LEFT JOIN templates ON (templates.id = campaigns.template_id)
|
||||||
WHERE campaigns.id = $1;
|
WHERE campaigns.id = $1;
|
||||||
|
|
|
@ -44,9 +44,9 @@ var jsonMap = []byte("{}")
|
||||||
func handleGetSubscriber(c echo.Context) error {
|
func handleGetSubscriber(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
out models.Subscribers
|
|
||||||
|
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
|
|
||||||
|
out models.Subscribers
|
||||||
)
|
)
|
||||||
|
|
||||||
if id < 1 {
|
if id < 1 {
|
||||||
|
@ -183,6 +183,10 @@ func handleCreateSubscriber(c echo.Context) error {
|
||||||
true,
|
true,
|
||||||
req.Lists)
|
req.Lists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "The e-mail already exists.")
|
||||||
|
}
|
||||||
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error creating subscriber: %v", err))
|
fmt.Sprintf("Error creating subscriber: %v", err))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue