From 3fcf291e6de6bff69e1a37752ed1fdbf7a52a37e Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Sun, 5 May 2024 22:34:05 +0700 Subject: [PATCH] schedule: added history page and version implementation --- api/schedule_history_list.go | 118 ++++++++++++++++++ api/schedule_set.go | 5 +- server/routes/page_home.go | 6 +- server/routes/page_schedule_history.go | 36 +++++- views/components/absolute_time_text.templ | 17 +++ views/schedulehistoriesview/data.go | 46 +++++++ .../schedulehistoriesview/schedulesview.templ | 101 +++++++++++++-- 7 files changed, 314 insertions(+), 15 deletions(-) create mode 100644 api/schedule_history_list.go create mode 100644 views/components/absolute_time_text.templ create mode 100644 views/schedulehistoriesview/data.go diff --git a/api/schedule_history_list.go b/api/schedule_history_list.go new file mode 100644 index 0000000..2f3fdf6 --- /dev/null +++ b/api/schedule_history_list.go @@ -0,0 +1,118 @@ +package api + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/sqlite" + "github.com/stephenafamo/bob/dialect/sqlite/dialect" + "github.com/stephenafamo/bob/dialect/sqlite/sm" + "github.com/tigorlazuardi/redmage/models" + "github.com/tigorlazuardi/redmage/pkg/errs" +) + +type ScheduleHistoryListParams struct { + Subreddit string + After time.Time + Before time.Time + + Limit int64 + Offset int64 + OrderBy string + Sort string +} + +func (params *ScheduleHistoryListParams) FillFromQuery(query Queryable) { + params.Subreddit = query.Get("subreddit") + params.Limit, _ = strconv.ParseInt(query.Get("limit"), 10, 64) + if params.Limit < 1 { + params.Limit = 100 + } + if params.Limit > 1000 { + params.Limit = 1000 + } + + params.Offset, _ = strconv.ParseInt(query.Get("offset"), 10, 64) + if params.Offset < 0 { + params.Offset = 0 + } + + now := time.Now() + + afterInt, _ := strconv.ParseInt(query.Get("after"), 10, 64) + if afterInt > 0 { + params.After = time.Unix(afterInt, 0) + } else if afterInt < 0 { + params.After = now.Add(time.Duration(afterInt) * time.Second) + } + + beforeInt, _ := strconv.ParseInt(query.Get("before"), 10, 64) + if beforeInt > 0 { + params.Before = time.Unix(beforeInt, 0) + } else if beforeInt < 0 { + params.Before = now.Add(time.Duration(beforeInt) * time.Second) + } + + params.OrderBy = query.Get("order_by") + params.Sort = query.Get("sort") +} + +func (params ScheduleHistoryListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) { + if params.Subreddit != "" { + expr = append(expr, models.SelectWhere.ScheduleHistories.Subreddit.EQ(params.Subreddit)) + } + if !params.After.IsZero() { + expr = append(expr, models.SelectWhere.ScheduleHistories.CreatedAt.GTE(params.After.Unix())) + } + if !params.Before.IsZero() { + expr = append(expr, models.SelectWhere.ScheduleHistories.CreatedAt.LTE(params.Before.Unix())) + } + + return expr +} + +func (params ScheduleHistoryListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) { + expr = append(expr, params.CountQuery()...) + if params.Limit > 0 { + expr = append(expr, sm.Limit(params.Limit)) + } + if params.Offset > 0 { + expr = append(expr, sm.Offset(params.Offset)) + } + if params.OrderBy != "" { + if strings.ToLower(params.Sort) == "desc" { + expr = append(expr, sm.OrderBy(sqlite.Quote(params.OrderBy)).Desc()) + } else { + expr = append(expr, sm.OrderBy(sqlite.Quote(params.OrderBy)).Asc()) + } + } else { + expr = append(expr, sm.OrderBy(models.ScheduleHistoryColumns.CreatedAt).Desc(), sm.OrderBy(models.ScheduleHistoryColumns.Status).Desc()) + } + + return expr +} + +type ScheduleHistoryListResult struct { + Schedules models.ScheduleHistorySlice `json:"schedules"` + Total int64 `json:"count"` +} + +func (api *API) ScheduleHistoryList(ctx context.Context, params ScheduleHistoryListParams) (result ScheduleHistoryListResult, err error) { + ctx, span := tracer.Start(ctx, "*API.ScheduleHistoryList") + defer span.End() + + result.Schedules, err = models.ScheduleHistories.Query(ctx, api.db, params.Query()...).All() + if err != nil { + return result, errs.Wrapw(err, "failed to list schedule histories", "query", params) + } + + result.Total, err = models.ScheduleHistories.Query(ctx, api.db, params.CountQuery()...).Count() + if err != nil { + return result, errs.Wrapw(err, "failed to count schedule histories", "query", params) + } + + return result, nil +} diff --git a/api/schedule_set.go b/api/schedule_set.go index 1451d2f..5aad802 100644 --- a/api/schedule_set.go +++ b/api/schedule_set.go @@ -12,16 +12,19 @@ type ScheduleStatus int8 const ( ScheduleStatusDisabled ScheduleStatus = iota + ScheduleStatusEnabled ScheduleStatusStandby - ScheduleStatusError ScheduleStatusEnqueued ScheduleStatusDownloading + ScheduleStatusError ) func (ss ScheduleStatus) String() string { switch ss { case ScheduleStatusDisabled: return "Disabled" + case ScheduleStatusEnabled: + return "Enabled" case ScheduleStatusStandby: return "Standby" case ScheduleStatusEnqueued: diff --git a/server/routes/page_home.go b/server/routes/page_home.go index 4ccfe4d..b7151cd 100644 --- a/server/routes/page_home.go +++ b/server/routes/page_home.go @@ -1,7 +1,6 @@ package routes import ( - "encoding/json" "net/http" "time" @@ -26,8 +25,11 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) { if err != nil { log.New(ctx).Err(err).Error("failed to list subreddits") code, message := errs.HTTPMessage(err) + data.Error = message rw.WriteHeader(code) - _ = json.NewEncoder(rw).Encode(map[string]string{"error": message}) + if err := homeview.Home(vc, data).Render(ctx, rw); err != nil { + log.New(ctx).Err(err).Error("failed to render home view") + } return } diff --git a/server/routes/page_schedule_history.go b/server/routes/page_schedule_history.go index 4ea448e..7a66614 100644 --- a/server/routes/page_schedule_history.go +++ b/server/routes/page_schedule_history.go @@ -2,10 +2,13 @@ package routes import ( "net/http" + "time" + "github.com/tigorlazuardi/redmage/api" + "github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/log" "github.com/tigorlazuardi/redmage/views" - scheduleshistoryview "github.com/tigorlazuardi/redmage/views/schedulehistoriesview" + "github.com/tigorlazuardi/redmage/views/schedulehistoriesview" ) func (routes *Routes) PageScheduleHistory(rw http.ResponseWriter, req *http.Request) { @@ -14,9 +17,36 @@ func (routes *Routes) PageScheduleHistory(rw http.ResponseWriter, req *http.Requ c := views.NewContext(routes.Config, req) - var data scheduleshistoryview.Data + var data schedulehistoriesview.Data + if tz := req.URL.Query().Get("tz"); tz == "" { + data.Timezone = time.Local + } else { + var err error + data.Timezone, err = time.LoadLocation(tz) + if err != nil { + data.Timezone = time.Local + } + } - if err := scheduleshistoryview.ScheduleHistoriesview(c, data).Render(ctx, rw); err != nil { + var params api.ScheduleHistoryListParams + params.FillFromQuery(req.URL.Query()) + + result, err := routes.API.ScheduleHistoryList(ctx, params) + if err != nil { + log.New(ctx).Err(err).Error("Failed to list schedule histories") + code, message := errs.HTTPMessage(err) + rw.WriteHeader(code) + data.Error = message + if err := schedulehistoriesview.ScheduleHistoriesview(c, data).Render(ctx, rw); err != nil { + log.New(ctx).Err(err).Error("Failed to render schedule histories view") + } + return + } + + data.Schedules = result.Schedules + data.Total = result.Total + + if err := schedulehistoriesview.ScheduleHistoriesview(c, data).Render(ctx, rw); err != nil { log.New(ctx).Err(err).Error("Failed to render schedule histories view") } } diff --git a/views/components/absolute_time_text.templ b/views/components/absolute_time_text.templ new file mode 100644 index 0000000..02cdbe1 --- /dev/null +++ b/views/components/absolute_time_text.templ @@ -0,0 +1,17 @@ +package components + +import "time" +import "strings" + +templ AbsoluteTimeText(id string, t int64, class ...string) { + + { time.Unix(t, 0).Format("Mon, _2 Jan 2006 15:04:05") } + + @absoluteFromTime(id, t) +} + +script absoluteFromTime(id string, time int64) { + const el = document.getElementById(id); + const timeText = dayjs.unix(time).tz(dayjs.tz.guess()).format('ddd, D MMM YYYY HH:mm:ss'); + el.textContent = timeText; +} diff --git a/views/schedulehistoriesview/data.go b/views/schedulehistoriesview/data.go new file mode 100644 index 0000000..fca3296 --- /dev/null +++ b/views/schedulehistoriesview/data.go @@ -0,0 +1,46 @@ +package schedulehistoriesview + +import ( + "time" + + "github.com/tigorlazuardi/redmage/models" +) + +type Data struct { + Error string + Schedules models.ScheduleHistorySlice + Total int64 + Timezone *time.Location +} + +func (data *Data) splitByDays() (out []*splitByDaySchedules) { + for _, schedule := range data.Schedules { + day := time.Unix(schedule.CreatedAt, 0).In(data.Timezone) + date := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, day.Location()) + + var found bool + inner: + for _, split := range out { + if split.Date.Equal(date) { + found = true + split.Schedules = append(split.Schedules, schedule) + break inner + } + } + if !found { + out = append(out, &splitByDaySchedules{ + Day: day, + Date: date, + Schedules: models.ScheduleHistorySlice{schedule}, + }) + } + } + + return out +} + +type splitByDaySchedules struct { + Day time.Time + Date time.Time + Schedules models.ScheduleHistorySlice +} diff --git a/views/schedulehistoriesview/schedulesview.templ b/views/schedulehistoriesview/schedulesview.templ index e2ad116..be3ffc5 100644 --- a/views/schedulehistoriesview/schedulesview.templ +++ b/views/schedulehistoriesview/schedulesview.templ @@ -1,9 +1,10 @@ -package scheduleshistoryview +package schedulehistoriesview import "github.com/tigorlazuardi/redmage/views" import "github.com/tigorlazuardi/redmage/views/components" - -type Data struct{} +import "time" +import "github.com/tigorlazuardi/redmage/api" +import "fmt" templ ScheduleHistoriesview(c *views.Context, data Data) { @components.Doctype() { @@ -17,10 +18,92 @@ templ ScheduleHistoriesview(c *views.Context, data Data) { } templ ScheduleHistoriesContent(c *views.Context, data Data) { - @components.Container() { -
-

