Add flush history feature
This commit is contained in:
parent
238b9e4c85
commit
549a4277b0
15 changed files with 79 additions and 17 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "History" }} ({{ .total }})</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ route "flushHistory" }}">{{ t "Flush history" }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{ if not .entries }}
|
||||
|
|
|
@ -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"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "History" }} ({{ .total }})</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ route "flushHistory" }}">{{ t "Flush history" }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{ 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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue