diff --git a/.travis.yml b/.travis.yml index 7f9be694..529b7bae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ before_install: - sudo apt-get --yes remove postgresql\* - sudo apt-get install -y postgresql-11 postgresql-client-11 - sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf + - sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/11/main/postgresql.conf - sudo service postgresql restart 11 before_script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1983bf49..ea780451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ All notable changes to the projects under this repository will be documented in The following log documents the history of the server project. +### Unreleased + +- Remove the deprecated features related to digests and repetition rules. + ### 0.5.0 - 2020-02-06 #### Changed diff --git a/jslib/src/operations/digests.ts b/jslib/src/operations/digests.ts deleted file mode 100644 index 70b7a8c5..00000000 --- a/jslib/src/operations/digests.ts +++ /dev/null @@ -1,34 +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 . - */ - -import initDigestsService from '../services/digests'; -import { HttpClientConfig } from '../helpers/http'; - -export default function init(c: HttpClientConfig) { - const digestsService = initDigestsService(c); - - return { - fetchAll: params => { - return digestsService.fetchAll(params); - }, - - fetch: (noteUUID: string) => { - return digestsService.fetch(noteUUID); - } - }; -} diff --git a/jslib/src/operations/index.ts b/jslib/src/operations/index.ts index 2d4826b6..7a13f93f 100644 --- a/jslib/src/operations/index.ts +++ b/jslib/src/operations/index.ts @@ -19,18 +19,15 @@ import { HttpClientConfig } from '../helpers/http'; import initBooksOperation from './books'; import initNotesOperation from './notes'; -import initDigestsOperation from './digests'; // init initializes operations with the given http configuration // and returns an object of all services. export default function initOperations(c: HttpClientConfig) { const booksOperation = initBooksOperation(c); const notesOperation = initNotesOperation(c); - const digestsOperation = initDigestsOperation(c); return { books: booksOperation, - notes: notesOperation, - digests: digestsOperation + notes: notesOperation }; } diff --git a/jslib/src/operations/types.ts b/jslib/src/operations/types.ts index 6ca0dcd0..6066020f 100644 --- a/jslib/src/operations/types.ts +++ b/jslib/src/operations/types.ts @@ -56,49 +56,3 @@ export type BookData = { updated_at: string; label: string; }; - -// BookDomain is the possible values for the field in the repetition_rule -// indicating how to derive the source books for the repetition_rule. -export enum BookDomain { - // All incidates that all books are eligible to be the source books - All = 'all', - // Including incidates that some specified books are eligible to be the source books - Including = 'including', - // Excluding incidates that all books except for some specified books are eligible to be the source books - Excluding = 'excluding' -} - -export interface RepetitionRuleData { - uuid: string; - title: string; - enabled: boolean; - hour: number; - minute: number; - bookDomain: BookDomain; - frequency: number; - books: BookData[]; - lastActive: number; - nextActive: number; - noteCount: number; - createdAt: string; - updatedAt: string; -} - -export interface ReceiptData { - createdAt: string; - updatedAt: string; -} - -export interface DigestData { - uuid: string; - createdAt: string; - updatedAt: string; - version: number; - notes: DigestNoteData[]; - isRead: boolean; - repetitionRule: RepetitionRuleData; -} - -export interface DigestNoteData extends NoteData { - isReviewed: boolean; -} diff --git a/jslib/src/services/digests.ts b/jslib/src/services/digests.ts deleted file mode 100644 index 618921b8..00000000 --- a/jslib/src/services/digests.ts +++ /dev/null @@ -1,88 +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 . - */ - -import { getHttpClient, HttpClientConfig } from '../helpers/http'; -import { getPath } from '../helpers/url'; -import { DigestData, DigestNoteData } from '../operations/types'; -import { mapNote } from './notes'; - -function mapDigestNote(item): DigestNoteData { - const note = mapNote(item); - - return { - ...note, - isReviewed: item.is_reviewed - }; -} - -// mapDigest maps the presented digest response to DigestData -function mapDigest(item): DigestData { - return { - uuid: item.uuid, - createdAt: item.created_at, - updatedAt: item.updated_at, - version: item.version, - notes: item.notes.map(mapDigestNote), - repetitionRule: { - uuid: item.repetition_rule.uuid, - title: item.repetition_rule.title, - enabled: item.repetition_rule.enabled, - hour: item.repetition_rule.hour, - minute: item.repetition_rule.minute, - bookDomain: item.repetition_rule.book_domain, - frequency: item.repetition_rule.frequency, - books: item.repetition_rule.books, - lastActive: item.repetition_rule.last_active, - nextActive: item.repetition_rule.next_active, - noteCount: item.repetition_rule.note_count, - createdAt: item.repetition_rule.created_at, - updatedAt: item.repetition_rule.updated_at - }, - isRead: item.is_read - }; -} - -export interface FetchAllResult { - total: number; - items: DigestData[]; -} - -export default function init(config: HttpClientConfig) { - const client = getHttpClient(config); - - return { - fetch: (digestUUID: string): Promise => { - const endpoint = `/digests/${digestUUID}`; - - return client.get(endpoint).then(mapDigest); - }, - - fetchAll: ({ page, status }): Promise => { - const path = '/digests'; - - const endpoint = getPath(path, { page, status }); - - return client.get(endpoint).then(res => { - return { - total: res.total, - items: res.items.map(mapDigest) - }; - }); - } - }; -} diff --git a/jslib/src/services/index.ts b/jslib/src/services/index.ts index 4e2966bf..f486a177 100644 --- a/jslib/src/services/index.ts +++ b/jslib/src/services/index.ts @@ -21,9 +21,6 @@ import initUsersService from './users'; import initBooksService from './books'; import initNotesService from './notes'; import initPaymentService from './payment'; -import initDigestsService from './digests'; -import initRepetitionRulesService from './repetitionRules'; -import initNoteReviews from './noteReviews'; // init initializes service helpers with the given http configuration // and returns an object of all services. @@ -32,17 +29,11 @@ export default function initServices(c: HttpClientConfig) { const booksService = initBooksService(c); const notesService = initNotesService(c); const paymentService = initPaymentService(c); - const digestsService = initDigestsService(c); - const repetitionRulesService = initRepetitionRulesService(c); - const noteReviewsService = initNoteReviews(c); return { users: usersService, books: booksService, notes: notesService, - payment: paymentService, - digests: digestsService, - repetitionRules: repetitionRulesService, - noteReviews: noteReviewsService + payment: paymentService }; } diff --git a/jslib/src/services/noteReviews.ts b/jslib/src/services/noteReviews.ts deleted file mode 100644 index 5d2996a6..00000000 --- a/jslib/src/services/noteReviews.ts +++ /dev/null @@ -1,56 +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 . - */ - -import { getHttpClient, HttpClientConfig } from '../helpers/http'; - -export interface CreateDeleteNoteReviewPayload { - digestUUID: string; - noteUUID: string; -} - -export default function init(config: HttpClientConfig) { - const client = getHttpClient(config); - - return { - create: ({ - digestUUID, - noteUUID - }: CreateDeleteNoteReviewPayload): Promise => { - const endpoint = '/note_review'; - const payload = { - digest_uuid: digestUUID, - note_uuid: noteUUID - }; - - return client.post(endpoint, payload); - }, - - remove: ({ - digestUUID, - noteUUID - }: CreateDeleteNoteReviewPayload): Promise => { - const endpoint = '/note_review'; - const payload = { - digest_uuid: digestUUID, - note_uuid: noteUUID - }; - - return client.del(endpoint, payload); - } - }; -} diff --git a/jslib/src/services/repetitionRules.ts b/jslib/src/services/repetitionRules.ts deleted file mode 100644 index 77333dc6..00000000 --- a/jslib/src/services/repetitionRules.ts +++ /dev/null @@ -1,94 +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 . - */ - -import { RepetitionRuleData, BookDomain } from '../operations/types'; -import { getHttpClient, HttpClientConfig } from '../helpers/http'; -import { getPath } from '../helpers/url'; - -export interface CreateParams { - title: string; - hour: number; - minute: number; - book_domain: BookDomain; - frequency: number; - note_count: number; - book_uuids: string[]; - enabled: boolean; -} - -export type UpdateParams = Partial; - -function mapData(d): RepetitionRuleData { - return { - uuid: d.uuid, - title: d.title, - enabled: d.enabled, - hour: d.hour, - minute: d.minute, - bookDomain: d.book_domain, - frequency: d.frequency, - books: d.books, - noteCount: d.note_count, - lastActive: d.last_active, - nextActive: d.next_active, - createdAt: d.created_at, - updatedAt: d.updated_at - }; -} - -export default function init(config: HttpClientConfig) { - const client = getHttpClient(config); - - return { - fetch: (uuid: string, queries = {}): Promise => { - const path = `/repetition_rules/${uuid}`; - const endpoint = getPath(path, queries); - - return client.get(endpoint).then(resp => { - return mapData(resp); - }); - }, - fetchAll: (): Promise => { - const endpoint = '/repetition_rules'; - - return client.get(endpoint).then(resp => { - return resp.map(mapData); - }); - }, - create: (params: CreateParams) => { - const endpoint = '/repetition_rules'; - - return client.post(endpoint, params).then(resp => { - return mapData(resp); - }); - }, - update: (uuid: string, params: UpdateParams, queries = {}) => { - const path = `/repetition_rules/${uuid}`; - const endpoint = getPath(path, queries); - - return client.patch(endpoint, params).then(resp => { - return mapData(resp); - }); - }, - remove: (uuid: string) => { - const endpoint = `/repetition_rules/${uuid}`; - - return client.del(endpoint); - } - }; -} diff --git a/pkg/cli/main_test.go b/pkg/cli/main_test.go index a2e81d65..ecf819af 100644 --- a/pkg/cli/main_test.go +++ b/pkg/cli/main_test.go @@ -51,7 +51,8 @@ func TestMain(m *testing.M) { func TestInit(t *testing.T) { // Execute - testutils.RunDnoteCmd(t, opts, binaryName) + // run an arbitrary command "view" due to https://github.com/spf13/cobra/issues/1056 + testutils.RunDnoteCmd(t, opts, binaryName, "view") defer testutils.RemoveDir(t, opts.HomeDir) db := database.OpenTestDB(t, opts.DnoteDir) 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 16590ea9..f3dbc86e 100644 --- a/pkg/server/app/notes.go +++ b/pkg/server/app/notes.go @@ -157,11 +157,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/notes_test.go b/pkg/server/app/notes_test.go index d15fbf74..e589dc7e 100644 --- a/pkg/server/app/notes_test.go +++ b/pkg/server/app/notes_test.go @@ -259,45 +259,3 @@ func TestDeleteNote(t *testing.T) { }() } } - -func TestDeleteNote_DigestNotes(t *testing.T) { - defer testutils.ClearData() - - user := testutils.SetupUserData() - - b1 := database.Book{UserID: user.ID, Label: "testBook"} - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - n1 := database.Note{UserID: user.ID, Deleted: false, Body: "n1", BookUUID: b1.UUID} - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - n2 := database.Note{UserID: user.ID, Deleted: false, Body: "n2", BookUUID: b1.UUID} - testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2") - - d1 := database.Digest{UserID: user.ID} - testutils.MustExec(t, testutils.DB.Save(&d1), "preparing d1") - dn1 := database.DigestNote{NoteID: n1.ID, DigestID: d1.ID} - testutils.MustExec(t, testutils.DB.Save(&dn1), "preparing dn1") - dn2 := database.DigestNote{NoteID: n2.ID, DigestID: d1.ID} - testutils.MustExec(t, testutils.DB.Save(&dn2), "preparing dn2") - - a := NewTest(nil) - - tx := testutils.DB.Begin() - if _, err := a.DeleteNote(tx, user, n1); err != nil { - tx.Rollback() - t.Fatal(errors.Wrap(err, "deleting note")) - } - tx.Commit() - - var noteCount, digestNoteCount int - var dn2Record database.DigestNote - - testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes") - testutils.MustExec(t, testutils.DB.Model(&database.DigestNote{}).Count(&digestNoteCount), "counting digest_notes") - - assert.Equal(t, noteCount, 2, "note count mismatch") - assert.Equal(t, digestNoteCount, 1, "digest_notes count mismatch") - - testutils.MustExec(t, testutils.DB.Where("id = ?", dn2.ID).First(&dn2Record), "finding dn2") - assert.Equal(t, dn2Record.NoteID, dn2.NoteID, "dn2 NoteID mismatch") - assert.Equal(t, dn2Record.DigestID, dn2.DigestID, "dn2 DigestID mismatch") -} 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/consts.go b/pkg/server/database/consts.go index fb8ab5e9..69f04711 100644 --- a/pkg/server/database/consts.go +++ b/pkg/server/database/consts.go @@ -25,8 +25,6 @@ const ( TokenTypeEmailVerification = "email_verification" // TokenTypeEmailPreference is a type of a token for updating email preference TokenTypeEmailPreference = "email_preference" - // TokenTypeRepetition is a type of a token for viewing and editing repetition rules - TokenTypeRepetition = "repetition_rules" ) const ( diff --git a/pkg/server/database/database.go b/pkg/server/database/database.go index 7558e703..30eea6f5 100644 --- a/pkg/server/database/database.go +++ b/pkg/server/database/database.go @@ -47,11 +47,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 af8feb92..01c667d3 100644 --- a/pkg/server/database/models.go +++ b/pkg/server/database/models.go @@ -46,21 +46,20 @@ 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:"-"` - Client string `gorm:"index"` + 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"` + Client string `gorm:"index"` } // User is a model for a user @@ -127,59 +126,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 ac1ff925..d0a85ee6 100644 --- a/pkg/server/handlers/routes.go +++ b/pkg/server/handlers/routes.go @@ -352,15 +352,6 @@ func (a *API) NewRouter() (*mux.Router, error) { {"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/v3_auth_test.go b/pkg/server/handlers/v3_auth_test.go index ca668d16..964a6c6f 100644 --- a/pkg/server/handlers/v3_auth_test.go +++ b/pkg/server/handlers/v3_auth_test.go @@ -130,10 +130,6 @@ func TestRegister(t *testing.T) { assert.Equal(t, user.StripeCustomerID, "", "StripeCustomerID mismatch") assert.Equal(t, user.MaxUSN, 0, "MaxUSN mismatch") - var repetitionRuleCount int - testutils.MustExec(t, testutils.DB.Model(&database.RepetitionRule{}).Where("user_id = ?", account.UserID).Count(&repetitionRuleCount), "counting repetition rules") - assert.Equal(t, repetitionRuleCount, 1, "repetitionRuleCount mismatch") - // welcome email assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch") assert.DeepEqual(t, emailBackend.Emails[0].To, []string{tc.email}, "email to mismatch") diff --git a/pkg/server/job/job.go b/pkg/server/job/job.go index da83e188..15450c00 100644 --- a/pkg/server/job/job.go +++ b/pkg/server/job/job.go @@ -24,7 +24,6 @@ import ( "github.com/dnote/dnote/pkg/clock" "github.com/dnote/dnote/pkg/server/config" "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" @@ -103,7 +102,6 @@ func scheduleJob(c *cron.Cron, spec string, cmd func()) { 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() @@ -131,29 +129,6 @@ func (r *Runner) Do() error { 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{ diff --git a/pkg/server/job/remind/inactive.go b/pkg/server/job/remind/inactive.go index 13d17a2a..e7fc0f03 100644 --- a/pkg/server/job/remind/inactive.go +++ b/pkg/server/job/remind/inactive.go @@ -143,17 +143,22 @@ func (c *Context) process(info inactiveUserInfo) error { return errors.Wrap(err, "getting sender email") } + tok, err := mailer.GetToken(c.DB, info.userID, database.TokenTypeEmailPreference) + if err != nil { + return errors.Wrap(err, "getting email token") + } + tmplData := mailer.InactiveReminderTmplData{ WebURL: c.Config.WebURL, SampleNoteUUID: info.sampleNoteUUID, - Token: "blah", + Token: tok.Value, } 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 { + if err := c.EmailBackend.Queue("Your Dnote stopped growing", sender, []string{info.email}, mailer.EmailKindText, body); err != nil { return errors.Wrap(err, "queueing email") } 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 24eb7488..00000000 --- a/pkg/server/job/repetition/repetition.go +++ /dev/null @@ -1,297 +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/config" - "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 config.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 := app.GetSenderEmail(c.Config, "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..60a95bac 100644 --- a/pkg/server/mailer/mailer.go +++ b/pkg/server/mailer/mailer.go @@ -36,8 +36,6 @@ var ( EmailTypeResetPassword = "reset_password" // EmailTypeResetPasswordAlert represents a password change notification email EmailTypeResetPasswordAlert = "reset_password_alert" - // EmailTypeDigest represents a weekly digest email - EmailTypeDigest = "digest" // EmailTypeEmailVerification represents an email verification email EmailTypeEmailVerification = "verify_email" // EmailTypeWelcome represents an welcome email @@ -117,10 +115,6 @@ func NewTemplates(srcDir *string) Templates { if err != nil { panic(errors.Wrap(err, "initializing password reset template")) } - digestText, err := initTextTmpl(box, EmailTypeDigest) - if err != nil { - panic(errors.Wrap(err, "initializing digest template")) - } T := Templates{} T.set(EmailTypeResetPassword, EmailKindText, passwordResetText) @@ -129,7 +123,6 @@ func NewTemplates(srcDir *string) Templates { T.set(EmailTypeWelcome, EmailKindText, welcomeText) T.set(EmailTypeInactiveReminder, EmailKindText, inactiveReminderText) T.set(EmailTypeSubscriptionConfirmation, EmailKindText, subscriptionConfirmationText) - T.set(EmailTypeDigest, EmailKindText, digestText) return T } diff --git a/pkg/server/mailer/templates/main.go b/pkg/server/mailer/templates/main.go index e64b0584..5d3d055e 100644 --- a/pkg/server/mailer/templates/main.go +++ b/pkg/server/mailer/templates/main.go @@ -21,61 +21,15 @@ package main import ( "log" "net/http" - "time" "github.com/dnote/dnote/pkg/server/config" "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/job/repetition" "github.com/dnote/dnote/pkg/server/mailer" "github.com/jinzhu/gorm" "github.com/joho/godotenv" _ "github.com/lib/pq" - "github.com/pkg/errors" ) -func (c Context) digestHandler(w http.ResponseWriter, r *http.Request) { - db := c.DB - - q := r.URL.Query() - digestUUID := q.Get("digest_uuid") - if digestUUID == "" { - http.Error(w, errors.New("Please provide digest_uuid query param").Error(), http.StatusBadRequest) - return - } - - var user database.User - if err := db.First(&user).Error; err != nil { - http.Error(w, errors.Wrap(err, "Failed to find user").Error(), http.StatusInternalServerError) - return - } - - var digest database.Digest - if err := db.Where("uuid = ?", digestUUID).Preload("Notes").First(&digest).Error; err != nil { - http.Error(w, errors.Wrap(err, "finding digest").Error(), http.StatusInternalServerError) - return - } - - var rule database.RepetitionRule - if err := db.Where("id = ?", digest.RuleID).First(&rule).Error; err != nil { - http.Error(w, errors.Wrap(err, "finding digest").Error(), http.StatusInternalServerError) - return - } - - now := time.Now() - _, body, err := repetition.BuildEmail(db, c.Tmpl, repetition.BuildEmailParams{ - Now: now, - User: user, - Digest: digest, - Rule: rule, - }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Write([]byte(body)) -} - func (c Context) passwordResetHandler(w http.ResponseWriter, r *http.Request) { data := mailer.EmailResetPasswordTmplData{ AccountEmail: "alice@example.com", @@ -176,7 +130,6 @@ func main() { ctx := Context{DB: db, Tmpl: tmpl} http.HandleFunc("/", ctx.homeHandler) - http.HandleFunc("/digest", ctx.digestHandler) http.HandleFunc("/email-verification", ctx.emailVerificationHandler) http.HandleFunc("/password-reset", ctx.passwordResetHandler) http.HandleFunc("/password-reset-alert", ctx.passwordResetAlertHandler) diff --git a/pkg/server/mailer/templates/src/digest.txt b/pkg/server/mailer/templates/src/digest.txt deleted file mode 100644 index 7b580a65..00000000 --- a/pkg/server/mailer/templates/src/digest.txt +++ /dev/null @@ -1,15 +0,0 @@ -REFRESH YOUR MEMORY - -There is a new automated spaced repetition "{{ .RuleTitle }} #{{ .DigestVersion }}" - - {{ .WebURL }}/digests/{{ .DigestUUID }} - - -MANAGE THE RULE - -Go to the following link to manage the notification and other settings for "{{ .RuleTitle }}" - - {{ .WebURL }}/preferences/repetitions/{{ .RuleUUID }}?token={{ .EmailSessionToken }} - -- Dnote team - diff --git a/pkg/server/mailer/templates/src/inactive.txt b/pkg/server/mailer/templates/src/inactive.txt index ae6ec6ec..b6f4d508 100644 --- a/pkg/server/mailer/templates/src/inactive.txt +++ b/pkg/server/mailer/templates/src/inactive.txt @@ -1,8 +1,8 @@ Hi, nothing has been added to your Dnote for some time. -What about revisiting one of your previous knowledge? {{ .WebURL }}/notes/{{ .SampleNoteUUID }} +What about revisiting one of your previous notes? {{ .WebURL }}/notes/{{ .SampleNoteUUID }} -Expand your knowledge base at {{ .WebURL }}/new or using Dnote apps. +You can add new notes at {{ .WebURL }}/new or using Dnote apps. - Dnote team diff --git a/pkg/server/mailer/tokens.go b/pkg/server/mailer/tokens.go index 705f4df5..a2ae431c 100644 --- a/pkg/server/mailer/tokens.go +++ b/pkg/server/mailer/tokens.go @@ -40,10 +40,10 @@ func generateRandomToken(bits int) (string, error) { // GetToken returns an token of the given kind for the user // by first looking up any unused record and creating one if none exists. -func GetToken(db *gorm.DB, user database.User, kind string) (database.Token, error) { +func GetToken(db *gorm.DB, userID int, kind string) (database.Token, error) { var tok database.Token conn := db. - Where("user_id = ? AND type =? AND used_at IS NULL", user.ID, kind). + Where("user_id = ? AND type =? AND used_at IS NULL", userID, kind). First(&tok) tokenVal, err := generateRandomToken(16) @@ -53,7 +53,7 @@ func GetToken(db *gorm.DB, user database.User, kind string) (database.Token, err if conn.RecordNotFound() { tok = database.Token{ - UserID: user.ID, + UserID: userID, Type: kind, Value: tokenVal, } diff --git a/pkg/server/mailer/types.go b/pkg/server/mailer/types.go index ed0f3000..5306fed6 100644 --- a/pkg/server/mailer/types.go +++ b/pkg/server/mailer/types.go @@ -18,45 +18,6 @@ package mailer -import ( - "time" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/justincampbell/timeago" -) - -// DigestNoteInfo contains note information for digest emails -type DigestNoteInfo struct { - UUID string - Content string - BookLabel string - TimeAgo string - Stage int -} - -// NewNoteInfo returns a new NoteInfo -func NewNoteInfo(note database.Note, stage int) DigestNoteInfo { - tm := time.Unix(0, int64(note.AddedOn)) - - return DigestNoteInfo{ - UUID: note.UUID, - Content: note.Body, - BookLabel: note.Book.Label, - TimeAgo: timeago.FromTime(tm), - Stage: stage, - } -} - -// DigestTmplData is a template data for digest emails -type DigestTmplData struct { - EmailSessionToken string - DigestUUID string - DigestVersion int - RuleUUID string - RuleTitle string - WebURL string -} - // EmailVerificationTmplData is a template data for email verification emails type EmailVerificationTmplData struct { Token string 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/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 99bcbb0c..62b06b81 100644 --- a/pkg/server/testutils/main.go +++ b/pkg/server/testutils/main.go @@ -84,21 +84,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 diff --git a/web/src/components/Common/EmailPreferenceForm.scss b/web/src/components/Common/EmailPreferenceForm.scss deleted file mode 100644 index 4ee397ef..00000000 --- a/web/src/components/Common/EmailPreferenceForm.scss +++ /dev/null @@ -1,34 +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 . - */ - -.heading { - font-size: 1.7rem; - margin-bottom: 7px; -} - -.radio { - display: inline-block; - - & ~ .radio { - margin-left: 26px; - } - - label { - padding-left: 3px; - } -} diff --git a/web/src/components/Common/EmailPreferenceForm.tsx b/web/src/components/Common/EmailPreferenceForm.tsx deleted file mode 100644 index 7abebae6..00000000 --- a/web/src/components/Common/EmailPreferenceForm.tsx +++ /dev/null @@ -1,138 +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 . - */ - -import React, { useState } from 'react'; - -import services from 'web/libs/services'; -import { useDispatch } from '../../store'; -import { receiveEmailPreference } from '../../store/auth'; -import Button from './Button'; - -import styles from './EmailPreferenceForm.scss'; - -const digestWeekly = 'weekly'; -const digestNever = 'never'; - -function getDigestFrequency(emailPreference: any): string { - if (emailPreference.inactive_reminder) { - return digestWeekly; - } - - return digestNever; -} - -interface Props { - emailPreference: any; - setSuccessMsg: (string) => void; - setFailureMsg: (string) => void; - token?: string; - actionsClassName?: string; -} - -const EmailPreferenceForm: React.FunctionComponent = ({ - emailPreference, - token, - setSuccessMsg, - setFailureMsg, - actionsClassName -}) => { - const freq = getDigestFrequency(emailPreference); - const [digestFrequency, setDigestFrequency] = useState(freq); - const [inProgress, setInProgress] = useState(false); - const dispatch = useDispatch(); - - function handleSubmit(e) { - e.preventDefault(); - - setSuccessMsg(''); - setFailureMsg(''); - setInProgress(true); - - services.users - .updateEmailPreference({ inactiveReminder: true, token }) - .then(updatedPreference => { - dispatch(receiveEmailPreference(updatedPreference)); - - setSuccessMsg('Updated email preference'); - setInProgress(false); - }) - .catch(err => { - setFailureMsg(`Failed to update. Error: ${err.message}`); - setInProgress(false); - }); - } - - return ( -
-
-
-
Email digest frequency
- -
-
- -
- -
- -
-
- -
- -
-
-
-
- ); -}; - -export default EmailPreferenceForm; diff --git a/web/src/components/Digest/ClearSearchBar.scss b/web/src/components/Digest/ClearSearchBar.scss deleted file mode 100644 index 30b03804..00000000 --- a/web/src/components/Digest/ClearSearchBar.scss +++ /dev/null @@ -1,43 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/variables'; -@import '../App/font'; -@import '../App/rem'; - -.wrapper { - margin-top: rem(12px); - font-weight: 600; -} - -.button { - color: $gray; - - &:hover { - color: $gray; - } -} - -.text { - @include font-size('small'); - - margin-left: rem(4px); - margin-top: rem(2px); -} diff --git a/web/src/components/Digest/ClearSearchBar.tsx b/web/src/components/Digest/ClearSearchBar.tsx deleted file mode 100644 index 0932baae..00000000 --- a/web/src/components/Digest/ClearSearchBar.tsx +++ /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 . - */ - -import React from 'react'; -import { Link } from 'react-router-dom'; - -import { getDigestPath } from 'web/libs/paths'; -import { SearchParams } from './types'; -import CloseIcon from '../Icons/Close'; -import styles from './ClearSearchBar.scss'; - -interface Props { - params: SearchParams; - digestUUID: string; -} - -const ClearSearchBar: React.FunctionComponent = ({ - params, - digestUUID -}) => { - const isActive = params.sort !== '' || params.status !== ''; - - if (!isActive) { - return null; - } - - return ( -
- - - - Clear the current filters, and sorts - - -
- ); -}; - -export default ClearSearchBar; diff --git a/web/src/components/Digest/Digest.scss b/web/src/components/Digest/Digest.scss deleted file mode 100644 index 7d0051e0..00000000 --- a/web/src/components/Digest/Digest.scss +++ /dev/null @@ -1,69 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/variables'; -@import '../App/font'; -@import '../App/rem'; - -.item { - text-align: left; - - & ~ & { - margin-top: rem(20px); - } -} - -.wrapper { - margin-top: rem(12px); - - @include breakpoint(lg) { - margin-top: rem(20px); - } -} - -.list { - width: 100%; - list-style: none; - padding-left: 0; - margin-bottom: 0; - display: inline-block; -} - -.action { - color: $light-gray; - - &:hover { - color: $link-hover; - text-decoration: underline; - } - - & ~ & { - margin-left: rem(12px); - } -} - -.error-flash { - margin-top: rem(20px); -} - -.clear-search-bar { - margin-top: rem(12px); - font-weight: 600; -} diff --git a/web/src/components/Digest/Empty.scss b/web/src/components/Digest/Empty.scss deleted file mode 100644 index 88b7abe2..00000000 --- a/web/src/components/Digest/Empty.scss +++ /dev/null @@ -1,29 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/variables'; -@import '../App/font'; -@import '../App/rem'; - -.wrapper { - padding: rem(40px) rem(16px); - text-align: center; - color: $gray; -} diff --git a/web/src/components/Digest/Empty.tsx b/web/src/components/Digest/Empty.tsx deleted file mode 100644 index 42f2ef49..00000000 --- a/web/src/components/Digest/Empty.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import { SearchParams, Status } from './types'; -import styles from './Empty.scss'; - -interface Props { - params: SearchParams; -} - -const Empty: React.FunctionComponent = ({ params }) => { - if (params.status === Status.Unreviewed) { - return ( -
- You have completed reviewing this digest. -
- ); - } - - return
No results matched your filters.
; -}; - -export default Empty; diff --git a/web/src/components/Digest/Header/Content.scss b/web/src/components/Digest/Header/Content.scss deleted file mode 100644 index 62307e90..00000000 --- a/web/src/components/Digest/Header/Content.scss +++ /dev/null @@ -1,70 +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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/variables'; -@import '../../App/font'; -@import '../../App/rem'; - -.meta { - margin-top: rem(4px); - @include font-size('small'); -} - -.sep { - margin: 0 rem(8px); -} - -.header { - display: flex; - flex-direction: column; - align-items: flex-start; - z-index: 1; - margin-bottom: rem(20px); - - padding-top: rem(12px); - - @include breakpoint(md) { - flex-direction: row; - justify-content: space-between; - align-items: flex-end; - background-color: transparent; - } - - @include breakpoint(lg) { - padding-top: 0; - } -} - -.header-container { - z-index: 1; - - &.header-sticky { - background-color: $white; - position: sticky; - top: $header-height; - box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18); - - .header { - padding-top: rem(12px); - padding-bottom: rem(12px); - margin-bottom: 0; - } - } -} diff --git a/web/src/components/Digest/Header/Content.tsx b/web/src/components/Digest/Header/Content.tsx deleted file mode 100644 index d0a55621..00000000 --- a/web/src/components/Digest/Header/Content.tsx +++ /dev/null @@ -1,86 +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 . - */ - -import React, { Fragment } from 'react'; - -import { pluralize } from 'web/libs/string'; -import { DigestData, DigestNoteData } from 'jslib/operations/types'; -import Time from '../../Common/Time'; -import formatTime from '../../../helpers/time/format'; -import { getDigestTitle } from '../helpers'; -import Progress from './Progress'; -import styles from './Content.scss'; - -function formatCreatedAt(d: Date) { - const now = new Date(); - - const currentYear = now.getFullYear(); - const year = d.getFullYear(); - - if (currentYear === year) { - return formatTime(d, '%MMM %DD'); - } - - return formatTime(d, '%MMM %DD, %YYYY'); -} - -function getViewedCount(notes: DigestNoteData[]): number { - let count = 0; - - for (let i = 0; i < notes.length; ++i) { - const n = notes[i]; - - if (n.isReviewed) { - count++; - } - } - - return count; -} - -interface Props { - digest: DigestData; -} - -const Content: React.FunctionComponent = ({ digest }) => { - const viewedCount = getViewedCount(digest.notes); - - return ( - -
-

