diff --git a/.gitignore b/.gitignore index b3ab3c6..e098088 100644 --- a/.gitignore +++ b/.gitignore @@ -29,11 +29,7 @@ data journal node_modules test/data/test.db +tests.xml .vscode .DS_Store .history -bootstrap -*.zip -*.pem -!test/cert.pem -!test/key.pem diff --git a/go.mod b/go.mod index e37fdf2..b99d9dd 100644 --- a/go.mod +++ b/go.mod @@ -12,4 +12,7 @@ require ( golang.org/x/sys v0.33.0 // indirect ) -require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b +require ( + github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b + golang.org/x/text v0.25.0 +) diff --git a/internal/app/controller/web/badrequest.go b/internal/app/controller/web/badrequest.go index bcd483c..00334b6 100644 --- a/internal/app/controller/web/badrequest.go +++ b/internal/app/controller/web/badrequest.go @@ -4,6 +4,7 @@ import ( "net/http" "text/template" + "github.com/jamiefdhurst/journal/internal/app" "github.com/jamiefdhurst/journal/pkg/controller" ) @@ -12,15 +13,22 @@ type BadRequest struct { controller.Super } +type badRequestTemplateData struct { + Container interface{} +} + // Run BadRequest func (c *BadRequest) Run(response http.ResponseWriter, request *http.Request) { + data := badRequestTemplateData{} + data.Container = c.Super.Container().(*app.Container) + response.WriteHeader(http.StatusNotFound) c.SaveSession(response) template, _ := template.ParseFiles( "./web/templates/_layout/default.html.tmpl", "./web/templates/error.html.tmpl") - template.ExecuteTemplate(response, "layout", c) + template.ExecuteTemplate(response, "layout", data) } // RunBadRequest calls the bad request from an existing controller diff --git a/internal/app/controller/web/calendar.go b/internal/app/controller/web/calendar.go new file mode 100644 index 0000000..cc2f798 --- /dev/null +++ b/internal/app/controller/web/calendar.go @@ -0,0 +1,138 @@ +package web + +import ( + "log" + "net/http" + "strconv" + "strings" + "text/template" + "time" + + "github.com/jamiefdhurst/journal/internal/app" + "github.com/jamiefdhurst/journal/internal/app/model" + "github.com/jamiefdhurst/journal/pkg/controller" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// Calendar Handle displaying a calendar with blog entries for given days +type Calendar struct { + controller.Super +} + +type day struct { + Date time.Time + IsEmpty bool +} + +type calendarTemplateData struct { + Container interface{} + Days map[int][]model.Journal + Weeks [][]day + CurrentDate time.Time + PrevYear int + PrevYearUrl string + NextYear int + NextYearUrl string + PrevMonth string + PrevMonthUrl string + NextMonth string + NextMonthUrl string +} + +// Run Calendar action +func (c *Calendar) Run(response http.ResponseWriter, request *http.Request) { + + data := calendarTemplateData{} + + container := c.Super.Container().(*app.Container) + data.Container = container + js := model.Journals{Container: container} + + // Load date from parameters if available (either 2006/jan or 2006) + date := time.Now() + var err error + if len(c.Params()) == 3 { + date, err = time.Parse("2006 Jan 02", c.Params()[1]+" "+cases.Title(language.English, cases.NoLower).String(c.Params()[2])+" 25") + } else if len(c.Params()) == 2 { + date, err = time.Parse("2006-01-02", c.Params()[1]+"-01-01") + } + if err != nil { + log.Print(err) + RunBadRequest(response, request, container) + return + } + + firstOfMonth := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) + startWeekday := int(firstOfMonth.Weekday()) + + // Find number of days in month + nextMonth := firstOfMonth.AddDate(0, 1, 0) + lastOfMonth := nextMonth.AddDate(0, 0, -1) + daysInMonth := lastOfMonth.Day() + + data.Days = map[int][]model.Journal{} + data.Weeks = [][]day{} + week := []day{} + + // Fill in blanks before first day + for range startWeekday { + week = append(week, day{IsEmpty: true}) + } + + // Fill in actual days + for d := 1; d <= daysInMonth; d++ { + thisDate := time.Date(date.Year(), date.Month(), d, 0, 0, 0, 0, date.Location()) + data.Days[d] = js.FetchByDate(thisDate.Format("2006-01-02")) + week = append(week, day{ + Date: thisDate, + IsEmpty: false, + }) + + // If Saturday, start a new week + if thisDate.Weekday() == time.Saturday { + data.Weeks = append(data.Weeks, week) + week = []day{} + } + } + + // Fill in blanks after last day + if len(week) > 0 { + for len(week) < 7 { + week = append(week, day{IsEmpty: true}) + } + data.Weeks = append(data.Weeks, week) + } + + // Load prev/next year and month + firstEntry := js.FindNext(0) + firstEntryDate, _ := time.Parse("2006-01-02", firstEntry.GetEditableDate()) + if date.Year() < time.Now().Year() { + data.NextYear = date.Year() + 1 + data.NextYearUrl = strconv.Itoa(data.NextYear) + "/" + strings.ToLower(date.Format("Jan")) + if date.AddDate(1, 0, 0).After(time.Now()) { + data.NextYearUrl = strconv.Itoa(data.NextYear) + "/" + strings.ToLower(time.Now().Format("Jan")) + } + } + if date.Year() > firstEntryDate.Year() { + data.PrevYear = date.Year() - 1 + data.PrevYearUrl = strconv.Itoa(data.PrevYear) + "/" + strings.ToLower(date.Format("Jan")) + if date.AddDate(-1, 0, 0).Before(firstEntryDate) { + data.PrevYearUrl = strconv.Itoa(data.PrevYear) + "/" + strings.ToLower(firstEntryDate.Format("Jan")) + } + } + if date.Year() < time.Now().Year() || date.Month() < time.Now().Month() { + data.NextMonth = date.AddDate(0, 0, 31).Format("January") + data.NextMonthUrl = strings.ToLower(date.AddDate(0, 0, 31).Format("2006/Jan")) + } + if date.Year() > firstEntryDate.Year() || date.Month() > firstEntryDate.Month() { + data.PrevMonth = date.AddDate(0, 0, -31).Format("January") + data.PrevMonthUrl = strings.ToLower(date.AddDate(0, 0, -31).Format("2006/Jan")) + } + data.CurrentDate = date + + template, _ := template.ParseFiles( + "./web/templates/_layout/default.html.tmpl", + "./web/templates/calendar.html.tmpl") + template.ExecuteTemplate(response, "layout", data) +} diff --git a/internal/app/controller/web/calendar_test.go b/internal/app/controller/web/calendar_test.go new file mode 100644 index 0000000..032fb75 --- /dev/null +++ b/internal/app/controller/web/calendar_test.go @@ -0,0 +1,154 @@ +package web + +import ( + "net/http" + "os" + "path" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/jamiefdhurst/journal/internal/app" + "github.com/jamiefdhurst/journal/test/mocks/controller" + "github.com/jamiefdhurst/journal/test/mocks/database" +) + +func init() { + _, filename, _, _ := runtime.Caller(0) + dir := path.Join(path.Dir(filename), "../../../..") + err := os.Chdir(dir) + if err != nil { + panic(err) + } +} + +func TestCalendarRun(t *testing.T) { + db := &database.MockSqlite{} + configuration := app.DefaultConfiguration() + container := &app.Container{Configuration: configuration, Db: db} + response := controller.NewMockResponse() + controller := &Calendar{} + controller.DisableTracking() + + // Test showing current year/month (only prev nav) + today := time.Now() + firstOfMonth := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, today.Location()) + daysInMonth := firstOfMonth.AddDate(0, 1, -1).Day() + db.EnableMultiMode() + db.AppendResult(&database.MockJournal_SingleRow{}) + for d := 2; d <= daysInMonth; d++ { + db.AppendResult(&database.MockRowsEmpty{}) + } + db.AppendResult(&database.MockJournal_SingleRow{}) + request, _ := http.NewRequest("GET", "/calendar", strings.NewReader("")) + controller.Init(container, []string{}, request) + controller.Run(response, request) + if !strings.Contains(response.Content, "Title") { + t.Error("Expected title of journal to be shown in calendar") + } + if !strings.Contains(response.Content, "class=\"prev prev-year\"") { + t.Error("Expected previous year link to be shown") + } + if !strings.Contains(response.Content, "class=\"prev prev-month\"") { + t.Error("Expected previous month link to be shown") + } + if strings.Contains(response.Content, "class=\"next next-year\"") { + t.Error("Expected next year link to be missing") + } + if strings.Contains(response.Content, "class=\"next next-month\"") { + t.Error("Expected next month link to be missing") + } + + // Test showing beginning (only next nav) + response.Reset() + db.EnableMultiMode() + db.AppendResult(&database.MockJournal_SingleRow{}) + for d := 2; d <= 28; d++ { + db.AppendResult(&database.MockRowsEmpty{}) + } + db.AppendResult(&database.MockJournal_SingleRow{}) + request, _ = http.NewRequest("GET", "/calendar/2018/feb", strings.NewReader("")) + controller.Init(container, []string{"", "2018", "feb"}, request) + controller.Run(response, request) + if !strings.Contains(response.Content, "Title") { + t.Error("Expected title of journal to be shown in calendar") + } + if !strings.Contains(response.Content, "
| Sun | +Mon | +Tue | +Wed | +Thu | +Fri | +Sat | +
|---|---|---|---|---|---|---|
| + {{else}} + |
+ {{.Date.Day}}+ {{range (index $.Days .Date.Day)}} + {{.Title}} + {{end}} + |
+ {{end}}
+ {{end}}
+