Schedule History

-
-
- } +
+ @components.Container() { + if data.Error != "" { + @components.ErrorToast(data.Error) + } else { +

Schedule History

+
+ if data.Total < 1 { +

There are no history schedules to be found. You can populate this page's history by manually trigger a Subreddit { "for" } downloading.

+ } + if data.Total > 0 { +
+ for _, scheduleGroup := range data.splitByDays() { +

{ scheduleGroup.Day.Format("Monday, _2 January 2006") }

+
+ + for _, schedule := range scheduleGroup.Schedules { +
+ +
+ { time.Unix(schedule.CreatedAt, 10).In(time.Local).Format("15:04:05") } +
+
+ if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusDisabled { + + Subreddit + @subredditLink(schedule.Subreddit) + scheduler has been set to { api.ScheduleStatusDisabled.String() } status. + + } else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusEnabled { + + Subreddit + @subredditLink(schedule.Subreddit) + { " " } + has been { api.ScheduleStatusEnabled.String() } { "for" } automatic scheduling. + + } else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusStandby { + + Subreddit + @subredditLink(schedule.Subreddit) + { " " } + has finished + { api.ScheduleStatusDownloading.String() } + and turned to { api.ScheduleStatusStandby.String() } status. + + } else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusEnqueued { + + Subreddit + @subredditLink(schedule.Subreddit) + { " " } + is { api.ScheduleStatusEnqueued.String() } { "for" } downloading. + + } else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusDownloading { + + Subreddit + @subredditLink(schedule.Subreddit) + { " " } + has started { api.ScheduleStatusDownloading.String() }. + + } else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusError { + + Subreddit + @subredditLink(schedule.Subreddit) + { " " } + finishes { api.ScheduleStatusDownloading.String() } + with { api.ScheduleStatusError.String() } of "{ schedule.ErrorMessage }". + + } +
+ } + } +
+ + } + } + } +
+} + +templ subredditLink(subreddit string) { + { subreddit } }