{getDigestTitle(digest)}

-
- Contains {pluralize('note', digest.notes.length, true)} - · - Created on{' '} -
-
- - -
- ); -}; - -export default Content; diff --git a/web/src/components/Digest/Header/Placeholder.scss b/web/src/components/Digest/Header/Placeholder.scss deleted file mode 100644 index 542081d8..00000000 --- a/web/src/components/Digest/Header/Placeholder.scss +++ /dev/null @@ -1,49 +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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/variables'; -@import '../../App/font'; -@import '../../App/rem'; - -.wrapper { - position: relative; - width: 100%; -} - -.title { - height: rem(24px); - width: 100%; - - @include breakpoint(md) { - height: rem(32px); - width: rem(400px); - } -} - -.meta { - width: rem(80px); - height: rem(16px); - margin-top: rem(12px); - - @include breakpoint(md) { - height: rem(20px); - width: rem(320px); - } -} diff --git a/web/src/components/Digest/Header/Placeholder.tsx b/web/src/components/Digest/Header/Placeholder.tsx deleted file mode 100644 index 34e0530e..00000000 --- a/web/src/components/Digest/Header/Placeholder.tsx +++ /dev/null @@ -1,36 +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 . - */ - -import React from 'react'; -import classnames from 'classnames'; - -import styles from './Placeholder.scss'; - -interface Props {} - -const HeaderPlaceholder: React.FunctionComponent = () => { - return ( -
-
- -
-
- ); -}; - -export default HeaderPlaceholder; diff --git a/web/src/components/Digest/Header/Progress.scss b/web/src/components/Digest/Header/Progress.scss deleted file mode 100644 index b45fe13f..00000000 --- a/web/src/components/Digest/Header/Progress.scss +++ /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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/variables'; -@import '../../App/font'; -@import '../../App/rem'; - -.wrapper { - margin-top: rem(4px); - - display: flex; - align-items: center; - width: 100%; - - @include breakpoint(md) { - width: rem(220px); - margin-top: rem(0); - margin-bottom: rem(8px); - - display: initial; - align-items: initial; - width: auto; - } -} - -.bar-wrapper { - display: flex; - height: 8px; - overflow: hidden; - background-color: #c5c6c8; - border-radius: 4px; - width: rem(120px); - margin-left: rem(12px); - - @include breakpoint(md) { - width: rem(220px); - margin-top: rem(4px); - margin-left: 0; - } -} - -.bar { - transition: width 0.5s ease-out 0s; - width: 0%; - background: $first; -} - -.perc { - font-style: italic; -} - -.caption { - @include font-size('small'); - - &.caption-strong { - font-weight: 600; - } -} diff --git a/web/src/components/Digest/Header/Progress.tsx b/web/src/components/Digest/Header/Progress.tsx deleted file mode 100644 index cc011d15..00000000 --- a/web/src/components/Digest/Header/Progress.tsx +++ /dev/null @@ -1,74 +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 . - */ - -import React from 'react'; -import classnames from 'classnames'; - -import { pluralize } from 'web/libs/string'; -import styles from './Progress.scss'; - -interface Props { - total: number; - current: number; -} - -function calcPercentage(current: number, total: number): number { - if (total === 0) { - return 100; - } - - return (current / total) * 100; -} - -function getCaption(current, total): string { - if (current === total && total !== 0) { - return 'Review completed'; - } - - return `${current} of ${total} ${pluralize('note', current)} reviewed`; -} - -const Progress: React.FunctionComponent = ({ total, current }) => { - const isComplete = current === total; - const perc = calcPercentage(current, total); - const width = `${perc}%`; - - return ( -
-
- {getCaption(current, total)}{' '} - ({perc.toFixed(0)}%) -
-
-
-
-
- ); -}; - -export default Progress; diff --git a/web/src/components/Digest/Header/index.tsx b/web/src/components/Digest/Header/index.tsx deleted file mode 100644 index 67d81612..00000000 --- a/web/src/components/Digest/Header/index.tsx +++ /dev/null @@ -1,71 +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 . - */ - -import React, { useState } from 'react'; -import classnames from 'classnames'; - -import { DigestData } from 'jslib/operations/types'; -import { useEventListener } from 'web/libs/hooks'; -import { getScrollYPos } from 'web/libs/dom'; -import Placeholder from './Placeholder'; -import Content from './Content'; -import styles from './Content.scss'; - -interface Props { - isFetched: boolean; - digest: DigestData; -} - -const stickyThresholdY = 24; - -function checkSticky(y: number): boolean { - return y > stickyThresholdY; -} - -const Header: React.FunctionComponent = ({ digest, isFetched }) => { - const [isSticky, setIsSticky] = useState(false); - - function handleScroll() { - const y = getScrollYPos(); - const nextSticky = checkSticky(y); - - if (nextSticky) { - setIsSticky(true); - } else if (!nextSticky) { - setIsSticky(false); - } - } - - useEventListener(document, 'scroll', handleScroll); - - return ( -
-
-
- {isFetched ? : } -
-
-
- ); -}; - -export default Header; diff --git a/web/src/components/Digest/NoteItem/Header.scss b/web/src/components/Digest/NoteItem/Header.scss deleted file mode 100644 index d66145c5..00000000 --- a/web/src/components/Digest/NoteItem/Header.scss +++ /dev/null @@ -1,71 +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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/variables'; -@import '../../App/font'; -@import '../../App/rem'; - -.wrapper { - position: relative; -} - -.title { - height: rem(24px); - width: 100%; - - @include breakpoint(md) { - height: rem(32px); - width: rem(400px); - } -} - -.meta { - width: rem(80px); - height: rem(16px); - margin-top: rem(12px); - - @include breakpoint(md) { - height: rem(20px); - width: rem(320px); - } -} - -.caret-collapsed { - transform: rotate(270deg); -} - -.header-action { - align-self: stretch; -} - -.book-label { - max-width: rem(200px); - margin-left: rem(8px); - - @include breakpoint(sm) { - max-width: rem(200px); - } - @include breakpoint(md) { - max-width: rem(420px); - } - @include breakpoint(lg) { - max-width: rem(600px); - } -} diff --git a/web/src/components/Digest/NoteItem/Header.tsx b/web/src/components/Digest/NoteItem/Header.tsx deleted file mode 100644 index ca1d0f21..00000000 --- a/web/src/components/Digest/NoteItem/Header.tsx +++ /dev/null @@ -1,95 +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 . - */ - -import React from 'react'; -import { Link } from 'react-router-dom'; -import classnames from 'classnames'; - -import { DigestNoteData } from 'jslib/operations/types'; -import { getHomePath } from 'web/libs/paths'; -import ReviewButton from './ReviewButton'; -import Button from '../../Common/Button'; -import CaretIcon from '../../Icons/CaretSolid'; - -import noteStyles from '../../Common/Note/Note.scss'; -import styles from './Header.scss'; - -interface Props { - note: DigestNoteData; - setCollapsed: (boolean) => void; - onSetReviewed: (string, boolean) => Promise; - setErrMessage: (string) => void; - collapsed: boolean; -} - -const Header: React.FunctionComponent = ({ - note, - collapsed, - setCollapsed, - onSetReviewed, - setErrMessage -}) => { - let fill; - if (collapsed) { - fill = '#8c8c8c'; - } else { - fill = '#000000'; - } - - return ( -
-
- - -

