diff --git a/README.md b/README.md index 1df1c8b4..a3a66b0d 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,7 @@ brew upgrade dnote On Linux or macOS, you can use the installation script: - curl -s https://raw.githubusercontent.com/dnote/dnote/master/pkg/cli/install.sh | sh - -In some cases, you might need an elevated permission: - - curl -s https://raw.githubusercontent.com/dnote/dnote/master/pkg/cli/install.sh | sudo sh + curl -s https://dl.getdnote.com | sh Otherwise, you can download the binary for your platform manually from the [releases page](https://github.com/dnote/dnote/releases). diff --git a/pkg/server/app/digests.go b/pkg/server/app/digests.go deleted file mode 100644 index d152a41b..00000000 --- a/pkg/server/app/digests.go +++ /dev/null @@ -1,206 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package app - -import ( - "fmt" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -func (a *App) getExistingDigestReceipt(userID, digestID int) (*database.DigestReceipt, error) { - var ret database.DigestReceipt - conn := a.DB.Where("user_id = ? AND digest_id = ?", userID, digestID).First(&ret) - - if conn.RecordNotFound() { - return nil, nil - } - if err := conn.Error; err != nil { - return nil, errors.Wrap(err, "querying existing digest receipt") - } - - return &ret, nil -} - -// GetUserDigestByUUID retrives a digest by the uuid for the given user -func (a *App) GetUserDigestByUUID(userID int, uuid string) (*database.Digest, error) { - var ret database.Digest - conn := a.DB.Where("user_id = ? AND uuid = ?", userID, uuid).First(&ret) - - if conn.RecordNotFound() { - return nil, nil - } - if err := conn.Error; err != nil { - return nil, errors.Wrap(err, "finding digest") - } - - return &ret, nil -} - -// MarkDigestRead creates a new digest receipt. If one already exists for -// the given digest and the user, it is a noop. -func (a *App) MarkDigestRead(digest database.Digest, user database.User) (database.DigestReceipt, error) { - db := a.DB - - existing, err := a.getExistingDigestReceipt(user.ID, digest.ID) - if err != nil { - return database.DigestReceipt{}, errors.Wrap(err, "checking existing digest receipt") - } - if existing != nil { - return *existing, nil - } - - dat := database.DigestReceipt{ - UserID: user.ID, - DigestID: digest.ID, - } - if err := db.Create(&dat).Error; err != nil { - return database.DigestReceipt{}, errors.Wrap(err, "creating digest receipt") - } - - return dat, nil -} - -// GetDigestsParam is the params for getting a list of digests -type GetDigestsParam struct { - UserID int - Status string - Offset int - PerPage int - Order string -} - -func (p GetDigestsParam) getSubQuery() string { - orderClause := p.getOrderClause("digests") - - return fmt.Sprintf(`SELECT - digests.id AS digest_id, - digests.created_at AS created_at, - COUNT(digest_receipts.id) AS receipt_count -FROM digests -LEFT JOIN digest_receipts ON digest_receipts.digest_id = digests.id -WHERE digests.user_id = %d -GROUP BY digests.id, digests.created_at -%s`, p.UserID, orderClause) -} - -func (p GetDigestsParam) getSubQueryWhere() string { - var ret string - - if p.Status == "unread" { - ret = "WHERE t1.receipt_count = 0" - } else if p.Status == "read" { - ret = "WHERE t1.receipt_count > 0" - } - - return ret -} - -func (p GetDigestsParam) getOrderClause(table string) string { - if p.Order == "" { - return "" - } - - return fmt.Sprintf(`ORDER BY %s.%s`, table, p.Order) -} - -// CountDigests counts digests with the given user using the given criteria -func (a *App) CountDigests(p GetDigestsParam) (int, error) { - subquery := p.getSubQuery() - whereClause := p.getSubQueryWhere() - query := fmt.Sprintf(`SELECT COUNT(*) FROM (%s) AS t1 %s`, subquery, whereClause) - - result := struct { - Count int - }{} - if err := a.DB.Raw(query).Scan(&result).Error; err != nil { - return 0, errors.Wrap(err, "running count query") - } - - return result.Count, nil -} - -func (a *App) queryDigestIDs(p GetDigestsParam) ([]int, error) { - subquery := p.getSubQuery() - whereClause := p.getSubQueryWhere() - orderClause := p.getOrderClause("t1") - query := fmt.Sprintf(`SELECT t1.digest_id FROM (%s) AS t1 %s %s OFFSET ? LIMIT ?;`, subquery, whereClause, orderClause) - - ret := []int{} - rows, err := a.DB.Raw(query, p.Offset, p.PerPage).Rows() - if err != nil { - return nil, errors.Wrap(err, "getting rows") - } - defer rows.Close() - - for rows.Next() { - var id int - if err := rows.Scan(&id); err != nil { - return []int{}, errors.Wrap(err, "scanning row") - } - - ret = append(ret, id) - } - - return ret, nil -} - -// GetDigests queries digests for the given user using the given criteria -func (a *App) GetDigests(p GetDigestsParam) ([]database.Digest, error) { - IDs, err := a.queryDigestIDs(p) - if err != nil { - return nil, errors.Wrap(err, "querying digest IDs") - } - - var ret []database.Digest - conn := a.DB.Where("id IN (?)", IDs). - Order(p.Order).Preload("Rule").Preload("Receipts"). - Find(&ret) - if err := conn.Error; err != nil && !conn.RecordNotFound() { - return nil, errors.Wrap(err, "finding digests") - } - - return ret, nil -} - -// PreloadDigest preloads associations for the given digest. It returns a new digest. -func (a *App) PreloadDigest(d database.Digest) (database.Digest, error) { - var ret database.Digest - - conn := a.DB.Where("id = ?", d.ID). - Preload("Notes", func(db *gorm.DB) *gorm.DB { - return db.Order("notes.created_at DESC") - }). - Preload("Notes.Book"). - Preload("Notes.NoteReview", func(db *gorm.DB) *gorm.DB { - return db.Where("note_reviews.digest_id = ?", d.ID) - }). - Preload("Rule"). - Preload("Receipts", func(db *gorm.DB) *gorm.DB { - return db.Where("digest_receipts.user_id = ?", d.UserID) - }).First(&ret) - - if err := conn.Error; err != nil { - return ret, errors.Wrap(err, "preloading") - } - - return ret, nil -} diff --git a/pkg/server/app/digests_test.go b/pkg/server/app/digests_test.go deleted file mode 100644 index a8c5ae23..00000000 --- a/pkg/server/app/digests_test.go +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package app - -import ( - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/testutils" -) - -func TestMarkDigestRead(t *testing.T) { - defer testutils.ClearData() - - user := testutils.SetupUserData() - digest := database.Digest{UserID: user.ID} - testutils.MustExec(t, testutils.DB.Save(&digest), "preparing digest") - - a := NewTest(nil) - - // Multiple calls should not create more than 1 receipt - for i := 0; i < 3; i++ { - ret, err := a.MarkDigestRead(digest, user) - if err != nil { - t.Fatal(err, "failed to perform") - } - - var receiptCount int - testutils.MustExec(t, testutils.DB.Model(&database.DigestReceipt{}).Count(&receiptCount), "counting receipts") - assert.Equalf(t, receiptCount, 1, "receipt count mismatch") - - var receipt database.DigestReceipt - testutils.MustExec(t, testutils.DB.Where("id = ?", ret.ID).First(&receipt), "getting receipt") - assert.Equalf(t, receipt.UserID, user.ID, "receipt UserID mismatch") - assert.Equalf(t, receipt.DigestID, digest.ID, "receipt DigestID mismatch") - } -} diff --git a/pkg/server/app/notes.go b/pkg/server/app/notes.go index 3a3c5ccf..d6c1cdda 100644 --- a/pkg/server/app/notes.go +++ b/pkg/server/app/notes.go @@ -156,11 +156,6 @@ func (a *App) DeleteNote(tx *gorm.DB, user database.User, note database.Note) (d return note, errors.Wrap(err, "deleting note") } - // Delete associations - if err := tx.Where("note_id = ?", note.ID).Delete(&database.DigestNote{}).Error; err != nil { - return note, errors.Wrap(err, "deleting digest_notes") - } - return note, nil } diff --git a/pkg/server/app/users.go b/pkg/server/app/users.go index 092d181c..68f73935 100644 --- a/pkg/server/app/users.go +++ b/pkg/server/app/users.go @@ -49,25 +49,6 @@ func createEmailPreference(user database.User, tx *gorm.DB) error { return nil } -func createDefaultRepetitionRule(user database.User, tx *gorm.DB) error { - r := database.RepetitionRule{ - Title: "Default repetition - all books", - UserID: user.ID, - Enabled: false, - Hour: 20, - Minute: 30, - Frequency: 604800000, - BookDomain: database.BookDomainAll, - Books: []database.Book{}, - NoteCount: 20, - } - if err := tx.Save(&r).Error; err != nil { - return errors.Wrap(err, "inserting repetition rule") - } - - return nil -} - // CreateUser creates a user func (a *App) CreateUser(email, password string) (database.User, error) { tx := a.DB.Begin() @@ -111,10 +92,6 @@ func (a *App) CreateUser(email, password string) (database.User, error) { tx.Rollback() return database.User{}, errors.Wrap(err, "creating email preference") } - if err := createDefaultRepetitionRule(user, tx); err != nil { - tx.Rollback() - return database.User{}, errors.Wrap(err, "creating default repetition rule") - } if err := a.TouchLastLoginAt(user, tx); err != nil { tx.Rollback() return database.User{}, errors.Wrap(err, "updating last login") diff --git a/pkg/server/database/database.go b/pkg/server/database/database.go index f8e1859d..dcac38a1 100644 --- a/pkg/server/database/database.go +++ b/pkg/server/database/database.go @@ -45,11 +45,6 @@ func InitSchema(db *gorm.DB) { Token{}, EmailPreference{}, Session{}, - Digest{}, - DigestNote{}, - RepetitionRule{}, - DigestReceipt{}, - NoteReview{}, ).Error; err != nil { panic(err) } diff --git a/pkg/server/database/models.go b/pkg/server/database/models.go index 8684ae2d..87f1557a 100644 --- a/pkg/server/database/models.go +++ b/pkg/server/database/models.go @@ -46,20 +46,19 @@ type Book struct { // Note is a model for a note type Note struct { Model - UUID string `json:"uuid" gorm:"index;type:uuid;default:uuid_generate_v4()"` - Book Book `json:"book" gorm:"foreignkey:BookUUID"` - User User `json:"user"` - UserID int `json:"user_id" gorm:"index"` - BookUUID string `json:"book_uuid" gorm:"index;type:uuid"` - Body string `json:"content"` - AddedOn int64 `json:"added_on"` - EditedOn int64 `json:"edited_on"` - TSV string `json:"-" gorm:"type:tsvector"` - Public bool `json:"public" gorm:"default:false"` - USN int `json:"-" gorm:"index"` - Deleted bool `json:"-" gorm:"default:false"` - Encrypted bool `json:"-" gorm:"default:false"` - NoteReview NoteReview `json:"-"` + UUID string `json:"uuid" gorm:"index;type:uuid;default:uuid_generate_v4()"` + Book Book `json:"book" gorm:"foreignkey:BookUUID"` + User User `json:"user"` + UserID int `json:"user_id" gorm:"index"` + BookUUID string `json:"book_uuid" gorm:"index;type:uuid"` + Body string `json:"content"` + AddedOn int64 `json:"added_on"` + EditedOn int64 `json:"edited_on"` + TSV string `json:"-" gorm:"type:tsvector"` + Public bool `json:"public" gorm:"default:false"` + USN int `json:"-" gorm:"index"` + Deleted bool `json:"-" gorm:"default:false"` + Encrypted bool `json:"-" gorm:"default:false"` } // User is a model for a user @@ -126,59 +125,3 @@ type Session struct { LastUsedAt time.Time ExpiresAt time.Time } - -// Digest is a digest of notes -type Digest struct { - Model - UUID string `json:"uuid" gorm:"type:uuid;index;default:uuid_generate_v4()"` - RuleID int `gorm:"index"` - Rule RepetitionRule `json:"rule"` - UserID int `gorm:"index"` - Version int `gorm:"version"` - Notes []Note `gorm:"many2many:digest_notes;"` - Receipts []DigestReceipt `gorm:"polymorphic:Target;"` -} - -// DigestNote is an intermediary to represent many-to-many relationship -// between digests and notes -type DigestNote struct { - Model - NoteID int `gorm:"index"` - DigestID int `gorm:"index"` -} - -// RepetitionRule is the rules for sending digest emails -type RepetitionRule struct { - Model - UUID string `json:"uuid" gorm:"type:uuid;index;default:uuid_generate_v4()"` - UserID int `json:"user_id" gorm:"index"` - Title string `json:"title"` - Enabled bool `json:"enabled"` - Hour int `json:"hour" gorm:"index"` - Minute int `json:"minute" gorm:"index"` - // in milliseconds - Frequency int64 `json:"frequency"` - // in milliseconds - LastActive int64 `json:"last_active"` - // in milliseconds - NextActive int64 `json:"next_active"` - BookDomain string `json:"book_domain"` - Books []Book `gorm:"many2many:repetition_rule_books;"` - NoteCount int `json:"note_count"` -} - -// DigestReceipt is a read receipt for digests -type DigestReceipt struct { - Model - UserID int `json:"user_id" gorm:"index"` - DigestID int `json:"digest_id" gorm:"index"` -} - -// NoteReview is a record for reviewing a note in a digest -type NoteReview struct { - Model - UUID string `json:"uuid" gorm:"index;type:uuid;default:uuid_generate_v4()"` - UserID int `json:"user_id" gorm:"index"` - DigestID int `json:"digest_id" gorm:"index"` - NoteID int `json:"note_id" gorm:"index"` -} diff --git a/pkg/server/handlers/digests.go b/pkg/server/handlers/digests.go deleted file mode 100644 index ea08e7af..00000000 --- a/pkg/server/handlers/digests.go +++ /dev/null @@ -1,144 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package handlers - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/helpers" - "github.com/dnote/dnote/pkg/server/log" - "github.com/dnote/dnote/pkg/server/presenters" - "github.com/gorilla/mux" - "github.com/pkg/errors" -) - -func (a *API) getDigest(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - vars := mux.Vars(r) - digestUUID := vars["digestUUID"] - - d, err := a.App.GetUserDigestByUUID(user.ID, digestUUID) - if d == nil { - RespondNotFound(w) - return - } - if err != nil { - HandleError(w, "finding digest", err, http.StatusInternalServerError) - return - } - - digest, err := a.App.PreloadDigest(*d) - if err != nil { - HandleError(w, "finding digest", err, http.StatusInternalServerError) - return - } - - // mark as read - if _, err := a.App.MarkDigestRead(digest, user); err != nil { - log.ErrorWrap(err, fmt.Sprintf("marking digest as read for %s", digest.UUID)) - } - - presented := presenters.PresentDigest(digest) - respondJSON(w, http.StatusOK, presented) -} - -// DigestsResponse is a response for getting digests -type DigestsResponse struct { - Total int `json:"total"` - Items []presenters.Digest `json:"items"` -} - -type getDigestsParams struct { - page int - status string -} - -func parseGetDigestsParams(r *http.Request) (getDigestsParams, error) { - var page int - var err error - - q := r.URL.Query() - - pageStr := q.Get("page") - if pageStr != "" { - page, err = strconv.Atoi(pageStr) - if err != nil { - return getDigestsParams{}, errors.Wrap(err, "parsing page") - } - } else { - page = 1 - } - - status := q.Get("status") - - return getDigestsParams{ - page: page, - status: status, - }, nil -} - -func (a *API) getDigests(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - params, err := parseGetDigestsParams(r) - if err != nil { - HandleError(w, "parsing params", err, http.StatusBadRequest) - return - } - - perPage := 30 - offset := (params.page - 1) * perPage - p := app.GetDigestsParam{ - UserID: user.ID, - Offset: offset, - PerPage: perPage, - Status: params.status, - Order: "created_at DESC", - } - - digests, err := a.App.GetDigests(p) - if err != nil { - HandleError(w, "querying digests", err, http.StatusInternalServerError) - return - } - - total, err := a.App.CountDigests(p) - if err != nil { - HandleError(w, "counting digests", err, http.StatusInternalServerError) - return - } - - respondJSON(w, http.StatusOK, DigestsResponse{ - Total: total, - Items: presenters.PresentDigests(digests), - }) -} diff --git a/pkg/server/handlers/digests_test.go b/pkg/server/handlers/digests_test.go deleted file mode 100644 index 0d4e89c6..00000000 --- a/pkg/server/handlers/digests_test.go +++ /dev/null @@ -1,132 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package handlers - -import ( - "fmt" - "net/http" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/testutils" -) - -func TestGetDigest_Permission(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, nil) - defer server.Close() - - owner := testutils.SetupUserData() - nonOwner := testutils.SetupUserData() - digest := database.Digest{ - UserID: owner.ID, - } - testutils.MustExec(t, testutils.DB.Save(&digest), "preparing digest") - - t.Run("owner", func(t *testing.T) { - // Execute - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/digests/%s", digest.UUID), "") - res := testutils.HTTPAuthDo(t, req, owner) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - }) - - t.Run("non owner", func(t *testing.T) { - // Execute - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/digests/%s", digest.UUID), "") - res := testutils.HTTPAuthDo(t, req, nonOwner) - - // Test - assert.StatusCodeEquals(t, res, http.StatusNotFound, "") - }) - - t.Run("guest", func(t *testing.T) { - // Execute - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/digests/%s", digest.UUID), "") - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") - }) -} - -func TestGetDigest_Receipt(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, nil) - defer server.Close() - - user := testutils.SetupUserData() - digest := database.Digest{ - UserID: user.ID, - } - testutils.MustExec(t, testutils.DB.Save(&digest), "preparing digest") - - // multiple requests should create at most one receipt - for i := 0; i < 3; i++ { - // Execute and test - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/digests/%s", digest.UUID), "") - res := testutils.HTTPAuthDo(t, req, user) - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var receiptCount int - testutils.MustExec(t, testutils.DB.Model(&database.DigestReceipt{}).Count(&receiptCount), "counting receipts") - assert.Equal(t, receiptCount, 1, "counting receipt") - - var receipt database.DigestReceipt - testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&receipt), "finding receipt") - } -} - -func TestGetDigests(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, nil) - defer server.Close() - - user := testutils.SetupUserData() - digest := database.Digest{ - UserID: user.ID, - } - testutils.MustExec(t, testutils.DB.Save(&digest), "preparing digest") - - t.Run("user", func(t *testing.T) { - // Execute - req := testutils.MakeReq(server.URL, "GET", "/digests", "") - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - }) - - t.Run("guest", func(t *testing.T) { - // Execute - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/digests/%s", digest.UUID), "") - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") - }) -} diff --git a/pkg/server/handlers/note_review.go b/pkg/server/handlers/note_review.go deleted file mode 100644 index 5ed2b921..00000000 --- a/pkg/server/handlers/note_review.go +++ /dev/null @@ -1,150 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/helpers" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -type createNoteReviewParams struct { - DigestUUID string `json:"digest_uuid"` - NoteUUID string `json:"note_uuid"` -} - -func getDigestByUUID(db *gorm.DB, uuid string) (*database.Digest, error) { - var ret database.Digest - conn := db.Where("uuid = ?", uuid).First(&ret) - - if conn.RecordNotFound() { - return nil, nil - } - if err := conn.Error; err != nil { - return nil, errors.Wrap(err, "finding digest") - } - - return &ret, nil -} - -func (a *API) createNoteReview(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var params createNoteReviewParams - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - HandleError(w, "decoding params", err, http.StatusInternalServerError) - return - } - - digest, err := a.App.GetUserDigestByUUID(user.ID, params.DigestUUID) - if digest == nil { - http.Error(w, "digest not found for the given uuid", http.StatusBadRequest) - return - } - if err != nil { - HandleError(w, "finding digest", err, http.StatusInternalServerError) - return - } - - note, err := a.App.GetUserNoteByUUID(user.ID, params.NoteUUID) - if note == nil { - http.Error(w, "note not found for the given uuid", http.StatusBadRequest) - return - } - if err != nil { - HandleError(w, "finding note", err, http.StatusInternalServerError) - return - } - - var nr database.NoteReview - if err := a.App.DB.FirstOrCreate(&nr, database.NoteReview{ - UserID: user.ID, - DigestID: digest.ID, - NoteID: note.ID, - }).Error; err != nil { - HandleError(w, "saving note review", err, http.StatusInternalServerError) - return - } -} - -type deleteNoteReviewParams struct { - DigestUUID string `json:"digest_uuid"` - NoteUUID string `json:"note_uuid"` -} - -func (a *API) deleteNoteReview(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var params deleteNoteReviewParams - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - HandleError(w, "decoding params", err, http.StatusInternalServerError) - return - } - - db := a.App.DB - - note, err := a.App.GetUserNoteByUUID(user.ID, params.NoteUUID) - if note == nil { - http.Error(w, "note not found for the given uuid", http.StatusBadRequest) - return - } - if err != nil { - HandleError(w, "finding note", err, http.StatusInternalServerError) - return - } - - digest, err := a.App.GetUserDigestByUUID(user.ID, params.DigestUUID) - if digest == nil { - http.Error(w, "digest not found for the given uuid", http.StatusBadRequest) - return - } - if err != nil { - HandleError(w, "finding digest", err, http.StatusInternalServerError) - return - } - - var nr database.NoteReview - conn := db.Where("note_id = ? AND digest_id = ? AND user_id = ?", note.ID, digest.ID, user.ID).First(&nr) - if conn.RecordNotFound() { - http.Error(w, "no record found", http.StatusBadRequest) - return - } else if err := conn.Error; err != nil { - HandleError(w, "finding record", err, http.StatusInternalServerError) - return - } - - if err := db.Delete(&nr).Error; err != nil { - HandleError(w, "deleting record", err, http.StatusInternalServerError) - return - } -} diff --git a/pkg/server/handlers/note_review_test.go b/pkg/server/handlers/note_review_test.go deleted file mode 100644 index 43e9386c..00000000 --- a/pkg/server/handlers/note_review_test.go +++ /dev/null @@ -1,113 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package handlers - -import ( - "fmt" - "net/http" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/testutils" -) - -func TestCreateNoteReview(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - b1 := database.Book{ - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - n1 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - d1 := database.Digest{ - UserID: user.ID, - } - testutils.MustExec(t, testutils.DB.Save(&d1), "preparing d1") - - // multiple requests should create at most one receipt - for i := 0; i < 3; i++ { - dat := fmt.Sprintf(`{"note_uuid": "%s", "digest_uuid": "%s"}`, n1.UUID, d1.UUID) - req := testutils.MakeReq(server.URL, http.MethodPost, "/note_review", dat) - res := testutils.HTTPAuthDo(t, req, user) - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var noteReviewCount int - testutils.MustExec(t, testutils.DB.Model(&database.NoteReview{}).Count(¬eReviewCount), "counting note_reviews") - assert.Equalf(t, noteReviewCount, 1, "counting note_review") - - var noteReviewRecord database.NoteReview - testutils.MustExec(t, testutils.DB.Where("user_id = ? AND note_id = ? AND digest_id = ?", user.ID, n1.ID, d1.ID).First(¬eReviewRecord), "finding note_review record") - } -} - -func TestDeleteNoteReview(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - b1 := database.Book{ - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - n1 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - d1 := database.Digest{ - UserID: user.ID, - } - testutils.MustExec(t, testutils.DB.Save(&d1), "preparing d1") - nr1 := database.NoteReview{ - UserID: user.ID, - NoteID: n1.ID, - DigestID: d1.ID, - } - testutils.MustExec(t, testutils.DB.Save(&nr1), "preparing nr1") - - dat := fmt.Sprintf(`{"note_uuid": "%s", "digest_uuid": "%s"}`, n1.UUID, d1.UUID) - req := testutils.MakeReq(server.URL, http.MethodDelete, "/note_review", dat) - res := testutils.HTTPAuthDo(t, req, user) - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var noteReviewCount int - testutils.MustExec(t, testutils.DB.Model(&database.NoteReview{}).Count(¬eReviewCount), "counting note_reviews") - assert.Equal(t, noteReviewCount, 0, "counting note_review") -} diff --git a/pkg/server/handlers/repetition_rules.go b/pkg/server/handlers/repetition_rules.go deleted file mode 100644 index 747bb101..00000000 --- a/pkg/server/handlers/repetition_rules.go +++ /dev/null @@ -1,454 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package handlers - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/helpers" - "github.com/dnote/dnote/pkg/server/presenters" - "github.com/gorilla/mux" - "github.com/pkg/errors" -) - -func (a *API) getRepetitionRule(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - vars := mux.Vars(r) - repetitionRuleUUID := vars["repetitionRuleUUID"] - - if ok := helpers.ValidateUUID(repetitionRuleUUID); !ok { - http.Error(w, "invalid uuid", http.StatusBadRequest) - return - } - - var repetitionRule database.RepetitionRule - if err := a.App.DB.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").Find(&repetitionRule).Error; err != nil { - HandleError(w, "getting repetition rules", err, http.StatusInternalServerError) - return - } - - resp := presenters.PresentRepetitionRule(repetitionRule) - respondJSON(w, http.StatusOK, resp) -} - -func (a *API) getRepetitionRules(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var repetitionRules []database.RepetitionRule - if err := a.App.DB.Where("user_id = ?", user.ID).Preload("Books").Order("last_active DESC").Find(&repetitionRules).Error; err != nil { - HandleError(w, "getting repetition rules", err, http.StatusInternalServerError) - return - } - - resp := presenters.PresentRepetitionRules(repetitionRules) - respondJSON(w, http.StatusOK, resp) -} - -func validateBookDomain(val string) error { - if val == database.BookDomainAll || val == database.BookDomainIncluding || val == database.BookDomainExluding { - return nil - } - - return errors.Errorf("invalid book_domain %s", val) -} - -type repetitionRuleParams struct { - Title *string `json:"title"` - Enabled *bool `json:"enabled"` - Hour *int `json:"hour"` - Minute *int `json:"minute"` - Frequency *int64 `json:"frequency"` - BookDomain *string `json:"book_domain"` - BookUUIDs *[]string `json:"book_uuids"` - NoteCount *int `json:"note_count"` -} - -func (r repetitionRuleParams) GetEnabled() bool { - if r.Enabled == nil { - return false - } - - return *r.Enabled -} - -func (r repetitionRuleParams) GetFrequency() int64 { - if r.Frequency == nil { - return 0 - } - - return *r.Frequency -} - -func (r repetitionRuleParams) GetTitle() string { - if r.Title == nil { - return "" - } - - return *r.Title -} - -func (r repetitionRuleParams) GetNoteCount() int { - if r.NoteCount == nil { - return 0 - } - - return *r.NoteCount -} - -func (r repetitionRuleParams) GetBookDomain() string { - if r.BookDomain == nil { - return "" - } - - return *r.BookDomain -} - -func (r repetitionRuleParams) GetBookUUIDs() []string { - if r.BookUUIDs == nil { - return []string{} - } - - return *r.BookUUIDs -} - -func (r repetitionRuleParams) GetHour() int { - if r.Hour == nil { - return 0 - } - - return *r.Hour -} - -func (r repetitionRuleParams) GetMinute() int { - if r.Minute == nil { - return 0 - } - - return *r.Minute -} - -func validateRepetitionRuleParams(p repetitionRuleParams) error { - if p.Frequency != nil && p.GetFrequency() == 0 { - return errors.New("frequency is required") - } - - if p.Title != nil { - title := p.GetTitle() - - if len(title) == 0 { - return errors.New("Title is required") - } - if len(title) > 50 { - return errors.New("Title is too long") - } - } - - if p.NoteCount != nil && p.GetNoteCount() == 0 { - return errors.New("note count has to be greater than 0") - } - - if p.BookDomain != nil { - bookDomain := p.GetBookDomain() - if err := validateBookDomain(bookDomain); err != nil { - return err - } - - bookUUIDs := p.GetBookUUIDs() - if bookDomain == database.BookDomainAll { - if len(bookUUIDs) > 0 { - return errors.New("a global repetition should not specify book_uuids") - } - } else { - if len(bookUUIDs) == 0 { - return errors.New("book_uuids is required") - } - } - } - - if p.Hour != nil { - hour := p.GetHour() - - if hour < 0 && hour > 23 { - return errors.New("invalid hour") - } - } - - if p.Minute != nil { - minute := p.GetMinute() - - if minute < 0 && minute > 60 { - return errors.New("invalid minute") - } - } - - return nil -} - -func validateCreateRepetitionRuleParams(p repetitionRuleParams) error { - if p.Title == nil { - return errors.New("title is required") - } - if p.Frequency == nil { - return errors.New("frequency is required") - } - if p.NoteCount == nil { - return errors.New("note_count is required") - } - if p.BookDomain == nil { - return errors.New("book_domain is required") - } - if p.Hour == nil { - return errors.New("hour is required") - } - if p.Minute == nil { - return errors.New("minute is required") - } - if p.Enabled == nil { - return errors.New("enabled is required") - } - - return nil -} - -func parseCreateRepetitionRuleParams(r *http.Request) (repetitionRuleParams, error) { - var ret repetitionRuleParams - - d := json.NewDecoder(r.Body) - d.DisallowUnknownFields() - - if err := d.Decode(&ret); err != nil { - return ret, errors.Wrap(err, "decoding json") - } - - if err := validateCreateRepetitionRuleParams(ret); err != nil { - return ret, errors.Wrap(err, "validating params") - } - - if err := validateRepetitionRuleParams(ret); err != nil { - return ret, errors.Wrap(err, "validating params") - } - - return ret, nil -} - -type calcNextActiveParams struct { - Hour int - Minute int - Frequency int64 -} - -// calcNextActive calculates the NextActive value for a repetition rule by adding the given -// frequency to the given present date time at the given hour and minute. -func calcNextActive(now time.Time, p calcNextActiveParams) int64 { - t0 := time.Date(now.Year(), now.Month(), now.Day(), p.Hour, p.Minute, 0, 0, now.Location()).UnixNano() / int64(time.Millisecond) - - return t0 + p.Frequency -} - -func (a *API) createRepetitionRule(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - params, err := parseCreateRepetitionRuleParams(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var books []database.Book - if err := a.App.DB.Where("user_id = ? AND uuid IN (?)", user.ID, params.GetBookUUIDs()).Find(&books).Error; err != nil { - HandleError(w, "finding books", nil, http.StatusInternalServerError) - return - } - - nextActive := calcNextActive(a.App.Clock.Now(), calcNextActiveParams{ - Hour: params.GetHour(), - Minute: params.GetMinute(), - Frequency: params.GetFrequency(), - }) - - record := database.RepetitionRule{ - UserID: user.ID, - Title: params.GetTitle(), - Hour: params.GetHour(), - Minute: params.GetMinute(), - Frequency: params.GetFrequency(), - BookDomain: params.GetBookDomain(), - NextActive: nextActive, - Books: books, - NoteCount: params.GetNoteCount(), - Enabled: params.GetEnabled(), - } - if err := a.App.DB.Create(&record).Error; err != nil { - HandleError(w, "creating a repetition rule", err, http.StatusInternalServerError) - return - } - - resp := presenters.PresentRepetitionRule(record) - respondJSON(w, http.StatusCreated, resp) -} - -func parseUpdateDigestParams(r *http.Request) (repetitionRuleParams, error) { - var ret repetitionRuleParams - - if err := json.NewDecoder(r.Body).Decode(&ret); err != nil { - return ret, errors.Wrap(err, "decoding json") - } - - if err := validateRepetitionRuleParams(ret); err != nil { - return ret, errors.Wrap(err, "validating params") - } - - return ret, nil -} - -func (a *API) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - vars := mux.Vars(r) - repetitionRuleUUID := vars["repetitionRuleUUID"] - - var rule database.RepetitionRule - conn := a.App.DB.Where("uuid = ? AND user_id = ?", repetitionRuleUUID, user.ID).First(&rule) - - if conn.RecordNotFound() { - http.Error(w, "Not found", http.StatusNotFound) - return - } else if err := conn.Error; err != nil { - HandleError(w, "finding the repetition rule", err, http.StatusInternalServerError) - return - } - - if err := a.App.DB.Exec("DELETE from repetition_rules WHERE uuid = ?", rule.UUID).Error; err != nil { - HandleError(w, "deleting the repetition rule", err, http.StatusInternalServerError) - } - - w.WriteHeader(http.StatusOK) -} - -func (a *API) updateRepetitionRule(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - vars := mux.Vars(r) - repetitionRuleUUID := vars["repetitionRuleUUID"] - - params, err := parseUpdateDigestParams(r) - if err != nil { - http.Error(w, "parsing params", http.StatusBadRequest) - return - } - - tx := a.App.DB.Begin() - - var repetitionRule database.RepetitionRule - if err := tx.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").First(&repetitionRule).Error; err != nil { - HandleError(w, "finding record", nil, http.StatusInternalServerError) - return - } - - if params.Title != nil { - repetitionRule.Title = params.GetTitle() - } - if params.Enabled != nil { - enabled := params.GetEnabled() - repetitionRule.Enabled = enabled - - if enabled && !repetitionRule.Enabled { - repetitionRule.NextActive = calcNextActive(a.App.Clock.Now(), calcNextActiveParams{ - Hour: repetitionRule.Hour, - Minute: repetitionRule.Minute, - Frequency: repetitionRule.Frequency, - }) - } else if !enabled && repetitionRule.Enabled { - repetitionRule.NextActive = 0 - } - } - if params.Hour != nil { - repetitionRule.Hour = params.GetHour() - } - if params.Minute != nil { - repetitionRule.Minute = params.GetMinute() - } - if params.Frequency != nil { - frequency := params.GetFrequency() - - repetitionRule.Frequency = frequency - repetitionRule.NextActive = calcNextActive(a.App.Clock.Now(), calcNextActiveParams{ - Hour: repetitionRule.Hour, - Minute: repetitionRule.Minute, - Frequency: frequency, - }) - } - if params.NoteCount != nil { - repetitionRule.NoteCount = params.GetNoteCount() - } - if params.BookDomain != nil { - repetitionRule.BookDomain = params.GetBookDomain() - } - if params.BookUUIDs != nil { - var books []database.Book - if err := tx.Where("user_id = ? AND uuid IN (?)", user.ID, *params.BookUUIDs).Find(&books).Error; err != nil { - HandleError(w, "finding books", err, http.StatusInternalServerError) - return - } - - if err := tx.Model(&repetitionRule).Association("Books").Replace(books).Error; err != nil { - tx.Rollback() - HandleError(w, "updating books association for a repetitionRule", err, http.StatusInternalServerError) - return - } - } - - if err := tx.Save(&repetitionRule).Error; err != nil { - tx.Rollback() - HandleError(w, "creating a repetition rule", err, http.StatusInternalServerError) - return - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - HandleError(w, "committing a transaction", err, http.StatusInternalServerError) - } - - resp := presenters.PresentRepetitionRule(repetitionRule) - respondJSON(w, http.StatusOK, resp) -} diff --git a/pkg/server/handlers/repetition_rules_test.go b/pkg/server/handlers/repetition_rules_test.go deleted file mode 100644 index bae5f0c0..00000000 --- a/pkg/server/handlers/repetition_rules_test.go +++ /dev/null @@ -1,656 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package handlers - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - "time" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/presenters" - "github.com/dnote/dnote/pkg/server/testutils" - "github.com/pkg/errors" -) - -func TestGetRepetitionRule(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - - b1 := database.Book{ - USN: 11, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book1") - - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 7).Milliseconds(), - Hour: 21, - Minute: 0, - LastActive: 0, - UserID: user.ID, - BookDomain: database.BookDomainExluding, - Books: []database.Book{b1}, - NoteCount: 5, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - // Execute - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("/repetition_rules/%s", r1.UUID), "") - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var payload presenters.RepetitionRule - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var r1Record database.RepetitionRule - testutils.MustExec(t, testutils.DB.Where("uuid = ?", r1.UUID).First(&r1Record), "finding r1Record") - var b1Record database.Book - testutils.MustExec(t, testutils.DB.Where("uuid = ?", b1.UUID).First(&b1Record), "finding b1Record") - - expected := presenters.RepetitionRule{ - UUID: r1Record.UUID, - Title: r1Record.Title, - Enabled: r1Record.Enabled, - Hour: r1Record.Hour, - Minute: r1Record.Minute, - Frequency: r1Record.Frequency, - BookDomain: r1Record.BookDomain, - NoteCount: r1Record.NoteCount, - LastActive: r1Record.LastActive, - Books: []presenters.Book{ - { - UUID: b1Record.UUID, - USN: b1Record.USN, - Label: b1Record.Label, - CreatedAt: presenters.FormatTS(b1Record.CreatedAt), - UpdatedAt: presenters.FormatTS(b1Record.UpdatedAt), - }, - }, - CreatedAt: presenters.FormatTS(r1Record.CreatedAt), - UpdatedAt: presenters.FormatTS(r1Record.UpdatedAt), - } - - assert.DeepEqual(t, payload, expected, "payload mismatch") -} - -func TestGetRepetitionRules(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - - b1 := database.Book{ - USN: 11, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book1") - - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 7).Milliseconds(), - Hour: 21, - Minute: 0, - LastActive: 1257714000000, - UserID: user.ID, - BookDomain: database.BookDomainExluding, - Books: []database.Book{b1}, - NoteCount: 5, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - r2 := database.RepetitionRule{ - Title: "Rule 2", - Frequency: (time.Hour * 24 * 7 * 2).Milliseconds(), - Hour: 2, - Minute: 0, - LastActive: 0, - UserID: user.ID, - BookDomain: database.BookDomainExluding, - Books: []database.Book{}, - NoteCount: 5, - } - testutils.MustExec(t, testutils.DB.Save(&r2), "preparing rule2") - - // Execute - req := testutils.MakeReq(server.URL, "GET", "/repetition_rules", "") - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var payload []presenters.RepetitionRule - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var r1Record, r2Record database.RepetitionRule - testutils.MustExec(t, testutils.DB.Where("uuid = ?", r1.UUID).First(&r1Record), "finding r1Record") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", r2.UUID).First(&r2Record), "finding r2Record") - var b1Record database.Book - testutils.MustExec(t, testutils.DB.Where("uuid = ?", b1.UUID).First(&b1Record), "finding b1Record") - - expected := []presenters.RepetitionRule{ - { - UUID: r1Record.UUID, - Title: r1Record.Title, - Enabled: r1Record.Enabled, - Hour: r1Record.Hour, - Minute: r1Record.Minute, - Frequency: r1Record.Frequency, - BookDomain: r1Record.BookDomain, - NoteCount: r1Record.NoteCount, - LastActive: r1Record.LastActive, - Books: []presenters.Book{ - { - UUID: b1Record.UUID, - USN: b1Record.USN, - Label: b1Record.Label, - CreatedAt: presenters.FormatTS(b1Record.CreatedAt), - UpdatedAt: presenters.FormatTS(b1Record.UpdatedAt), - }, - }, - CreatedAt: presenters.FormatTS(r1Record.CreatedAt), - UpdatedAt: presenters.FormatTS(r1Record.UpdatedAt), - }, - { - UUID: r2Record.UUID, - Title: r2Record.Title, - Enabled: r2Record.Enabled, - Hour: r2Record.Hour, - Minute: r2Record.Minute, - Frequency: r2Record.Frequency, - BookDomain: r2Record.BookDomain, - NoteCount: r2Record.NoteCount, - LastActive: r2Record.LastActive, - Books: []presenters.Book{}, - CreatedAt: presenters.FormatTS(r2Record.CreatedAt), - UpdatedAt: presenters.FormatTS(r2Record.UpdatedAt), - }, - } - - assert.DeepEqual(t, payload, expected, "payload mismatch") -} - -func TestCreateRepetitionRules(t *testing.T) { - t.Run("all books", func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - c := clock.NewMock() - t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC) - c.SetNow(t0) - - server := MustNewServer(t, &app.App{ - - Clock: c, - }) - defer server.Close() - - user := testutils.SetupUserData() - - // Execute - dat := `{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "all", - "book_uuids": [], - "note_count": 20 -}` - req := testutils.MakeReq(server.URL, "POST", "/repetition_rules", dat) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusCreated, "") - - var ruleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&ruleCount), "counting rules") - assert.Equalf(t, ruleCount, 1, "reperition rule count mismatch") - - var rule database.RepetitionRule - testutils.MustExec(t, testutils.DB.Preload("Books").First(&rule), "finding b1Record") - - assert.NotEqual(t, rule.UUID, "", "rule UUID mismatch") - assert.Equal(t, rule.Title, "Rule 1", "rule Title mismatch") - assert.Equal(t, rule.Enabled, true, "rule Enabled mismatch") - assert.Equal(t, rule.Hour, 8, "rule HourTitle mismatch") - assert.Equal(t, rule.Minute, 30, "rule Minute mismatch") - assert.Equal(t, rule.Frequency, int64(604800000), "rule Frequency mismatch") - assert.Equal(t, rule.LastActive, int64(0), "rule LastActive mismatch") - assert.Equal(t, rule.NextActive, int64(1257064200000+604800000), "rule NextActive mismatch") - assert.Equal(t, rule.BookDomain, "all", "rule BookDomain mismatch") - assert.DeepEqual(t, rule.Books, []database.Book{}, "rule Books mismatch") - assert.Equal(t, rule.NoteCount, 20, "rule NoteCount mismatch") - }) - - bookDomainTestCases := []string{ - "including", - "excluding", - } - for _, tc := range bookDomainTestCases { - t.Run(tc, func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - c := clock.NewMock() - t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC) - c.SetNow(t0) - - server := MustNewServer(t, &app.App{ - - Clock: c, - }) - defer server.Close() - - user := testutils.SetupUserData() - - b1 := database.Book{ - UserID: user.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - - // Execute - dat := fmt.Sprintf(`{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "%s", - "book_uuids": ["%s"], - "note_count": 20 -}`, tc, b1.UUID) - req := testutils.MakeReq(server.URL, "POST", "/repetition_rules", dat) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusCreated, "") - - var ruleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&ruleCount), "counting rules") - assert.Equalf(t, ruleCount, 1, "reperition rule count mismatch") - - var rule database.RepetitionRule - testutils.MustExec(t, testutils.DB.Preload("Books").First(&rule), "finding b1Record") - - var b1Record database.Book - testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&b1Record), "finding b1Record") - - assert.NotEqual(t, rule.UUID, "", "rule UUID mismatch") - assert.Equal(t, rule.Title, "Rule 1", "rule Title mismatch") - assert.Equal(t, rule.Enabled, true, "rule Enabled mismatch") - assert.Equal(t, rule.Hour, 8, "rule HourTitle mismatch") - assert.Equal(t, rule.Minute, 30, "rule Minute mismatch") - assert.Equal(t, rule.LastActive, int64(0), "rule LastActive mismatch") - assert.Equal(t, rule.NextActive, int64(1257064200000+604800000), "rule NextActive mismatch") - assert.Equal(t, rule.Frequency, int64(604800000), "rule Frequency mismatch") - assert.Equal(t, rule.BookDomain, tc, "rule BookDomain mismatch") - assert.DeepEqual(t, rule.Books, []database.Book{b1Record}, "rule Books mismatch") - assert.Equal(t, rule.NoteCount, 20, "rule NoteCount mismatch") - }) - } -} - -func TestUpdateRepetitionRules(t *testing.T) { - - defer testutils.ClearData() - - // Setup - c := clock.NewMock() - t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC) - c.SetNow(t0) - server := MustNewServer(t, &app.App{ - - Clock: c, - }) - defer server.Close() - - user := testutils.SetupUserData() - - // Execute - r1 := database.RepetitionRule{ - Title: "Rule 1", - UserID: user.ID, - Enabled: false, - Hour: 8, - Minute: 30, - Frequency: 604800000, - LastActive: 1257064200000, - NextActive: 1263088980000, - BookDomain: "all", - Books: []database.Book{}, - NoteCount: 20, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing r1") - b1 := database.Book{ - UserID: user.ID, - USN: 11, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book1") - - dat := fmt.Sprintf(`{ - "title": "Rule 1 - edited", - "enabled": true, - "hour": 18, - "minute": 40, - "frequency": 259200000, - "book_domain": "including", - "book_uuids": ["%s"], - "note_count": 30 -}`, b1.UUID) - endpoint := fmt.Sprintf("/repetition_rules/%s", r1.UUID) - req := testutils.MakeReq(server.URL, "PATCH", endpoint, dat) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var totalRuleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&totalRuleCount), "counting rules") - assert.Equalf(t, totalRuleCount, 1, "reperition rule count mismatch") - - var rule database.RepetitionRule - testutils.MustExec(t, testutils.DB.Preload("Books").First(&rule), "finding b1Record") - - var b1Record database.Book - testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&b1Record), "finding b1Record") - - assert.NotEqual(t, rule.UUID, "", "rule UUID mismatch") - assert.Equal(t, rule.Title, "Rule 1 - edited", "rule Title mismatch") - assert.Equal(t, rule.Enabled, true, "rule Enabled mismatch") - assert.Equal(t, rule.Hour, 18, "rule HourTitle mismatch") - assert.Equal(t, rule.Minute, 40, "rule Minute mismatch") - assert.Equal(t, rule.Frequency, int64(259200000), "rule Frequency mismatch") - assert.Equal(t, rule.LastActive, int64(1257064200000), "rule LastActive mismatch") - assert.Equal(t, rule.NextActive, int64(1257100800000+259200000), "rule NextActive mismatch") - assert.Equal(t, rule.BookDomain, "including", "rule BookDomain mismatch") - assert.DeepEqual(t, rule.Books, []database.Book{b1Record}, "rule Books mismatch") - assert.Equal(t, rule.NoteCount, 30, "rule NoteCount mismatch") -} - -func TestDeleteRepetitionRules(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - - // Execute - r1 := database.RepetitionRule{ - Title: "Rule 1", - UserID: user.ID, - Enabled: true, - Hour: 8, - Minute: 30, - Frequency: 604800000, - BookDomain: "all", - Books: []database.Book{}, - NoteCount: 20, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing r1") - - r2 := database.RepetitionRule{ - Title: "Rule 1", - UserID: user.ID, - Enabled: true, - Hour: 8, - Minute: 30, - Frequency: 604800000, - BookDomain: "all", - Books: []database.Book{}, - NoteCount: 20, - } - testutils.MustExec(t, testutils.DB.Save(&r2), "preparing r2") - - endpoint := fmt.Sprintf("/repetition_rules/%s", r1.UUID) - req := testutils.MakeReq(server.URL, "DELETE", endpoint, "") - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var totalRuleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&totalRuleCount), "counting rules") - assert.Equalf(t, totalRuleCount, 1, "reperition rule count mismatch") - - var r2Count int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Where("id = ?", r2.ID).Count(&r2Count), "counting r2") - assert.Equalf(t, r2Count, 1, "r2 count mismatch") -} - -func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) { - testCases := []string{ - // empty title - `{ - "title": "", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "all", - "book_uuids": [], - "note_count": 20 - }`, - // empty frequency - `{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 0, - "book_domain": "some_invalid_book_domain", - "book_uuids": [], - "note_count": 20 - }`, - // empty note count - `{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "all", - "book_uuids": [], - "note_count": 0 - }`, - // invalid book doamin - `{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "some_invalid_book_domain", - "book_uuids": [], - "note_count": 20 - }`, - // invalid combination of book domain and book_uuids - `{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "excluding", - "book_uuids": [], - "note_count": 20 - }`, - `{ - "title": "Rule 1", - "enabled": true, - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "including", - "book_uuids": [], - "note_count": 20 - }`, - } - - for idx, tc := range testCases { - t.Run(fmt.Sprintf("test case - create %d", idx), func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - - // Execute - req := testutils.MakeReq(server.URL, "POST", "/repetition_rules", tc) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusBadRequest, "") - - var ruleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&ruleCount), "counting rules") - assert.Equalf(t, ruleCount, 0, "reperition rule count mismatch") - }) - - t.Run(fmt.Sprintf("test case %d - update", idx), func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - user := testutils.SetupUserData() - r1 := database.RepetitionRule{ - Title: "Rule 1", - UserID: user.ID, - Enabled: false, - Hour: 8, - Minute: 30, - Frequency: 604800000, - BookDomain: "all", - Books: []database.Book{}, - NoteCount: 20, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing r1") - b1 := database.Book{ - UserID: user.ID, - USN: 11, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book1") - - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - // Execute - req := testutils.MakeReq(server.URL, "PATCH", fmt.Sprintf("/repetition_rules/%s", r1.UUID), tc) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusBadRequest, "") - - var ruleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&ruleCount), "counting rules") - assert.Equalf(t, ruleCount, 1, "reperition rule count mismatch") - }) - } -} - -func TestCreateRepetitionRules_BadRequest(t *testing.T) { - testCases := []string{ - // no enabeld field - `{ - "title": "Rule #1", - "hour": 8, - "minute": 30, - "frequency": 604800000, - "book_domain": "all", - "book_uuids": [], - "note_count": 20 - }`, - } - - for idx, tc := range testCases { - t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - - // Execute - req := testutils.MakeReq(server.URL, "POST", "/repetition_rules", tc) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusBadRequest, "") - - var ruleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Count(&ruleCount), "counting rules") - assert.Equalf(t, ruleCount, 0, "reperition rule count mismatch") - }) - } -} diff --git a/pkg/server/handlers/routes.go b/pkg/server/handlers/routes.go index 2368df42..eaad495e 100644 --- a/pkg/server/handlers/routes.go +++ b/pkg/server/handlers/routes.go @@ -350,16 +350,6 @@ func (a *API) NewRouter() (*mux.Router, error) { {"PATCH", "/stripe_source", a.auth(a.updateStripeSource, nil), true}, {"GET", "/notes", a.auth(a.getNotes, nil), false}, {"GET", "/notes/{noteUUID}", a.getNote, true}, - {"GET", "/calendar", a.auth(a.getCalendar, nil), true}, - {"GET", "/repetition_rules", a.auth(a.getRepetitionRules, nil), true}, - {"GET", "/repetition_rules/{repetitionRuleUUID}", a.tokenAuth(a.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true}, - {"POST", "/repetition_rules", a.auth(a.createRepetitionRule, &proOnly), true}, - {"PATCH", "/repetition_rules/{repetitionRuleUUID}", a.tokenAuth(a.updateRepetitionRule, database.TokenTypeRepetition, &proOnly), true}, - {"DELETE", "/repetition_rules/{repetitionRuleUUID}", a.auth(a.deleteRepetitionRule, &proOnly), true}, - {"GET", "/digests/{digestUUID}", a.auth(a.getDigest, nil), true}, - {"GET", "/digests", a.auth(a.getDigests, nil), true}, - {"POST", "/note_review", a.auth(a.createNoteReview, nil), true}, - {"DELETE", "/note_review", a.auth(a.deleteNoteReview, nil), true}, // migration of classic users {"GET", "/classic/presignin", cors(a.classicPresignin), true}, diff --git a/pkg/server/handlers/user.go b/pkg/server/handlers/user.go index 578dc6da..c001873f 100644 --- a/pkg/server/handlers/user.go +++ b/pkg/server/handlers/user.go @@ -240,16 +240,7 @@ func (a *API) verifyEmail(w http.ResponseWriter, r *http.Request) { } type emailPreferernceParams struct { - InactiveReminder *bool `json:"inactive_reminder"` - ProductUpdate *bool `json:"product_update"` -} - -func (p emailPreferernceParams) getInactiveReminder() bool { - if p.InactiveReminder == nil { - return false - } - - return *p.InactiveReminder + ProductUpdate *bool `json:"product_update"` } func (p emailPreferernceParams) getProductUpdate() bool { @@ -281,9 +272,6 @@ func (a *API) updateEmailPreference(w http.ResponseWriter, r *http.Request) { tx := a.App.DB.Begin() - if params.InactiveReminder != nil { - pref.InactiveReminder = params.getInactiveReminder() - } if params.ProductUpdate != nil { pref.ProductUpdate = params.getProductUpdate() } diff --git a/pkg/server/handlers/user_test.go b/pkg/server/handlers/user_test.go index 986dbe28..c1865186 100644 --- a/pkg/server/handlers/user_test.go +++ b/pkg/server/handlers/user_test.go @@ -20,7 +20,6 @@ package handlers import ( "encoding/json" - "fmt" "net/http" "testing" "time" @@ -393,241 +392,241 @@ func TestUpdateEmail(t *testing.T) { }) } -func TestUpdateEmailPreference(t *testing.T) { - t.Run("with login", func(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - testutils.SetupEmailPreferenceData(u, false) - - // Execute - dat := `{"inactive_reminder": true}` - req := testutils.MakeReq(server.URL, "PATCH", "/account/email-preference", dat) - res := testutils.HTTPAuthDo(t, req, u) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var preference database.EmailPreference - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding account") - assert.Equal(t, preference.InactiveReminder, true, "preference mismatch") - }) - - t.Run("with an unused token", func(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - testutils.SetupEmailPreferenceData(u, false) - tok := database.Token{ - UserID: u.ID, - Type: database.TokenTypeEmailPreference, - Value: "someTokenValue", - } - testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") - - // Execute - dat := `{"inactive_reminder": true}` - url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") - req := testutils.MakeReq(server.URL, "PATCH", url, dat) - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var preference database.EmailPreference - var preferenceCount int - var token database.Token - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") - testutils.MustExec(t, testutils.DB.Model(database.EmailPreference{}).Count(&preferenceCount), "counting preference") - testutils.MustExec(t, testutils.DB.Where("id = ?", tok.ID).First(&token), "failed to find token") - - assert.Equal(t, preferenceCount, 1, "preference count mismatch") - assert.Equal(t, preference.InactiveReminder, true, "email mismatch") - assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used") - }) - - t.Run("with nonexistent token", func(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - testutils.SetupEmailPreferenceData(u, true) - tok := database.Token{ - UserID: u.ID, - Type: database.TokenTypeEmailPreference, - Value: "someTokenValue", - } - testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") - - dat := `{"inactive_reminder": false}` - url := fmt.Sprintf("/account/email-preference?token=%s", "someNonexistentToken") - req := testutils.MakeReq(server.URL, "PATCH", url, dat) - - // Execute - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") - - var preference database.EmailPreference - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") - assert.Equal(t, preference.InactiveReminder, true, "email mismatch") - }) - - t.Run("with expired token", func(t *testing.T) { - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - testutils.SetupEmailPreferenceData(u, true) - - usedAt := time.Now().Add(-11 * time.Minute) - tok := database.Token{ - UserID: u.ID, - Type: database.TokenTypeEmailPreference, - Value: "someTokenValue", - UsedAt: &usedAt, - } - testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") - - // Execute - dat := `{"inactive_reminder": false}` - url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") - req := testutils.MakeReq(server.URL, "PATCH", url, dat) - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") - - var preference database.EmailPreference - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") - assert.Equal(t, preference.InactiveReminder, true, "email mismatch") - }) - - t.Run("with a used but unexpired token", func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - testutils.SetupEmailPreferenceData(u, true) - usedAt := time.Now().Add(-9 * time.Minute) - tok := database.Token{ - UserID: u.ID, - Type: database.TokenTypeEmailPreference, - Value: "someTokenValue", - UsedAt: &usedAt, - } - testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") - - dat := `{"inactive_reminder": false}` - url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") - req := testutils.MakeReq(server.URL, "PATCH", url, dat) - - // Execute - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var preference database.EmailPreference - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") - assert.Equal(t, preference.InactiveReminder, false, "InactiveReminder mismatch") - }) - - t.Run("no user and no token", func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - testutils.SetupEmailPreferenceData(u, true) - - // Execute - dat := `{"inactive_reminder": false}` - req := testutils.MakeReq(server.URL, "PATCH", "/account/email-preference", dat) - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") - - var preference database.EmailPreference - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") - assert.Equal(t, preference.InactiveReminder, true, "email mismatch") - }) - - t.Run("create a record if not exists", func(t *testing.T) { - - defer testutils.ClearData() - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - u := testutils.SetupUserData() - tok := database.Token{ - UserID: u.ID, - Type: database.TokenTypeEmailPreference, - Value: "someTokenValue", - } - testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") - - // Execute - dat := `{"inactive_reminder": false}` - url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") - req := testutils.MakeReq(server.URL, "PATCH", url, dat) - res := testutils.HTTPDo(t, req) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var preferenceCount int - testutils.MustExec(t, testutils.DB.Model(database.EmailPreference{}).Count(&preferenceCount), "counting preference") - assert.Equal(t, preferenceCount, 1, "preference count mismatch") - - var preference database.EmailPreference - testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") - assert.Equal(t, preference.InactiveReminder, false, "email mismatch") - }) -} +// func TestUpdateEmailPreference(t *testing.T) { +// t.Run("with login", func(t *testing.T) { +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// testutils.SetupEmailPreferenceData(u, false) +// +// // Execute +// dat := `{"inactive_reminder": true}` +// req := testutils.MakeReq(server.URL, "PATCH", "/account/email-preference", dat) +// res := testutils.HTTPAuthDo(t, req, u) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusOK, "") +// +// var preference database.EmailPreference +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding account") +// assert.Equal(t, preference.InactiveReminder, true, "preference mismatch") +// }) +// +// t.Run("with an unused token", func(t *testing.T) { +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// testutils.SetupEmailPreferenceData(u, false) +// tok := database.Token{ +// UserID: u.ID, +// Type: database.TokenTypeEmailPreference, +// Value: "someTokenValue", +// } +// testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") +// +// // Execute +// dat := `{"inactive_reminder": true}` +// url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") +// req := testutils.MakeReq(server.URL, "PATCH", url, dat) +// res := testutils.HTTPDo(t, req) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusOK, "") +// +// var preference database.EmailPreference +// var preferenceCount int +// var token database.Token +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") +// testutils.MustExec(t, testutils.DB.Model(database.EmailPreference{}).Count(&preferenceCount), "counting preference") +// testutils.MustExec(t, testutils.DB.Where("id = ?", tok.ID).First(&token), "failed to find token") +// +// assert.Equal(t, preferenceCount, 1, "preference count mismatch") +// assert.Equal(t, preference.InactiveReminder, true, "email mismatch") +// assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used") +// }) +// +// t.Run("with nonexistent token", func(t *testing.T) { +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// testutils.SetupEmailPreferenceData(u, true) +// tok := database.Token{ +// UserID: u.ID, +// Type: database.TokenTypeEmailPreference, +// Value: "someTokenValue", +// } +// testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") +// +// dat := `{"inactive_reminder": false}` +// url := fmt.Sprintf("/account/email-preference?token=%s", "someNonexistentToken") +// req := testutils.MakeReq(server.URL, "PATCH", url, dat) +// +// // Execute +// res := testutils.HTTPDo(t, req) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") +// +// var preference database.EmailPreference +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") +// assert.Equal(t, preference.InactiveReminder, true, "email mismatch") +// }) +// +// t.Run("with expired token", func(t *testing.T) { +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// testutils.SetupEmailPreferenceData(u, true) +// +// usedAt := time.Now().Add(-11 * time.Minute) +// tok := database.Token{ +// UserID: u.ID, +// Type: database.TokenTypeEmailPreference, +// Value: "someTokenValue", +// UsedAt: &usedAt, +// } +// testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") +// +// // Execute +// dat := `{"inactive_reminder": false}` +// url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") +// req := testutils.MakeReq(server.URL, "PATCH", url, dat) +// res := testutils.HTTPDo(t, req) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") +// +// var preference database.EmailPreference +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") +// assert.Equal(t, preference.InactiveReminder, true, "email mismatch") +// }) +// +// t.Run("with a used but unexpired token", func(t *testing.T) { +// +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// testutils.SetupEmailPreferenceData(u, true) +// usedAt := time.Now().Add(-9 * time.Minute) +// tok := database.Token{ +// UserID: u.ID, +// Type: database.TokenTypeEmailPreference, +// Value: "someTokenValue", +// UsedAt: &usedAt, +// } +// testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") +// +// dat := `{"inactive_reminder": false}` +// url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") +// req := testutils.MakeReq(server.URL, "PATCH", url, dat) +// +// // Execute +// res := testutils.HTTPDo(t, req) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusOK, "") +// +// var preference database.EmailPreference +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") +// assert.Equal(t, preference.InactiveReminder, false, "InactiveReminder mismatch") +// }) +// +// t.Run("no user and no token", func(t *testing.T) { +// +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// testutils.SetupEmailPreferenceData(u, true) +// +// // Execute +// dat := `{"inactive_reminder": false}` +// req := testutils.MakeReq(server.URL, "PATCH", "/account/email-preference", dat) +// res := testutils.HTTPDo(t, req) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") +// +// var preference database.EmailPreference +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") +// assert.Equal(t, preference.InactiveReminder, true, "email mismatch") +// }) +// +// t.Run("create a record if not exists", func(t *testing.T) { +// +// defer testutils.ClearData() +// +// // Setup +// server := MustNewServer(t, &app.App{ +// +// Clock: clock.NewMock(), +// }) +// defer server.Close() +// +// u := testutils.SetupUserData() +// tok := database.Token{ +// UserID: u.ID, +// Type: database.TokenTypeEmailPreference, +// Value: "someTokenValue", +// } +// testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") +// +// // Execute +// dat := `{"inactive_reminder": false}` +// url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") +// req := testutils.MakeReq(server.URL, "PATCH", url, dat) +// res := testutils.HTTPDo(t, req) +// +// // Test +// assert.StatusCodeEquals(t, res, http.StatusOK, "") +// +// var preferenceCount int +// testutils.MustExec(t, testutils.DB.Model(database.EmailPreference{}).Count(&preferenceCount), "counting preference") +// assert.Equal(t, preferenceCount, 1, "preference count mismatch") +// +// var preference database.EmailPreference +// testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") +// assert.Equal(t, preference.InactiveReminder, false, "email mismatch") +// }) +// } func TestGetEmailPreference(t *testing.T) { defer testutils.ClearData() @@ -654,10 +653,9 @@ func TestGetEmailPreference(t *testing.T) { } expected := presenters.EmailPreference{ - InactiveReminder: pref.InactiveReminder, - ProductUpdate: pref.ProductUpdate, - CreatedAt: presenters.FormatTS(pref.CreatedAt), - UpdatedAt: presenters.FormatTS(pref.UpdatedAt), + ProductUpdate: pref.ProductUpdate, + CreatedAt: presenters.FormatTS(pref.CreatedAt), + UpdatedAt: presenters.FormatTS(pref.UpdatedAt), } assert.DeepEqual(t, got, expected, "payload mismatch") } diff --git a/pkg/server/job/job.go b/pkg/server/job/job.go deleted file mode 100644 index 8e6ab48a..00000000 --- a/pkg/server/job/job.go +++ /dev/null @@ -1,178 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package job - -import ( - slog "log" - - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/job/remind" - "github.com/dnote/dnote/pkg/server/job/repetition" - "github.com/dnote/dnote/pkg/server/log" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" - "github.com/robfig/cron" -) - -var ( - // ErrEmptyDB is an error for missing database connection in the app configuration - ErrEmptyDB = errors.New("No database connection was provided") - // ErrEmptyClock is an error for missing clock in the app configuration - ErrEmptyClock = errors.New("No clock was provided") - // ErrEmptyWebURL is an error for missing WebURL content in the app configuration - ErrEmptyWebURL = errors.New("No WebURL was provided") - // ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration - ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided") - // ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration - ErrEmptyEmailBackend = errors.New("No EmailBackend was provided") -) - -// Runner is a configuration for job -type Runner struct { - DB *gorm.DB - Clock clock.Clock - EmailTmpl mailer.Templates - EmailBackend mailer.Backend - Config app.Config -} - -// NewRunner returns a new runner -func NewRunner(db *gorm.DB, c clock.Clock, t mailer.Templates, b mailer.Backend, config app.Config) (Runner, error) { - ret := Runner{ - DB: db, - EmailTmpl: t, - EmailBackend: b, - Clock: c, - Config: config, - } - - if err := ret.validate(); err != nil { - return Runner{}, errors.Wrap(err, "validating runner configuration") - } - - return ret, nil -} - -func (r *Runner) validate() error { - if r.DB == nil { - return ErrEmptyDB - } - if r.Clock == nil { - return ErrEmptyClock - } - if r.EmailTmpl == nil { - return ErrEmptyEmailTemplates - } - if r.EmailBackend == nil { - return ErrEmptyEmailBackend - } - if r.Config.WebURL == "" { - return ErrEmptyWebURL - } - - return nil -} - -func scheduleJob(c *cron.Cron, spec string, cmd func()) { - s, err := cron.ParseStandard(spec) - if err != nil { - panic(errors.Wrap(err, "parsing schedule")) - } - - c.Schedule(s, cron.FuncJob(cmd)) -} - -func (r *Runner) schedule(ch chan error) { - // Schedule jobs - cr := cron.New() - scheduleJob(cr, "* * * * *", func() { r.DoRepetition() }) - scheduleJob(cr, "0 8 * * *", func() { r.RemindNoRecentNotes() }) - cr.Start() - - ch <- nil - - // Block forever - select {} -} - -// Do starts the background tasks in a separate goroutine that runs forever -func (r *Runner) Do() error { - // validate - if err := r.validate(); err != nil { - return errors.Wrap(err, "validating job configurations") - } - - ch := make(chan error) - go r.schedule(ch) - if err := <-ch; err != nil { - return errors.Wrap(err, "scheduling jobs") - } - - slog.Println("Started background tasks") - - return nil -} - -// DoRepetition creates spaced repetitions and delivers the results based on the rules -func (r *Runner) DoRepetition() { - c := repetition.Context{ - DB: r.DB, - Clock: r.Clock, - EmailTmpl: r.EmailTmpl, - EmailBackend: r.EmailBackend, - Config: r.Config, - } - - result, err := repetition.Do(c) - m := log.WithFields(log.Fields{ - "success_count": result.SuccessCount, - "failed_rule_uuids": result.FailedRuleUUIDs, - }) - - if err == nil { - m.Info("successfully processed repetition job") - } else { - m.ErrorWrap(err, "error processing repetition job") - } -} - -// RemindNoRecentNotes remind users if no notes have been added recently -func (r *Runner) RemindNoRecentNotes() { - c := remind.Context{ - DB: r.DB, - Clock: r.Clock, - EmailTmpl: r.EmailTmpl, - EmailBackend: r.EmailBackend, - Config: r.Config, - } - - result, err := remind.DoInactive(c) - m := log.WithFields(log.Fields{ - "success_count": result.SuccessCount, - "failed_user_ids": result.FailedUserIDs, - }) - - if err == nil { - m.Info("successfully processed no recent note reminder job") - } else { - m.ErrorWrap(err, "error processing no recent note reminder job") - } -} diff --git a/pkg/server/job/job_test.go b/pkg/server/job/job_test.go deleted file mode 100644 index 8a92c263..00000000 --- a/pkg/server/job/job_test.go +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package job - -import ( - "fmt" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/dnote/dnote/pkg/server/testutils" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -func TestNewRunner(t *testing.T) { - testCases := []struct { - db *gorm.DB - clock clock.Clock - emailTmpl mailer.Templates - emailBackend mailer.Backend - webURL string - expectedErr error - }{ - { - db: &gorm.DB{}, - clock: clock.NewMock(), - emailTmpl: mailer.Templates{}, - emailBackend: &testutils.MockEmailbackendImplementation{}, - webURL: "http://mock.url", - expectedErr: nil, - }, - { - db: nil, - clock: clock.NewMock(), - emailTmpl: mailer.Templates{}, - emailBackend: &testutils.MockEmailbackendImplementation{}, - webURL: "http://mock.url", - expectedErr: ErrEmptyDB, - }, - { - db: &gorm.DB{}, - clock: nil, - emailTmpl: mailer.Templates{}, - emailBackend: &testutils.MockEmailbackendImplementation{}, - webURL: "http://mock.url", - expectedErr: ErrEmptyClock, - }, - { - db: &gorm.DB{}, - clock: clock.NewMock(), - emailTmpl: nil, - emailBackend: &testutils.MockEmailbackendImplementation{}, - webURL: "http://mock.url", - expectedErr: ErrEmptyEmailTemplates, - }, - { - db: &gorm.DB{}, - clock: clock.NewMock(), - emailTmpl: mailer.Templates{}, - emailBackend: nil, - webURL: "http://mock.url", - expectedErr: ErrEmptyEmailBackend, - }, - { - db: &gorm.DB{}, - clock: clock.NewMock(), - emailTmpl: mailer.Templates{}, - emailBackend: &testutils.MockEmailbackendImplementation{}, - webURL: "", - expectedErr: ErrEmptyWebURL, - }, - } - - for idx, tc := range testCases { - t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) { - _, err := NewRunner(tc.db, tc.clock, tc.emailTmpl, tc.emailBackend, app.Config{ - WebURL: tc.webURL, - }) - - assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch") - }) - } -} diff --git a/pkg/server/job/remind/inactive.go b/pkg/server/job/remind/inactive.go deleted file mode 100644 index 5363b16e..00000000 --- a/pkg/server/job/remind/inactive.go +++ /dev/null @@ -1,204 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package remind - -import ( - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/log" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -// Context holds data that repetition job needs in order to perform -type Context struct { - DB *gorm.DB - Clock clock.Clock - EmailTmpl mailer.Templates - EmailBackend mailer.Backend - Config app.Config -} - -type inactiveUserInfo struct { - userID int - email string - sampleNoteUUID string -} - -func (c *Context) sampleUserNote(userID int) (database.Note, error) { - var ret database.Note - // FIXME: ordering by random() requires a sequential scan on the whole table and does not scale - if err := c.DB.Where("user_id = ?", userID).Order("random() DESC").First(&ret).Error; err != nil { - return ret, errors.Wrap(err, "getting a random note") - } - - return ret, nil -} - -func (c *Context) getInactiveUserInfo() ([]inactiveUserInfo, error) { - ret := []inactiveUserInfo{} - - threshold := c.Clock.Now().AddDate(0, 0, -14).Unix() - - rows, err := c.DB.Raw(` -SELECT - notes.user_id AS user_id, - accounts.email, - SUM( - CASE - WHEN notes.created_at > to_timestamp(?) THEN 1 - ELSE 0 - END - ) AS recent_note_count, - COUNT(*) AS total_note_count -FROM notes -INNER JOIN accounts ON accounts.user_id = notes.user_id -WHERE accounts.email IS NOT NULL AND accounts.email_verified IS TRUE -GROUP BY notes.user_id, accounts.email`, threshold).Rows() - if err != nil { - return ret, errors.Wrap(err, "executing note count SQL query") - } - defer rows.Close() - for rows.Next() { - var userID, recentNoteCount, totalNoteCount int - var email string - if err := rows.Scan(&userID, &email, &recentNoteCount, &totalNoteCount); err != nil { - return nil, errors.Wrap(err, "scanning a row") - } - - if recentNoteCount == 0 && totalNoteCount > 0 { - note, err := c.sampleUserNote(userID) - if err != nil { - return nil, errors.Wrap(err, "sampling user note") - } - - ret = append(ret, inactiveUserInfo{ - userID: userID, - email: email, - sampleNoteUUID: note.UUID, - }) - } - } - - return ret, nil -} - -func (c *Context) canNotify(info inactiveUserInfo) (bool, error) { - var pref database.EmailPreference - if err := c.DB.Where("user_id = ?", info.userID).First(&pref).Error; err != nil { - return false, errors.Wrap(err, "getting email preference") - } - - if !pref.InactiveReminder { - return false, nil - } - - var notif database.Notification - conn := c.DB.Where("user_id = ? AND type = ?", info.userID, mailer.EmailTypeInactiveReminder).Order("created_at DESC").First(¬if) - - if conn.RecordNotFound() { - return true, nil - } else if err := conn.Error; err != nil { - return false, errors.Wrap(err, "checking cooldown") - } - - t := c.Clock.Now().AddDate(0, 0, -14) - if notif.CreatedAt.Before(t) { - return true, nil - } - - return false, nil -} - -func (c *Context) process(info inactiveUserInfo) error { - ok, err := c.canNotify(info) - if err != nil { - return errors.Wrap(err, "checking if user can be notified") - } - if !ok { - return nil - } - - sender, err := c.Config.GetSenderEmail("noreply@getdnote.com") - if err != nil { - return errors.Wrap(err, "getting sender email") - } - - tmplData := mailer.InactiveReminderTmplData{ - WebURL: c.Config.WebURL, - SampleNoteUUID: info.sampleNoteUUID, - Token: "blah", - } - body, err := c.EmailTmpl.Execute(mailer.EmailTypeInactiveReminder, mailer.EmailKindText, tmplData) - if err != nil { - return errors.Wrap(err, "executing inactive email template") - } - - if err := c.EmailBackend.Queue("Your knowledge base stopped growing", sender, []string{info.email}, mailer.EmailKindText, body); err != nil { - return errors.Wrap(err, "queueing email") - } - - if err := c.DB.Create(&database.Notification{ - Type: mailer.EmailTypeInactiveReminder, - UserID: info.userID, - }).Error; err != nil { - return errors.Wrap(err, "creating notification") - } - - return nil -} - -// Result holds the result of the job -type Result struct { - SuccessCount int - FailedUserIDs []int -} - -// DoInactive sends reminder for users with no recent notes -func DoInactive(c Context) (Result, error) { - log.Info("performing reminder for no recent notes") - - result := Result{} - items, err := c.getInactiveUserInfo() - if err != nil { - return result, errors.Wrap(err, "getting inactive user information") - } - - log.WithFields(log.Fields{ - "user_count": len(items), - }).Info("counted inactive users") - - for _, item := range items { - err := c.process(item) - - if err == nil { - result.SuccessCount = result.SuccessCount + 1 - } else { - log.WithFields(log.Fields{ - "user_id": item.userID, - }).ErrorWrap(err, "Could not process no recent notes reminder") - - result.FailedUserIDs = append(result.FailedUserIDs, item.userID) - } - } - - return result, nil -} diff --git a/pkg/server/job/remind/inactive_test.go b/pkg/server/job/remind/inactive_test.go deleted file mode 100644 index d8380d5f..00000000 --- a/pkg/server/job/remind/inactive_test.go +++ /dev/null @@ -1,194 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package remind - -import ( - "os" - "sort" - "testing" - "time" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/dnote/dnote/pkg/server/testutils" - "github.com/pkg/errors" -) - -func getTestContext(c clock.Clock, be *testutils.MockEmailbackendImplementation) Context { - emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR") - - con := Context{ - DB: testutils.DB, - Clock: c, - EmailTmpl: mailer.NewTemplates(&emailTmplDir), - EmailBackend: be, - } - - return con -} - -func TestDoInactive(t *testing.T) { - defer testutils.ClearData() - - t1 := time.Now() - - // u1 is an active user - u1 := testutils.SetupUserData() - a1 := testutils.SetupAccountData(u1, "alice@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a1).Update("email_verified", true), "setting email verified") - testutils.MustExec(t, testutils.DB.Save(&database.EmailPreference{UserID: u1.ID, InactiveReminder: true}), "preparing email preference") - - b1 := database.Book{ - UserID: u1.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - n1 := database.Note{ - BookUUID: b1.UUID, - UserID: u1.ID, - } - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - - // u2 is an inactive user - u2 := testutils.SetupUserData() - a2 := testutils.SetupAccountData(u2, "bob@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a2).Update("email_verified", true), "setting email verified") - testutils.MustExec(t, testutils.DB.Save(&database.EmailPreference{UserID: u2.ID, InactiveReminder: true}), "preparing email preference") - - b2 := database.Book{ - UserID: u2.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2") - n2 := database.Note{ - UserID: u2.ID, - BookUUID: b2.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2") - testutils.MustExec(t, testutils.DB.Model(&n2).Update("created_at", t1.AddDate(0, 0, -15)), "preparing n2") - - // u3 is an inactive user with inactive alert email preference disabled - u3 := testutils.SetupUserData() - a3 := testutils.SetupAccountData(u3, "alice@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a3).Update("email_verified", true), "setting email verified") - emailPref3 := database.EmailPreference{UserID: u3.ID} - testutils.MustExec(t, testutils.DB.Save(&emailPref3), "preparing email preference") - testutils.MustExec(t, testutils.DB.Model(&emailPref3).Update(map[string]interface{}{"inactive_reminder": false}), "updating email preference") - - b3 := database.Book{ - UserID: u3.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3") - n3 := database.Note{ - BookUUID: b3.UUID, - UserID: u3.ID, - } - testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3") - testutils.MustExec(t, testutils.DB.Model(&n3).Update("created_at", t1.AddDate(0, 0, -15)), "preparing n3") - - c := clock.NewMock() - c.SetNow(t1) - be := &testutils.MockEmailbackendImplementation{} - - con := getTestContext(c, be) - if _, err := DoInactive(con); err != nil { - t.Fatal(errors.Wrap(err, "performing")) - } - - assert.Equalf(t, len(be.Emails), 1, "email queue count mismatch") - assert.DeepEqual(t, be.Emails[0].To, []string{a2.Email.String}, "email address mismatch") -} - -func TestDoInactive_Cooldown(t *testing.T) { - defer testutils.ClearData() - - // setup sets up an inactive user - setup := func(t *testing.T, now time.Time, email string) database.User { - u := testutils.SetupUserData() - a := testutils.SetupAccountData(u, email, "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a).Update("email_verified", true), "setting email verified") - testutils.MustExec(t, testutils.DB.Save(&database.EmailPreference{UserID: u.ID, InactiveReminder: true}), "preparing email preference") - - b := database.Book{ - UserID: u.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b), "preparing book") - n := database.Note{ - UserID: u.ID, - BookUUID: b.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n), "preparing note") - testutils.MustExec(t, testutils.DB.Model(&n).Update("created_at", now.AddDate(0, 0, -15)), "preparing note") - - return u - } - - // Set up - now := time.Now() - - setup(t, now, "alice@example.com") - - bob := setup(t, now, "bob@example.com") - bobNotif := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: bob.ID} - testutils.MustExec(t, testutils.DB.Create(&bobNotif), "preparing inactive notification for bob") - testutils.MustExec(t, testutils.DB.Model(&bobNotif).Update("created_at", now.AddDate(0, 0, -7)), "preparing created_at for inactive notification for bob") - - chuck := setup(t, now, "chuck@example.com") - chuckNotif := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: chuck.ID} - testutils.MustExec(t, testutils.DB.Create(&chuckNotif), "preparing inactive notification for chuck") - testutils.MustExec(t, testutils.DB.Model(&chuckNotif).Update("created_at", now.AddDate(0, 0, -15)), "preparing created_at for inactive notification for chuck") - - dan := setup(t, now, "dan@example.com") - danNotif1 := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: dan.ID} - testutils.MustExec(t, testutils.DB.Create(&danNotif1), "preparing inactive notification 1 for dan") - testutils.MustExec(t, testutils.DB.Model(&danNotif1).Update("created_at", now.AddDate(0, 0, -10)), "preparing created_at for inactive notification for dan") - danNotif2 := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: dan.ID} - testutils.MustExec(t, testutils.DB.Create(&danNotif2), "preparing inactive notification 2 for dan") - testutils.MustExec(t, testutils.DB.Model(&danNotif2).Update("created_at", now.AddDate(0, 0, -15)), "preparing created_at for inactive notification for dan") - - c := clock.NewMock() - c.SetNow(now) - be := &testutils.MockEmailbackendImplementation{} - - // Execute - con := getTestContext(c, be) - if _, err := DoInactive(con); err != nil { - t.Fatal(errors.Wrap(err, "performing")) - } - - // Test - assert.Equalf(t, len(be.Emails), 2, "email queue count mismatch") - - var recipients []string - for _, email := range be.Emails { - recipients = append(recipients, email.To[0]) - } - sort.SliceStable(recipients, func(i, j int) bool { - r1 := recipients[i] - r2 := recipients[j] - - return r1 < r2 - }) - - assert.DeepEqual(t, recipients, []string{"alice@example.com", "chuck@example.com"}, "email address mismatch") -} diff --git a/pkg/server/job/remind/main_test.go b/pkg/server/job/remind/main_test.go deleted file mode 100644 index 8a6f5f73..00000000 --- a/pkg/server/job/remind/main_test.go +++ /dev/null @@ -1,35 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package remind - -import ( - "os" - "testing" - - "github.com/dnote/dnote/pkg/server/testutils" -) - -func TestMain(m *testing.M) { - testutils.InitTestDB() - - code := m.Run() - testutils.ClearData() - - os.Exit(code) -} diff --git a/pkg/server/job/repetition/main_test.go b/pkg/server/job/repetition/main_test.go deleted file mode 100644 index 5d88f0c9..00000000 --- a/pkg/server/job/repetition/main_test.go +++ /dev/null @@ -1,35 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package repetition - -import ( - "os" - "testing" - - "github.com/dnote/dnote/pkg/server/testutils" -) - -func TestMain(m *testing.M) { - testutils.InitTestDB() - - code := m.Run() - testutils.ClearData() - - os.Exit(code) -} diff --git a/pkg/server/job/repetition/repetition.go b/pkg/server/job/repetition/repetition.go deleted file mode 100644 index e938b148..00000000 --- a/pkg/server/job/repetition/repetition.go +++ /dev/null @@ -1,296 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package repetition - -import ( - "fmt" - "os" - "time" - - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/log" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/dnote/dnote/pkg/server/operations" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -// Context holds data that repetition job needs in order to perform -type Context struct { - DB *gorm.DB - Clock clock.Clock - EmailTmpl mailer.Templates - EmailBackend mailer.Backend - Config app.Config -} - -// BuildEmailParams is the params for building an email -type BuildEmailParams struct { - Now time.Time - User database.User - Digest database.Digest - Rule database.RepetitionRule -} - -// BuildEmail builds an email for the spaced repetition -func BuildEmail(db *gorm.DB, emailTmpl mailer.Templates, p BuildEmailParams) (string, string, error) { - subject := fmt.Sprintf("%s #%d", p.Rule.Title, p.Digest.Version) - tok, err := mailer.GetToken(db, p.User, database.TokenTypeRepetition) - if err != nil { - return "", "", errors.Wrap(err, "getting email frequency token") - } - - t1 := p.Now.AddDate(0, 0, -3).UnixNano() - t2 := p.Now.AddDate(0, 0, -7).UnixNano() - - noteInfos := []mailer.DigestNoteInfo{} - for _, note := range p.Digest.Notes { - var stage int - if note.AddedOn > t1 { - stage = 1 - } else if note.AddedOn > t2 && note.AddedOn < t1 { - stage = 2 - } else if note.AddedOn < t2 { - stage = 3 - } - - info := mailer.NewNoteInfo(note, stage) - noteInfos = append(noteInfos, info) - } - - bookCount := 0 - bookMap := map[string]bool{} - for _, n := range p.Digest.Notes { - if ok := bookMap[n.Book.Label]; !ok { - bookCount++ - bookMap[n.Book.Label] = true - } - } - - tmplData := mailer.DigestTmplData{ - EmailSessionToken: tok.Value, - DigestUUID: p.Digest.UUID, - DigestVersion: p.Digest.Version, - RuleUUID: p.Rule.UUID, - RuleTitle: p.Rule.Title, - WebURL: os.Getenv("WebURL"), - } - body, err := emailTmpl.Execute(mailer.EmailTypeDigest, mailer.EmailKindText, tmplData) - if err != nil { - return "", "", errors.Wrap(err, "executing digest email template") - } - - return subject, body, nil -} - -func (c Context) getEligibleRules(now time.Time) ([]database.RepetitionRule, error) { - hour := now.Hour() - minute := now.Minute() - - var ret []database.RepetitionRule - if err := c.DB. - Where("users.cloud AND repetition_rules.hour = ? AND repetition_rules.minute = ? AND repetition_rules.enabled", hour, minute). - Joins("INNER JOIN users ON users.id = repetition_rules.user_id"). - Find(&ret).Error; err != nil { - return nil, errors.Wrap(err, "querying db") - } - - return ret, nil -} - -func build(tx *gorm.DB, rule database.RepetitionRule) (database.Digest, error) { - notes, err := getBalancedNotes(tx, rule) - if err != nil { - return database.Digest{}, errors.Wrap(err, "getting notes") - } - - digest, err := operations.CreateDigest(tx, rule, notes) - if err != nil { - return database.Digest{}, errors.Wrap(err, "creating digest") - } - - return digest, nil -} - -func (c Context) notify(now time.Time, user database.User, digest database.Digest, rule database.RepetitionRule) error { - var account database.Account - if err := c.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil { - return errors.Wrap(err, "getting account") - } - - if !account.Email.Valid || !account.EmailVerified { - log.WithFields(log.Fields{ - "user_id": user.ID, - }).Info("Skipping repetition delivery because email is not valid or verified") - return nil - } - - subject, body, err := BuildEmail(c.DB, c.EmailTmpl, BuildEmailParams{ - Now: now, - User: user, - Digest: digest, - Rule: rule, - }) - if err != nil { - return errors.Wrap(err, "making email") - } - - sender, err := c.Config.GetSenderEmail("noreply@getdnote.com") - if err != nil { - return errors.Wrap(err, "getting sender email") - } - - if err := c.EmailBackend.Queue(subject, sender, []string{account.Email.String}, mailer.EmailKindText, body); err != nil { - return errors.Wrap(err, "queueing email") - } - - if err := c.DB.Create(&database.Notification{ - Type: mailer.EmailTypeDigest, - UserID: user.ID, - }).Error; err != nil { - return errors.Wrap(err, "creating notification") - } - - return nil -} - -func checkCooldown(now time.Time, rule database.RepetitionRule) bool { - present := now.UnixNano() / int64(time.Millisecond) - - return present >= rule.NextActive -} - -func getNextActive(base int64, frequency int64, now time.Time) int64 { - candidate := base + frequency - if candidate >= now.UnixNano()/int64(time.Millisecond) { - return candidate - } - - return getNextActive(candidate, frequency, now) -} - -func touchTimestamp(tx *gorm.DB, rule database.RepetitionRule, now time.Time) error { - lastActive := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, now.Location()).UnixNano() / int64(time.Millisecond) - - rule.LastActive = lastActive - rule.NextActive = getNextActive(rule.LastActive, rule.Frequency, now) - - if err := tx.Save(&rule).Error; err != nil { - return errors.Wrap(err, "updating repetition rule") - } - - return nil -} - -func (c Context) process(now time.Time, rule database.RepetitionRule) error { - log.WithFields(log.Fields{ - "uuid": rule.UUID, - }).Info("processing repetition") - - tx := c.DB.Begin() - - if !checkCooldown(now, rule) { - log.WithFields(log.Fields{ - "uuid": rule.UUID, - }).Info("Skipping repetition processing due to cooldown") - return nil - } - - var user database.User - if err := tx.Where("id = ?", rule.UserID).First(&user).Error; err != nil { - return errors.Wrap(err, "getting user") - } - if !user.Cloud { - log.WithFields(log.Fields{ - "user_id": user.ID, - }).Info("Skipping repetition due to lack of subscription") - return nil - } - - digest, err := build(tx, rule) - if err != nil { - tx.Rollback() - return errors.Wrap(err, "building repetition") - } - - if err := touchTimestamp(tx, rule, now); err != nil { - tx.Rollback() - return errors.Wrap(err, "touching last_active") - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return errors.Wrap(err, "committing transaction") - } - - if err := c.notify(now, user, digest, rule); err != nil { - return errors.Wrap(err, "notifying user") - } - - log.WithFields(log.Fields{ - "uuid": rule.UUID, - }).Info("finished processing repetition") - - return nil -} - -// Result holds the result of the job -type Result struct { - SuccessCount int - FailedRuleUUIDs []string -} - -// Do creates spaced repetitions and delivers the results based on the rules -func Do(c Context) (Result, error) { - now := c.Clock.Now().UTC() - result := Result{} - - rules, err := c.getEligibleRules(now) - if err != nil { - return result, errors.Wrap(err, "getting eligible repetition rules") - } - - log.WithFields(log.Fields{ - "hour": now.Hour(), - "minute": now.Minute(), - "num_rules": len(rules), - }).Info("processing rules") - - for _, rule := range rules { - err := c.process(now, rule) - - if err == nil { - result.SuccessCount = result.SuccessCount + 1 - } else { - log.WithFields(log.Fields{ - "rule uuid": rule.UUID, - }).ErrorWrap(err, "Could not process the repetition rule") - - result.FailedRuleUUIDs = append(result.FailedRuleUUIDs, rule.UUID) - } - } - - if len(result.FailedRuleUUIDs) > 0 { - return result, errors.New("failed to process some rules") - } - - return result, nil -} diff --git a/pkg/server/job/repetition/repetition_test.go b/pkg/server/job/repetition/repetition_test.go deleted file mode 100644 index 64cc6412..00000000 --- a/pkg/server/job/repetition/repetition_test.go +++ /dev/null @@ -1,501 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package repetition - -import ( - "os" - "sort" - "testing" - "time" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/dnote/dnote/pkg/server/testutils" - "github.com/pkg/errors" -) - -func assertLastActive(t *testing.T, ruleUUID string, lastActive int64) { - var rule database.RepetitionRule - testutils.MustExec(t, testutils.DB.Where("uuid = ?", ruleUUID).First(&rule), "finding rule1") - - assert.Equal(t, rule.LastActive, lastActive, "LastActive mismatch") -} - -func assertDigestCount(t *testing.T, rule database.RepetitionRule, expected int) { - var digestCount int - testutils.MustExec(t, testutils.DB.Model(&database.Digest{}).Where("rule_id = ? AND user_id = ?", rule.ID, rule.UserID).Count(&digestCount), "counting digest") - assert.Equal(t, digestCount, expected, "digest count mismatch") -} - -func getTestContext(c clock.Clock, be *testutils.MockEmailbackendImplementation) Context { - emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR") - - return Context{ - DB: testutils.DB, - Clock: c, - EmailTmpl: mailer.NewTemplates(&emailTmplDir), - EmailBackend: be, - } -} - -func mustDo(t *testing.T, c Context) { - _, err := Do(c) - if err != nil { - t.Fatal(errors.Wrap(err, "performing")) - } -} - -func TestDo(t *testing.T) { - t.Run("processes the rule on time", func(t *testing.T) { - defer testutils.ClearData() - - // Set up - user := testutils.SetupUserData() - a := testutils.SetupAccountData(user, "alice@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a).Update("email_verified", true), "updating email_verified") - - t0 := time.Date(2009, time.November, 1, 0, 0, 0, 0, time.UTC) - t1 := time.Date(2009, time.November, 4, 12, 2, 0, 0, time.UTC) - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 3).Milliseconds(), // three days - Hour: 12, - Minute: 2, - Enabled: true, - LastActive: 0, - NextActive: t1.UnixNano() / int64(time.Millisecond), - UserID: user.ID, - BookDomain: database.BookDomainAll, - Model: database.Model{ - CreatedAt: t0, - UpdatedAt: t0, - }, - } - - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - c := clock.NewMock() - be := testutils.MockEmailbackendImplementation{} - con := getTestContext(c, &be) - - // Test - // 1 day later - c.SetNow(time.Date(2009, time.November, 2, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(0)) - assertDigestCount(t, r1, 0) - assert.Equalf(t, len(be.Emails), 0, "email queue count mismatch") - - // 2 days later - c.SetNow(time.Date(2009, time.November, 3, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(0)) - assertDigestCount(t, r1, 0) - assert.Equal(t, len(be.Emails), 0, "email queue count mismatch") - - // 3 days later - should be processed - c.SetNow(time.Date(2009, time.November, 4, 12, 1, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(0)) - assertDigestCount(t, r1, 0) - assert.Equal(t, len(be.Emails), 0, "email queue count mismatch") - - c.SetNow(time.Date(2009, time.November, 4, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257336120000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - c.SetNow(time.Date(2009, time.November, 4, 12, 3, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257336120000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - // 4 day later - c.SetNow(time.Date(2009, time.November, 5, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257336120000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - // 5 days later - c.SetNow(time.Date(2009, time.November, 6, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257336120000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - // 6 days later - should be processed - c.SetNow(time.Date(2009, time.November, 7, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257595320000)) - assertDigestCount(t, r1, 2) - assert.Equal(t, len(be.Emails), 2, "email queue count mismatch") - - // 7 days later - c.SetNow(time.Date(2009, time.November, 8, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257595320000)) - assertDigestCount(t, r1, 2) - assert.Equal(t, len(be.Emails), 2, "email queue count mismatch") - - // 8 days later - c.SetNow(time.Date(2009, time.November, 9, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257595320000)) - assertDigestCount(t, r1, 2) - assert.Equal(t, len(be.Emails), 2, "email queue count mismatch") - - // 9 days later - should be processed - c.SetNow(time.Date(2009, time.November, 10, 12, 2, 1, 0, time.UTC)) - mustDo(t, con) - assertLastActive(t, r1.UUID, int64(1257854520000)) - assertDigestCount(t, r1, 3) - assert.Equal(t, len(be.Emails), 3, "email queue count mismatch") - }) - - /* - * |----|----|----|----|----|----|----|----|----|----|----|----|----| - * t0 t1 td t2 tu t3 t4 - * - * Suppose a repetition with a frequency of 3 days. - * - * t0 - original last_active value (Nov 1, 2009) - * t1 - original next_active value (Nov 4, 2009) - * td - server goes down - * t2 - repetition processing is missed (Nov 7, 2009) - * tu - server comes up - * t3 - new last_active value (Nov 10, 2009) - * t4 - new next_active value (Nov 13, 2009) - */ - t.Run("recovers correct next_active value if missed processing in the past", func(t *testing.T) { - defer testutils.ClearData() - - // Set up - user := testutils.SetupUserData() - a := testutils.SetupAccountData(user, "alice@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a).Update("email_verified", true), "updating email_verified") - - t0 := time.Date(2009, time.November, 1, 12, 2, 0, 0, time.UTC) - t1 := time.Date(2009, time.November, 4, 12, 2, 0, 0, time.UTC) - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 3).Milliseconds(), // three days - Hour: 12, - Minute: 2, - Enabled: true, - LastActive: t0.UnixNano() / int64(time.Millisecond), - NextActive: t1.UnixNano() / int64(time.Millisecond), - UserID: user.ID, - BookDomain: database.BookDomainAll, - Model: database.Model{ - CreatedAt: t0, - UpdatedAt: t0, - }, - } - - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - c := clock.NewMock() - c.SetNow(time.Date(2009, time.November, 10, 12, 2, 1, 0, time.UTC)) - be := &testutils.MockEmailbackendImplementation{} - - mustDo(t, getTestContext(c, be)) - - var rule database.RepetitionRule - testutils.MustExec(t, testutils.DB.Where("uuid = ?", r1.UUID).First(&rule), "finding rule1") - - assert.Equal(t, rule.LastActive, time.Date(2009, time.November, 10, 12, 2, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond), "LastActive mismsatch") - assert.Equal(t, rule.NextActive, time.Date(2009, time.November, 13, 12, 2, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond), "NextActive mismsatch") - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - }) -} - -func TestDo_Disabled(t *testing.T) { - defer testutils.ClearData() - - // Set up - user := testutils.SetupUserData() - a := testutils.SetupAccountData(user, "alice@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a).Update("email_verified", true), "updating email_verified") - - t0 := time.Date(2009, time.November, 1, 0, 0, 0, 0, time.UTC) - t1 := time.Date(2009, time.November, 4, 12, 2, 0, 0, time.UTC) - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 3).Milliseconds(), // three days - Hour: 12, - Minute: 2, - LastActive: 0, - NextActive: t1.UnixNano() / int64(time.Millisecond), - UserID: user.ID, - Enabled: false, - BookDomain: database.BookDomainAll, - Model: database.Model{ - CreatedAt: t0, - UpdatedAt: t0, - }, - } - - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - // Execute - c := clock.NewMock() - c.SetNow(time.Date(2009, time.November, 4, 12, 2, 0, 0, time.UTC)) - be := &testutils.MockEmailbackendImplementation{} - - mustDo(t, getTestContext(c, be)) - - // Test - assertLastActive(t, r1.UUID, int64(0)) - assertDigestCount(t, r1, 0) - assert.Equal(t, len(be.Emails), 0, "email queue count mismatch") -} - -func TestDo_BalancedStrategy(t *testing.T) { - type testData struct { - User database.User - Book1 database.Book - Book2 database.Book - Book3 database.Book - Note1 database.Note - Note2 database.Note - Note3 database.Note - } - - setup := func() testData { - user := testutils.SetupUserData() - a := testutils.SetupAccountData(user, "alice@example.com", "pass1234") - testutils.MustExec(t, testutils.DB.Model(&a).Update("email_verified", true), "updating email_verified") - - b1 := database.Book{ - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - b2 := database.Book{ - UserID: user.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2") - b3 := database.Book{ - UserID: user.ID, - Label: "golang", - } - testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3") - - n1 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - n2 := database.Note{ - UserID: user.ID, - BookUUID: b2.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2") - n3 := database.Note{ - UserID: user.ID, - BookUUID: b3.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3") - - return testData{ - User: user, - Book1: b1, - Book2: b2, - Book3: b3, - Note1: n1, - Note2: n2, - Note3: n3, - } - } - - t.Run("all books", func(t *testing.T) { - defer testutils.ClearData() - - // Set up - dat := setup() - - t0 := time.Date(2009, time.November, 1, 12, 0, 0, 0, time.UTC) - t1 := time.Date(2009, time.November, 8, 21, 0, 0, 0, time.UTC) - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 7).Milliseconds(), - Hour: 21, - Minute: 0, - LastActive: 0, - NextActive: t1.UnixNano() / int64(time.Millisecond), - Enabled: true, - UserID: dat.User.ID, - BookDomain: database.BookDomainAll, - NoteCount: 5, - Model: database.Model{ - CreatedAt: t0, - UpdatedAt: t0, - }, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - // Execute - c := clock.NewMock() - c.SetNow(time.Date(2009, time.November, 8, 21, 0, 0, 0, time.UTC)) - be := &testutils.MockEmailbackendImplementation{} - - mustDo(t, getTestContext(c, be)) - - // Test - assertLastActive(t, r1.UUID, int64(1257714000000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - var repetition database.Digest - testutils.MustExec(t, testutils.DB.Where("rule_id = ? AND user_id = ?", r1.ID, r1.UserID).Preload("Notes").First(&repetition), "finding repetition") - - sort.SliceStable(repetition.Notes, func(i, j int) bool { - n1 := repetition.Notes[i] - n2 := repetition.Notes[j] - - return n1.ID < n2.ID - }) - - var n1Record, n2Record, n3Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note1.UUID).First(&n1Record), "finding n1") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note2.UUID).First(&n2Record), "finding n2") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note3.UUID).First(&n3Record), "finding n3") - expected := []database.Note{n1Record, n2Record, n3Record} - assert.DeepEqual(t, repetition.Notes, expected, "result mismatch") - }) - - t.Run("excluding books", func(t *testing.T) { - defer testutils.ClearData() - - // Set up - dat := setup() - - t0 := time.Date(2009, time.November, 1, 12, 0, 0, 0, time.UTC) - t1 := time.Date(2009, time.November, 8, 21, 0, 0, 0, time.UTC) - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 7).Milliseconds(), - Hour: 21, - Enabled: true, - Minute: 0, - LastActive: 0, - NextActive: t1.UnixNano() / int64(time.Millisecond), - UserID: dat.User.ID, - BookDomain: database.BookDomainExluding, - Books: []database.Book{dat.Book1}, - NoteCount: 5, - Model: database.Model{ - CreatedAt: t0, - UpdatedAt: t0, - }, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - // Execute - c := clock.NewMock() - c.SetNow(time.Date(2009, time.November, 8, 21, 0, 1, 0, time.UTC)) - be := &testutils.MockEmailbackendImplementation{} - - mustDo(t, getTestContext(c, be)) - - // Test - assertLastActive(t, r1.UUID, int64(1257714000000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - var repetition database.Digest - testutils.MustExec(t, testutils.DB.Where("rule_id = ? AND user_id = ?", r1.ID, r1.UserID).Preload("Notes").First(&repetition), "finding repetition") - - sort.SliceStable(repetition.Notes, func(i, j int) bool { - n1 := repetition.Notes[i] - n2 := repetition.Notes[j] - - return n1.ID < n2.ID - }) - - var n2Record, n3Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note2.UUID).First(&n2Record), "finding n2") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note3.UUID).First(&n3Record), "finding n3") - expected := []database.Note{n2Record, n3Record} - assert.DeepEqual(t, repetition.Notes, expected, "result mismatch") - }) - - t.Run("including books", func(t *testing.T) { - defer testutils.ClearData() - - // Set up - dat := setup() - - t0 := time.Date(2009, time.November, 1, 12, 0, 0, 0, time.UTC) - t1 := time.Date(2009, time.November, 8, 21, 0, 0, 0, time.UTC) - r1 := database.RepetitionRule{ - Title: "Rule 1", - Frequency: (time.Hour * 24 * 7).Milliseconds(), - Hour: 21, - Enabled: true, - Minute: 0, - LastActive: 0, - NextActive: t1.UnixNano() / int64(time.Millisecond), - UserID: dat.User.ID, - BookDomain: database.BookDomainIncluding, - Books: []database.Book{dat.Book1, dat.Book2}, - NoteCount: 5, - Model: database.Model{ - CreatedAt: t0, - UpdatedAt: t0, - }, - } - testutils.MustExec(t, testutils.DB.Save(&r1), "preparing rule1") - - // Execute - c := clock.NewMock() - c.SetNow(time.Date(2009, time.November, 8, 21, 0, 0, 0, time.UTC)) - be := &testutils.MockEmailbackendImplementation{} - - mustDo(t, getTestContext(c, be)) - - // Test - assertLastActive(t, r1.UUID, int64(1257714000000)) - assertDigestCount(t, r1, 1) - assert.Equal(t, len(be.Emails), 1, "email queue count mismatch") - - var repetition database.Digest - testutils.MustExec(t, testutils.DB.Where("rule_id = ? AND user_id = ?", r1.ID, r1.UserID).Preload("Notes").First(&repetition), "finding repetition") - - sort.SliceStable(repetition.Notes, func(i, j int) bool { - n1 := repetition.Notes[i] - n2 := repetition.Notes[j] - - return n1.ID < n2.ID - }) - - var n1Record, n2Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note1.UUID).First(&n1Record), "finding n1") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", dat.Note2.UUID).First(&n2Record), "finding n2") - expected := []database.Note{n1Record, n2Record} - assert.DeepEqual(t, repetition.Notes, expected, "result mismatch") - }) -} diff --git a/pkg/server/job/repetition/strategy.go b/pkg/server/job/repetition/strategy.go deleted file mode 100644 index f8bfd499..00000000 --- a/pkg/server/job/repetition/strategy.go +++ /dev/null @@ -1,140 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package repetition - -import ( - "sort" - "time" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -func getRuleBookIDs(db *gorm.DB, ruleID int) ([]int, error) { - var ret []int - if err := db.Table("repetition_rule_books").Select("book_id").Where("repetition_rule_id = ?", ruleID).Pluck("book_id", &ret).Error; err != nil { - return nil, errors.Wrap(err, "querying book_ids") - } - - return ret, nil -} - -func applyBookDomain(db *gorm.DB, noteQuery *gorm.DB, rule database.RepetitionRule) (*gorm.DB, error) { - ret := noteQuery - - if rule.BookDomain != database.BookDomainAll { - bookIDs, err := getRuleBookIDs(db, rule.ID) - if err != nil { - return nil, errors.Wrap(err, "getting book_ids") - } - - ret = ret.Joins("INNER JOIN books ON notes.book_uuid = books.uuid") - - if rule.BookDomain == database.BookDomainExluding { - ret = ret.Where("books.id NOT IN (?)", bookIDs) - } else if rule.BookDomain == database.BookDomainIncluding { - ret = ret.Where("books.id IN (?)", bookIDs) - } - } - - return ret, nil -} - -func getNotes(db, conn *gorm.DB, rule database.RepetitionRule, dst *[]database.Note) error { - c, err := applyBookDomain(db, conn, rule) - if err != nil { - return errors.Wrap(err, "building query for book threahold 1") - } - - // TODO: ordering by random() does not scale if table grows large - if err := c.Where("notes.user_id = ?", rule.UserID).Order("random()").Limit(rule.NoteCount).Preload("Book").Find(&dst).Error; err != nil { - return errors.Wrap(err, "getting notes") - } - - return nil -} - -// getBalancedNotes returns a set of notes with a 'balanced' ratio of added_on dates -func getBalancedNotes(db *gorm.DB, rule database.RepetitionRule) ([]database.Note, error) { - now := time.Now() - t1 := now.AddDate(0, 0, -3).UnixNano() - t2 := now.AddDate(0, 0, -7).UnixNano() - - baseConn := db.Where("notes.deleted IS NOT true") - - // Get notes into three buckets with different threshold values - var stage1, stage2, stage3 []database.Note - if err := getNotes(db, baseConn.Where("notes.added_on > ?", t1), rule, &stage1); err != nil { - return nil, errors.Wrap(err, "Failed to get notes with threshold 1") - } - if err := getNotes(db, baseConn.Where("notes.added_on > ? AND notes.added_on < ?", t2, t1), rule, &stage2); err != nil { - return nil, errors.Wrap(err, "Failed to get notes with threshold 2") - } - if err := getNotes(db, baseConn.Where("notes.added_on < ?", t2), rule, &stage3); err != nil { - return nil, errors.Wrap(err, "Failed to get notes with threshold 3") - } - - notes := []database.Note{} - - // pick one from each bucket at a time until the result is filled - i1 := 0 - i2 := 0 - i3 := 0 - k := 0 - for { - if i1+i2+i3 >= rule.NoteCount { - break - } - - // if there are not enough notes to fill the result, break - if len(stage1) == i1 && len(stage2) == i2 && len(stage3) == i3 { - break - } - - if k%3 == 0 { - if len(stage1) > i1 { - i1++ - } - } else if k%3 == 1 { - if len(stage2) > i2 { - i2++ - } - } else if k%3 == 2 { - if len(stage3) > i3 { - i3++ - } - } - - k++ - } - - notes = append(notes, stage1[:i1]...) - notes = append(notes, stage2[:i2]...) - notes = append(notes, stage3[:i3]...) - - sort.SliceStable(notes, func(i, j int) bool { - n1 := notes[i] - n2 := notes[j] - - return n1.AddedOn > n2.AddedOn - }) - - return notes, nil -} diff --git a/pkg/server/job/repetition/strategy_test.go b/pkg/server/job/repetition/strategy_test.go deleted file mode 100644 index 80d9bbe7..00000000 --- a/pkg/server/job/repetition/strategy_test.go +++ /dev/null @@ -1,112 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package repetition - -import ( - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/testutils" - "github.com/pkg/errors" -) - -func init() { - testutils.InitTestDB() -} - -func TestApplyBookDomain(t *testing.T) { - defer testutils.ClearData() - - user := testutils.SetupUserData() - b1 := database.Book{ - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - b2 := database.Book{ - UserID: user.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2") - b3 := database.Book{ - UserID: user.ID, - Label: "golang", - } - testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3") - - n1 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - n2 := database.Note{ - UserID: user.ID, - BookUUID: b2.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2") - n3 := database.Note{ - UserID: user.ID, - BookUUID: b3.UUID, - } - testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3") - - var n1Record, n2Record, n3Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", n3.UUID).First(&n3Record), "finding n3") - - t.Run("book domain all", func(t *testing.T) { - rule := database.RepetitionRule{ - UserID: user.ID, - BookDomain: database.BookDomainAll, - } - - conn, err := applyBookDomain(testutils.DB, testutils.DB, rule) - if err != nil { - t.Fatal(errors.Wrap(err, "executing").Error()) - } - - var result []database.Note - testutils.MustExec(t, conn.Order("id ASC").Find(&result), "finding notes") - - expected := []database.Note{n1Record, n2Record, n3Record} - assert.DeepEqual(t, result, expected, "result mismatch") - }) - - t.Run("book domain exclude", func(t *testing.T) { - rule := database.RepetitionRule{ - UserID: user.ID, - BookDomain: database.BookDomainExluding, - Books: []database.Book{b1}, - } - testutils.MustExec(t, testutils.DB.Save(&rule), "preparing rule") - - conn, err := applyBookDomain(testutils.DB, testutils.DB, rule) - if err != nil { - t.Fatal(errors.Wrap(err, "executing").Error()) - } - - var result []database.Note - testutils.MustExec(t, conn.Order("id ASC").Find(&result), "finding notes") - - expected := []database.Note{n2Record, n3Record} - assert.DeepEqual(t, result, expected, "result mismatch") - }) -} diff --git a/pkg/server/mailer/mailer.go b/pkg/server/mailer/mailer.go index aa940f22..cbc66e33 100644 --- a/pkg/server/mailer/mailer.go +++ b/pkg/server/mailer/mailer.go @@ -42,8 +42,6 @@ var ( EmailTypeEmailVerification = "verify_email" // EmailTypeWelcome represents an welcome email EmailTypeWelcome = "welcome" - // EmailTypeInactiveReminder represents an inactivity reminder email - EmailTypeInactiveReminder = "inactive" // EmailTypeSubscriptionConfirmation represents an inactivity reminder email EmailTypeSubscriptionConfirmation = "subscription_confirmation" ) @@ -109,10 +107,6 @@ func NewTemplates(srcDir *string) Templates { if err != nil { panic(errors.Wrap(err, "initializing password reset template")) } - inactiveReminderText, err := initTextTmpl(box, EmailTypeInactiveReminder) - if err != nil { - panic(errors.Wrap(err, "initializing password reset template")) - } subscriptionConfirmationText, err := initTextTmpl(box, EmailTypeSubscriptionConfirmation) if err != nil { panic(errors.Wrap(err, "initializing password reset template")) @@ -127,7 +121,6 @@ func NewTemplates(srcDir *string) Templates { T.set(EmailTypeResetPasswordAlert, EmailKindText, passwordResetAlertText) T.set(EmailTypeEmailVerification, EmailKindText, verifyEmailText) T.set(EmailTypeWelcome, EmailKindText, welcomeText) - T.set(EmailTypeInactiveReminder, EmailKindText, inactiveReminderText) T.set(EmailTypeSubscriptionConfirmation, EmailKindText, subscriptionConfirmationText) T.set(EmailTypeDigest, EmailKindText, digestText) diff --git a/pkg/server/mailer/templates/main.go b/pkg/server/mailer/templates/main.go index b0d12547..1925a4a2 100644 --- a/pkg/server/mailer/templates/main.go +++ b/pkg/server/mailer/templates/main.go @@ -134,21 +134,6 @@ func (c Context) welcomeHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte(body)) } -func (c Context) inactiveHandler(w http.ResponseWriter, r *http.Request) { - data := mailer.InactiveReminderTmplData{ - SampleNoteUUID: "some-uuid", - WebURL: "http://localhost:3000", - Token: "some-random-token", - } - body, err := c.Tmpl.Execute(mailer.EmailTypeInactiveReminder, mailer.EmailKindText, data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Write([]byte(body)) -} - func (c Context) homeHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Email development server is running.")) } @@ -187,6 +172,5 @@ func main() { http.HandleFunc("/password-reset", ctx.passwordResetHandler) http.HandleFunc("/password-reset-alert", ctx.passwordResetAlertHandler) http.HandleFunc("/welcome", ctx.welcomeHandler) - http.HandleFunc("/inactive-reminder", ctx.inactiveHandler) log.Fatal(http.ListenAndServe(":2300", nil)) } diff --git a/pkg/server/mailer/types.go b/pkg/server/mailer/types.go index ed0f3000..e43b3226 100644 --- a/pkg/server/mailer/types.go +++ b/pkg/server/mailer/types.go @@ -82,13 +82,6 @@ type WelcomeTmplData struct { WebURL string } -// InactiveReminderTmplData is a template data for welcome emails -type InactiveReminderTmplData struct { - SampleNoteUUID string - WebURL string - Token string -} - // EmailTypeSubscriptionConfirmationTmplData is a template data for reset password emails type EmailTypeSubscriptionConfirmationTmplData struct { AccountEmail string diff --git a/pkg/server/main.go b/pkg/server/main.go index 60fcfe37..f9a7ebdf 100644 --- a/pkg/server/main.go +++ b/pkg/server/main.go @@ -30,7 +30,6 @@ import ( "github.com/dnote/dnote/pkg/server/database" "github.com/dnote/dnote/pkg/server/dbconn" "github.com/dnote/dnote/pkg/server/handlers" - "github.com/dnote/dnote/pkg/server/job" "github.com/dnote/dnote/pkg/server/mailer" "github.com/dnote/dnote/pkg/server/web" "github.com/jinzhu/gorm" @@ -121,18 +120,6 @@ func initApp() app.App { } } -func runJob(a app.App) error { - jobRunner, err := job.NewRunner(a.DB, a.Clock, a.EmailTemplates, a.EmailBackend, a.Config) - if err != nil { - return errors.Wrap(err, "getting a job runner") - } - if err := jobRunner.Do(); err != nil { - return errors.Wrap(err, "running job") - } - - return nil -} - func startCmd() { app := initApp() defer app.DB.Close() @@ -141,10 +128,6 @@ func startCmd() { panic(errors.Wrap(err, "running migrations")) } - if err := runJob(app); err != nil { - panic(errors.Wrap(err, "running job")) - } - srv, err := initServer(app) if err != nil { panic(errors.Wrap(err, "initializing server")) diff --git a/pkg/server/operations/digests.go b/pkg/server/operations/digests.go deleted file mode 100644 index acd062e6..00000000 --- a/pkg/server/operations/digests.go +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package operations - -import ( - "github.com/dnote/dnote/pkg/server/database" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -// CreateDigest creates a new digest -func CreateDigest(db *gorm.DB, rule database.RepetitionRule, notes []database.Note) (database.Digest, error) { - var maxVersion int - if err := db.Raw("SELECT COALESCE(max(version), 0) FROM digests WHERE rule_id = ?", rule.ID).Row().Scan(&maxVersion); err != nil { - return database.Digest{}, errors.Wrap(err, "finding max version") - } - - digest := database.Digest{ - RuleID: rule.ID, - UserID: rule.UserID, - Version: maxVersion + 1, - Notes: notes, - } - if err := db.Save(&digest).Error; err != nil { - return database.Digest{}, errors.Wrap(err, "saving digest") - } - - return digest, nil -} diff --git a/pkg/server/operations/digests_test.go b/pkg/server/operations/digests_test.go deleted file mode 100644 index 9fab1125..00000000 --- a/pkg/server/operations/digests_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package operations - -import ( - // "fmt" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/testutils" - "github.com/pkg/errors" -) - -func TestCreateDigest(t *testing.T) { - t.Run("no previous digest", func(t *testing.T) { - defer testutils.ClearData() - - db := testutils.DB - - user := testutils.SetupUserData() - rule := database.RepetitionRule{UserID: user.ID} - testutils.MustExec(t, testutils.DB.Save(&rule), "preparing rule") - - result, err := CreateDigest(db, rule, nil) - if err != nil { - t.Fatal(errors.Wrap(err, "performing")) - } - - assert.Equal(t, result.Version, 1, "Version mismatch") - }) - - t.Run("with previous digest", func(t *testing.T) { - defer testutils.ClearData() - - db := testutils.DB - - user := testutils.SetupUserData() - rule := database.RepetitionRule{UserID: user.ID} - testutils.MustExec(t, testutils.DB.Save(&rule), "preparing rule") - - d := database.Digest{UserID: user.ID, RuleID: rule.ID, Version: 8} - testutils.MustExec(t, testutils.DB.Save(&d), "preparing digest") - - result, err := CreateDigest(db, rule, nil) - if err != nil { - t.Fatal(errors.Wrap(err, "performing")) - } - - assert.Equal(t, result.Version, 9, "Version mismatch") - }) -} diff --git a/pkg/server/presenters/digest.go b/pkg/server/presenters/digest.go deleted file mode 100644 index 64966ab1..00000000 --- a/pkg/server/presenters/digest.go +++ /dev/null @@ -1,89 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package presenters - -import ( - "time" - - "github.com/dnote/dnote/pkg/server/database" -) - -// Digest is a presented digest -type Digest struct { - UUID string `json:"uuid"` - Version int `json:"version"` - RepetitionRule RepetitionRule `json:"repetition_rule"` - Notes []DigestNote `json:"notes"` - IsRead bool `json:"is_read"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// DigestNote is a presented note inside a digest -type DigestNote struct { - Note - IsReviewed bool `json:"is_reviewed"` -} - -func presentDigestNote(note database.Note) DigestNote { - ret := DigestNote{ - Note: PresentNote(note), - IsReviewed: note.NoteReview.UUID != "", - } - - return ret -} - -func presentDigestNotes(notes []database.Note) []DigestNote { - ret := []DigestNote{} - - for _, note := range notes { - n := presentDigestNote(note) - ret = append(ret, n) - } - - return ret -} - -// PresentDigest presents a digest -func PresentDigest(digest database.Digest) Digest { - ret := Digest{ - UUID: digest.UUID, - Notes: presentDigestNotes(digest.Notes), - Version: digest.Version, - RepetitionRule: PresentRepetitionRule(digest.Rule), - IsRead: len(digest.Receipts) > 0, - CreatedAt: digest.CreatedAt, - UpdatedAt: digest.UpdatedAt, - } - - return ret -} - -// PresentDigests presetns digests -func PresentDigests(digests []database.Digest) []Digest { - ret := []Digest{} - - for _, digest := range digests { - p := PresentDigest(digest) - ret = append(ret, p) - } - - return ret -} diff --git a/pkg/server/presenters/digest_receipt.go b/pkg/server/presenters/digest_receipt.go deleted file mode 100644 index 674fe018..00000000 --- a/pkg/server/presenters/digest_receipt.go +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package presenters - -import ( - "time" - - "github.com/dnote/dnote/pkg/server/database" -) - -// DigestReceipt is a presented receipt -type DigestReceipt struct { - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// PresentDigestReceipt presents a receipt -func PresentDigestReceipt(receipt database.DigestReceipt) DigestReceipt { - ret := DigestReceipt{ - CreatedAt: receipt.CreatedAt, - UpdatedAt: receipt.UpdatedAt, - } - - return ret -} - -// PresentDigestReceipts presents receipts -func PresentDigestReceipts(receipts []database.DigestReceipt) []DigestReceipt { - ret := []DigestReceipt{} - - for _, receipt := range receipts { - r := PresentDigestReceipt(receipt) - ret = append(ret, r) - } - - return ret -} diff --git a/pkg/server/presenters/email_preference.go b/pkg/server/presenters/email_preference.go index 66569132..244ea071 100644 --- a/pkg/server/presenters/email_preference.go +++ b/pkg/server/presenters/email_preference.go @@ -26,19 +26,17 @@ import ( // EmailPreference is a presented email digest type EmailPreference struct { - InactiveReminder bool `json:"inactive_reminder"` - ProductUpdate bool `json:"product_update"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ProductUpdate bool `json:"product_update"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // PresentEmailPreference presents a digest func PresentEmailPreference(p database.EmailPreference) EmailPreference { ret := EmailPreference{ - InactiveReminder: p.InactiveReminder, - ProductUpdate: p.ProductUpdate, - CreatedAt: FormatTS(p.CreatedAt), - UpdatedAt: FormatTS(p.UpdatedAt), + ProductUpdate: p.ProductUpdate, + CreatedAt: FormatTS(p.CreatedAt), + UpdatedAt: FormatTS(p.UpdatedAt), } return ret diff --git a/pkg/server/presenters/repetition_rule.go b/pkg/server/presenters/repetition_rule.go deleted file mode 100644 index 9219a746..00000000 --- a/pkg/server/presenters/repetition_rule.go +++ /dev/null @@ -1,75 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package presenters - -import ( - "time" - - "github.com/dnote/dnote/pkg/server/database" -) - -// RepetitionRule is a presented digest rule -type RepetitionRule struct { - UUID string `json:"uuid"` - Title string `json:"title"` - Enabled bool `json:"enabled"` - Hour int `json:"hour" gorm:"index"` - Minute int `json:"minute" gorm:"index"` - Frequency int64 `json:"frequency"` - BookDomain string `json:"book_domain"` - LastActive int64 `json:"last_active"` - NextActive int64 `json:"next_active"` - Books []Book `json:"books"` - NoteCount int `json:"note_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// PresentRepetitionRule presents a digest rule -func PresentRepetitionRule(d database.RepetitionRule) RepetitionRule { - ret := RepetitionRule{ - UUID: d.UUID, - Title: d.Title, - Enabled: d.Enabled, - Hour: d.Hour, - Minute: d.Minute, - Frequency: d.Frequency, - BookDomain: d.BookDomain, - NoteCount: d.NoteCount, - LastActive: d.LastActive, - NextActive: d.NextActive, - Books: PresentBooks(d.Books), - CreatedAt: FormatTS(d.CreatedAt), - UpdatedAt: FormatTS(d.UpdatedAt), - } - - return ret -} - -// PresentRepetitionRules presents a slice of digest rules -func PresentRepetitionRules(ds []database.RepetitionRule) []RepetitionRule { - ret := []RepetitionRule{} - - for _, d := range ds { - p := PresentRepetitionRule(d) - ret = append(ret, p) - } - - return ret -} diff --git a/pkg/server/presenters/repetition_rule_test.go b/pkg/server/presenters/repetition_rule_test.go deleted file mode 100644 index d3597688..00000000 --- a/pkg/server/presenters/repetition_rule_test.go +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd - * - * This file is part of Dnote. - * - * Dnote is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Dnote is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Dnote. If not, see . - */ - -package presenters - -import ( - "fmt" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/server/database" -) - -func TestPresentRepetitionRule(t *testing.T) { - b1 := database.Book{UUID: "1cf8794f-4d61-4a9d-a9da-18f8db9e53cc", Label: "foo"} - b2 := database.Book{UUID: "ede00f3b-eab1-469c-ae12-c60cebeeef17", Label: "bar"} - d1 := database.RepetitionRule{ - UUID: "c725afb5-8bf1-4581-a0e7-0f683c15f3d0", - Title: "test title", - Enabled: true, - Hour: 1, - Minute: 2, - LastActive: 1571293000, - NextActive: 1571394000, - NoteCount: 10, - BookDomain: database.BookDomainAll, - Books: []database.Book{b1, b2}, - } - - testCases := []struct { - input database.RepetitionRule - expected RepetitionRule - }{ - { - input: d1, - expected: RepetitionRule{ - UUID: d1.UUID, - Title: d1.Title, - Enabled: d1.Enabled, - Hour: d1.Hour, - Minute: d1.Minute, - BookDomain: d1.BookDomain, - NoteCount: d1.NoteCount, - LastActive: d1.LastActive, - NextActive: d1.NextActive, - Books: []Book{ - { - UUID: b1.UUID, - USN: b1.USN, - CreatedAt: b1.CreatedAt, - UpdatedAt: b1.UpdatedAt, - Label: b1.Label, - }, - { - UUID: b2.UUID, - USN: b2.USN, - CreatedAt: b2.CreatedAt, - UpdatedAt: b2.UpdatedAt, - Label: b2.Label, - }, - }, - CreatedAt: d1.CreatedAt, - UpdatedAt: d1.UpdatedAt, - }, - }, - } - - for idx, tc := range testCases { - t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) { - result := PresentRepetitionRule(tc.input) - - assert.DeepEqual(t, result, tc.expected, "result mismatch") - }) - } -} diff --git a/pkg/server/testutils/main.go b/pkg/server/testutils/main.go index 3b890f12..956820d3 100644 --- a/pkg/server/testutils/main.go +++ b/pkg/server/testutils/main.go @@ -88,21 +88,6 @@ func ClearData() { if err := DB.Delete(&database.Session{}).Error; err != nil { panic(errors.Wrap(err, "Failed to clear sessions")) } - if err := DB.Delete(&database.Digest{}).Error; err != nil { - panic(errors.Wrap(err, "Failed to clear digests")) - } - if err := DB.Delete(&database.DigestNote{}).Error; err != nil { - panic(errors.Wrap(err, "Failed to clear digests")) - } - if err := DB.Delete(&database.DigestReceipt{}).Error; err != nil { - panic(errors.Wrap(err, "Failed to clear digest receipts")) - } - if err := DB.Delete(&database.RepetitionRule{}).Error; err != nil { - panic(errors.Wrap(err, "Failed to clear repetition rules")) - } - if err := DB.Delete(&database.NoteReview{}).Error; err != nil { - panic(errors.Wrap(err, "Failed to clear note review")) - } } // SetupUserData creates and returns a new user for testing purposes @@ -182,10 +167,9 @@ func SetupSession(t *testing.T, user database.User) database.Session { } // SetupEmailPreferenceData creates and returns a new email frequency for a user -func SetupEmailPreferenceData(user database.User, inactiveReminder bool) database.EmailPreference { +func SetupEmailPreferenceData(user database.User) database.EmailPreference { frequency := database.EmailPreference{ - UserID: user.ID, - InactiveReminder: inactiveReminder, + UserID: user.ID, } if err := DB.Save(&frequency).Error; err != nil { diff --git a/web/src/components/Header/Nav/index.tsx b/web/src/components/Header/Nav/index.tsx index b991e3c1..76c2a740 100644 --- a/web/src/components/Header/Nav/index.tsx +++ b/web/src/components/Header/Nav/index.tsx @@ -19,12 +19,7 @@ import React from 'react'; import classnames from 'classnames'; -import { - getNewPath, - getBooksPath, - getRepetitionsPath, - getDigestsPath -} from 'web/libs/paths'; +import { getNewPath, getBooksPath } from 'web/libs/paths'; import { Filters, toSearchObj } from 'jslib/helpers/filters'; import Item from './Item'; import styles from './Nav.scss'; @@ -41,8 +36,6 @@ const Nav: React.FunctionComponent = ({ filters }) => {
    - -
); diff --git a/web/src/components/Settings/Notifications/Form.tsx b/web/src/components/Settings/Notifications/Form.tsx index 110a4f3d..7cfb8ef5 100644 --- a/web/src/components/Settings/Notifications/Form.tsx +++ b/web/src/components/Settings/Notifications/Form.tsx @@ -26,17 +26,11 @@ import { useDispatch } from '../../../store'; import styles from './Form.scss'; enum Action { - setInactiveReminder, setProductUpdate } function formReducer(state, action): EmailPrefData { switch (action.type) { - case Action.setInactiveReminder: - return { - ...state, - inactiveReminder: action.data - }; case Action.setProductUpdate: return { ...state, @@ -92,31 +86,6 @@ const Form: React.FunctionComponent = ({ return (
-
-

Alerts

-

Email me when:

-
    -
  • - { - const { checked } = e.target; - - formDispatch({ - type: Action.setInactiveReminder, - data: checked - }); - }} - /> - -
  • -
-
-

News

diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 78aba35c..f136a6ff 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -37,14 +37,9 @@ import Books from './components/Books'; import Subscription from './components/Subscription'; import Classic from './components/Classic'; import Checkout from './components/Subscription/Checkout'; -import Repetition from './components/Repetition'; -import NewRepetition from './components/Repetition/New'; -import EditRepetition from './components/Repetition/Edit'; import PasswordResetRequest from './components/PasswordReset/Request'; import PasswordResetConfirm from './components/PasswordReset/Confirm'; import EmailPreference from './components/EmailPreference'; -import Digests from './components/Digests'; -import Digest from './components/Digest'; // paths import { @@ -64,12 +59,7 @@ import { prefEditRepetitionPathDef, verifyEmailPathDef, classicMigrationPathDef, - repetitionsPathDef, - newRepetitionRulePathDef, - editRepetitionRulePathDef, - emailPreferencePathDef, - digestsPathDef, - digestPathDef + emailPreferencePathDef } from './libs/paths'; const AuthenticatedHome = userOnly(Home); @@ -89,9 +79,6 @@ const AuthenticatedSubscriptionCheckout = userOnly( Checkout, getJoinPath().pathname ); -const AuthenticatedRepetition = userOnly(Repetition); -const AuthenticatedNewRepetition = userOnly(NewRepetition); -const AuthenticatedEditRepetition = userOnly(EditRepetition); const routes = [ { @@ -169,36 +156,11 @@ const routes = [ exact: true, component: GuestPasswordResetConfirm }, - { - path: repetitionsPathDef, - exact: true, - component: AuthenticatedRepetition - }, - { - path: newRepetitionRulePathDef, - exact: true, - component: AuthenticatedNewRepetition - }, - { - path: editRepetitionRulePathDef, - exact: true, - component: AuthenticatedEditRepetition - }, { path: emailPreferencePathDef, exact: true, component: EmailPreference }, - { - path: digestsPathDef, - exact: true, - component: Digests - }, - { - path: digestPathDef, - exact: true, - component: Digest - }, { component: NotFound }