From 549a4277b0c000d7cf0b2867c895599d283d70a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Tue, 21 Nov 2017 15:46:59 -0800 Subject: [PATCH] Add flush history feature --- README.md | 3 ++- locale/translations.go | 2 +- model/entry.go | 7 +++++++ server/core/json_response.go | 5 ++++- server/routes.go | 1 + server/static/bin.go | 2 +- server/static/css.go | 2 +- server/static/js.go | 2 +- server/template/common.go | 2 +- server/template/html/history.html | 5 +++++ server/template/views.go | 9 +++++++-- server/ui/controller/entry.go | 12 +++++++++--- server/ui/controller/history.go | 14 ++++++++++++++ sql/sql.go | 2 +- storage/entry.go | 28 ++++++++++++++++++++++++---- 15 files changed, 79 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 25e58bd0..564fdb72 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Miniflux is a minimalist and opinionated feed reader: - Works only with Postgresql - Doesn't use any ORM - Doesn't use any complicated framework +- Use only modern vanilla Javascript (ES6 and fetch) - The number of features is volountary limited It's simple, fast, lightweight and super easy to install. @@ -29,7 +30,7 @@ TODO - [ ] External integrations (Pinboard, Wallabag...) - [ ] Gzip compression - [ ] Integration tests -- [ ] Flush history +- [X] Flush history - [ ] OAuth2 Credits diff --git a/locale/translations.go b/locale/translations.go index bbe62d3c..f4532a23 100644 --- a/locale/translations.go +++ b/locale/translations.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.456403496 -0800 PST m=+0.037949400 +// 2017-11-21 15:41:59.495654213 -0800 PST m=+0.041889871 package locale diff --git a/model/entry.go b/model/entry.go index 6858935b..1053130b 100644 --- a/model/entry.go +++ b/model/entry.go @@ -9,6 +9,7 @@ import ( "time" ) +// Entry statuses const ( EntryStatusUnread = "unread" EntryStatusRead = "read" @@ -17,6 +18,7 @@ const ( DefaultSortingDirection = "desc" ) +// Entry represents a feed item in the system. type Entry struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` @@ -33,8 +35,10 @@ type Entry struct { Category *Category `json:"category,omitempty"` } +// Entries represents a list of entries. type Entries []*Entry +// ValidateEntryStatus makes sure the entry status is valid. func ValidateEntryStatus(status string) error { switch status { case EntryStatusRead, EntryStatusUnread, EntryStatusRemoved: @@ -44,6 +48,7 @@ func ValidateEntryStatus(status string) error { return fmt.Errorf(`Invalid entry status, valid status values are: "%s", "%s" and "%s"`, EntryStatusRead, EntryStatusUnread, EntryStatusRemoved) } +// ValidateEntryOrder makes sure the sorting order is valid. func ValidateEntryOrder(order string) error { switch order { case "id", "status", "published_at", "category_title", "category_id": @@ -53,6 +58,7 @@ func ValidateEntryOrder(order string) error { return fmt.Errorf(`Invalid entry order, valid order values are: "id", "status", "published_at", "category_title", "category_id"`) } +// ValidateDirection makes sure the sorting direction is valid. func ValidateDirection(direction string) error { switch direction { case "asc", "desc": @@ -62,6 +68,7 @@ func ValidateDirection(direction string) error { return fmt.Errorf(`Invalid direction, valid direction values are: "asc" or "desc"`) } +// GetOppositeDirection returns the opposite sorting direction. func GetOppositeDirection(direction string) string { if direction == "asc" { return "desc" diff --git a/server/core/json_response.go b/server/core/json_response.go index 51a9ede8..c72fd731 100644 --- a/server/core/json_response.go +++ b/server/core/json_response.go @@ -54,7 +54,10 @@ func (j *JsonResponse) ServerError(err error) { log.Println("[API:ServerError]", err) j.writer.WriteHeader(http.StatusInternalServerError) j.commonHeaders() - j.writer.Write(j.encodeError(err)) + + if err != nil { + j.writer.Write(j.encodeError(err)) + } } func (j *JsonResponse) Forbidden() { diff --git a/server/routes.go b/server/routes.go index 239f5fa1..36471b09 100644 --- a/server/routes.go +++ b/server/routes.go @@ -81,6 +81,7 @@ func getRoutes(store *storage.Storage, feedHandler *feed.Handler) *mux.Router { router.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET") router.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET") + router.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET") router.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET") router.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET") diff --git a/server/static/bin.go b/server/static/bin.go index dc411fa6..ea9db5a4 100644 --- a/server/static/bin.go +++ b/server/static/bin.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.42928305 -0800 PST m=+0.010828954 +// 2017-11-21 15:41:59.461181295 -0800 PST m=+0.007416953 package static diff --git a/server/static/css.go b/server/static/css.go index e2d14e30..5243f897 100644 --- a/server/static/css.go +++ b/server/static/css.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.43289693 -0800 PST m=+0.014442834 +// 2017-11-21 15:41:59.464123652 -0800 PST m=+0.010359310 package static diff --git a/server/static/js.go b/server/static/js.go index f6e89705..23df072c 100644 --- a/server/static/js.go +++ b/server/static/js.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.43700259 -0800 PST m=+0.018548494 +// 2017-11-21 15:41:59.4687788 -0800 PST m=+0.015014458 package static diff --git a/server/template/common.go b/server/template/common.go index 8c6db156..b108f0ea 100644 --- a/server/template/common.go +++ b/server/template/common.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.455330256 -0800 PST m=+0.036876160 +// 2017-11-21 15:41:59.491806442 -0800 PST m=+0.038042100 package template diff --git a/server/template/html/history.html b/server/template/html/history.html index a344da1e..8de640f9 100644 --- a/server/template/html/history.html +++ b/server/template/html/history.html @@ -3,6 +3,11 @@ {{ define "content"}} {{ if not .entries }} diff --git a/server/template/views.go b/server/template/views.go index 0611d6cb..7f192d0b 100644 --- a/server/template/views.go +++ b/server/template/views.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.438565193 -0800 PST m=+0.020111097 +// 2017-11-21 15:41:59.472545112 -0800 PST m=+0.018780770 package template @@ -649,6 +649,11 @@ var templateViewsMap = map[string]string{ {{ define "content"}} {{ if not .entries }} @@ -980,7 +985,7 @@ var templateViewsMapChecksums = map[string]string{ "entry": "32e605edd6d43773ac31329d247ebd81d38d974cd43689d91de79fffec7fe04b", "feed_entries": "9aff923b6c7452dec1514feada7e0d2bbc1ec21c6f5e9f48b2de41d1b731ffe4", "feeds": "94e43404a4044490c065c888a49bebd3ff51b588b9fb47d03c2598003aa40dca", - "history": "439000d0be8fd716f3b89860af4d721e05baef0c2ccd2325ba020c940d6aa847", + "history": "947603cbde888516e62925f5d08fb0b13d930623d3ee4c690dbc22612fdda75e", "import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f", "login": "568f2f69f248048f3e55e9bbc719077a74ae23fe18f237aa40e3de37e97b7a41", "sessions": "5ac3793f0ee74d0807bab6a173a1aa6508e98add5c022fa54c8fdf5c6b4a0e75", diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go index 5a3a979c..c4ca5873 100644 --- a/server/ui/controller/entry.go +++ b/server/ui/controller/entry.go @@ -6,12 +6,14 @@ package controller import ( "errors" + "log" + "github.com/miniflux/miniflux2/model" "github.com/miniflux/miniflux2/server/core" "github.com/miniflux/miniflux2/server/ui/payload" - "log" ) +// ShowFeedEntry shows a single feed entry in "feed" mode. func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) { user := ctx.GetLoggedUser() sortingDirection := model.DefaultSortingDirection @@ -102,6 +104,7 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res })) } +// ShowCategoryEntry shows a single feed entry in "category" mode. func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request, response *core.Response) { user := ctx.GetLoggedUser() sortingDirection := model.DefaultSortingDirection @@ -192,6 +195,7 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request, })) } +// ShowUnreadEntry shows a single feed entry in "unread" mode. func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, response *core.Response) { user := ctx.GetLoggedUser() sortingDirection := model.DefaultSortingDirection @@ -275,6 +279,7 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r })) } +// ShowReadEntry shows a single feed entry in "history" mode. func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, response *core.Response) { user := ctx.GetLoggedUser() sortingDirection := model.DefaultSortingDirection @@ -349,6 +354,7 @@ func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, res })) } +// UpdateEntriesStatus handles Ajax request to update a list of entries. func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Request, response *core.Response) { user := ctx.GetLoggedUser() @@ -360,14 +366,14 @@ func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Reques } if len(entryIDs) == 0 { - response.Html().BadRequest(errors.New("The list of entryID is empty")) + response.Json().BadRequest(errors.New("The list of entryID is empty")) return } err = c.store.SetEntriesStatus(user.ID, entryIDs, status) if err != nil { log.Println(err) - response.Html().ServerError(nil) + response.Json().ServerError(nil) return } diff --git a/server/ui/controller/history.go b/server/ui/controller/history.go index 2c067373..e4612e17 100644 --- a/server/ui/controller/history.go +++ b/server/ui/controller/history.go @@ -9,6 +9,7 @@ import ( "github.com/miniflux/miniflux2/server/core" ) +// ShowHistoryPage renders the page with all read entries. func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, response *core.Response) { user := ctx.GetLoggedUser() offset := request.GetQueryIntegerParam("offset", 0) @@ -45,3 +46,16 @@ func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, r "menu": "history", })) } + +// FlushHistory changes all "read" items to "removed". +func (c *Controller) FlushHistory(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + err := c.store.FlushHistory(user.ID) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Redirect(ctx.GetRoute("history")) +} diff --git a/sql/sql.go b/sql/sql.go index 198779bd..882a6d6f 100644 --- a/sql/sql.go +++ b/sql/sql.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-21 14:55:14.420877594 -0800 PST m=+0.002423498 +// 2017-11-21 15:41:59.457985225 -0800 PST m=+0.004220883 package sql diff --git a/storage/entry.go b/storage/entry.go index 84cfb0f2..fe8c6f22 100644 --- a/storage/entry.go +++ b/storage/entry.go @@ -7,17 +7,20 @@ package storage import ( "errors" "fmt" + "time" + "github.com/miniflux/miniflux2/helper" "github.com/miniflux/miniflux2/model" - "time" "github.com/lib/pq" ) +// GetEntryQueryBuilder returns a new EntryQueryBuilder func (s *Storage) GetEntryQueryBuilder(userID int64, timezone string) *EntryQueryBuilder { return NewEntryQueryBuilder(s, userID, timezone) } +// CreateEntry add a new entry. func (s *Storage) CreateEntry(entry *model.Entry) error { query := ` INSERT INTO entries @@ -55,6 +58,7 @@ func (s *Storage) CreateEntry(entry *model.Entry) error { return nil } +// UpdateEntry update an entry when a feed is refreshed. func (s *Storage) UpdateEntry(entry *model.Entry) error { query := ` UPDATE entries SET @@ -76,6 +80,7 @@ func (s *Storage) UpdateEntry(entry *model.Entry) error { return err } +// EntryExists checks if an entry already exists based on its hash when refreshing a feed. func (s *Storage) EntryExists(entry *model.Entry) bool { var result int query := `SELECT count(*) as c FROM entries WHERE user_id=$1 AND feed_id=$2 AND hash=$3` @@ -83,6 +88,7 @@ func (s *Storage) EntryExists(entry *model.Entry) bool { return result >= 1 } +// UpdateEntries update a list of entries while refreshing a feed. func (s *Storage) UpdateEntries(userID, feedID int64, entries model.Entries) (err error) { for _, entry := range entries { entry.UserID = userID @@ -102,22 +108,36 @@ func (s *Storage) UpdateEntries(userID, feedID int64, entries model.Entries) (er return nil } +// SetEntriesStatus update the status of the given list of entries. func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string) error { defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:SetEntriesStatus] userID=%d, entryIDs=%v, status=%s", userID, entryIDs, status)) query := `UPDATE entries SET status=$1 WHERE user_id=$2 AND id=ANY($3)` result, err := s.db.Exec(query, status, userID, pq.Array(entryIDs)) if err != nil { - return fmt.Errorf("Unable to update entry status: %v", err) + return fmt.Errorf("unable to update entries status: %v", err) } count, err := result.RowsAffected() if err != nil { - return fmt.Errorf("Unable to update this entry: %v", err) + return fmt.Errorf("unable to update these entries: %v", err) } if count == 0 { - return errors.New("Nothing has been updated") + return errors.New("nothing has been updated") + } + + return nil +} + +// FlushHistory set all entries with the status "read" to "removed". +func (s *Storage) FlushHistory(userID int64) error { + defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:FlushHistory] userID=%d", userID)) + + query := `UPDATE entries SET status=$1 WHERE user_id=$2 AND status=$3` + _, err := s.db.Exec(query, model.EntryStatusRemoved, userID, model.EntryStatusRead) + if err != nil { + return fmt.Errorf("unable to flush history: %v", err) } return nil