Make providing name in subscriber creation optional and assign internally. Closes #1630.

This commit is contained in:
Kailash Nadh 2023-12-30 15:51:42 +05:30
parent a9a715696a
commit 0d74619cac
4 changed files with 41 additions and 51 deletions

View file

@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"github.com/knadh/listmonk/internal/subimporter"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
)
@ -183,12 +184,7 @@ loop:
func handleCreateSubscriber(c echo.Context) error {
var (
app = c.Get("app").(*App)
req struct {
models.Subscriber
Lists []int `json:"lists"`
ListUUIDs []string `json:"list_uuids"`
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
}
req subimporter.SubReq
)
// Get and validate fields.
@ -197,20 +193,10 @@ func handleCreateSubscriber(c echo.Context) error {
}
// Validate fields.
if len(req.Email) > 1000 {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidEmail"))
}
em, err := app.importer.SanitizeEmail(req.Email)
req, err := app.importer.ValidateFields(req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
req.Email = em
req.Name = strings.TrimSpace(req.Name)
if len(req.Name) == 0 || len(req.Name) > stdInputMaxLen {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
}
// Insert the subscriber into the DB.
sub, _, err := app.core.InsertSubscriber(req.Subscriber, req.Lists, req.ListUUIDs, req.PreconfirmSubs)

View file

@ -104,3 +104,12 @@ func strSliceContains(str string, sl []string) bool {
func trimNullBytes(b []byte) string {
return string(bytes.Trim(b, "\x00"))
}
func titleCase(input string) string {
parts := strings.Fields(input)
for n, p := range parts {
parts[n] = strings.Title(p)
}
return strings.Join(parts, " ")
}

View file

@ -222,18 +222,6 @@ export default Vue.extend({
},
onSubmit() {
// If there is no name, auto-generate one from the e-mail.
if (!this.form.name) {
let name = '';
[name] = this.form.email.toLowerCase().split('@');
if (name.includes('.')) {
this.form.name = name.split('.').map((c) => this.$utils.titleCase(c)).join(' ');
} else {
this.form.name = this.$utils.titleCase(name);
}
}
if (this.isEditing) {
this.updateSubscriber();
return;

View file

@ -103,9 +103,9 @@ type Status struct {
// SubReq is a wrapper over the Subscriber model.
type SubReq struct {
models.Subscriber
Lists pq.Int64Array `json:"lists"`
ListUUIDs pq.StringArray `json:"list_uuids"`
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
Lists []int `json:"lists"`
ListUUIDs []string `json:"list_uuids"`
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
}
type importStatusTpl struct {
@ -263,11 +263,11 @@ func (s *Session) Start() {
total = 0
cur = 0
listIDs = make(pq.Int64Array, len(s.opt.ListIDs))
listIDs = make([]int, len(s.opt.ListIDs))
)
for i, v := range s.opt.ListIDs {
listIDs[i] = int64(v)
listIDs[i] = v
}
for sub := range s.subQueue {
@ -294,7 +294,7 @@ func (s *Session) Start() {
}
if s.opt.Mode == ModeSubscribe {
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, listIDs, s.opt.SubStatus, s.opt.Overwrite)
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, pq.Array(listIDs), s.opt.SubStatus, s.opt.Overwrite)
} else if s.opt.Mode == ModeBlocklist {
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs)
}
@ -324,7 +324,7 @@ func (s *Session) Start() {
if cur == 0 {
s.im.setStatus(StatusFinished)
s.log.Printf("imported finished")
if _, err := s.im.opt.UpdateListDateStmt.Exec(listIDs); err != nil {
if _, err := s.im.opt.UpdateListDateStmt.Exec(pq.Array(listIDs)); err != nil {
s.log.Printf("error updating lists date: %v", err)
}
s.im.sendNotif(StatusFinished)
@ -343,7 +343,7 @@ func (s *Session) Start() {
s.im.incrementImportCount(cur)
s.im.setStatus(StatusFinished)
s.log.Printf("imported finished")
if _, err := s.im.opt.UpdateListDateStmt.Exec(listIDs); err != nil {
if _, err := s.im.opt.UpdateListDateStmt.Exec(pq.Array(listIDs)); err != nil {
s.log.Printf("error updating lists date: %v", err)
}
s.im.sendNotif(StatusFinished)
@ -486,15 +486,11 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error {
}
hdrKeys := s.mapCSVHeaders(csvHdr, csvHeaders)
// email, and name are required headers.
// email is a required header.
if _, ok := hdrKeys["email"]; !ok {
s.log.Printf("'email' column not found in '%s'", srcPath)
return errors.New("'email' column not found")
}
if _, ok := hdrKeys["name"]; !ok {
s.log.Printf("'name' column not found in '%s'", srcPath)
return errors.New("'name' column not found")
}
var (
lnHdr = len(hdrKeys)
@ -541,9 +537,12 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error {
sub := SubReq{}
sub.Email = row["email"]
sub.Name = row["name"]
sub, err = s.im.validateFields(sub)
if v, ok := row["name"]; ok {
sub.Name = v
}
sub, err = s.im.ValidateFields(sub)
if err != nil {
s.log.Printf("skipping line %d: %s: %v", i, sub.Email, err)
continue
@ -630,23 +629,31 @@ func (im *Importer) SanitizeEmail(email string) (string, error) {
return em.Address, nil
}
// validateFields validates incoming subscriber field values and returns sanitized fields.
func (im *Importer) validateFields(s SubReq) (SubReq, error) {
// ValidateFields validates incoming subscriber field values and returns sanitized fields.
func (im *Importer) ValidateFields(s SubReq) (SubReq, error) {
if len(s.Email) > 1000 {
return s, errors.New(im.i18n.T("subscribers.invalidEmail"))
}
s.Name = strings.TrimSpace(s.Name)
if len(s.Name) == 0 || len(s.Name) > stdInputMaxLen {
return s, errors.New(im.i18n.T("subscribers.invalidName"))
}
em, err := im.SanitizeEmail(s.Email)
if err != nil {
return s, err
}
s.Email = strings.ToLower(em)
// If there's no name, use the name part of the e-mail.
s.Name = strings.TrimSpace(s.Name)
if len(s.Name) == 0 {
name := strings.ToLower(strings.Split(s.Email, "@")[0])
parts := strings.Fields(strings.ReplaceAll(name, ".", " "))
for n, p := range parts {
parts[n] = strings.Title(p)
}
s.Name = strings.Join(parts, " ")
}
return s, nil
}