Make entries sorting configurable
This commit is contained in:
parent
453ff64f29
commit
2f1367a8d4
28 changed files with 253 additions and 193 deletions
|
@ -21,10 +21,15 @@ Notes
|
|||
|
||||
Miniflux 2 still in development and **it's not ready to use**.
|
||||
|
||||
- [Features](https://docs.miniflux.net/en/latest/features.html)
|
||||
- [Requirements](https://docs.miniflux.net/en/latest/requirements.html)
|
||||
- [Installation](https://docs.miniflux.net/en/latest/installation.html)
|
||||
- [Documentation](https://docs.miniflux.net/)
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
- [ ] Custom entries sorting
|
||||
- [X] Custom entries sorting
|
||||
- [ ] Webpage scraper (Readability)
|
||||
- [X] Bookmarklet
|
||||
- [ ] External integrations (Pinboard, Instapaper, Pocket?)
|
||||
|
@ -32,7 +37,7 @@ TODO
|
|||
- [X] Integration tests
|
||||
- [X] Flush history
|
||||
- [X] OAuth2
|
||||
- [ ] Touch events
|
||||
- [X] Touch events
|
||||
- [ ] Fever API?
|
||||
|
||||
Credits
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2017-11-27 21:07:53.23444885 -0800 PST m=+0.028635078
|
||||
// 2017-12-02 16:12:47.287568844 -0800 PST m=+0.033078160
|
||||
|
||||
package locale
|
||||
|
||||
|
@ -150,12 +150,15 @@ var translations = map[string]string{
|
|||
"Unlink my Google account": "Dissocier mon compte Google",
|
||||
"Link my Google account": "Associer mon compte Google",
|
||||
"Category not found for this user.": "Cette catégorie n'existe pas pour cet utilisateur.",
|
||||
"Invalid theme.": "Le thème est invalide."
|
||||
"Invalid theme.": "Le thème est invalide.",
|
||||
"Entry Sorting": "Ordre des éléments",
|
||||
"Older entries first": "Ancien éléments en premier",
|
||||
"Recent entries first": "Éléments récents en premier"
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
var translationsChecksums = map[string]string{
|
||||
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
|
||||
"fr_FR": "48622d4796fe4a461221565d84f52e22fb167a44a870b08ba32887897bdfbb1a",
|
||||
"fr_FR": "5c054c06fa687f05fd4f6041b002207fe1fe304d6c0c0d094b8caa61a5071ba5",
|
||||
}
|
||||
|
|
|
@ -134,5 +134,8 @@
|
|||
"Unlink my Google account": "Dissocier mon compte Google",
|
||||
"Link my Google account": "Associer mon compte Google",
|
||||
"Category not found for this user.": "Cette catégorie n'existe pas pour cet utilisateur.",
|
||||
"Invalid theme.": "Le thème est invalide."
|
||||
"Invalid theme.": "Le thème est invalide.",
|
||||
"Entry Sorting": "Ordre des éléments",
|
||||
"Older entries first": "Ancien éléments en premier",
|
||||
"Recent entries first": "Éléments récents en premier"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
EntryStatusRead = "read"
|
||||
EntryStatusRemoved = "removed"
|
||||
DefaultSortingOrder = "published_at"
|
||||
DefaultSortingDirection = "desc"
|
||||
DefaultSortingDirection = "asc"
|
||||
)
|
||||
|
||||
// Entry represents a feed item in the system.
|
||||
|
@ -81,8 +81,8 @@ func ValidateRange(offset, limit int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetOppositeDirection returns the opposite sorting direction.
|
||||
func GetOppositeDirection(direction string) string {
|
||||
// OppositeDirection returns the opposite sorting direction.
|
||||
func OppositeDirection(direction string) string {
|
||||
if direction == "asc" {
|
||||
return "desc"
|
||||
}
|
||||
|
|
|
@ -57,15 +57,15 @@ func TestValidateRange(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetOppositeDirection(t *testing.T) {
|
||||
if GetOppositeDirection("asc") != "desc" {
|
||||
if OppositeDirection("asc") != "desc" {
|
||||
t.Errorf(`The opposite direction of "asc" should be "desc"`)
|
||||
}
|
||||
|
||||
if GetOppositeDirection("desc") != "asc" {
|
||||
if OppositeDirection("desc") != "asc" {
|
||||
t.Errorf(`The opposite direction of "desc" should be "asc"`)
|
||||
}
|
||||
|
||||
if GetOppositeDirection("invalid") != "asc" {
|
||||
if OppositeDirection("invalid") != "asc" {
|
||||
t.Errorf(`An invalid direction should return "asc"`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,14 @@
|
|||
|
||||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Token represents a CSRF token in the system.
|
||||
type Token struct {
|
||||
ID string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t Token) String() string {
|
||||
return fmt.Sprintf(`ID="%s"`, t.ID)
|
||||
}
|
||||
|
|
|
@ -11,15 +11,16 @@ import (
|
|||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
Extra map[string]string `json:"-"`
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
EntryDirection string `json:"entry_sorting_direction,omitempty"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
// NewUser returns a new User.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2017-11-27 21:07:53.21170439 -0800 PST m=+0.005890618
|
||||
// 2017-12-02 16:12:47.261744369 -0800 PST m=+0.007253685
|
||||
|
||||
package static
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -10,7 +10,7 @@ body {
|
|||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.main {
|
||||
main {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2017-12-02 14:53:55.175825378 -0800 PST m=+0.009022020
|
||||
// 2017-12-02 16:12:47.268772139 -0800 PST m=+0.014281455
|
||||
|
||||
package static
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2017-12-02 14:53:55.184002045 -0800 PST m=+0.017198687
|
||||
// 2017-12-02 16:12:47.286110197 -0800 PST m=+0.031619513
|
||||
|
||||
package template
|
||||
|
||||
|
@ -76,9 +76,9 @@ var templateCommonMap = map[string]string{
|
|||
</nav>
|
||||
</header>
|
||||
{{ end }}
|
||||
<section class="main">
|
||||
<main>
|
||||
{{template "content" .}}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}`,
|
||||
|
@ -106,6 +106,6 @@ var templateCommonMap = map[string]string{
|
|||
|
||||
var templateCommonMapChecksums = map[string]string{
|
||||
"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
|
||||
"layout": "0a06f790d6caad2918c5038f7aa4a2f88ff3b31ed8f52749d45344f2be7bee53",
|
||||
"layout": "d1f96640bf90eca64571cfa4fe73be55b09d1d5a49da85b1ea9f9d4f9c670a07",
|
||||
"pagination": "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
|
||||
}
|
||||
|
|
|
@ -51,9 +51,9 @@
|
|||
</nav>
|
||||
</header>
|
||||
{{ end }}
|
||||
<section class="main">
|
||||
<main>
|
||||
{{template "content" .}}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
|
@ -58,6 +58,12 @@
|
|||
{{ end }}
|
||||
</select>
|
||||
|
||||
<label for="form-entry-direction">{{ t "Entry Sorting" }}</label>
|
||||
<select id="form-entry-direction" name="entry_direction">
|
||||
<option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Older entries first" }}</option>
|
||||
<option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Recent entries first" }}</option>
|
||||
</select>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2017-12-02 14:53:55.176980108 -0800 PST m=+0.010176750
|
||||
// 2017-12-02 16:12:47.271430439 -0800 PST m=+0.016939755
|
||||
|
||||
package template
|
||||
|
||||
|
@ -922,6 +922,12 @@ var templateViewsMap = map[string]string{
|
|||
{{ end }}
|
||||
</select>
|
||||
|
||||
<label for="form-entry-direction">{{ t "Entry Sorting" }}</label>
|
||||
<select id="form-entry-direction" name="entry_direction">
|
||||
<option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Older entries first" }}</option>
|
||||
<option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Recent entries first" }}</option>
|
||||
</select>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
|
||||
</div>
|
||||
|
@ -1070,7 +1076,7 @@ var templateViewsMapChecksums = map[string]string{
|
|||
"integrations": "c485d6d9ed996635e55e73320610e6bcb01a41c1153e8e739ae2294b0b14b243",
|
||||
"login": "04f3ce79bfa5753f69e0d956c2a8999c0da549c7925634a3e8134975da0b0e0f",
|
||||
"sessions": "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf",
|
||||
"settings": "1e2df11f5436eb2d05ae1fae30dd6f1362613011edbfcc79ae8b23854fa348b4",
|
||||
"settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
|
||||
"unread": "3d8deab9119dc11f0d74a461e1ac89dc29931ba4645a043bb5b3eccba3cba5b8",
|
||||
"users": "44677e28bb5347799ed0020c90ec785aadec4b1454446d92411cfdaf6e32110b",
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ func (c *Controller) ShowCategoryEntries(ctx *core.Context, request *core.Reques
|
|||
builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithCategoryID(category.ID)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
|
|
@ -11,12 +11,12 @@ import (
|
|||
"github.com/miniflux/miniflux2/model"
|
||||
"github.com/miniflux/miniflux2/server/core"
|
||||
"github.com/miniflux/miniflux2/server/ui/payload"
|
||||
"github.com/miniflux/miniflux2/storage"
|
||||
)
|
||||
|
||||
// ShowFeedEntry shows a single feed entry in "feed" mode.
|
||||
func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
|
||||
user := ctx.LoggedUser()
|
||||
sortingDirection := model.DefaultSortingDirection
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
|
@ -46,6 +46,15 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res
|
|||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
|
@ -53,26 +62,9 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res
|
|||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", "<=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
nextEntry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", ">=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.GetOppositeDirection(sortingDirection))
|
||||
prevEntry, err := builder.GetEntry()
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
|
@ -88,14 +80,6 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res
|
|||
prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
|
@ -109,7 +93,6 @@ 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.LoggedUser()
|
||||
sortingDirection := model.DefaultSortingDirection
|
||||
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
if err != nil {
|
||||
|
@ -139,6 +122,15 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request,
|
|||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
|
@ -146,26 +138,9 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request,
|
|||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", "<=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(sortingDirection)
|
||||
nextEntry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", ">=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.GetOppositeDirection(sortingDirection))
|
||||
prevEntry, err := builder.GetEntry()
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
|
@ -181,15 +156,6 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request,
|
|||
prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
|
@ -203,7 +169,6 @@ 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.LoggedUser()
|
||||
sortingDirection := model.DefaultSortingDirection
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
|
@ -226,6 +191,15 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r
|
|||
return
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
args, err := c.getCommonTemplateArgs(ctx)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
|
@ -233,26 +207,9 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r
|
|||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", "<=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(sortingDirection)
|
||||
nextEntry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", ">=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.GetOppositeDirection(sortingDirection))
|
||||
prevEntry, err := builder.GetEntry()
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
|
@ -268,15 +225,6 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r
|
|||
prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
if entry.Status == model.EntryStatusUnread {
|
||||
err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
response.HTML().ServerError(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.HTML().Render("entry", args.Merge(tplParams{
|
||||
"entry": entry,
|
||||
"prevEntry": prevEntry,
|
||||
|
@ -290,7 +238,6 @@ 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.LoggedUser()
|
||||
sortingDirection := model.DefaultSortingDirection
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
|
@ -320,26 +267,9 @@ func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, res
|
|||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", "<=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(sortingDirection)
|
||||
nextEntry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
builder.WithCondition("e.id", "!=", entryID)
|
||||
builder.WithCondition("e.published_at", ">=", entry.Date)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.GetOppositeDirection(sortingDirection))
|
||||
prevEntry, err := builder.GetEntry()
|
||||
prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
|
||||
if err != nil {
|
||||
response.HTML().ServerError(err)
|
||||
return
|
||||
|
@ -390,3 +320,29 @@ func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Reques
|
|||
|
||||
response.JSON().Standard("OK")
|
||||
}
|
||||
|
||||
func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
n := len(entries)
|
||||
for i := 0; i < n; i++ {
|
||||
if entries[i].ID == entryID {
|
||||
if i-1 > 0 {
|
||||
prev = entries[i-1]
|
||||
}
|
||||
|
||||
if i+1 < n {
|
||||
next = entries[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prev, next, nil
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ func (c *Controller) ShowFeedEntries(ctx *core.Context, request *core.Request, r
|
|||
builder.WithFeedID(feed.ID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, r
|
|||
builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithStatus(model.EntryStatusRead)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
|
|
|
@ -74,10 +74,11 @@ func (c *Controller) getSettingsFormTemplateArgs(ctx *core.Context, user *model.
|
|||
|
||||
if settingsForm == nil {
|
||||
args["form"] = form.SettingsForm{
|
||||
Username: user.Username,
|
||||
Theme: user.Theme,
|
||||
Language: user.Language,
|
||||
Timezone: user.Timezone,
|
||||
Username: user.Username,
|
||||
Theme: user.Theme,
|
||||
Language: user.Language,
|
||||
Timezone: user.Timezone,
|
||||
EntryDirection: user.EntryDirection,
|
||||
}
|
||||
} else {
|
||||
args["form"] = settingsForm
|
||||
|
|
|
@ -17,7 +17,7 @@ func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, re
|
|||
builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithOrder(model.DefaultSortingOrder)
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
builder.WithDirection(user.EntryDirection)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(nbItemsPerPage)
|
||||
|
||||
|
|
|
@ -13,12 +13,13 @@ import (
|
|||
|
||||
// SettingsForm represents the settings form.
|
||||
type SettingsForm struct {
|
||||
Username string
|
||||
Password string
|
||||
Confirmation string
|
||||
Theme string
|
||||
Language string
|
||||
Timezone string
|
||||
Username string
|
||||
Password string
|
||||
Confirmation string
|
||||
Theme string
|
||||
Language string
|
||||
Timezone string
|
||||
EntryDirection string
|
||||
}
|
||||
|
||||
// Merge updates the fields of the given user.
|
||||
|
@ -27,6 +28,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
|
|||
user.Theme = s.Theme
|
||||
user.Language = s.Language
|
||||
user.Timezone = s.Timezone
|
||||
user.EntryDirection = s.EntryDirection
|
||||
|
||||
if s.Password != "" {
|
||||
user.Password = s.Password
|
||||
|
@ -37,7 +39,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
|
|||
|
||||
// Validate makes sure the form values are valid.
|
||||
func (s *SettingsForm) Validate() error {
|
||||
if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" {
|
||||
if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" {
|
||||
return errors.NewLocalizedError("The username, theme, language and timezone fields are mandatory.")
|
||||
}
|
||||
|
||||
|
@ -57,11 +59,12 @@ func (s *SettingsForm) Validate() error {
|
|||
// NewSettingsForm returns a new SettingsForm.
|
||||
func NewSettingsForm(r *http.Request) *SettingsForm {
|
||||
return &SettingsForm{
|
||||
Username: r.FormValue("username"),
|
||||
Password: r.FormValue("password"),
|
||||
Confirmation: r.FormValue("confirmation"),
|
||||
Theme: r.FormValue("theme"),
|
||||
Language: r.FormValue("language"),
|
||||
Timezone: r.FormValue("timezone"),
|
||||
Username: r.FormValue("username"),
|
||||
Password: r.FormValue("password"),
|
||||
Confirmation: r.FormValue("confirmation"),
|
||||
Theme: r.FormValue("theme"),
|
||||
Language: r.FormValue("language"),
|
||||
Timezone: r.FormValue("timezone"),
|
||||
EntryDirection: r.FormValue("entry_direction"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ create table feeds (
|
|||
foreign key (category_id) references categories(id) on delete cascade
|
||||
);
|
||||
|
||||
create type entry_status as enum ('unread', 'read', 'removed');
|
||||
create type entry_status as enum('unread', 'read', 'removed');
|
||||
|
||||
create table entries (
|
||||
id bigserial not null,
|
||||
|
|
2
sql/schema_version_4.sql
Normal file
2
sql/schema_version_4.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
create type entry_sorting_direction as enum('asc', 'desc');
|
||||
alter table users add column entry_direction entry_sorting_direction default 'asc';
|
10
sql/sql.go
10
sql/sql.go
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2017-12-01 21:46:13.639273113 -0800 PST m=+0.002204900
|
||||
// 2017-12-02 16:12:47.256865279 -0800 PST m=+0.002374595
|
||||
|
||||
package sql
|
||||
|
||||
|
@ -59,7 +59,7 @@ create table feeds (
|
|||
foreign key (category_id) references categories(id) on delete cascade
|
||||
);
|
||||
|
||||
create type entry_status as enum ('unread', 'read', 'removed');
|
||||
create type entry_status as enum('unread', 'read', 'removed');
|
||||
|
||||
create table entries (
|
||||
id bigserial not null,
|
||||
|
@ -118,10 +118,14 @@ create index users_extra_idx on users using gin(extra);
|
|||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id, value)
|
||||
);`,
|
||||
"schema_version_4": `create type entry_sorting_direction as enum('asc', 'desc');
|
||||
alter table users add column entry_direction entry_sorting_direction default 'asc';
|
||||
`,
|
||||
}
|
||||
|
||||
var SqlMapChecksums = map[string]string{
|
||||
"schema_version_1": "cb85ca7dd97a6e1348e00b65ea004253a7165bed9a772746613276e47ef93213",
|
||||
"schema_version_1": "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
|
||||
"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
|
||||
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
|
||||
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
||||
}
|
||||
|
|
|
@ -27,15 +27,6 @@ type EntryQueryBuilder struct {
|
|||
limit int
|
||||
offset int
|
||||
entryID int64
|
||||
conditions []string
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
// WithCondition defines a new condition.
|
||||
func (e *EntryQueryBuilder) WithCondition(column, operator string, value interface{}) *EntryQueryBuilder {
|
||||
e.args = append(e.args, value)
|
||||
e.conditions = append(e.conditions, fmt.Sprintf("%s %s $%d", column, operator, len(e.args)+1))
|
||||
return e
|
||||
}
|
||||
|
||||
// WithEntryID set the entryID.
|
||||
|
@ -187,7 +178,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
|||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to fetch entry row: %v", err)
|
||||
return nil, fmt.Errorf("unable to fetch entry row: %v", err)
|
||||
}
|
||||
|
||||
if iconID == nil {
|
||||
|
@ -208,11 +199,6 @@ func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
|
|||
args := []interface{}{e.userID}
|
||||
conditions := []string{"e.user_id = $1"}
|
||||
|
||||
if len(e.conditions) > 0 {
|
||||
conditions = append(conditions, e.conditions...)
|
||||
args = append(args, e.args...)
|
||||
}
|
||||
|
||||
if e.categoryID != 0 {
|
||||
conditions = append(conditions, fmt.Sprintf("f.category_id=$%d", len(args)+1))
|
||||
args = append(args, e.categoryID)
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/miniflux/miniflux2/sql"
|
||||
)
|
||||
|
||||
const schemaVersion = 3
|
||||
const schemaVersion = 4
|
||||
|
||||
// Migrate run database migrations.
|
||||
func (s *Storage) Migrate() {
|
||||
|
|
108
storage/user.go
108
storage/user.go
|
@ -8,6 +8,7 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -110,23 +111,59 @@ func (s *Storage) RemoveExtraField(userID int64, field string) error {
|
|||
|
||||
// UpdateUser updates a user.
|
||||
func (s *Storage) UpdateUser(user *model.User) error {
|
||||
defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UpdateUser] username=%s", user.Username))
|
||||
user.Username = strings.ToLower(user.Username)
|
||||
|
||||
defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UpdateUser] userID=%d", user.ID))
|
||||
log.Println(user.EntryDirection)
|
||||
if user.Password != "" {
|
||||
hashedPassword, err := hashPassword(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "UPDATE users SET username=$1, password=$2, is_admin=$3, theme=$4, language=$5, timezone=$6 WHERE id=$7"
|
||||
_, err = s.db.Exec(query, user.Username, hashedPassword, user.IsAdmin, user.Theme, user.Language, user.Timezone, user.ID)
|
||||
query := `UPDATE users SET
|
||||
username=LOWER($1),
|
||||
password=$2,
|
||||
is_admin=$3,
|
||||
theme=$4,
|
||||
language=$5,
|
||||
timezone=$6,
|
||||
entry_direction=$7
|
||||
WHERE id=$8`
|
||||
|
||||
_, err = s.db.Exec(
|
||||
query,
|
||||
user.Username,
|
||||
hashedPassword,
|
||||
user.IsAdmin,
|
||||
user.Theme,
|
||||
user.Language,
|
||||
user.Timezone,
|
||||
user.EntryDirection,
|
||||
user.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
} else {
|
||||
query := "UPDATE users SET username=$1, is_admin=$2, theme=$3, language=$4, timezone=$5 WHERE id=$6"
|
||||
_, err := s.db.Exec(query, user.Username, user.IsAdmin, user.Theme, user.Language, user.Timezone, user.ID)
|
||||
query := `UPDATE users SET
|
||||
username=$1,
|
||||
is_admin=$2,
|
||||
theme=$3,
|
||||
language=$4,
|
||||
timezone=$5,
|
||||
entry_direction=$6
|
||||
WHERE id=$7`
|
||||
|
||||
_, err := s.db.Exec(
|
||||
query,
|
||||
user.Username,
|
||||
user.IsAdmin,
|
||||
user.Theme,
|
||||
user.Language,
|
||||
user.Timezone,
|
||||
user.EntryDirection,
|
||||
user.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
|
@ -138,11 +175,24 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
|||
// UserByID finds a user by the ID.
|
||||
func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
||||
defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByID] userID=%d", userID))
|
||||
query := `SELECT
|
||||
id, username, is_admin, theme, language, timezone, entry_direction, extra
|
||||
FROM users
|
||||
WHERE id = $1`
|
||||
|
||||
var user model.User
|
||||
var extra hstore.Hstore
|
||||
row := s.db.QueryRow("SELECT id, username, is_admin, theme, language, timezone, extra FROM users WHERE id = $1", userID)
|
||||
err := row.Scan(&user.ID, &user.Username, &user.IsAdmin, &user.Theme, &user.Language, &user.Timezone, &extra)
|
||||
row := s.db.QueryRow(query, userID)
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.IsAdmin,
|
||||
&user.Theme,
|
||||
&user.Language,
|
||||
&user.Timezone,
|
||||
&user.EntryDirection,
|
||||
&extra,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
|
@ -162,10 +212,22 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
|||
// UserByUsername finds a user by the username.
|
||||
func (s *Storage) UserByUsername(username string) (*model.User, error) {
|
||||
defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByUsername] username=%s", username))
|
||||
query := `SELECT
|
||||
id, username, is_admin, theme, language, timezone, entry_direction
|
||||
FROM users
|
||||
WHERE username=LOWER($1)`
|
||||
|
||||
var user model.User
|
||||
row := s.db.QueryRow("SELECT id, username, is_admin, theme, language, timezone FROM users WHERE username=$1", username)
|
||||
err := row.Scan(&user.ID, &user.Username, &user.IsAdmin, &user.Theme, &user.Language, &user.Timezone)
|
||||
row := s.db.QueryRow(query, username)
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.IsAdmin,
|
||||
&user.Theme,
|
||||
&user.Language,
|
||||
&user.Timezone,
|
||||
&user.EntryDirection,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
|
@ -178,10 +240,22 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
|
|||
// UserByExtraField finds a user by an extra field value.
|
||||
func (s *Storage) UserByExtraField(field, value string) (*model.User, error) {
|
||||
defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByExtraField] field=%s", field))
|
||||
query := `SELECT
|
||||
id, username, is_admin, theme, language, timezone, entry_direction
|
||||
FROM users
|
||||
WHERE extra->$1=$2`
|
||||
|
||||
var user model.User
|
||||
query := `SELECT id, username, is_admin, theme, language, timezone FROM users WHERE extra->$1=$2`
|
||||
row := s.db.QueryRow(query, field, value)
|
||||
err := row.Scan(&user.ID, &user.Username, &user.IsAdmin, &user.Theme, &user.Language, &user.Timezone)
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.IsAdmin,
|
||||
&user.Theme,
|
||||
&user.Language,
|
||||
&user.Timezone,
|
||||
&user.EntryDirection,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
|
@ -215,14 +289,18 @@ func (s *Storage) RemoveUser(userID int64) error {
|
|||
// Users returns all users.
|
||||
func (s *Storage) Users() (model.Users, error) {
|
||||
defer helper.ExecutionTime(time.Now(), "[Storage:Users]")
|
||||
query := `SELECT
|
||||
id, username, is_admin, theme, language, timezone, last_login_at
|
||||
FROM users
|
||||
ORDER BY username ASC`
|
||||
|
||||
var users model.Users
|
||||
rows, err := s.db.Query("SELECT id, username, is_admin, theme, language, timezone, last_login_at FROM users ORDER BY username ASC")
|
||||
rows, err := s.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch users: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users model.Users
|
||||
for rows.Next() {
|
||||
var user model.User
|
||||
err := rows.Scan(
|
||||
|
|
Loading…
Reference in a new issue