Skip to content

Commit

Permalink
Redirect when scrape existing recipe (#463)
Browse files Browse the repository at this point in the history
  • Loading branch information
reaper47 authored Dec 22, 2024
1 parent f3e7ddc commit a3db8bd
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 59 deletions.
14 changes: 10 additions & 4 deletions internal/models/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ func NewReport(reportType ReportType) Report {
}

// NewReportLog creates a new ReportLog from the title and error.
func NewReportLog(title string, err error) ReportLog {
func NewReportLog(title string, isSuccess bool, err error, action string) ReportLog {
var errStr string
if err != nil {
errStr = err.Error()
}

return ReportLog{
Error: errStr,
IsSuccess: err == nil,
IsError: err != nil,
IsSuccess: isSuccess && err == nil,
IsWarning: !isSuccess && err == nil,
Title: title,
Action: action,
}
}

Expand All @@ -42,8 +45,11 @@ type Report struct {

// ReportLog holds information on a report's log.
type ReportLog struct {
Error string
ID int64
IsSuccess bool
Title string
IsError bool
IsSuccess bool
IsWarning bool
Error string
Action string
}
93 changes: 57 additions & 36 deletions internal/server/handlers_recipes.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,12 +579,13 @@ func (s *Server) recipesAddWebsiteHandler() http.HandlerFunc {

go func() {
var (
count atomic.Int64
processed int
progress = make(chan models.Progress)
report = models.NewReport(models.ImportReportType)
total = len(validURLs)
recipeIDs = make([]int64, 0, total)
countSuccess atomic.Int64
countWarning atomic.Int64
processed int
progress = make(chan models.Progress)
report = models.NewReport(models.ImportReportType)
total = len(validURLs)
recipeIDs = make([]int64, 0, total)
)

s.Brokers.SendProgress(fmt.Sprintf("Fetching 1/%d", total), 1, total, userID)
Expand All @@ -598,28 +599,37 @@ func (s *Server) recipesAddWebsiteHandler() http.HandlerFunc {
progress <- models.Progress{Total: total}
}()

rs, err := s.Scraper.Scrape(u, s.Files)
if err != nil {
report.Logs = append(report.Logs, models.ReportLog{Error: err.Error(), Title: u})
return
}

recipe, err := rs.Recipe()
retryAction := "retry"
var ids []int64
recipe, err := s.Repository.RecipeWithSource(u, userID)
if err != nil {
report.Logs = append(report.Logs, models.ReportLog{Error: err.Error(), Title: u})
return
rs, err := s.Scraper.Scrape(u, s.Files)
if err != nil {
report.Logs = append(report.Logs, models.NewReportLog(u, false, err, retryAction))
return
}

recipe, err = rs.Recipe()
if err != nil {
report.Logs = append(report.Logs, models.NewReportLog(u, false, err, retryAction))
return
}

ids, _, err = s.Repository.AddRecipes(models.Recipes{*recipe}, userID, nil)
if err != nil {
report.Logs = append(report.Logs, models.NewReportLog(u, false, err, retryAction))
return
}

report.Logs = append(report.Logs, models.NewReportLog(u, true, err, "/recipes/"+strconv.FormatInt(ids[0], 10)))
countSuccess.Add(1)
} else {
ids = []int64{recipe.ID}
report.Logs = append(report.Logs, models.NewReportLog(u, false, nil, "/recipes/"+strconv.FormatInt(recipe.ID, 10)))
countWarning.Add(1)
}

ids, _, err := s.Repository.AddRecipes(models.Recipes{*recipe}, userID, nil)
if err != nil {
report.Logs = append(report.Logs, models.ReportLog{Error: err.Error(), Title: u})
return
}

report.Logs = append(report.Logs, models.ReportLog{IsSuccess: true, Title: u})
recipeIDs = append(recipeIDs, ids[0])

count.Add(1)
}(rawURL)
}
}()
Expand All @@ -639,21 +649,32 @@ func (s *Server) recipesAddWebsiteHandler() http.HandlerFunc {

var (
toast models.Toast
numSuccess = count.Load()
numSuccess = countSuccess.Load()
numWarning = countWarning.Load()
)

if numSuccess == 0 && total == 1 {
msg := "Fetching the recipe failed."
toast = models.NewErrorToast("Operation Failed", msg, "View /reports?view=latest")
slog.Error(msg, userIDAttr)
} else if numSuccess == 1 && total == 1 {
msg := "Recipe has been added to your collection."
toast = models.NewInfoToast("Operation Successful", msg, fmt.Sprintf("View /recipes/%d", recipeIDs[0]))
slog.Info(msg, userIDAttr, "recipeID", recipeIDs[0])
if total == 1 {
if numWarning == 1 {
recipeID := recipeIDs[0]
viewRecipeLink := fmt.Sprintf("View /recipes/%d", recipeID)
msg := "The recipe exists."
toast = models.NewWarningToast("Operation Warning", msg, viewRecipeLink)
slog.Warn(msg, userIDAttr, "recipeID", recipeID)
} else if numSuccess == 0 {
msg := "Fetching the recipe failed."
toast = models.NewErrorToast("Operation Failed", msg, "View /reports?view=latest")
slog.Error(msg, userIDAttr)
} else if numSuccess == 1 {
recipeID := recipeIDs[0]
viewRecipeLink := fmt.Sprintf("View /recipes/%d", recipeID)
msg := "Recipe has been added to your collection."
toast = models.NewInfoToast("Operation Successful", msg, viewRecipeLink)
slog.Info(msg, userIDAttr, "recipeID", recipeID)
}
} else {
skipped := int64(total) - numSuccess
toast = models.NewInfoToast("Operation Successful", fmt.Sprintf("Fetched %d recipes. %d skipped", numSuccess, skipped), "View /reports?view=latest")
slog.Info("Fetched recipes", userIDAttr, "recipes", recipeIDs, "fetched", numSuccess, "skipped", skipped, "total", total)
numSkipped := int64(total) - (numSuccess + numWarning)
toast = models.NewInfoToast("Operation Successful", fmt.Sprintf("Fetched: %d. Skipped: %d.", numSuccess, numSkipped), "View /reports?view=latest")
slog.Info("Fetched recipes", userIDAttr, "recipes", recipeIDs, "fetched", numSuccess, "skipped", numSkipped, "existing", numWarning, "total", total)
}

s.Brokers.SendToast(toast, userID)
Expand Down
31 changes: 30 additions & 1 deletion internal/server/handlers_recipes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,35 @@ func TestHandlers_Recipes_AddWebsite(t *testing.T) {
}
})

t.Run("add a website that has already been added", func(t *testing.T) {
repo := prepare()
defer func() {
srv.Repository = repo
}()
aURL := "urls=https://www.bestrecipes/brazilian"
sendHxRequestAsLoggedIn(srv, http.MethodPost, uri, formHeader, strings.NewReader(aURL))
readMessage(t, c, 4)

rr := sendHxRequestAsLoggedIn(srv, http.MethodPost, uri, formHeader, strings.NewReader(aURL))

assertStatus(t, rr.Code, http.StatusAccepted)
assertWebsocket(t, c, 4, `{"type":"toast","fileName":"","data":"","toast":{"action":"View /recipes/1","background":"alert-warning","message":"The recipe exists.","title":"Operation Warning"}}`)
wantReportLogs := []models.ReportLog{
{
Title: "https://www.bestrecipes/brazilian",
IsWarning: true,
Action: "/recipes/1",
},
}
if !cmp.Equal(repo.Reports[1][1].Logs, wantReportLogs) {
t.Log(cmp.Diff(repo.Reports[1][1].Logs, wantReportLogs))
t.Fail()
}
if len(repo.RecipesRegistered[1]) != 1 {
t.Fatal("expected 1 recipe")
}
})

t.Run("add many valid URLs from supported websites", func(t *testing.T) {
repo := prepare()
defer func() {
Expand All @@ -572,7 +601,7 @@ func TestHandlers_Recipes_AddWebsite(t *testing.T) {
rr := sendHxRequestAsLoggedIn(srv, http.MethodPost, uri, formHeader, strings.NewReader("urls=https://www.example.com\nhttps://www.hello.com\nhttp://helloiam.bob.com\njesus.com"))

assertStatus(t, rr.Code, http.StatusAccepted)
assertWebsocket(t, c, 6, `{"type":"toast","fileName":"","data":"","toast":{"action":"View /reports?view=latest","background":"alert-info","message":"Fetched 3 recipes. 0 skipped","title":"Operation Successful"}}`)
assertWebsocket(t, c, 6, `{"type":"toast","fileName":"","data":"","toast":{"action":"View /reports?view=latest","background":"alert-info","message":"Fetched: 3. Skipped: 0.","title":"Operation Successful"}}`)
if len(repo.Reports[1]) != 1 {
t.Fatalf("got reports %v but want one report added", repo.Reports[1])
}
Expand Down
4 changes: 4 additions & 0 deletions internal/server/handlers_reports.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/reaper47/recipya/internal/models"
"github.com/reaper47/recipya/internal/templates"
"github.com/reaper47/recipya/web/components"
"log/slog"
"net/http"
"slices"
)
Expand All @@ -19,6 +20,7 @@ func (s *Server) reportsHandler() http.HandlerFunc {
if err != nil {
s.Brokers.SendToast(models.NewErrorDBToast("Failed to fetch reports."), userID)
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to fetch reports.", "error", err)
return
}

Expand All @@ -42,6 +44,7 @@ func (s *Server) reportsHandler() http.HandlerFunc {
if err != nil {
s.Brokers.SendToast(models.NewErrorDBToast("Failed to fetch report."), userID)
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to fetch view reports", "error", err)
return
}
isHighlightFirst = true
Expand Down Expand Up @@ -74,6 +77,7 @@ func (s *Server) reportsReportHandler() http.HandlerFunc {
if err != nil {
s.Brokers.SendToast(models.NewErrorDBToast("Failed to fetch report."), userID)
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to fetch reports", "error", err)
return
}

Expand Down
11 changes: 6 additions & 5 deletions internal/server/handlers_reports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestHandlers_Reports(t *testing.T) {
`<title hx-swap-oob="true">Reports | Recipya</title>`,
`<button class="active" hx-get="/reports?tab=imports" hx-target="#tab-content" hx-push-url="true">Imports</button>`,
`<ul class="col-span-1 border-r overflow-auto max-h-44 border-b md:border-b-0 md:max-h-full dark:border-r-gray-800"><li class="item p-2 hover:bg-slate-200 cursor-default dark:hover:bg-slate-700 bg-slate-200 dark:bg-slate-700" hx-get="/reports/1" hx-target="#report-view-pane" hx-swap="outerHTML" hx-trigger="mousedown" _="on mousedown remove .bg-slate-200 .dark:bg-slate-700 from .item then add .bg-slate-200 .dark:bg-slate-700"><span><b>14 Mar 20 01:06 UTC</b><br><span class="text-sm">Execution time: 3s</span></span> <span class="badge badge-primary float-right select-none">2</span></li><li class="item p-2 hover:bg-slate-200 cursor-default dark:hover:bg-slate-700" hx-get="/reports/2" hx-target="#report-view-pane" hx-swap="outerHTML" hx-trigger="mousedown" _="on mousedown remove .bg-slate-200 .dark:bg-slate-700 from .item then add .bg-slate-200 .dark:bg-slate-700"><span><b>15 Mar 20 04:09 UTC</b><br><span class="text-sm">Execution time: 9s</span></span> <span class="badge badge-primary float-right select-none">1</span></li></ul>`,
`<div id="report-view-pane" class="col-span-3"><div class="overflow-auto h-[77vh] md:h-[89vh]"><table class="table table-xs md:table-md"><thead><tr><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=id-reverse" hx-target="#report-view-pane">ID <span>▾</span></th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=title" hx-target="#report-view-pane">Title</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=success" hx-target="#report-view-pane">Success</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=error" hx-target="#report-view-pane">Error</th></tr></thead> <tbody><tr class="bg-red-200 dark:bg-red-700"><th>1</th><td></td><td>X</td><td>-</td></tr><tr class="bg-red-200 dark:bg-red-700"><th>2</th><td></td><td>X</td><td>-</td></tr></tbody></table></div>`,
`<div id="report-view-pane" class="col-span-3"><div class="overflow-auto h-[77vh] md:h-[89vh]"><table class="table table-xs md:table-md"><thead><tr><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=id-reverse" hx-target="#report-view-pane">ID <span>▾</span></th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=title" hx-target="#report-view-pane">Title</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=success" hx-target="#report-view-pane">Success</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=error" hx-target="#report-view-pane">Error</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700">Action</th></tr></thead> <tbody><tr class=""><th>1</th><td></td><td>X</td><td>-</td><td><button hx-get="" hx-target="#content" hx-push-url="true">View</button></td></tr><tr class=""><th>2</th><td></td><td>X</td><td>-</td><td><button hx-get="" hx-target="#content" hx-push-url="true">View</button></td></tr></tbody></table></div>`,
})
})

Expand Down Expand Up @@ -284,7 +284,7 @@ func TestHandlers_Reports_Report(t *testing.T) {
tab: "imports",
want: []string{
`<div id="report-view-pane" class="col-span-3"><div class="overflow-auto h-[77vh] md:h-[89vh]">`,
`<table class="table table-xs md:table-md"><thead><tr><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=id-reverse" hx-target="#report-view-pane">ID <span>▾</span></th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=title" hx-target="#report-view-pane">Title</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=success" hx-target="#report-view-pane">Success</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=error" hx-target="#report-view-pane">Error</th></tr></thead> <tbody><tr class=""><th>1</th><td>Fried Chicken</td><td>&#x2713;</td><td>-</td></tr><tr class="bg-red-200 dark:bg-red-700"><th>2</th><td>Coq au vin with fries</td><td>X</td><td>Meaning of life not found.</td></tr></tbody></table>`,
`<table class="table table-xs md:table-md"><thead><tr><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=id-reverse" hx-target="#report-view-pane">ID <span>▾</span></th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=title" hx-target="#report-view-pane">Title</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=success" hx-target="#report-view-pane">Success</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=error" hx-target="#report-view-pane">Error</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700">Action</th></tr></thead> <tbody><tr class=""><th>1</th><td>Fried Chicken</td><td>&#x2713;</td><td>-</td><td><button hx-get="/recipes/1" hx-target="#content" hx-push-url="true">View</button></td></tr><tr class="bg-yellow-400 dark:bg-yellow-600"><th>1</th><td>Fried Chicken</td><td>X</td><td>-</td><td><button hx-get="/recipes/1" hx-target="#content" hx-push-url="true">View</button></td></tr><tr class=""><th>2</th><td>Coq au vin with fries</td><td>X</td><td>Meaning of life not found.</td><td><button>Retry (to implement)</button></td></tr></tbody></table>`,
},
},
}
Expand All @@ -298,8 +298,9 @@ func TestHandlers_Reports_Report(t *testing.T) {
CreatedAt: time.Date(2020, 03, 14, 1, 6, 0, 0, time.Local),
ExecTime: 3 * time.Second,
Logs: []models.ReportLog{
{ID: 1, Title: "Fried Chicken", IsSuccess: true},
{ID: 2, Title: "Coq au vin with fries", IsSuccess: false, Error: "Meaning of life not found."},
{ID: 1, Title: "Fried Chicken", IsSuccess: true, Action: "/recipes/1"},
{ID: 1, Title: "Fried Chicken", IsWarning: true, Action: "/recipes/1"},
{ID: 2, Title: "Coq au vin with fries", Error: "Meaning of life not found.", Action: "retry"},
},
},
},
Expand Down Expand Up @@ -347,7 +348,7 @@ func TestHandlers_Reports_Report(t *testing.T) {
body := getBodyHTML(rr)
assertStringsInHTML(t, body, []string{
`<div id="report-view-pane" class="col-span-3"><div class="overflow-auto h-[77vh] md:h-[89vh]">`,
`<table class="table table-xs md:table-md"><thead><tr><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=id-reverse" hx-target="#report-view-pane">ID <span>▾</span></th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=title" hx-target="#report-view-pane">Title</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=success" hx-target="#report-view-pane">Success</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=error" hx-target="#report-view-pane">Error</th></tr></thead> <tbody><tr class=""><th>1</th><td>Fried Chicken</td><td>&#x2713;</td><td>-</td></tr><tr class="bg-red-200 dark:bg-red-700"><th>2</th><td>Coq au vin with fries</td><td>X</td><td>Meaning of life not found.</td></tr></tbody></table>`,
`<table class="table table-xs md:table-md"><thead><tr><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=id-reverse" hx-target="#report-view-pane">ID <span>▾</span></th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=title" hx-target="#report-view-pane">Title</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=success" hx-target="#report-view-pane">Success</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700" hx-get="?sort=error" hx-target="#report-view-pane">Error</th><th class="cursor-default hover:bg-blue-50 dark:hover:bg-blue-700">Action</th></tr></thead> <tbody><tr class=""><th>1</th><td>Fried Chicken</td><td>&#x2713;</td><td>-</td><td><button hx-get="" hx-target="#content" hx-push-url="true">View</button></td></tr><tr class=""><th>2</th><td>Coq au vin with fries</td><td>X</td><td>Meaning of life not found.</td><td><button hx-get="" hx-target="#content" hx-push-url="true">View</button></td></tr></tbody></table>`,
})
assertStringsNotInHTML(t, body, []string{`<p>No report selected. Please select a report to view its content.</p>`})
})
Expand Down
2 changes: 1 addition & 1 deletion internal/server/server_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func readMessage(tb testing.TB, c *websocket.Conn, number int) (websocket.Messag
)

for i := 0; i < number; i++ {
time.Sleep(200 * time.Millisecond)
time.Sleep(1 * time.Second)
mt, data, err = c.Read(ctx)
if err != nil {
tb.Fatalf("failed to read message: %v", err)
Expand Down
17 changes: 17 additions & 0 deletions internal/server/server_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,23 @@ func (m *mockRepository) Recipe(id, userID int64) (*models.Recipe, error) {
return nil, errors.New("recipe not found")
}

func (m *mockRepository) RecipeWithSource(source string, userID int64) (*models.Recipe, error) {
/*if m.RecipeFunc != nil {
return m.RecipeFunc(id, userID)
}*/

if recipes, ok := m.RecipesRegistered[userID]; ok {
idx := slices.IndexFunc(recipes, func(r models.Recipe) bool {
return r.URL == source
})
if idx == -1 {
return nil, errors.New("recipe not found")
}
return &recipes[idx], nil
}
return nil, errors.New("recipe not found")
}

func (m *mockRepository) Recipes(userID int64, opts models.SearchOptionsRecipes) models.Recipes {
if recipes, ok := m.RecipesRegistered[userID]; ok {
return recipes
Expand Down
7 changes: 7 additions & 0 deletions internal/services/migrations/20241222035239_improve_logs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE report_logs ADD COLUMN warning INTEGER NOT NULL DEFAULT 0;
ALTER TABLE report_logs ADD COLUMN action TEXT;

-- +goose Down
ALTER TABLE report_logs DROP COLUMN warning;
ALTER TABLE report_logs DROP COLUMN action;
3 changes: 3 additions & 0 deletions internal/services/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ type RepositoryService interface {
// Recipe gets the user's recipe of the given id.
Recipe(id, userID int64) (*models.Recipe, error)

// RecipeWithSource gets the user's recipe with the given source.
RecipeWithSource(source string, userID int64) (*models.Recipe, error)

// Recipes gets the user's recipes.
Recipes(userID int64, opts models.SearchOptionsRecipes) models.Recipes

Expand Down
Loading

0 comments on commit a3db8bd

Please sign in to comment.