diff --git a/locale/translations.go b/locale/translations.go index 7b096be7..5ff72a37 100644 --- a/locale/translations.go +++ b/locale/translations.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-12-22 11:25:01.98320223 -0800 PST m=+0.048169992 +// 2017-12-24 14:32:38.883179987 -0800 PST m=+0.040204882 package locale diff --git a/model/feed.go b/model/feed.go index 8d15d7ad..c2ee6e8e 100644 --- a/model/feed.go +++ b/model/feed.go @@ -6,7 +6,6 @@ package model import ( "fmt" - "reflect" "time" ) @@ -17,17 +16,17 @@ type Feed struct { FeedURL string `json:"feed_url"` SiteURL string `json:"site_url"` Title string `json:"title"` - CheckedAt time.Time `json:"checked_at,omitempty"` - EtagHeader string `json:"etag_header,omitempty"` - LastModifiedHeader string `json:"last_modified_header,omitempty"` - ParsingErrorMsg string `json:"parsing_error_message,omitempty"` - ParsingErrorCount int `json:"parsing_error_count,omitempty"` + CheckedAt time.Time `json:"checked_at"` + EtagHeader string `json:"etag_header"` + LastModifiedHeader string `json:"last_modified_header"` + ParsingErrorMsg string `json:"parsing_error_message"` + ParsingErrorCount int `json:"parsing_error_count"` ScraperRules string `json:"scraper_rules"` RewriteRules string `json:"rewrite_rules"` Crawler bool `json:"crawler"` Category *Category `json:"category,omitempty"` Entries Entries `json:"entries,omitempty"` - Icon *FeedIcon `json:"icon,omitempty"` + Icon *FeedIcon `json:"icon"` } func (f *Feed) String() string { @@ -41,27 +40,34 @@ func (f *Feed) String() string { ) } -// Merge combine src to the current struct -func (f *Feed) Merge(src *Feed) { - src.ID = f.ID - src.UserID = f.UserID +// Merge combine override to the current struct +func (f *Feed) Merge(override *Feed) { + if override.Title != "" && override.Title != f.Title { + f.Title = override.Title + } - new := reflect.ValueOf(src).Elem() - for i := 0; i < new.NumField(); i++ { - field := new.Field(i) + if override.SiteURL != "" && override.SiteURL != f.SiteURL { + f.SiteURL = override.SiteURL + } - switch field.Interface().(type) { - case int64: - value := field.Int() - if value != 0 { - reflect.ValueOf(f).Elem().Field(i).SetInt(value) - } - case string: - value := field.String() - if value != "" { - reflect.ValueOf(f).Elem().Field(i).SetString(value) - } - } + if override.FeedURL != "" && override.FeedURL != f.FeedURL { + f.FeedURL = override.FeedURL + } + + if override.ScraperRules != "" && override.ScraperRules != f.ScraperRules { + f.ScraperRules = override.ScraperRules + } + + if override.RewriteRules != "" && override.RewriteRules != f.RewriteRules { + f.RewriteRules = override.RewriteRules + } + + if override.Crawler != f.Crawler { + f.Crawler = override.Crawler + } + + if override.Category != nil && override.Category.ID != 0 && override.Category.ID != f.Category.ID { + f.Category.ID = override.Category.ID } } diff --git a/model/feed_test.go b/model/feed_test.go new file mode 100644 index 00000000..138949df --- /dev/null +++ b/model/feed_test.go @@ -0,0 +1,59 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package model + +import "testing" + +func TestMergeFeedTitle(t *testing.T) { + feed1 := &Feed{Title: "Feed 1"} + feed2 := &Feed{Title: "Feed 2"} + feed1.Merge(feed2) + + if feed1.Title != "Feed 2" { + t.Fatal(`The title of feed1 should be merged`) + } + + feed1 = &Feed{Title: "Feed 1"} + feed2 = &Feed{} + feed1.Merge(feed2) + + if feed1.Title != "Feed 1" { + t.Fatal(`The title of feed1 should not be merged`) + } + + feed1 = &Feed{Title: "Feed 1"} + feed2 = &Feed{Title: "Feed 1"} + feed1.Merge(feed2) + + if feed1.Title != "Feed 1" { + t.Fatal(`The title of feed1 should not be changed`) + } +} + +func TestMergeFeedCategory(t *testing.T) { + feed1 := &Feed{Category: &Category{ID: 222}} + feed2 := &Feed{Category: &Category{ID: 333}} + feed1.Merge(feed2) + + if feed1.Category.ID != 333 { + t.Fatal(`The category of feed1 should be merged`) + } + + feed1 = &Feed{Category: &Category{ID: 222}} + feed2 = &Feed{} + feed1.Merge(feed2) + + if feed1.Category.ID != 222 { + t.Fatal(`The category of feed1 should not be merged`) + } + + feed1 = &Feed{Category: &Category{ID: 222}} + feed2 = &Feed{Category: &Category{ID: 0}} + feed1.Merge(feed2) + + if feed1.Category.ID != 222 { + t.Fatal(`The category of feed1 should not be merged`) + } +} diff --git a/model/theme.go b/model/theme.go index 6b87ec84..f4990e59 100644 --- a/model/theme.go +++ b/model/theme.go @@ -22,5 +22,5 @@ func ValidateTheme(theme string) error { } } - return errors.NewLocalizedError("Invalid theme.") + return errors.NewLocalizedError("Invalid theme") } diff --git a/model/user.go b/model/user.go index 4889dc4a..8026a368 100644 --- a/model/user.go +++ b/model/user.go @@ -37,21 +37,17 @@ func (u User) ValidateUserCreation() error { return u.ValidatePassword() } -// ValidateUserModification validates user for modification. +// ValidateUserModification validates user modification payload. func (u User) ValidateUserModification() error { - if u.ID <= 0 { - return errors.New("The ID is mandatory") + if u.Theme != "" { + return ValidateTheme(u.Theme) } - if u.Username == "" { - return errors.New("The username is mandatory") + if u.Password != "" { + return u.ValidatePassword() } - if err := u.ValidatePassword(); err != nil { - return err - } - - return ValidateTheme(u.Theme) + return nil } // ValidateUserLogin validates user credential requirements. @@ -78,11 +74,11 @@ func (u User) ValidatePassword() error { // Merge update the current user with another user. func (u *User) Merge(override *User) { - if u.Username != override.Username { + if override.Username != "" && u.Username != override.Username { u.Username = override.Username } - if u.Password != override.Password { + if override.Password != "" && u.Password != override.Password { u.Password = override.Password } @@ -90,15 +86,15 @@ func (u *User) Merge(override *User) { u.IsAdmin = override.IsAdmin } - if u.Theme != override.Theme { + if override.Theme != "" && u.Theme != override.Theme { u.Theme = override.Theme } - if u.Language != override.Language { + if override.Language != "" && u.Language != override.Language { u.Language = override.Language } - if u.Timezone != override.Timezone { + if override.Timezone != "" && u.Timezone != override.Timezone { u.Timezone = override.Timezone } } diff --git a/model/user_test.go b/model/user_test.go index 769696fa..ac075a83 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -35,42 +35,59 @@ func TestValidateUserCreation(t *testing.T) { func TestValidateUserModification(t *testing.T) { user := &User{} - if err := user.ValidateUserModification(); err == nil { - t.Error(`An empty user should generate an error`) - } - - user = &User{ID: 42, Username: "test", Password: "", Theme: "default"} if err := user.ValidateUserModification(); err != nil { - t.Error(`User without password should not generate an error`) + t.Error(`There is no changes, so we should not have an error`) } - user = &User{ID: 42, Username: "test", Password: "a", Theme: "default"} - if err := user.ValidateUserModification(); err == nil { - t.Error(`Passwords shorter than 6 characters should generate an error`) + user = &User{Theme: "default"} + if err := user.ValidateUserModification(); err != nil { + t.Error(`A valid theme should not generate any errors`) } - user = &User{ID: 42, Username: "", Password: "secret", Theme: "default"} - if err := user.ValidateUserModification(); err == nil { - t.Error(`An empty username should generate an error`) - } - - user = &User{ID: -1, Username: "test", Password: "secret", Theme: "default"} - if err := user.ValidateUserModification(); err == nil { - t.Error(`An invalid userID should generate an error`) - } - - user = &User{ID: 0, Username: "test", Password: "secret", Theme: "default"} - if err := user.ValidateUserModification(); err == nil { - t.Error(`An invalid userID should generate an error`) - } - - user = &User{ID: 42, Username: "test", Password: "secret", Theme: "invalid"} + user = &User{Theme: "invalid theme"} if err := user.ValidateUserModification(); err == nil { t.Error(`An invalid theme should generate an error`) } - user = &User{ID: 42, Username: "test", Password: "secret", Theme: "default"} + user = &User{Password: "test123"} if err := user.ValidateUserModification(); err != nil { - t.Error(`A valid user should not generate any error`) + t.Error(`A valid password should not generate any errors`) + } + + user = &User{Password: "a"} + if err := user.ValidateUserModification(); err == nil { + t.Error(`An invalid password should generate an error`) + } +} + +func TestMergeUsername(t *testing.T) { + user1 := &User{ID: 42, Username: "user1", Password: "secret", Theme: "default"} + user2 := &User{ID: 42, Username: "user2"} + user1.Merge(user2) + + if user1.Username != "user2" { + t.Fatal(`The username should be merged into user1`) + } + + if user1.Theme != "default" { + t.Fatal(`The theme should not be merged into user1`) + } +} + +func TestMergeIsAdmin(t *testing.T) { + user1 := &User{ID: 42, Username: "user1", Password: "secret", Theme: "default"} + user2 := &User{ID: 42, IsAdmin: true} + user1.Merge(user2) + + if !user1.IsAdmin { + t.Fatal(`The is_admin flag should be merged into user1`) + } + + user1 = &User{ID: 42, Username: "user1", Password: "secret", Theme: "default"} + user2 = &User{ID: 42} + user1.Merge(user2) + + if user1.IsAdmin { + t.Fatal(`The is_admin flag should not be merged into user1`) } } diff --git a/server/api/controller/category.go b/server/api/controller/category.go index 13a7438e..d7b2922b 100644 --- a/server/api/controller/category.go +++ b/server/api/controller/category.go @@ -13,15 +13,21 @@ import ( // CreateCategory is the API handler to create a new category. func (c *Controller) CreateCategory(ctx *core.Context, request *core.Request, response *core.Response) { + userID := ctx.UserID() category, err := payload.DecodeCategoryPayload(request.Body()) if err != nil { response.JSON().BadRequest(err) return } - category.UserID = ctx.UserID() + category.UserID = userID if err := category.ValidateCategoryCreation(); err != nil { - response.JSON().ServerError(err) + response.JSON().BadRequest(err) + return + } + + if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil { + response.JSON().BadRequest(errors.New("This category already exists")) return } diff --git a/server/api/controller/feed.go b/server/api/controller/feed.go index f0212c97..fcaeee78 100644 --- a/server/api/controller/feed.go +++ b/server/api/controller/feed.go @@ -20,6 +20,26 @@ func (c *Controller) CreateFeed(ctx *core.Context, request *core.Request, respon return } + if feedURL == "" { + response.JSON().BadRequest(errors.New("The feed_url is required")) + return + } + + if categoryID <= 0 { + response.JSON().BadRequest(errors.New("The category_id is required")) + return + } + + if c.store.FeedURLExists(userID, feedURL) { + response.JSON().BadRequest(errors.New("This feed_url already exists")) + return + } + + if !c.store.CategoryExists(userID, categoryID) { + response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user")) + return + } + feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false) if err != nil { response.JSON().ServerError(errors.New("Unable to create this feed")) @@ -42,6 +62,11 @@ func (c *Controller) RefreshFeed(ctx *core.Context, request *core.Request, respo return } + if !c.store.FeedExists(userID, feedID) { + response.JSON().NotFound(errors.New("Unable to find this feed")) + return + } + err = c.feedHandler.RefreshFeed(userID, feedID) if err != nil { response.JSON().ServerError(errors.New("Unable to refresh this feed")) @@ -66,6 +91,11 @@ func (c *Controller) UpdateFeed(ctx *core.Context, request *core.Request, respon return } + if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) { + response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user")) + return + } + originalFeed, err := c.store.FeedByID(userID, feedID) if err != nil { response.JSON().NotFound(errors.New("Unable to find this feed")) @@ -83,6 +113,12 @@ func (c *Controller) UpdateFeed(ctx *core.Context, request *core.Request, respon return } + originalFeed, err = c.store.FeedByID(userID, feedID) + if err != nil { + response.JSON().ServerError(errors.New("Unable to fetch this feed")) + return + } + response.JSON().Created(originalFeed) } diff --git a/server/template/common.go b/server/template/common.go index a7202c67..7ef71be6 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-22 20:01:12.434799064 -0800 PST m=+0.047000060 +// 2017-12-24 14:32:38.881953886 -0800 PST m=+0.038978781 package template diff --git a/server/template/html/feeds.html b/server/template/html/feeds.html index f84e59f4..04d0bd2d 100644 --- a/server/template/html/feeds.html +++ b/server/template/html/feeds.html @@ -27,7 +27,7 @@
- {{ if ne .Icon.IconID 0 }} + {{ if .Icon }} {{ end }} {{ .Title }} diff --git a/server/template/views.go b/server/template/views.go index 86f28a9d..a33c2162 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-22 16:02:42.156291192 -0800 PST m=+0.011241908 +// 2017-12-24 14:32:38.873134292 -0800 PST m=+0.030159187 package template @@ -699,7 +699,7 @@ var templateViewsMap = map[string]string{
- {{ if ne .Icon.IconID 0 }} + {{ if .Icon }} {{ end }} {{ .Title }} @@ -1332,7 +1332,7 @@ var templateViewsMapChecksums = map[string]string{ "edit_user": "82d9749d76ddbd2352816d813c4b1f6d92f2222de678b4afe5821090246735c7", "entry": "6b4405e0c8e4a7d31874659f8835f4e43e01dc3c20686091517ac750196dd70f", "feed_entries": "ac93cb9a90f93ddd9dd8a67d7e160592ecb9f5e465ee9679bb14eecd8d4caf20", - "feeds": "c22af39b42ba9ca69ea0914ca789303ec2c5b484abcd4eaa49016e365381257c", + "feeds": "2f04664f9daf1ea0259bccd7d5ab4c0917d3208f1dcdc89620ffa9fa3094efd2", "history": "abc7ea29f7d54f28f73fe14979bbd03dbc41fa6a7c86f95f56d6e94f7b09b9ba", "import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f", "integrations": "3c14d7de904911aad7f3ebec6d1a20b50843287f58125c526e167f429f3d455d", diff --git a/sql/sql.go b/sql/sql.go index 9b84d97d..f098915c 100644 --- a/sql/sql.go +++ b/sql/sql.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2017-12-22 16:35:30.206583656 -0800 PST m=+0.002482805 +// 2017-12-24 14:32:38.84708161 -0800 PST m=+0.004106505 package sql diff --git a/storage/entry.go b/storage/entry.go index 8a12f5ec..7f3eaf4b 100644 --- a/storage/entry.go +++ b/storage/entry.go @@ -21,8 +21,8 @@ func (s *Storage) GetEntryQueryBuilder(userID int64, timezone string) *EntryQuer return NewEntryQueryBuilder(s, userID, timezone) } -// CreateEntry add a new entry. -func (s *Storage) CreateEntry(entry *model.Entry) error { +// createEntry add a new entry. +func (s *Storage) createEntry(entry *model.Entry) error { query := ` INSERT INTO entries (title, hash, url, published_at, content, author, user_id, feed_id) @@ -76,8 +76,8 @@ func (s *Storage) UpdateEntryContent(entry *model.Entry) error { return err } -// UpdateEntry update an entry when a feed is refreshed. -func (s *Storage) UpdateEntry(entry *model.Entry) error { +// updateEntry update an entry when a feed is refreshed. +func (s *Storage) updateEntry(entry *model.Entry) error { query := ` UPDATE entries SET title=$1, url=$2, published_at=$3, content=$4, author=$5 @@ -108,8 +108,8 @@ func (s *Storage) UpdateEntry(entry *model.Entry) error { return s.UpdateEnclosures(entry.Enclosures) } -// EntryExists checks if an entry already exists based on its hash when refreshing a feed. -func (s *Storage) EntryExists(entry *model.Entry) bool { +// 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` s.db.QueryRow(query, entry.UserID, entry.FeedID, entry.Hash).Scan(&result) @@ -123,10 +123,10 @@ func (s *Storage) UpdateEntries(userID, feedID int64, entries model.Entries) (er entry.UserID = userID entry.FeedID = feedID - if s.EntryExists(entry) { - err = s.UpdateEntry(entry) + if s.entryExists(entry) { + err = s.updateEntry(entry) } else { - err = s.CreateEntry(entry) + err = s.createEntry(entry) } if err != nil { @@ -136,15 +136,15 @@ func (s *Storage) UpdateEntries(userID, feedID int64, entries model.Entries) (er entryHashes = append(entryHashes, entry.Hash) } - if err := s.CleanupEntries(feedID, entryHashes); err != nil { + if err := s.cleanupEntries(feedID, entryHashes); err != nil { logger.Error("[Storage:CleanupEntries] %v", err) } return nil } -// CleanupEntries deletes from the database entries marked as "removed" and not visible anymore in the feed. -func (s *Storage) CleanupEntries(feedID int64, entryHashes []string) error { +// cleanupEntries deletes from the database entries marked as "removed" and not visible anymore in the feed. +func (s *Storage) cleanupEntries(feedID int64, entryHashes []string) error { query := ` DELETE FROM entries WHERE feed_id=$1 AND @@ -184,9 +184,18 @@ func (s *Storage) ToggleBookmark(userID int64, entryID int64) error { defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:ToggleBookmark] userID=%d, entryID=%d", userID, entryID)) query := `UPDATE entries SET starred = NOT starred WHERE user_id=$1 AND id=$2` - _, err := s.db.Exec(query, userID, entryID) + result, err := s.db.Exec(query, userID, entryID) if err != nil { - return fmt.Errorf("unable to update toggle bookmark: %v", err) + return fmt.Errorf("unable to toggle bookmark flag: %v", err) + } + + count, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("unable to toogle bookmark flag: %v", err) + } + + if count == 0 { + return errors.New("nothing has been updated") } return nil diff --git a/storage/feed.go b/storage/feed.go index 602bec7b..72210944 100644 --- a/storage/feed.go +++ b/storage/feed.go @@ -70,9 +70,8 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) { for rows.Next() { var feed model.Feed - var iconID, errorMsg interface{} + var iconID interface{} feed.Category = &model.Category{UserID: userID} - feed.Icon = &model.FeedIcon{} err := rows.Scan( &feed.ID, @@ -84,7 +83,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) { &feed.UserID, &feed.CheckedAt, &feed.ParsingErrorCount, - &errorMsg, + &feed.ParsingErrorMsg, &feed.ScraperRules, &feed.RewriteRules, &feed.Crawler, @@ -94,22 +93,13 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) { ) if err != nil { - return nil, fmt.Errorf("Unable to fetch feeds row: %v", err) + return nil, fmt.Errorf("unable to fetch feeds row: %v", err) } - if iconID == nil { - feed.Icon.IconID = 0 - } else { - feed.Icon.IconID = iconID.(int64) + if iconID != nil { + feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)} } - if errorMsg == nil { - feed.ParsingErrorMsg = "" - } else { - feed.ParsingErrorMsg = errorMsg.(string) - } - - feed.Icon.FeedID = feed.ID feeds = append(feeds, &feed) } @@ -121,6 +111,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) { defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:FeedByID] feedID=%d", feedID)) var feed model.Feed + var iconID interface{} feed.Category = &model.Category{UserID: userID} query := ` @@ -128,9 +119,11 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) { f.id, f.feed_url, f.site_url, f.title, f.etag_header, f.last_modified_header, f.user_id, f.checked_at, f.parsing_error_count, f.parsing_error_msg, f.scraper_rules, f.rewrite_rules, f.crawler, - f.category_id, c.title as category_title + f.category_id, c.title as category_title, + fi.icon_id FROM feeds f LEFT JOIN categories c ON c.id=f.category_id + LEFT JOIN feed_icons fi ON fi.feed_id=f.id WHERE f.user_id=$1 AND f.id=$2` err := s.db.QueryRow(query, userID, feedID).Scan( @@ -149,6 +142,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) { &feed.Crawler, &feed.Category.ID, &feed.Category.Title, + &iconID, ) switch { @@ -158,6 +152,10 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) { return nil, fmt.Errorf("unable to fetch feed: %v", err) } + if iconID != nil { + feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)} + } + return &feed, nil } @@ -189,7 +187,7 @@ func (s *Storage) CreateFeed(feed *model.Feed) error { for i := 0; i < len(feed.Entries); i++ { feed.Entries[i].FeedID = feed.ID feed.Entries[i].UserID = feed.UserID - err := s.CreateEntry(feed.Entries[i]) + err := s.createEntry(feed.Entries[i]) if err != nil { return err } diff --git a/storage/user.go b/storage/user.go index 06a50e2c..427b3914 100644 --- a/storage/user.go +++ b/storage/user.go @@ -72,13 +72,14 @@ func (s *Storage) CreateUser(user *model.User) (err error) { (username, password, is_admin, extra) VALUES ($1, $2, $3, $4) - RETURNING id, language, theme, timezone` + RETURNING id, language, theme, timezone, entry_direction` err = s.db.QueryRow(query, strings.ToLower(user.Username), password, user.IsAdmin, extra).Scan( &user.ID, &user.Language, &user.Theme, &user.Timezone, + &user.EntryDirection, ) if err != nil { return fmt.Errorf("unable to create user: %v", err)