From 2f1367a8d4c33e7c6ba459cfc6756e079c7a1af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Dec 2017 17:04:01 -0800 Subject: [PATCH] Make entries sorting configurable --- README.md | 9 +- locale/translations.go | 9 +- locale/translations/fr_FR.json | 5 +- model/entry.go | 6 +- model/entry_test.go | 6 +- model/token.go | 6 + model/user.go | 19 +-- server/static/bin.go | 2 +- server/static/css.go | 6 +- server/static/css/common.css | 2 +- server/static/js.go | 2 +- server/template/common.go | 8 +- server/template/html/common/layout.html | 4 +- server/template/html/settings.html | 6 + server/template/views.go | 10 +- server/ui/controller/category.go | 2 +- server/ui/controller/entry.go | 160 +++++++++--------------- server/ui/controller/feed.go | 2 +- server/ui/controller/history.go | 2 +- server/ui/controller/settings.go | 9 +- server/ui/controller/unread.go | 2 +- server/ui/form/settings.go | 29 +++-- sql/schema_version_1.sql | 2 +- sql/schema_version_4.sql | 2 + sql/sql.go | 10 +- storage/entry_query_builder.go | 16 +-- storage/migration.go | 2 +- storage/user.go | 108 +++++++++++++--- 28 files changed, 253 insertions(+), 193 deletions(-) create mode 100644 sql/schema_version_4.sql diff --git a/README.md b/README.md index 807042dd..9926dae4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/locale/translations.go b/locale/translations.go index b9164fa1..95530182 100644 --- a/locale/translations.go +++ b/locale/translations.go @@ -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", } diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index 3b774cfb..08f07292 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -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" } diff --git a/model/entry.go b/model/entry.go index 79a3bcbb..f9a9638a 100644 --- a/model/entry.go +++ b/model/entry.go @@ -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" } diff --git a/model/entry_test.go b/model/entry_test.go index 2f8c25de..8b92d3f0 100644 --- a/model/entry_test.go +++ b/model/entry_test.go @@ -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"`) } } diff --git a/model/token.go b/model/token.go index 5626a77d..3c5c3232 100644 --- a/model/token.go +++ b/model/token.go @@ -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) +} diff --git a/model/user.go b/model/user.go index c030d524..4889dc4a 100644 --- a/model/user.go +++ b/model/user.go @@ -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. diff --git a/server/static/bin.go b/server/static/bin.go index 9987f8aa..21cd59fb 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-27 21:07:53.21170439 -0800 PST m=+0.005890618 +// 2017-12-02 16:12:47.261744369 -0800 PST m=+0.007253685 package static diff --git a/server/static/css.go b/server/static/css.go index e338124d..d30f2758 100644 --- a/server/static/css.go +++ b/server/static/css.go @@ -1,14 +1,14 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-11-29 21:35:36.026428194 -0800 PST m=+0.009240880 +// 2017-12-02 16:12:47.265273764 -0800 PST m=+0.010783080 package static var Stylesheets = map[string]string{ "black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555}.unread-counter{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-enclosure{border-color:#333}`, - "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#f0f0f0;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`, + "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#f0f0f0;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`, } var StylesheetsChecksums = map[string]string{ "black": "38e7fee92187a036ce37f3c15fde2deff59a55c5ab693c7b8578af79d6a117d2", - "common": "c0c71c5451d0d22d2826b0525c33f37e54a8f20d826eb182aac0b5636019056c", + "common": "921e622bf833cb8a843766b4bf71263e4cf8cc4a0c8678692b363a2d3b44f465", } diff --git a/server/static/css/common.css b/server/static/css/common.css index 23dd897d..411ab8f5 100644 --- a/server/static/css/common.css +++ b/server/static/css/common.css @@ -10,7 +10,7 @@ body { text-rendering: optimizeLegibility; } -.main { +main { padding-left: 5px; padding-right: 5px; } diff --git a/server/static/js.go b/server/static/js.go index 27bf3616..a0c535a7 100644 --- a/server/static/js.go +++ b/server/static/js.go @@ -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 diff --git a/server/template/common.go b/server/template/common.go index 79aae944..461f8e5e 100644 --- a/server/template/common.go +++ b/server/template/common.go @@ -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{ {{ end }} -
+
{{template "content" .}} -
+ {{ end }}`, @@ -106,6 +106,6 @@ var templateCommonMap = map[string]string{ var templateCommonMapChecksums = map[string]string{ "entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27", - "layout": "0a06f790d6caad2918c5038f7aa4a2f88ff3b31ed8f52749d45344f2be7bee53", + "layout": "d1f96640bf90eca64571cfa4fe73be55b09d1d5a49da85b1ea9f9d4f9c670a07", "pagination": "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924", } diff --git a/server/template/html/common/layout.html b/server/template/html/common/layout.html index 5fa87146..fcc89676 100644 --- a/server/template/html/common/layout.html +++ b/server/template/html/common/layout.html @@ -51,9 +51,9 @@ {{ end }} -
+
{{template "content" .}} -
+ {{ end }} \ No newline at end of file diff --git a/server/template/html/settings.html b/server/template/html/settings.html index 23a5f42e..8e66a104 100644 --- a/server/template/html/settings.html +++ b/server/template/html/settings.html @@ -58,6 +58,12 @@ {{ end }} + + +
diff --git a/server/template/views.go b/server/template/views.go index 400f3d96..bc780759 100644 --- a/server/template/views.go +++ b/server/template/views.go @@ -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 }} + + +
@@ -1070,7 +1076,7 @@ var templateViewsMapChecksums = map[string]string{ "integrations": "c485d6d9ed996635e55e73320610e6bcb01a41c1153e8e739ae2294b0b14b243", "login": "04f3ce79bfa5753f69e0d956c2a8999c0da549c7925634a3e8134975da0b0e0f", "sessions": "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf", - "settings": "1e2df11f5436eb2d05ae1fae30dd6f1362613011edbfcc79ae8b23854fa348b4", + "settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9", "unread": "3d8deab9119dc11f0d74a461e1ac89dc29931ba4645a043bb5b3eccba3cba5b8", "users": "44677e28bb5347799ed0020c90ec785aadec4b1454446d92411cfdaf6e32110b", } diff --git a/server/ui/controller/category.go b/server/ui/controller/category.go index b718d0d7..75b7f90d 100644 --- a/server/ui/controller/category.go +++ b/server/ui/controller/category.go @@ -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) diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go index 7915af5b..933f0c63 100644 --- a/server/ui/controller/entry.go +++ b/server/ui/controller/entry.go @@ -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 +} diff --git a/server/ui/controller/feed.go b/server/ui/controller/feed.go index 85a94cf5..eeb66c41 100644 --- a/server/ui/controller/feed.go +++ b/server/ui/controller/feed.go @@ -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) diff --git a/server/ui/controller/history.go b/server/ui/controller/history.go index 9b4e3163..10525178 100644 --- a/server/ui/controller/history.go +++ b/server/ui/controller/history.go @@ -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) diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go index e8ab20ba..a951e5df 100644 --- a/server/ui/controller/settings.go +++ b/server/ui/controller/settings.go @@ -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 diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go index a114250e..e0f120a0 100644 --- a/server/ui/controller/unread.go +++ b/server/ui/controller/unread.go @@ -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) diff --git a/server/ui/form/settings.go b/server/ui/form/settings.go index 3d37a0fc..2ebaa6de 100644 --- a/server/ui/form/settings.go +++ b/server/ui/form/settings.go @@ -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"), } } diff --git a/sql/schema_version_1.sql b/sql/schema_version_1.sql index e32a38c7..05f8f34d 100644 --- a/sql/schema_version_1.sql +++ b/sql/schema_version_1.sql @@ -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, diff --git a/sql/schema_version_4.sql b/sql/schema_version_4.sql new file mode 100644 index 00000000..988c4865 --- /dev/null +++ b/sql/schema_version_4.sql @@ -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'; diff --git a/sql/sql.go b/sql/sql.go index 1f6b597b..1a0da74c 100644 --- a/sql/sql.go +++ b/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", } diff --git a/storage/entry_query_builder.go b/storage/entry_query_builder.go index 2c2f2707..6f7f4bd8 100644 --- a/storage/entry_query_builder.go +++ b/storage/entry_query_builder.go @@ -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) diff --git a/storage/migration.go b/storage/migration.go index 5060a34b..9cfde8bf 100644 --- a/storage/migration.go +++ b/storage/migration.go @@ -12,7 +12,7 @@ import ( "github.com/miniflux/miniflux2/sql" ) -const schemaVersion = 3 +const schemaVersion = 4 // Migrate run database migrations. func (s *Storage) Migrate() { diff --git a/storage/user.go b/storage/user.go index fdbbfda5..7d296a96 100644 --- a/storage/user.go +++ b/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(