- - {note.book.label} - -

-
- -
- -
-
- ); -}; - -export default Header; diff --git a/web/src/components/Digest/NoteItem/ReviewButton.scss b/web/src/components/Digest/NoteItem/ReviewButton.scss deleted file mode 100644 index bef7c873..00000000 --- a/web/src/components/Digest/NoteItem/ReviewButton.scss +++ /dev/null @@ -1,39 +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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/variables'; -@import '../../App/font'; -@import '../../App/rem'; - -.wrapper { - border: 1px solid $border-color; - margin-bottom: 0; - display: flex; - // align-items: center; - padding: rem(4px) rem(8px); - border-radius: rem(4px); - margin-left: rem(12px); -} - -.text { - @include font-size('small'); - margin-left: rem(4px); - user-select: none; -} diff --git a/web/src/components/Digest/NoteItem/ReviewButton.tsx b/web/src/components/Digest/NoteItem/ReviewButton.tsx deleted file mode 100644 index ec97eaaf..00000000 --- a/web/src/components/Digest/NoteItem/ReviewButton.tsx +++ /dev/null @@ -1,71 +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 . - */ - -import React, { useState } from 'react'; -import classnames from 'classnames'; - -import digestStyles from '../Digest.scss'; -import styles from './ReviewButton.scss'; - -interface Props { - noteUUID: string; - isReviewed: boolean; - setCollapsed: (boolean) => void; - onSetReviewed: (string, boolean) => Promise; - setErrMessage: (string) => void; -} - -const ReviewButton: React.FunctionComponent = ({ - noteUUID, - isReviewed, - setCollapsed, - onSetReviewed, - setErrMessage -}) => { - const [checked, setChecked] = useState(isReviewed); - - return ( - - ); -}; - -export default ReviewButton; diff --git a/web/src/components/Digest/NoteItem/index.tsx b/web/src/components/Digest/NoteItem/index.tsx deleted file mode 100644 index ecf6e3ab..00000000 --- a/web/src/components/Digest/NoteItem/index.tsx +++ /dev/null @@ -1,74 +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 . - */ - -import React, { Fragment, useState } from 'react'; -import { Link } from 'react-router-dom'; - -import { DigestNoteData } from 'jslib/operations/types'; -import { getNotePath } from 'web/libs/paths'; -import Note from '../../Common/Note'; -import Flash from '../../Common/Flash'; -import NoteItemHeader from './Header'; -import styles from '../Digest.scss'; - -interface Props { - note: DigestNoteData; - onSetReviewed: (string, boolean) => Promise; -} - -const NoteItem: React.FunctionComponent = ({ note, onSetReviewed }) => { - const [collapsed, setCollapsed] = useState(note.isReviewed); - const [errorMessage, setErrMessage] = useState(''); - - return ( -
  • - - - - - {errorMessage} - - - } - footerActions={ - - Go to note › - - } - footerUseTimeAgo - /> -
  • - ); -}; - -export default NoteItem; diff --git a/web/src/components/Digest/NoteList.tsx b/web/src/components/Digest/NoteList.tsx deleted file mode 100644 index 790197f1..00000000 --- a/web/src/components/Digest/NoteList.tsx +++ /dev/null @@ -1,94 +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 . - */ - -import React from 'react'; -import Helmet from 'react-helmet'; - -import { DigestData } from 'jslib/operations/types'; -import { DigestNoteData } from 'jslib/operations/types'; -import { getDigestTitle } from './helpers'; -import { useDispatch } from '../../store'; -import { setDigestNoteReviewed } from '../../store/digest'; -import Placeholder from '../Common/Note/Placeholder'; -import NoteItem from './NoteItem'; -import Empty from './Empty'; -import { SearchParams } from './types'; -import styles from './Digest.scss'; - -interface Props { - notes: DigestNoteData[]; - digest: DigestData; - params: SearchParams; - isFetched: boolean; - isFetching: boolean; -} - -const NoteList: React.FunctionComponent = ({ - isFetched, - isFetching, - params, - notes, - digest -}) => { - const dispatch = useDispatch(); - - function handleSetReviewed(noteUUID: string, isReviewed: boolean) { - return dispatch( - setDigestNoteReviewed({ digestUUID: digest.uuid, noteUUID, isReviewed }) - ); - } - - if (isFetching) { - return ( -
    - - - -
    - ); - } - if (!isFetched) { - return null; - } - - if (notes.length === 0) { - return ; - } - - return ( -
    - - {`${getDigestTitle(digest)} - Digest`} - - -
      - {notes.map(note => { - return ( - - ); - })} -
    -
    - ); -}; - -export default NoteList; diff --git a/web/src/components/Digest/Toolbar/SortMenu.tsx b/web/src/components/Digest/Toolbar/SortMenu.tsx deleted file mode 100644 index 46b8de71..00000000 --- a/web/src/components/Digest/Toolbar/SortMenu.tsx +++ /dev/null @@ -1,121 +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 . - */ - -import React, { useState, useRef } from 'react'; -import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; -import classnames from 'classnames'; - -import { parseSearchString } from 'jslib/helpers/url'; -import { getDigestPath } from 'web/libs/paths'; -import { blacklist } from 'jslib/helpers/obj'; -import SelectMenu from '../../Common/PageToolbar/SelectMenu'; -import selectMenuStyles from '../../Common/PageToolbar/SelectMenu.scss'; -import { Sort } from '../types'; -import styles from './Toolbar.scss'; - -interface Props extends RouteComponentProps { - digestUUID: string; - sort: Sort; - disabled?: boolean; -} - -const SortMenu: React.FunctionComponent = ({ - digestUUID, - sort, - disabled, - location -}) => { - const [isOpen, setIsOpen] = useState(false); - const optRefs = [useRef(null), useRef(null)]; - const searchObj = parseSearchString(location.search); - - const options = [ - { - name: 'newest', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[0]} - tabIndex={-1} - > - Newest - - ) - }, - { - name: 'oldest', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[1]} - tabIndex={-1} - > - Oldest - - ) - } - ]; - - const isActive = sort === Sort.Oldest; - - let defaultCurrentOptionIdx: number; - let sortText: string; - if (sort === Sort.Oldest) { - defaultCurrentOptionIdx = 1; - sortText = 'Oldest'; - } else { - defaultCurrentOptionIdx = 0; - sortText = 'Newest'; - } - - return ( - - ); -}; - -export default withRouter(SortMenu); diff --git a/web/src/components/Digest/Toolbar/StatusMenu.tsx b/web/src/components/Digest/Toolbar/StatusMenu.tsx deleted file mode 100644 index 2cc6f830..00000000 --- a/web/src/components/Digest/Toolbar/StatusMenu.tsx +++ /dev/null @@ -1,145 +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 . - */ - -import React, { useState, useRef } from 'react'; -import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; -import classnames from 'classnames'; - -import { getDigestPath } from 'web/libs/paths'; -import { parseSearchString } from 'jslib/helpers/url'; -import { blacklist } from 'jslib/helpers/obj'; -import SelectMenu from '../../Common/PageToolbar/SelectMenu'; -import selectMenuStyles from '../../Common/PageToolbar/SelectMenu.scss'; -import { Status } from '../types'; -import styles from './Toolbar.scss'; - -interface Props extends RouteComponentProps { - digestUUID: string; - status: Status; - disabled?: boolean; -} - -const StatusMenu: React.FunctionComponent = ({ - digestUUID, - status, - disabled, - location -}) => { - const [isOpen, setIsOpen] = useState(false); - const optRefs = [useRef(null), useRef(null), useRef(null)]; - const searchObj = parseSearchString(location.search); - - const options = [ - { - name: 'unreviewed', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[0]} - tabIndex={-1} - > - Unreviewed - - ) - }, - { - name: 'reviewed', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[1]} - tabIndex={-1} - > - Reviewed - - ) - }, - { - name: 'all', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[2]} - tabIndex={-1} - > - All - - ) - } - ]; - - const isActive = status === Status.Reviewed || status === Status.All; - - let defaultCurrentOptionIdx: number; - let statusText: string; - if (status === Status.Reviewed) { - defaultCurrentOptionIdx = 1; - statusText = 'Reviewed'; - } else if (status === Status.All) { - defaultCurrentOptionIdx = 2; - statusText = 'All'; - } else { - defaultCurrentOptionIdx = 0; - statusText = 'Unreviewed'; - } - - return ( - - ); -}; - -export default withRouter(StatusMenu); diff --git a/web/src/components/Digest/Toolbar/Toolbar.scss b/web/src/components/Digest/Toolbar/Toolbar.scss deleted file mode 100644 index ad43cde5..00000000 --- a/web/src/components/Digest/Toolbar/Toolbar.scss +++ /dev/null @@ -1,47 +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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/variables'; -@import '../../App/font'; -@import '../../App/rem'; - -.wrapper { - display: flex; - justify-content: flex-start; - align-items: center; - margin-top: rem(12px); - - @include breakpoint(md) { - justify-content: flex-end; - } - - @include breakpoint(lg) { - padding: 0 rem(16px); - margin-top: 0; - } -} - -.active-menu-trigger { - font-weight: 600; -} - -.menu-trigger ~ .menu-trigger { - margin-left: rem(12px); -} diff --git a/web/src/components/Digest/Toolbar/index.tsx b/web/src/components/Digest/Toolbar/index.tsx deleted file mode 100644 index 4647972a..00000000 --- a/web/src/components/Digest/Toolbar/index.tsx +++ /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 . - */ - -import React from 'react'; - -import PageToolbar from '../../Common/PageToolbar'; -import SortMenu from './SortMenu'; -import StatusMenu from './StatusMenu'; -import { Sort, Status } from '../types'; -import styles from './Toolbar.scss'; - -interface Props { - digestUUID: string; - sort: Sort; - status: Status; - isFetched: boolean; -} - -const Toolbar: React.FunctionComponent = ({ - digestUUID, - sort, - status, - isFetched -}) => { - return ( - - - - - - ); -}; - -export default Toolbar; diff --git a/web/src/components/Digest/helpers.ts b/web/src/components/Digest/helpers.ts deleted file mode 100644 index 7262b2e8..00000000 --- a/web/src/components/Digest/helpers.ts +++ /dev/null @@ -1,24 +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 . - */ - -import { DigestData } from 'jslib/operations/types'; - -// getDigestTitle returns a title for the digest -export function getDigestTitle(digest: DigestData) { - return `${digest.repetitionRule.title} #${digest.version}`; -} diff --git a/web/src/components/Digest/index.tsx b/web/src/components/Digest/index.tsx deleted file mode 100644 index 9027ded9..00000000 --- a/web/src/components/Digest/index.tsx +++ /dev/null @@ -1,169 +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 . - */ - -import React, { useEffect } from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import Helmet from 'react-helmet'; -import { Location } from 'history'; - -import { DigestNoteData } from 'jslib/operations/types'; -import { parseSearchString } from 'jslib/helpers/url'; -import { usePrevious } from 'web/libs/hooks'; -import { Sort, Status, SearchParams } from './types'; -import { getDigest } from '../../store/digest'; -import { useDispatch, useSelector } from '../../store'; -import Header from './Header'; -import Toolbar from './Toolbar'; -import NoteList from './NoteList'; -import Flash from '../Common/Flash'; -import ClearSearchBar from './ClearSearchBar'; -import styles from './Digest.scss'; - -function useFetchData(digestUUID: string) { - const dispatch = useDispatch(); - - const { digest } = useSelector(state => { - return { - digest: state.digest - }; - }); - - const prevDigestUUID = usePrevious(digestUUID); - - useEffect(() => { - if (!digest.isFetched || (digestUUID && prevDigestUUID !== digestUUID)) { - dispatch(getDigest(digestUUID)); - } - }, [dispatch, digestUUID, digest.isFetched, prevDigestUUID]); -} - -interface Match { - digestUUID: string; -} - -interface Props extends RouteComponentProps {} - -function getNotes(notes: DigestNoteData[], p: SearchParams): DigestNoteData[] { - const filtered = notes.filter(note => { - if (p.status === Status.Reviewed) { - return note.isReviewed; - } - if (p.status === Status.Unreviewed) { - return !note.isReviewed; - } - - return true; - }); - - return filtered.concat().sort((i, j) => { - if (p.sort === Sort.Oldest) { - return new Date(i.createdAt).getTime() - new Date(j.createdAt).getTime(); - } - - return new Date(j.createdAt).getTime() - new Date(i.createdAt).getTime(); - }); -} - -const statusMap = { - [Status.All]: Status.All, - [Status.Reviewed]: Status.Reviewed, - [Status.Unreviewed]: Status.Unreviewed -}; - -const sortMap = { - [Sort.Newest]: Sort.Newest, - [Sort.Oldest]: Sort.Oldest -}; - -function parseSearchParams(location: Location): SearchParams { - const searchObj = parseSearchString(location.search); - - const status = statusMap[searchObj.status] || Status.Unreviewed; - const sort = sortMap[searchObj.sort] || Sort.Newest; - - return { - sort, - status, - books: [] - }; -} - -const Digest: React.FunctionComponent = ({ location, match }) => { - const { digestUUID } = match.params; - - useFetchData(digestUUID); - - const { digest } = useSelector(state => { - return { - digest: state.digest - }; - }); - - const params = parseSearchParams(location); - const notes = getNotes(digest.data.notes, params); - - return ( -
    - - Digest - - -
    - -
    - - Spaced repetition is deprecated and will be removed in the next major - release. - - - -
    - -
    - -
    - -
    - - Error getting digest: {digest.errorMessage} - -
    - -
    - -
    -
    - ); -}; - -export default withRouter(Digest); diff --git a/web/src/components/Digest/types.ts b/web/src/components/Digest/types.ts deleted file mode 100644 index 576c06a2..00000000 --- a/web/src/components/Digest/types.ts +++ /dev/null @@ -1,36 +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 . - */ - -// Sort is a set of possible values for sort query parameters -export enum Sort { - Newest = '', - Oldest = 'created-asc' -} - -// Status is a set of possible values for status query parameters -export enum Status { - Unreviewed = '', - Reviewed = 'reviewed', - All = 'all' -} - -export interface SearchParams { - sort: Sort; - status: Status; - books: string[]; -} diff --git a/web/src/components/Digests/Digests.scss b/web/src/components/Digests/Digests.scss deleted file mode 100644 index ab1a9bb9..00000000 --- a/web/src/components/Digests/Digests.scss +++ /dev/null @@ -1,31 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/variables'; -@import '../App/font'; -@import '../App/rem'; - -.flash { - margin-top: rem(20px); - - @include breakpoint(lg) { - margin-top: 0; - } -} diff --git a/web/src/components/Digests/Empty.scss b/web/src/components/Digests/Empty.scss deleted file mode 100644 index 93845513..00000000 --- a/web/src/components/Digests/Empty.scss +++ /dev/null @@ -1,41 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/variables'; -@import '../App/font'; -@import '../App/rem'; - -.wrapper { - padding: rem(40px) rem(16px); - text-align: center; - color: $gray; -} - -.support { - margin-top: rem(20px); -} - -.md-support { - display: none; - - @include breakpoint(md) { - display: block; - } -} diff --git a/web/src/components/Digests/Empty.tsx b/web/src/components/Digests/Empty.tsx deleted file mode 100644 index f83c57fa..00000000 --- a/web/src/components/Digests/Empty.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -import { getRepetitionsPath } from 'web/libs/paths'; -import styles from './Empty.scss'; - -interface Props {} - -const Empty: React.FunctionComponent = () => { - return ( -
    -

    No digests were found.

    - -

    - You could create repetition rules{' '} - first. -

    - -

    - Digests are automatically created based on your repetition rules. -

    -
    - ); -}; - -export default Empty; diff --git a/web/src/components/Digests/Item.scss b/web/src/components/Digests/Item.scss deleted file mode 100644 index f890ee3f..00000000 --- a/web/src/components/Digests/Item.scss +++ /dev/null @@ -1,67 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/rem'; -@import '../App/font'; - -.wrapper { - background: white; - position: relative; - border-bottom: 1px solid $border-color; - - &:first-child { - border-top-left-radius: rem(4px); - border-top-right-radius: rem(4px); - } - &:last-child { - border-bottom-left-radius: rem(4px); - border-bottom-right-radius: rem(4px); - } - - &.unread { - .title { - font-weight: 600; - } - } - &.read { - .title { - color: $gray; - } - } -} - -.link { - color: $black; - display: flex; - justify-content: space-between; - padding: rem(12px) rem(16px); - border: 2px solid transparent; - - &:hover { - text-decoration: none; - background: $light-blue; - color: inherit; - } -} - -.ts { - color: $gray; - @include font-size('small'); -} diff --git a/web/src/components/Digests/Item.tsx b/web/src/components/Digests/Item.tsx deleted file mode 100644 index dec76c0b..00000000 --- a/web/src/components/Digests/Item.tsx +++ /dev/null @@ -1,62 +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 . - */ - -import React from 'react'; -import { Link } from 'react-router-dom'; -import classnames from 'classnames'; - -import { DigestData } from 'jslib/operations/types'; -import { getDigestPath } from 'web/libs/paths'; -import Time from '../Common/Time'; -import { timeAgo } from '../../helpers/time'; -import styles from './Item.scss'; - -interface Props { - item: DigestData; -} - -const Item: React.FunctionComponent = ({ item }) => { - const createdAt = new Date(item.createdAt); - - return ( -
  • - - - {item.repetitionRule.title} #{item.version} - -
  • - ); -}; - -export default Item; diff --git a/web/src/components/Digests/List.scss b/web/src/components/Digests/List.scss deleted file mode 100644 index 5bafcd83..00000000 --- a/web/src/components/Digests/List.scss +++ /dev/null @@ -1,31 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/rem'; -@import '../App/font'; - -.wrapper { - box-shadow: 0 0 8px rgba(0, 0, 0, 0.14); - border-radius: rem(4px); - - @include breakpoint(md) { - margin-top: rem(16px); - } -} diff --git a/web/src/components/Digests/List.tsx b/web/src/components/Digests/List.tsx deleted file mode 100644 index 99426a24..00000000 --- a/web/src/components/Digests/List.tsx +++ /dev/null @@ -1,66 +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 . - */ - -import React from 'react'; -import classnames from 'classnames'; - -import { DigestData } from 'jslib/operations/types'; -import { getRange } from 'jslib/helpers/arr'; -import Item from './Item'; -import Empty from './Empty'; -import Placeholder from './Placeholder'; -import styles from './List.scss'; - -interface Props { - isFetched: boolean; - isFetching: boolean; - items: DigestData[]; -} - -const List: React.FunctionComponent = ({ - items, - isFetched, - isFetching -}) => { - if (isFetching) { - return ( -
    - {getRange(10).map(key => { - return ; - })} -
    - ); - } - if (!isFetched) { - return null; - } - - if (items.length === 0) { - return ; - } - - return ( -
      - {items.map(item => { - return ; - })} -
    - ); -}; - -export default List; diff --git a/web/src/components/Digests/Placeholder.scss b/web/src/components/Digests/Placeholder.scss deleted file mode 100644 index 3dd1e0f1..00000000 --- a/web/src/components/Digests/Placeholder.scss +++ /dev/null @@ -1,33 +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 . - */ - -@import '../App/responsive'; -@import '../App/theme'; -@import '../App/rem'; - -.wrapper { - padding: rem(12px) rem(16px); -} - -.title { - height: rem(20px); - - @include breakpoint(md) { - width: 152px; - } -} diff --git a/web/src/components/Digests/Placeholder.tsx b/web/src/components/Digests/Placeholder.tsx deleted file mode 100644 index 84009207..00000000 --- a/web/src/components/Digests/Placeholder.tsx +++ /dev/null @@ -1,31 +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 . - */ - -import React from 'react'; -import classnames from 'classnames'; - -import itemStyles from './Item.scss'; -import styles from './Placeholder.scss'; - -export default () => { - return ( -
    -
    -
    - ); -}; diff --git a/web/src/components/Digests/Toolbar/StatusMenu.tsx b/web/src/components/Digests/Toolbar/StatusMenu.tsx deleted file mode 100644 index 6bfc74c2..00000000 --- a/web/src/components/Digests/Toolbar/StatusMenu.tsx +++ /dev/null @@ -1,123 +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 . - */ - -import React, { useState, useRef } from 'react'; -import { Link } from 'react-router-dom'; - -import { getDigestsPath } from 'web/libs/paths'; -import SelectMenu from '../../Common/PageToolbar/SelectMenu'; -import selectMenuStyles from '../../Common/PageToolbar/SelectMenu.scss'; -import { Status } from '../types'; -import styles from './Toolbar.scss'; - -interface Props { - status: Status; - disabled?: boolean; -} - -const StatusMenu: React.FunctionComponent = ({ status, disabled }) => { - const [isOpen, setIsOpen] = useState(false); - const optRefs = [useRef(null), useRef(null), useRef(null)]; - - const options = [ - { - name: 'all', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[0]} - tabIndex={-1} - > - All - - ) - }, - { - name: 'unread', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[1]} - tabIndex={-1} - > - Unread - - ) - }, - { - name: 'read', - value: ( - { - setIsOpen(false); - }} - ref={optRefs[2]} - tabIndex={-1} - > - Read - - ) - } - ]; - - let defaultCurrentOptionIdx: number; - let triggerText: string; - if (status === Status.Read) { - defaultCurrentOptionIdx = 2; - triggerText = 'Read'; - } else if (status === Status.Unread) { - defaultCurrentOptionIdx = 1; - triggerText = 'Unread'; - } else { - defaultCurrentOptionIdx = 0; - triggerText = 'All'; - } - - return ( - - ); -}; - -export default StatusMenu; diff --git a/web/src/components/Digests/Toolbar/Toolbar.scss b/web/src/components/Digests/Toolbar/Toolbar.scss deleted file mode 100644 index b94afc9a..00000000 --- a/web/src/components/Digests/Toolbar/Toolbar.scss +++ /dev/null @@ -1,33 +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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/rem'; -@import '../../App/font'; - -.toolbar { - display: flex; - justify-content: space-between; -} - -.select-menu-wrapper { - display: flex; - align-items: center; - margin-left: rem(8px); -} diff --git a/web/src/components/Digests/Toolbar/index.tsx b/web/src/components/Digests/Toolbar/index.tsx deleted file mode 100644 index a81bb2d7..00000000 --- a/web/src/components/Digests/Toolbar/index.tsx +++ /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 . - */ - -import React from 'react'; - -import { getDigestsPath } from 'web/libs/paths'; -import PageToolbar from '../../Common/PageToolbar'; -import Paginator from '../../Common/PageToolbar/Paginator'; -import StatusMenu from './StatusMenu'; -import { Status } from '../types'; -import styles from './Toolbar.scss'; - -interface Props { - total: number; - page: number; - status: Status; -} - -const PER_PAGE = 30; - -const Toolbar: React.FunctionComponent = ({ total, page, status }) => { - return ( - - - - { - return getDigestsPath({ page: p }); - }} - /> - - ); -}; - -export default Toolbar; diff --git a/web/src/components/Digests/index.tsx b/web/src/components/Digests/index.tsx deleted file mode 100644 index 5487f46a..00000000 --- a/web/src/components/Digests/index.tsx +++ /dev/null @@ -1,104 +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 . - */ - -import React, { useEffect } from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import Helmet from 'react-helmet'; - -import { usePrevious } from 'web/libs/hooks'; -import { parseSearchString } from 'jslib/helpers/url'; -import { useDispatch, useSelector } from '../../store'; -import { getDigests } from '../../store/digests'; -import { Status } from './types'; -import Flash from '../Common/Flash'; -import List from './List'; -import Toolbar from './Toolbar'; -import styles from './Digests.scss'; - -function useFetchDigests(params: { page: number; status: Status }) { - const dispatch = useDispatch(); - - const prevParams = usePrevious(params); - - useEffect(() => { - if ( - !prevParams || - prevParams.page !== params.page || - prevParams.status !== params.status - ) { - dispatch(getDigests(params)); - } - }, [dispatch, params, prevParams]); -} - -interface Props extends RouteComponentProps {} - -const Digests: React.FunctionComponent = ({ location }) => { - const { digests } = useSelector(state => { - return { - digests: state.digests, - user: state.auth.user.data - }; - }); - const { page, status } = parseSearchString(location.search); - useFetchDigests({ - page: page || 1, - status - }); - - return ( -
    - - Digests - - -
    -
    -

    Digests

    -
    -
    - -
    - - Error getting digests: {digests.errorMessage} - - - - Spaced repetition is deprecated and will be removed in the next major - release. - -
    - -
    - - - -
    -
    - ); -}; - -export default Digests; diff --git a/web/src/components/Digests/types.tsx b/web/src/components/Digests/types.tsx deleted file mode 100644 index e2780e25..00000000 --- a/web/src/components/Digests/types.tsx +++ /dev/null @@ -1,23 +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 . - */ - -export enum Status { - All = '', - Read = 'read', - Unread = 'unread' -} diff --git a/web/src/components/Preferences/Repetitions/Content.tsx b/web/src/components/Preferences/Repetitions/Content.tsx deleted file mode 100644 index 423f30a1..00000000 --- a/web/src/components/Preferences/Repetitions/Content.tsx +++ /dev/null @@ -1,110 +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 . - */ - -import React, { useState } from 'react'; - -import { RepetitionRuleData } from 'jslib/operations/types'; -import services from 'web/libs/services'; -import Button from '../../Common/Button'; -import styles from './EmailPreferenceRepetition.scss'; - -interface Props { - data: RepetitionRuleData; - setSuccessMsg: (string) => void; - setFailureMsg: (string) => void; - token?: string; -} - -const Content: React.FunctionComponent = ({ - data, - token, - setSuccessMsg, - setFailureMsg -}) => { - const [inProgress, setInProgress] = useState(false); - const [isEnabled, setIsEnabled] = useState(data.enabled); - - function handleSubmit(e) { - e.preventDefault(); - - setSuccessMsg(''); - setFailureMsg(''); - setInProgress(true); - - services.repetitionRules - .update(data.uuid, { enabled: isEnabled }, { token }) - .then(() => { - setSuccessMsg('Updated the repetition.'); - setInProgress(false); - }) - .catch(err => { - setFailureMsg(`Failed to update. Error: ${err.message}`); - setInProgress(false); - }); - } - - return ( -
    -

    Toggle the repetition for "{data.title}"

    - -
    -
    -
    - -
    - -
    - -
    -
    - -
    - -
    -
    -
    - ); -}; - -export default Content; diff --git a/web/src/components/Preferences/Repetitions/EmailPreferenceRepetition.scss b/web/src/components/Preferences/Repetitions/EmailPreferenceRepetition.scss deleted file mode 100644 index fce2807f..00000000 --- a/web/src/components/Preferences/Repetitions/EmailPreferenceRepetition.scss +++ /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 . - */ - -@import '../../App/responsive'; -@import '../../App/theme'; -@import '../../App/rem'; -@import '../../App/font'; - -.wrapper { - text-align: center; - height: 100vh; - padding: rem(52px) 0; - background: $lighter-gray; -} - -.heading { - @include font-size('2x-large'); - color: $black; - font-weight: 300; - margin-top: rem(16px); -} - -.body { - text-align: left; - padding: rem(20px) rem(28px); - margin-top: rem(20px); - max-width: rem(700px); - margin-left: auto; - margin-right: auto; - background-color: #fff; -} - -.footer { - @include font-size('small'); - text-align: center; - margin-top: rem(20px); - - a { - color: $gray; - } -} - -.radio { - display: inline-block; - - & ~ .radio { - margin-left: 26px; - } - - label { - padding-left: 3px; - } -} diff --git a/web/src/components/Preferences/Repetitions/index.tsx b/web/src/components/Preferences/Repetitions/index.tsx deleted file mode 100644 index 01080cfa..00000000 --- a/web/src/components/Preferences/Repetitions/index.tsx +++ /dev/null @@ -1,125 +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 . - */ - -import classnames from 'classnames'; -import { parseSearchString } from 'jslib/helpers/url'; -import React, { useEffect, useState } from 'react'; -import Helmet from 'react-helmet'; -import { Link, RouteComponentProps, withRouter } from 'react-router-dom'; -import { getLoginPath } from 'web/libs/paths'; -import services from 'web/libs/services'; -import Flash from '../../Common/Flash'; -import Logo from '../../Icons/Logo'; -import Content from './Content'; -import styles from './EmailPreferenceRepetition.scss'; - -interface Match { - repetitionUUID: string; -} -interface Props extends RouteComponentProps {} - -const EmailPreferenceRepetition: React.FunctionComponent = ({ - location, - match -}) => { - const [data, setData] = useState(null); - const [isFetching, setIsFetching] = useState(false); - const [successMsg, setSuccessMsg] = useState(''); - const [failureMsg, setFailureMsg] = useState(''); - - const { token } = parseSearchString(location.search); - const { repetitionUUID } = match.params; - - useEffect(() => { - if (data !== null) { - return; - } - - setIsFetching(true); - - services.repetitionRules - .fetch(repetitionUUID, { token }) - .then(repetition => { - setData(repetition); - setIsFetching(false); - }) - .catch(err => { - if (err.response.status === 401) { - setFailureMsg('Your email token has expired or is not valid.'); - } else { - setFailureMsg(err.message); - } - - setIsFetching(false); - }); - }, [data, repetitionUUID, setData, setFailureMsg, setIsFetching, token]); - - const isFetched = data !== null; - - return ( -
    - - Toggle repetition - - - - - -

    Toggle repetition

    - -
    -
    - - {failureMsg}{' '} - - Please login and try again. - - - - - {successMsg} - - - {isFetching &&
    Loading
    } - - {isFetched && ( - - )} -
    -
    - Back to Dnote home -
    -
    -
    - ); -}; - -export default withRouter(EmailPreferenceRepetition); diff --git a/web/src/components/Repetition/Content.tsx b/web/src/components/Repetition/Content.tsx deleted file mode 100644 index 6ff1857d..00000000 --- a/web/src/components/Repetition/Content.tsx +++ /dev/null @@ -1,126 +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 . - */ - -import React, { Fragment, useState, useEffect } from 'react'; - -import classnames from 'classnames'; -import { - getNewRepetitionPath, - getSettingsPath, - SettingSections, - repetitionsPathDef -} from 'web/libs/paths'; -import { Link } from 'react-router-dom'; -import { useDispatch, useSelector } from '../../store'; -import { getRepetitionRules } from '../../store/repetitionRules'; -import RepetitionList from './RepetitionList'; -import DeleteRepetitionRuleModal from './DeleteRepetitionRuleModal'; -import Flash from '../Common/Flash'; -import { setMessage } from '../../store/ui'; -import styles from './Repetition.scss'; - -const Content: React.FunctionComponent = () => { - const dispatch = useDispatch(); - useEffect(() => { - dispatch(getRepetitionRules()); - }, [dispatch]); - - const { repetitionRules, user } = useSelector(state => { - return { - repetitionRules: state.repetitionRules, - user: state.auth.user.data - }; - }); - - const [ruleUUIDToDelete, setRuleUUIDToDelete] = useState(''); - - return ( - -
    -
    -

    Repetition

    - - {!user.pro ? ( - - ) : ( - - New - - )} -
    -
    - -
    - - Spaced repetition is deprecated and will be removed in the next major - release. - - - - Please verify your email address in order to receive digests.{' '} - - Go to settings. - - - -
    - -
    -
    - - { - setRuleUUIDToDelete(''); - }} - setSuccessMessage={message => { - dispatch( - setMessage({ - message, - kind: 'info', - path: repetitionsPathDef - }) - ); - }} - /> -
    - ); -}; - -export default Content; diff --git a/web/src/components/Repetition/DeleteRepetitionRuleModal.scss b/web/src/components/Repetition/DeleteRepetitionRuleModal.scss deleted file mode 100644 index 59e75dcb..00000000 --- a/web/src/components/Repetition/DeleteRepetitionRuleModal.scss +++ /dev/null @@ -1,46 +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 . - */ - -@import '../App/rem'; -@import '../App/font'; -@import '../App/theme'; - -.wrapper { - position: relative; - border-radius: 4px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.14); -} - -.title { - @include font-size('medium'); -} - -.label { - width: 100%; -} -.input { - width: 100%; -} -.actions { - display: flex; - justify-content: flex-end; - margin-top: rem(8px); -} -.rule-label { - font-weight: 600; -} diff --git a/web/src/components/Repetition/DeleteRepetitionRuleModal.tsx b/web/src/components/Repetition/DeleteRepetitionRuleModal.tsx deleted file mode 100644 index 7f388e0a..00000000 --- a/web/src/components/Repetition/DeleteRepetitionRuleModal.tsx +++ /dev/null @@ -1,174 +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 . - */ - -import { RepetitionRuleData } from 'jslib/operations/types'; -import React, { useEffect, useState } from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import services from 'web/libs/services'; -import { useDispatch, useSelector } from '../../store'; -import { removeRepetitionRule } from '../../store/repetitionRules'; -import Button from '../Common/Button'; -import Flash from '../Common/Flash'; -import Modal, { Body, Header } from '../Common/Modal'; -import styles from './DeleteRepetitionRuleModal.scss'; - -function getRepetitionRuleByUUID( - repetitionRules, - uuid -): RepetitionRuleData | null { - for (let i = 0; i < repetitionRules.length; ++i) { - const r = repetitionRules[i]; - - if (r.uuid === uuid) { - return r; - } - } - - return null; -} - -interface Props extends RouteComponentProps { - isOpen: boolean; - onDismiss: () => void; - setSuccessMessage: (string) => void; - repetitionRuleUUID: string; -} - -const DeleteRepetitionModal: React.FunctionComponent = ({ - isOpen, - onDismiss, - setSuccessMessage, - repetitionRuleUUID -}) => { - const [inProgress, setInProgress] = useState(false); - const [errMessage, setErrMessage] = useState(''); - const dispatch = useDispatch(); - - const { repetitionRules } = useSelector(state => { - return { - repetitionRules: state.repetitionRules - }; - }); - - const rule = getRepetitionRuleByUUID( - repetitionRules.data, - repetitionRuleUUID - ); - - const labelId = 'delete-rule-modal-label'; - const descId = 'delete-rule-modal-desc'; - - useEffect(() => { - if (!isOpen) { - setErrMessage(''); - } - }, [isOpen]); - - if (rule === null) { - return null; - } - - return ( - -
    - - { - setErrMessage(''); - }} - hasBorder={false} - when={Boolean(errMessage)} - noMargin - > - {errMessage} - - - - - This action will permanently remove the following repetition rule:{' '} - - {rule.title} - - - -
    { - e.preventDefault(); - - setSuccessMessage(''); - setInProgress(true); - - services.repetitionRules - .remove(repetitionRuleUUID) - .then(() => { - dispatch(removeRepetitionRule(repetitionRuleUUID)); - setInProgress(false); - onDismiss(); - - // Scroll to top so that the message is visible. - setSuccessMessage( - `Successfully removed the rule "${rule.title}"` - ); - window.scrollTo(0, 0); - }) - .catch(err => { - console.log('Error deleting rule', err); - setInProgress(false); - setErrMessage(err.message); - }); - }} - > -
    - - -
    -
    - - - ); -}; - -export default withRouter(DeleteRepetitionModal); diff --git a/web/src/components/Repetition/Edit/Content.tsx b/web/src/components/Repetition/Edit/Content.tsx deleted file mode 100644 index 45bb4e18..00000000 --- a/web/src/components/Repetition/Edit/Content.tsx +++ /dev/null @@ -1,84 +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 . - */ - -import { booksToOptions } from 'jslib/helpers/select'; -import { RepetitionRuleData } from 'jslib/operations/types'; -import React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { getRepetitionsPath, repetitionsPathDef } from 'web/libs/paths'; -import services from 'web/libs/services'; -import { useDispatch } from '../../../store'; -import { setMessage } from '../../../store/ui'; -import Form, { FormState, serializeFormState } from '../Form'; - -interface Props extends RouteComponentProps { - setErrMsg: (string) => void; - data: RepetitionRuleData; -} - -const RepetitionEditContent: React.FunctionComponent = ({ - history, - setErrMsg, - data -}) => { - const dispatch = useDispatch(); - - async function handleSubmit(state: FormState) { - const payload = serializeFormState(state); - - try { - await services.repetitionRules.update(data.uuid, payload); - - const dest = getRepetitionsPath(); - history.push(dest); - - dispatch( - setMessage({ - message: `Updated the repetition rule: "${data.title}"`, - kind: 'info', - path: repetitionsPathDef - }) - ); - } catch (e) { - console.log(e); - setErrMsg(e.message); - } - } - - const initialFormState = { - title: data.title, - enabled: data.enabled, - hour: data.hour, - minute: data.minute, - frequency: data.frequency, - noteCount: data.noteCount, - bookDomain: data.bookDomain, - books: booksToOptions(data.books) - }; - - return ( -
    - ); -}; - -export default withRouter(RepetitionEditContent); diff --git a/web/src/components/Repetition/Edit/index.tsx b/web/src/components/Repetition/Edit/index.tsx deleted file mode 100644 index 421b194b..00000000 --- a/web/src/components/Repetition/Edit/index.tsx +++ /dev/null @@ -1,91 +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 . - */ - -import React, { useEffect, useState } from 'react'; -import classnames from 'classnames'; -import Helmet from 'react-helmet'; -import { Link, RouteComponentProps } from 'react-router-dom'; - -import { RepetitionRuleData } from 'jslib/operations/types'; -import services from 'web/libs/services'; -import { getRepetitionsPath } from 'web/libs/paths'; -import PayWall from '../../Common/PayWall'; -import { useDispatch } from '../../../store'; -import Flash from '../../Common/Flash'; -import repetitionStyles from '../Repetition.scss'; -import Content from './Content'; - -interface Match { - repetitionUUID: string; -} - -interface Props extends RouteComponentProps {} - -const EditRepetition: React.FunctionComponent = ({ match }) => { - const dispatch = useDispatch(); - const [errMsg, setErrMsg] = useState(''); - const [data, setData] = useState(null); - - useEffect(() => { - const { repetitionUUID } = match.params; - services.repetitionRules - .fetch(repetitionUUID) - .then(rule => { - setData(rule); - }) - .catch(err => { - setErrMsg(err.message); - }); - }, [dispatch, match]); - - return ( -
    - - Edit Repetition - - - -
    -
    -

    Edit Repetition

    - - Back -
    - - { - setErrMsg(''); - }} - > - Error: {errMsg} - - - {data === null ? ( -
    loading
    - ) : ( - - )} -
    -
    -
    - ); -}; - -export default EditRepetition; diff --git a/web/src/components/Repetition/Form/Form.scss b/web/src/components/Repetition/Form/Form.scss deleted file mode 100644 index 4410d997..00000000 --- a/web/src/components/Repetition/Form/Form.scss +++ /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 . - */ - -@import '../../App/rem'; -@import '../../App/font'; -@import '../../App/theme'; - -.form { - background: white; - border: 1px solid $border-color; - padding: rem(12px) rem(20px); -} - -.help { - margin-top: rem(4px); - margin-bottom: 0; - color: $dark-gray; - @include font-size('x-small'); -} - -.schedule-wrapper { - display: flex; - flex-direction: column; -} - -.schedule-content { - display: flex; - flex-direction: row; - justify-content: space-between; - - @include breakpoint(md) { - justify-content: initial; - } -} - -.schedule-input-wrapper { - display: flex; - flex-direction: column; - - @include breakpoint(md) { - & ~ .schedule-input-wrapper { - margin-left: rem(24px); - } - } -} - -.time-select { - width: auto; - - @include breakpoint(md) { - width: rem(60px); - } -} - -.timezone { - display: flex; - align-items: flex-end; -} - -.book-domain-wrapper { - display: flex; - flex-direction: column; - margin-top: rem(4px); - - @include breakpoint(md) { - flex-direction: row; - margin-top: 0; - } -} -.book-domain-option { - & ~ .book-domain-option { - margin-left: 0; - margin-top: rem(4px); - - @include breakpoint(md) { - margin-left: rem(8px); - margin-top: 0; - } - } -} -.book-domain-label { - margin-left: rem(4px); - margin-bottom: 0; -} - -.input-row { - & ~ .input-row { - margin-top: rem(16px); - } -} - -.book-selector { - margin-top: rem(12px); - - @include breakpoint(md) { - margin-top: rem(8px); - } -} diff --git a/web/src/components/Repetition/Form/index.tsx b/web/src/components/Repetition/Form/index.tsx deleted file mode 100644 index 9ea3efa4..00000000 --- a/web/src/components/Repetition/Form/index.tsx +++ /dev/null @@ -1,517 +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 . - */ - -import React, { useEffect, useReducer, useRef } from 'react'; -import classnames from 'classnames'; -import { booksToOptions, Option } from 'jslib/helpers/select'; - -import { BookDomain } from 'jslib/operations/types'; -import { CreateParams } from 'jslib/services/repetitionRules'; -import { Link } from 'react-router-dom'; -import { getRepetitionsPath } from 'web/libs/paths'; -import { daysToMs } from '../../../helpers/time'; -import { useSelector } from '../../../store'; -import Button from '../../Common/Button'; -import modalStyles from '../../Common/Modal/Modal.scss'; -import MultiSelect from '../../Common/MultiSelect'; -import styles from './Form.scss'; - -export interface FormState { - title: string; - enabled: boolean; - hour: number; - minute: number; - frequency: number; - noteCount: number; - bookDomain: BookDomain; - books: Option[]; -} - -// serializeFormState serializes the given form state into a payload -export function serializeFormState(s: FormState): CreateParams { - let bookUUIDs = []; - if (s.bookDomain === BookDomain.All) { - bookUUIDs = []; - } else { - bookUUIDs = s.books.map(b => { - return b.value; - }); - } - - return { - title: s.title, - hour: s.hour, - minute: s.minute, - frequency: s.frequency, - book_domain: s.bookDomain, - book_uuids: bookUUIDs, - note_count: s.noteCount, - enabled: s.enabled - }; -} - -interface Props { - onSubmit: (formState) => void; - setErrMsg: (string) => void; - cancelPath?: string; - initialState?: FormState; - isEditing?: boolean; - // TODO: implement inProgress - inProgress?: boolean; -} - -enum Action { - setTitle, - setFrequency, - setHour, - setMinutes, - setNoteCount, - setBookDomain, - setBooks, - toggleEnabled -} - -function formReducer(state, action): FormState { - switch (action.type) { - case Action.setTitle: - return { - ...state, - title: action.data - }; - case Action.setFrequency: - return { - ...state, - frequency: action.data - }; - case Action.setHour: - return { - ...state, - hour: action.data - }; - case Action.setMinutes: - return { - ...state, - minute: action.data - }; - case Action.setNoteCount: - return { - ...state, - noteCount: action.data - }; - case Action.setBooks: - return { - ...state, - books: action.data - }; - case Action.setBookDomain: - return { - ...state, - bookDomain: action.data - }; - case Action.toggleEnabled: - return { - ...state, - enabled: !state.enabled - }; - default: - return state; - } -} - -const formInitialState: FormState = { - title: '', - enabled: true, - hour: 8, - minute: 0, - frequency: daysToMs(7), - noteCount: 20, - bookDomain: BookDomain.All, - books: [] -}; - -function validateForm(state: FormState): Error | null { - if (state.title === '') { - return new Error('Title is required.'); - } - if (state.bookDomain !== BookDomain.All && state.books.length === 0) { - return new Error('Please select books.'); - } - if (state.noteCount <= 0) { - return new Error('Please specify note count greater than 0.'); - } - - return null; -} - -const Form: React.FunctionComponent = ({ - onSubmit, - setErrMsg, - cancelPath = getRepetitionsPath(), - initialState = formInitialState, - isEditing = false, - inProgress = false -}) => { - const bookSelectorInputRef = useRef(null); - const [formState, formDispatch] = useReducer(formReducer, initialState); - const { books } = useSelector(state => { - return { - books: state.books.data - }; - }); - const bookOptions = booksToOptions(books); - const booksSelectTextId = 'book-select-text-input'; - - let bookSelectorPlaceholder; - if (formState.bookDomain === BookDomain.All) { - bookSelectorPlaceholder = 'All books'; - } else if (formState.bookDomain === BookDomain.Including) { - bookSelectorPlaceholder = 'Select books to include'; - } else if (formState.bookDomain === BookDomain.Excluding) { - bookSelectorPlaceholder = 'Select books to exclude'; - } - - let bookSelectorCurrentOptions; - if (formState.bookDomain === BookDomain.All) { - bookSelectorCurrentOptions = []; - } else { - bookSelectorCurrentOptions = formState.books; - } - - useEffect(() => { - if (isEditing) { - return; - } - - if (formState.bookDomain === BookDomain.All) { - if (bookSelectorInputRef.current) { - bookSelectorInputRef.current.blur(); - } - } else if (bookSelectorInputRef.current) { - bookSelectorInputRef.current.focus(); - } - }, [formState.bookDomain, isEditing]); - - return ( - { - e.preventDefault(); - - const err = validateForm(formState); - if (err !== null) { - setErrMsg(err.message); - return; - } - - onSubmit(formState); - }} - className={styles.form} - > -
    - - - { - const data = e.target.value; - - formDispatch({ - type: Action.setTitle, - data - }); - }} - /> -
    - -
    - - -
    -
    - { - const data = e.target.value; - - formDispatch({ - type: Action.setBookDomain, - data - }); - }} - /> - -
    - -
    - { - const data = e.target.value; - - formDispatch({ - type: Action.setBookDomain, - data - }); - }} - /> - -
    - -
    - { - const data = e.target.value; - - formDispatch({ - type: Action.setBookDomain, - data - }); - }} - /> - -
    -
    - - { - formDispatch({ type: Action.setBooks, data }); - }} - placeholder={bookSelectorPlaceholder} - wrapperClassName={styles['book-selector']} - inputInnerRef={bookSelectorInputRef} - /> -
    - -
    -
    -
    - - - -
    - -
    - - - -
    - -
    - - - -
    -
    - -
    - When to deliver a digest in the UTC (Coordinated Universal Time). -
    -
    - -
    - - - { - const { value } = e.target; - - let data; - if (value === '') { - data = ''; - } else { - data = Number.parseInt(value, 10); - } - - formDispatch({ - type: Action.setNoteCount, - data - }); - }} - /> - -
    - Maximum number of notes to include in each repetition -
    -
    - -
    - - -
    - { - const data = e.target.value; - - formDispatch({ - type: Action.toggleEnabled, - data - }); - }} - /> -
    -
    - -
    - - - { - const ok = window.confirm('Are you sure?'); - if (!ok) { - e.preventDefault(); - } - }} - className="button button-second button-normal" - > - Cancel - -
    - - ); -}; - -export default Form; diff --git a/web/src/components/Repetition/New/index.tsx b/web/src/components/Repetition/New/index.tsx deleted file mode 100644 index 0ee87bab..00000000 --- a/web/src/components/Repetition/New/index.tsx +++ /dev/null @@ -1,99 +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 . - */ - -import React, { useState, useEffect } from 'react'; -import Helmet from 'react-helmet'; -import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; -import classnames from 'classnames'; - -import { getRepetitionsPath, repetitionsPathDef } from 'web/libs/paths'; -import PayWall from '../../Common/PayWall'; -import { - getRepetitionRules, - createRepetitionRule -} from '../../../store/repetitionRules'; -import { useDispatch } from '../../../store'; -import Form, { FormState, serializeFormState } from '../Form'; -import Flash from '../../Common/Flash'; -import { setMessage } from '../../../store/ui'; -import repetitionStyles from '../Repetition.scss'; - -interface Props extends RouteComponentProps {} - -const NewRepetition: React.FunctionComponent = ({ history }) => { - const dispatch = useDispatch(); - const [errMsg, setErrMsg] = useState(''); - - useEffect(() => { - dispatch(getRepetitionRules()); - }, [dispatch]); - - async function handleSubmit(state: FormState) { - const payload = serializeFormState(state); - - try { - await dispatch(createRepetitionRule(payload)); - - const dest = getRepetitionsPath(); - history.push(dest); - - dispatch( - setMessage({ - message: 'Created a repetition rule', - kind: 'info', - path: repetitionsPathDef - }) - ); - } catch (e) { - console.log(e); - setErrMsg(e.message); - } - } - - return ( -
    - - New Repetition - - - -
    -
    -

    New Repetition

    - - Back -
    - - { - setErrMsg(''); - }} - > - Error creating a rule: {errMsg} - - -
    -
    -
    -
    - ); -}; - -export default withRouter(NewRepetition); diff --git a/web/src/components/Repetition/Repetition.scss b/web/src/components/Repetition/Repetition.scss deleted file mode 100644 index b0198c58..00000000 --- a/web/src/components/Repetition/Repetition.scss +++ /dev/null @@ -1,40 +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 . - */ - -@import '../App/rem'; -@import '../App/font'; -@import '../App/theme'; - -.header { - display: flex; - justify-content: space-between; - align-items: center; - height: 40px; -} - -.content { - margin-top: rem(20px); -} - -.flash { - margin-top: rem(20px); - - @include breakpoint(lg) { - margin-top: 0; - } -} diff --git a/web/src/components/Repetition/RepetitionItem/Actions.tsx b/web/src/components/Repetition/RepetitionItem/Actions.tsx deleted file mode 100644 index 20d6ab7e..00000000 --- a/web/src/components/Repetition/RepetitionItem/Actions.tsx +++ /dev/null @@ -1,94 +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 . - */ - -import classnames from 'classnames'; -import React, { useRef, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { getEditRepetitionPath } from '../../../libs/paths'; -import ItemActions from '../../Common/ItemActions'; -import ItemActionsStyles from '../../Common/ItemActions/ItemActions.scss'; - -interface Props { - isActive: boolean; - onDelete: () => void; - repetitionUUID: string; - disabled?: boolean; -} - -const Actions: React.FunctionComponent = ({ - isActive, - onDelete, - repetitionUUID, - disabled -}) => { - const [isOpen, setIsOpen] = useState(false); - - const optRefs = [useRef(null), useRef(null)]; - const options = [ - { - name: 'edit', - value: ( - - Edit - - ) - }, - { - name: 'remove', - value: ( - - ) - } - ]; - - return ( - - ); -}; - -export default Actions; diff --git a/web/src/components/Repetition/RepetitionItem/BookMeta.tsx b/web/src/components/Repetition/RepetitionItem/BookMeta.tsx deleted file mode 100644 index bbfe54d3..00000000 --- a/web/src/components/Repetition/RepetitionItem/BookMeta.tsx +++ /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 . - */ - -import React from 'react'; - -import { BookDomain } from 'jslib/operations/types'; -import { pluralize } from 'web/libs/string'; -import styles from './RepetitionItem.scss'; - -interface ContentProps { - bookDomain: BookDomain; - bookCount: number; -} - -const Content: React.FunctionComponent = ({ - bookDomain, - bookCount -}) => { - if (bookDomain === BookDomain.All) { - return From all books; - } - - let verb; - if (bookDomain === BookDomain.Excluding) { - verb = 'Excluding'; - } else if (bookDomain === BookDomain.Including) { - verb = 'From'; - } - - return ( - - {verb} {bookCount} {pluralize('book', bookCount)} - - ); -}; - -interface Props { - bookDomain: BookDomain; - bookCount: number; -} - -const BookMeta: React.FunctionComponent = ({ - bookDomain, - bookCount -}) => { - return ( - - - - ); -}; - -export default BookMeta; diff --git a/web/src/components/Repetition/RepetitionItem/Placeholder.scss b/web/src/components/Repetition/RepetitionItem/Placeholder.scss deleted file mode 100644 index 5c012fee..00000000 --- a/web/src/components/Repetition/RepetitionItem/Placeholder.scss +++ /dev/null @@ -1,44 +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 . - */ - -@import '../../App/rem'; -@import '../../App/font'; -@import '../../App/theme'; - -.wrapper { - display: block; -} - -.line { - height: rem(16px); - - & ~ & { - margin-top: rem(12px); - } -} - -.title { - width: rem(200px); -} - -.line1 { - width: rem(160px); -} -.line2 { - width: rem(180px); -} diff --git a/web/src/components/Repetition/RepetitionItem/Placeholder.tsx b/web/src/components/Repetition/RepetitionItem/Placeholder.tsx deleted file mode 100644 index fd05356d..00000000 --- a/web/src/components/Repetition/RepetitionItem/Placeholder.tsx +++ /dev/null @@ -1,37 +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 . - */ - -import React from 'react'; -import classnames from 'classnames'; - -import styles from './Placeholder.scss'; -import itemStyles from './RepetitionItem.scss'; - -interface Props {} - -const Placeholder: React.FunctionComponent = () => { - return ( -
    -
    -
    -
    -
    - ); -}; - -export default Placeholder; diff --git a/web/src/components/Repetition/RepetitionItem/RepetitionItem.scss b/web/src/components/Repetition/RepetitionItem/RepetitionItem.scss deleted file mode 100644 index 1b566db6..00000000 --- a/web/src/components/Repetition/RepetitionItem/RepetitionItem.scss +++ /dev/null @@ -1,110 +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 . - */ - -@import '../../App/rem'; -@import '../../App/font'; -@import '../../App/theme'; - -.wrapper { - background: #fff; - position: relative; - display: flex; - padding: rem(16px) rem(24px); - - &:not(:last-child) { - border-bottom: 1px solid #d8d8d8; - } -} - -.content { - display: flex; - flex-grow: 1; - flex-direction: column; - - @include breakpoint(md) { - flex-direction: row; - } -} - -.left { -} - -.right { - display: flex; - flex-grow: 1; - padding-right: rem(24px); - - @include breakpoint(md) { - justify-content: flex-end; - } -} - -.meta { - @include font-size('small'); - margin-top: rem(4px); - - @include breakpoint(md) { - color: $dark-gray; - } -} - -.sep { - margin: 0 rem(4px); -} - -.item { - background: #fff; - position: relative; - border-bottom: 1px solid #d8d8d8; -} - -.title { - @include font-size('regular'); -} - -.status { - @include font-size('x-small'); - border: 1px solid $green; - border-radius: rem(4px); - display: inline-block; - padding: rem(2px) rem(4px); - font-weight: 600; - - &.active { - color: $green; - } -} - -.detail-list { - @include font-size('small'); - display: flex; - flex-direction: column; - justify-content: center; - - @include breakpoint(md) { - min-width: rem(180px); - } -} - -.book-meta { - display: none; - - @include breakpoint(md) { - display: block; - } -} diff --git a/web/src/components/Repetition/RepetitionItem/index.tsx b/web/src/components/Repetition/RepetitionItem/index.tsx deleted file mode 100644 index 2ae13866..00000000 --- a/web/src/components/Repetition/RepetitionItem/index.tsx +++ /dev/null @@ -1,156 +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 . - */ - -import classnames from 'classnames'; -import { RepetitionRuleData } from 'jslib/operations/types'; -import React, { useState } from 'react'; -import { - msToDuration, - msToHTMLTimeDuration, - relativeTimeDiff, - timeAgo -} from 'web/helpers/time'; -import Time from '../../Common/Time'; -import Actions from './Actions'; -import BookMeta from './BookMeta'; -import styles from './RepetitionItem.scss'; - -interface Props { - item: RepetitionRuleData; - setRuleUUIDToDelete: React.Dispatch; - pro: boolean; -} - -function formatLastActive(ms: number): string { - return timeAgo(ms); -} - -function formatNextActive(ms: number): string { - const now = new Date().getTime(); - const diff = relativeTimeDiff(now, ms); - - return diff.text; -} - -const RepetitionItem: React.FunctionComponent = ({ - item, - pro, - setRuleUUIDToDelete -}) => { - const [isHovered, setIsHovered] = useState(false); - - return ( -
  • { - setIsHovered(true); - }} - onMouseLeave={() => { - setIsHovered(false); - }} - > -
    -
    -

    - {item.title} -

    - -
    -
    - - Every{' '} - - - · - email -
    - - -
    -
    - -
    -
      -
    • - {item.enabled && item.nextActive !== 0 ? ( - - Scheduled in{' '} - - ) : ( - Not scheduled - )} -
    • -
    • - Last active:{' '} - {item.lastActive === 0 ? ( - Never - ) : ( -
    • - {/* -
    • - Created:{' '} -
    • - */} -
    -
    -
    - - { - setRuleUUIDToDelete(item.uuid); - }} - repetitionUUID={item.uuid} - disabled={!pro} - /> -
  • - ); -}; - -export default RepetitionItem; diff --git a/web/src/components/Repetition/RepetitionList.scss b/web/src/components/Repetition/RepetitionList.scss deleted file mode 100644 index 0a260d69..00000000 --- a/web/src/components/Repetition/RepetitionList.scss +++ /dev/null @@ -1,31 +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 . - */ - -@import '../App/rem'; -@import '../App/font'; -@import '../App/theme'; - -.wrapper { - position: relative; - border-radius: 4px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.14); -} - -.title { - @include font-size('medium'); -} diff --git a/web/src/components/Repetition/RepetitionList.tsx b/web/src/components/Repetition/RepetitionList.tsx deleted file mode 100644 index 658f06a2..00000000 --- a/web/src/components/Repetition/RepetitionList.tsx +++ /dev/null @@ -1,77 +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 . - */ - -import React, { Fragment } from 'react'; -import classnames from 'classnames'; - -import { RepetitionRuleData } from 'jslib/operations/types'; -import RepetitionItem from './RepetitionItem'; -import Placeholder from './RepetitionItem/Placeholder'; -import styles from './RepetitionList.scss'; - -interface Props { - isFetching: boolean; - isFetched: boolean; - items: RepetitionRuleData[]; - setRuleUUIDToDelete: React.Dispatch; - pro: boolean; -} - -const ReptitionList: React.FunctionComponent = ({ - isFetching, - isFetched, - items, - setRuleUUIDToDelete, - pro -}) => { - if (isFetching) { - return ( - - - - - - - ); - } - if (!isFetched) { - return null; - } - - return ( -
      - {items.map(i => { - return ( - - ); - })} -
    - ); -}; - -export default ReptitionList; diff --git a/web/src/components/Repetition/index.tsx b/web/src/components/Repetition/index.tsx deleted file mode 100644 index dd79ffe3..00000000 --- a/web/src/components/Repetition/index.tsx +++ /dev/null @@ -1,36 +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 . - */ - -import React from 'react'; -import Helmet from 'react-helmet'; - -import Content from './Content'; - -const Repetition: React.FunctionComponent = () => { - return ( -
    - - Repetition - - - -
    - ); -}; - -export default Repetition; diff --git a/web/src/libs/paths.ts b/web/src/libs/paths.ts index 2c0e56af..986bf167 100644 --- a/web/src/libs/paths.ts +++ b/web/src/libs/paths.ts @@ -30,25 +30,16 @@ export const loginPathDef = '/login'; export const joinPathDef = '/join'; export const settingsPathDef = '/settings/:section'; export const subscriptionsPathDef = '/subscriptions'; -export const prefEditRepetitionPathDef = - '/preferences/repetitions/:repetitionUUID'; export const verifyEmailPathDef = '/verify-email/:token'; export const classicMigrationPathDef = '/classic/:step?'; export const passwordResetRequestPathDef = '/password-reset'; export const passwordResetConfirmPathDef = '/password-reset/:token'; -export const repetitionsPathDef = '/repetition'; -export const repetitionPathDef = '/repetition/:repetitionUUID'; -export const newRepetitionRulePathDef = '/repetition/new'; -export const editRepetitionRulePathDef = '/repetition/:repetitionUUID/edit'; export const emailPreferencePathDef = '/email-preferences'; -export const digestsPathDef = '/digests'; -export const digestPathDef = '/digests/:digestUUID'; // layout definitions export const noHeaderPaths = [ loginPathDef, joinPathDef, - prefEditRepetitionPathDef, verifyEmailPathDef, classicMigrationPathDef, passwordResetRequestPathDef, @@ -59,7 +50,6 @@ export const noFooterPaths = [ loginPathDef, joinPathDef, subscriptionsPathDef, - prefEditRepetitionPathDef, verifyEmailPathDef, classicMigrationPathDef, passwordResetRequestPathDef, @@ -132,27 +122,6 @@ export function getBooksPath(searchObj = {}): Location { return getLocation({ pathname: booksPathDef, searchObj }); } -export function getRepetitionsPath(searchObj = {}): Location { - return getLocation({ pathname: repetitionsPathDef, searchObj }); -} - -export function getDigestsPath(searchObj = {}): Location { - return getLocation({ pathname: digestsPathDef, searchObj }); -} - -export function getDigestPath(digestUUID: string, searchObj = {}): Location { - const path = `/digests/${digestUUID}`; - - return getLocation({ - pathname: path, - searchObj - }); -} - -export function getNewRepetitionPath(searchObj = {}): Location { - return getLocation({ pathname: newRepetitionRulePathDef, searchObj }); -} - export function populateParams(pathDef: string, params: any) { const parts = pathDef.split('/'); @@ -173,13 +142,6 @@ export function populateParams(pathDef: string, params: any) { return builder.join('/'); } -export function getEditRepetitionPath(uuid: string, searchObj = {}): Location { - const pathname = populateParams(editRepetitionRulePathDef, { - repetitionUUID: uuid - }); - return getLocation({ pathname, searchObj }); -} - export function getNotePath(noteUUID: string, searchObj = {}): Location { const path = `/notes/${noteUUID}`; diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 5b99d0db..9c0cb8d2 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -29,21 +29,15 @@ import Join from './components/Join'; import Settings from './components/Settings'; import NotFound from './components/Common/NotFound'; import VerifyEmail from './components/VerifyEmail'; -import PreferenceEditRepetition from './components/Preferences/Repetitions'; import New from './components/New'; import Edit from './components/Edit'; import Note from './components/Note'; import Books from './components/Books'; 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 { @@ -59,15 +53,9 @@ import { passwordResetRequestPathDef, passwordResetConfirmPathDef, getJoinPath, - prefEditRepetitionPathDef, verifyEmailPathDef, classicMigrationPathDef, - repetitionsPathDef, - newRepetitionRulePathDef, - editRepetitionRulePathDef, - emailPreferencePathDef, - digestsPathDef, - digestPathDef + emailPreferencePathDef } from './libs/paths'; const AuthenticatedHome = userOnly(Home); @@ -83,9 +71,6 @@ const AuthenticatedSubscriptionCheckout = userOnly( Checkout, getJoinPath().pathname ); -const AuthenticatedRepetition = userOnly(Repetition); -const AuthenticatedNewRepetition = userOnly(NewRepetition); -const AuthenticatedEditRepetition = userOnly(EditRepetition); const routes = [ { @@ -133,11 +118,6 @@ const routes = [ exact: true, component: VerifyEmail }, - { - path: prefEditRepetitionPathDef, - exact: true, - component: PreferenceEditRepetition - }, { path: noteNewPathDef, exact: true, @@ -158,36 +138,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 } diff --git a/web/src/store/digest/actions.ts b/web/src/store/digest/actions.ts deleted file mode 100644 index 22d73e4f..00000000 --- a/web/src/store/digest/actions.ts +++ /dev/null @@ -1,115 +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 . - */ - -import operations from 'web/libs/operations'; -import services from 'web/libs/services'; -import { DigestData } from 'jslib/operations/types'; -import { - RECEIVE, - START_FETCHING, - ERROR, - RESET, - SET_NOTE_REVIEWED, - SetNoteReviewed -} from './type'; -import { ThunkAction } from '../types'; - -export function receiveDigest(digest: DigestData) { - return { - type: RECEIVE, - data: { digest } - }; -} - -export function resetDigest() { - return { - type: RESET - }; -} - -function startFetchingDigest() { - return { - type: START_FETCHING - }; -} - -function receiveDigestError(errorMessage: string) { - return { - type: ERROR, - data: { errorMessage } - }; -} - -function setNoteReviewed( - noteUUID: string, - isReviewed: boolean -): SetNoteReviewed { - return { - type: SET_NOTE_REVIEWED, - data: { - noteUUID, - isReviewed - } - }; -} - -interface GetDigestFacets { - q?: string; -} - -export const getDigest = ( - digestUUID: string -): ThunkAction => { - return dispatch => { - dispatch(startFetchingDigest()); - - return operations.digests - .fetch(digestUUID) - .then(digest => { - dispatch(receiveDigest(digest)); - - return digest; - }) - .catch(err => { - console.log('getDigest error', err.message); - dispatch(receiveDigestError(err.message)); - }); - }; -}; - -export const setDigestNoteReviewed = ({ - digestUUID, - noteUUID, - isReviewed -}: { - digestUUID: string; - noteUUID: string; - isReviewed: boolean; -}): ThunkAction => { - return dispatch => { - if (!isReviewed) { - return services.noteReviews.remove({ noteUUID, digestUUID }).then(() => { - dispatch(setNoteReviewed(noteUUID, false)); - }); - } - - return services.noteReviews.create({ digestUUID, noteUUID }).then(() => { - dispatch(setNoteReviewed(noteUUID, true)); - }); - }; -}; diff --git a/web/src/store/digest/index.ts b/web/src/store/digest/index.ts deleted file mode 100644 index 82479ec2..00000000 --- a/web/src/store/digest/index.ts +++ /dev/null @@ -1,21 +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 . - */ - -export * from './actions'; -export * from './reducers'; -export * from './type'; diff --git a/web/src/store/digest/reducers.ts b/web/src/store/digest/reducers.ts deleted file mode 100644 index 136cad2e..00000000 --- a/web/src/store/digest/reducers.ts +++ /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 . - */ - -import { BookDomain } from 'jslib/operations/types'; -import { - RECEIVE, - START_FETCHING, - ERROR, - RESET, - SET_NOTE_REVIEWED, - DigestState, - DigestActionType -} from './type'; - -const initialState: DigestState = { - data: { - uuid: '', - createdAt: '', - updatedAt: '', - version: 0, - notes: [], - isRead: false, - repetitionRule: { - uuid: '', - title: '', - enabled: false, - hour: 0, - minute: 0, - bookDomain: BookDomain.All, - frequency: 0, - books: [], - lastActive: 0, - nextActive: 0, - noteCount: 0, - createdAt: '', - updatedAt: '' - } - }, - isFetching: false, - isFetched: false, - errorMessage: null -}; - -export default function( - state = initialState, - action: DigestActionType -): DigestState { - switch (action.type) { - case START_FETCHING: { - return { - ...state, - errorMessage: null, - isFetching: true, - isFetched: false - }; - } - case ERROR: { - return { - ...state, - isFetching: false, - errorMessage: action.data.errorMessage - }; - } - case RECEIVE: { - return { - ...state, - data: action.data.digest, - isFetching: false, - isFetched: true - }; - } - case SET_NOTE_REVIEWED: { - return { - ...state, - data: { - ...state.data, - notes: state.data.notes.map(note => { - if (action.data.noteUUID === note.uuid) { - const isReviewed = action.data.isReviewed; - - return { - ...note, - isReviewed - }; - } - - return note; - }) - } - }; - } - case RESET: { - return initialState; - } - default: - return state; - } -} diff --git a/web/src/store/digest/type.ts b/web/src/store/digest/type.ts deleted file mode 100644 index 7c63ae3a..00000000 --- a/web/src/store/digest/type.ts +++ /dev/null @@ -1,65 +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 . - */ - -import { DigestData } from 'jslib/operations/types'; -import { RemoteData } from '../types'; - -export type DigestState = RemoteData; - -export const RECEIVE = 'digest/RECEIVE'; -export const START_FETCHING = 'digest/START_FETCHING'; -export const ERROR = 'digest/ERROR'; -export const RESET = 'digest/RESET'; -export const SET_NOTE_REVIEWED = 'digest/SET_NOTE_REVIEWED'; - -export interface ReceiveDigest { - type: typeof RECEIVE; - data: { - digest: DigestData; - }; -} - -export interface StartFetchingDigest { - type: typeof START_FETCHING; -} - -export interface ResetDigest { - type: typeof RESET; -} - -export interface ReceiveDigestError { - type: typeof ERROR; - data: { - errorMessage: string; - }; -} - -export interface SetNoteReviewed { - type: typeof SET_NOTE_REVIEWED; - data: { - noteUUID: string; - isReviewed: boolean; - }; -} - -export type DigestActionType = - | ReceiveDigest - | StartFetchingDigest - | ReceiveDigestError - | ResetDigest - | SetNoteReviewed; diff --git a/web/src/store/digests/actions.ts b/web/src/store/digests/actions.ts deleted file mode 100644 index f037db00..00000000 --- a/web/src/store/digests/actions.ts +++ /dev/null @@ -1,67 +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 . - */ - -import operations from 'web/libs/operations'; -import { START_FETCHING, RECEIVE, RECEIVE_ERROR, RESET } from './type'; - -function receiveDigests(total, items, page) { - return { - type: RECEIVE, - data: { - total, - items, - page - } - }; -} - -function startFetchingDigests() { - return { - type: START_FETCHING - }; -} - -function receiveError(error) { - return { - type: RECEIVE_ERROR, - data: { - error - } - }; -} - -export function resetDigests() { - return { - type: RESET - }; -} - -export function getDigests(params: { page: number; status: string }) { - return async dispatch => { - try { - dispatch(startFetchingDigests()); - - const res = await operations.digests.fetchAll(params); - - dispatch(receiveDigests(res.total, res.items, params.page)); - } catch (err) { - console.log('Error fetching digests', err.stack); - dispatch(receiveError(err.message)); - } - }; -} diff --git a/web/src/store/digests/index.ts b/web/src/store/digests/index.ts deleted file mode 100644 index 82479ec2..00000000 --- a/web/src/store/digests/index.ts +++ /dev/null @@ -1,21 +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 . - */ - -export * from './actions'; -export * from './reducers'; -export * from './type'; diff --git a/web/src/store/digests/reducers.ts b/web/src/store/digests/reducers.ts deleted file mode 100644 index cecc212f..00000000 --- a/web/src/store/digests/reducers.ts +++ /dev/null @@ -1,72 +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 . - */ - -import { - START_FETCHING, - RECEIVE, - RECEIVE_ERROR, - RESET, - DigestsState, - DigestsActionType -} from './type'; - -const initialState: DigestsState = { - data: [], - total: 0, - page: 0, - isFetching: false, - isFetched: false, - errorMessage: null -}; - -export default function( - state = initialState, - action: DigestsActionType -): DigestsState { - switch (action.type) { - case START_FETCHING: { - return { - ...state, - errorMessage: null, - isFetching: true, - isFetched: false - }; - } - case RECEIVE: { - return { - ...state, - isFetching: false, - isFetched: true, - total: action.data.total, - page: action.data.page, - data: action.data.items - }; - } - case RECEIVE_ERROR: { - return { - ...state, - errorMessage: action.data.error - }; - } - case RESET: { - return initialState; - } - default: - return state; - } -} diff --git a/web/src/store/digests/type.ts b/web/src/store/digests/type.ts deleted file mode 100644 index 1c103391..00000000 --- a/web/src/store/digests/type.ts +++ /dev/null @@ -1,61 +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 . - */ - -import { DigestData } from 'jslib/operations/types'; - -import { RemoteData } from '../types'; - -export interface DigestsState extends RemoteData { - total: number; - page: number; -} - -export const START_FETCHING = 'digests/START_FETCHING'; -export const RECEIVE = 'digests/RECEIVE'; -export const RECEIVE_ERROR = 'digests/RECEIVE_ERROR'; -export const RESET = 'digests/RESET'; - -export interface StartFetchingAction { - type: typeof START_FETCHING; -} - -export interface ResetAction { - type: typeof RESET; -} - -export interface ReceiveErrorAction { - type: typeof RECEIVE_ERROR; - data: { - error: string; - }; -} - -export interface ReceiveAction { - type: typeof RECEIVE; - data: { - items: DigestData[]; - total: number; - page: number; - }; -} - -export type DigestsActionType = - | StartFetchingAction - | ReceiveAction - | ReceiveErrorAction - | ResetAction; diff --git a/web/src/store/index.ts b/web/src/store/index.ts index f13160aa..873d0859 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -28,9 +28,6 @@ import ui from './ui/reducers'; import route from './route/reducers'; import notes from './notes/reducers'; import filters from './filters/reducers'; -import repetitionRules from './repetitionRules/reducers'; -import digests from './digests/reducers'; -import digest from './digest/reducers'; const rootReducer = combineReducers({ auth, @@ -41,10 +38,7 @@ const rootReducer = combineReducers({ note, ui, route, - filters, - digests, - digest, - repetitionRules + filters }); // configuruStore returns a new store that contains the appliation state diff --git a/web/src/store/repetitionRules/actions.ts b/web/src/store/repetitionRules/actions.ts deleted file mode 100644 index 5516d294..00000000 --- a/web/src/store/repetitionRules/actions.ts +++ /dev/null @@ -1,111 +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 . - */ - -import services from 'web/libs/services'; -import { RepetitionRuleData } from 'jslib/operations/types'; -import { CreateParams } from 'jslib/services/repetitionRules'; -import { - RECEIVE, - ADD, - REMOVE, - START_FETCHING, - FINISH_FETCHING, - RECEIVE_ERROR, - ReceiveRepetitionRulesAction, - ReceiveRepetitionRulesErrorAction, - StartFetchingRepetitionRulesAction, - FinishFetchingRepetitionRulesAction, - AddRepetitionRuleAction, - RemoveRepetitionRuleAction -} from './type'; -import { ThunkAction } from '../types'; - -function receiveRepetitionRules( - repetitionRules: RepetitionRuleData[] -): ReceiveRepetitionRulesAction { - return { - type: RECEIVE, - data: { repetitionRules } - }; -} - -function receiveRepetitionRulesError( - err: string -): ReceiveRepetitionRulesErrorAction { - return { - type: RECEIVE_ERROR, - data: { err } - }; -} - -function startFetchingRepetitionRules(): StartFetchingRepetitionRulesAction { - return { - type: START_FETCHING - }; -} - -function finishFetchingRepetitionRules(): FinishFetchingRepetitionRulesAction { - return { - type: FINISH_FETCHING - }; -} - -export const getRepetitionRules = (): ThunkAction => { - return dispatch => { - dispatch(startFetchingRepetitionRules()); - - return services.repetitionRules - .fetchAll() - .then(data => { - dispatch(receiveRepetitionRules(data)); - dispatch(finishFetchingRepetitionRules()); - }) - .catch(err => { - console.log('getRepetitionRules error', err); - dispatch(receiveRepetitionRulesError(err)); - }); - }; -}; - -export function addRepetitionRule( - repetitionRule: RepetitionRuleData -): AddRepetitionRuleAction { - return { - type: ADD, - data: { repetitionRule } - }; -} - -export const createRepetitionRule = ( - p: CreateParams -): ThunkAction => { - return dispatch => { - return services.repetitionRules.create(p).then(data => { - dispatch(addRepetitionRule(data)); - - return data; - }); - }; -}; - -export function removeRepetitionRule(uuid: string): RemoveRepetitionRuleAction { - return { - type: REMOVE, - data: { uuid } - }; -} diff --git a/web/src/store/repetitionRules/index.ts b/web/src/store/repetitionRules/index.ts deleted file mode 100644 index 82479ec2..00000000 --- a/web/src/store/repetitionRules/index.ts +++ /dev/null @@ -1,21 +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 . - */ - -export * from './actions'; -export * from './reducers'; -export * from './type'; diff --git a/web/src/store/repetitionRules/reducers.ts b/web/src/store/repetitionRules/reducers.ts deleted file mode 100644 index 36137a24..00000000 --- a/web/src/store/repetitionRules/reducers.ts +++ /dev/null @@ -1,87 +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 . - */ - -import { - RepetitionRulesState, - RepetitionRulesActionType, - RECEIVE, - RECEIVE_ERROR, - ADD, - REMOVE, - START_FETCHING, - FINISH_FETCHING -} from './type'; - -const initialState: RepetitionRulesState = { - data: [], - isFetching: false, - isFetched: false, - errorMessage: '' -}; - -export default function( - state = initialState, - action: RepetitionRulesActionType -): RepetitionRulesState { - switch (action.type) { - case START_FETCHING: { - return { - ...state, - isFetching: true, - isFetched: false - }; - } - case FINISH_FETCHING: { - return { - ...state, - isFetching: false, - isFetched: true - }; - } - case RECEIVE: { - return { - ...state, - data: action.data.repetitionRules - }; - } - case RECEIVE_ERROR: { - return { - ...state, - errorMessage: action.data.err - }; - } - case REMOVE: { - return { - ...state, - data: state.data.filter(item => { - return item.uuid !== action.data.uuid; - }) - }; - } - case ADD: { - const data = [...state.data, action.data.repetitionRule]; - - return { - ...state, - data - }; - } - default: - return state; - } -} diff --git a/web/src/store/repetitionRules/type.ts b/web/src/store/repetitionRules/type.ts deleted file mode 100644 index 9d9af1fc..00000000 --- a/web/src/store/repetitionRules/type.ts +++ /dev/null @@ -1,73 +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 . - */ - -import { RepetitionRuleData } from 'jslib/operations/types'; -import { RemoteData } from '../types'; - -export type RepetitionRulesState = RemoteData; - -export const RECEIVE = 'repetitionRules/RECEIVE'; -export const RECEIVE_ERROR = 'repetitionRules/RECEIVE_ERROR'; -export const ADD = 'repetitionRules/ADD'; -export const REMOVE = 'repetitionRules/REMOVE'; -export const START_FETCHING = 'repetitionRules/START_FETCHING'; -export const FINISH_FETCHING = 'repetitionRules/FINISH_FETCHING'; - -export interface ReceiveRepetitionRulesAction { - type: typeof RECEIVE; - data: { - repetitionRules: RepetitionRuleData[]; - }; -} - -export interface ReceiveRepetitionRulesErrorAction { - type: typeof RECEIVE_ERROR; - data: { - err: string; - }; -} - -export interface StartFetchingRepetitionRulesAction { - type: typeof START_FETCHING; -} - -export interface FinishFetchingRepetitionRulesAction { - type: typeof FINISH_FETCHING; -} - -export interface AddRepetitionRuleAction { - type: typeof ADD; - data: { - repetitionRule: RepetitionRuleData; - }; -} - -export interface RemoveRepetitionRuleAction { - type: typeof REMOVE; - data: { - uuid: string; - }; -} - -export type RepetitionRulesActionType = - | ReceiveRepetitionRulesAction - | ReceiveRepetitionRulesErrorAction - | StartFetchingRepetitionRulesAction - | FinishFetchingRepetitionRulesAction - | AddRepetitionRuleAction - | RemoveRepetitionRuleAction; diff --git a/web/src/store/types.ts b/web/src/store/types.ts index 3a03c5ff..4142d4a4 100644 --- a/web/src/store/types.ts +++ b/web/src/store/types.ts @@ -28,10 +28,6 @@ import { NotesState } from './notes/type'; import { UIState } from './ui/type'; import { RouteState } from './route/type'; import { FiltersState } from './filters/type'; -import { DigestsState } from './digests/type'; -import { DigestState } from './digest/type'; - -import { RepetitionRulesState } from './repetitionRules/type'; // RemoteData represents a data in Redux store that is fetched from a remote source. // It contains the state related to the fetching of the data as well as the data itself. @@ -53,9 +49,6 @@ export interface AppState { ui: UIState; route: RouteState; filters: FiltersState; - digests: DigestsState; - digest: DigestState; - repetitionRules: RepetitionRulesState; } // ThunkAction is a thunk action type