Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
10 changes: 9 additions & 1 deletion internal/app/controller/web/badrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"text/template"

"github.com/jamiefdhurst/journal/internal/app"
"github.com/jamiefdhurst/journal/pkg/controller"
)

Expand All @@ -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
Expand Down
138 changes: 138 additions & 0 deletions internal/app/controller/web/calendar.go
Original file line number Diff line number Diff line change
@@ -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)
}
154 changes: 154 additions & 0 deletions internal/app/controller/web/calendar_test.go
Original file line number Diff line number Diff line change
@@ -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, "<h2>2018</h2>") || !strings.Contains(response.Content, "<h2>February</h2") {
t.Error("Expected correct year and month to be shown")
}
if strings.Contains(response.Content, "class=\"prev prev-year\"") {
t.Error("Expected previous year link to be missing")
}
if strings.Contains(response.Content, "class=\"prev prev-month\"") {
t.Error("Expected previous month link to be missing")
}
if !strings.Contains(response.Content, "class=\"next next-year\"") {
t.Error("Expected next year link to be shown")
}
if !strings.Contains(response.Content, "class=\"next next-month\"") {
t.Error("Expected next month link to be shown")
}

// Test showing middle (both prev and next nav)
response.Reset()
lastYear := today.Year() - 1
nextMonth := strings.ToLower(today.AddDate(0, 1, 0).Format("Jan"))
firstOfMonth = time.Date(lastYear, today.AddDate(0, 1, 0).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/"+strconv.Itoa(lastYear)+"/"+nextMonth, strings.NewReader(""))
controller.Init(container, []string{"", strconv.Itoa(lastYear), nextMonth}, 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 shown")
}
if !strings.Contains(response.Content, "class=\"next next-month\"") {
t.Error("Expected next month link to be shown")
}

// Test year only
response.Reset()
db.EnableMultiMode()
db.AppendResult(&database.MockJournal_SingleRow{})
for d := 2; d <= 31; d++ {
db.AppendResult(&database.MockRowsEmpty{})
}
db.AppendResult(&database.MockJournal_SingleRow{})
request, _ = http.NewRequest("GET", "/calendar/2019", strings.NewReader(""))
controller.Init(container, []string{"", "2019"}, 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, "<h2>2019</h2>") || !strings.Contains(response.Content, "<h2>January</h2") {
t.Error("Expected correct year and month to be shown")
}
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 shown")
}
if !strings.Contains(response.Content, "class=\"next next-month\"") {
t.Error("Expected next month link to be shown")
}
}
4 changes: 2 additions & 2 deletions internal/app/controller/web/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ func (c *Edit) Run(response http.ResponseWriter, request *http.Request) {
container := c.Super.Container().(*app.Container)
data.Container = container
if !container.Configuration.EnableEdit {
RunBadRequest(response, request, c.Super.Container)
RunBadRequest(response, request, container)
return
}

js := model.Journals{Container: container}
data.Journal = js.FindBySlug(c.Params()[1])

if data.Journal.ID == 0 {
RunBadRequest(response, request, c.Super.Container)
RunBadRequest(response, request, container)
return
}

Expand Down
2 changes: 1 addition & 1 deletion internal/app/controller/web/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (c *View) Run(response http.ResponseWriter, request *http.Request) {
data.Journal = js.FindBySlug(c.Params()[1])

if data.Journal.ID == 0 {
RunBadRequest(response, request, c.Super.Container)
RunBadRequest(response, request, container)
} else {
data.Next = js.FindNext(data.Journal.ID)
data.Prev = js.FindPrev(data.Journal.ID)
Expand Down
10 changes: 10 additions & 0 deletions internal/app/model/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,16 @@ func (js *Journals) FetchAll() []Journal {
return js.loadFromRows(rows)
}

// FetchByDate Get all journal entries on a given date
func (js *Journals) FetchByDate(date string) []Journal {
rows, err := js.Container.Db.Query("SELECT * FROM `"+journalTable+"` WHERE `date` LIKE ? ORDER BY `id`", date+"%")
if err != nil {
return []Journal{}
}

return js.loadFromRows(rows)
}

// FetchPaginated returns a set of paginated journal entries
func (js *Journals) FetchPaginated(query database.PaginationQuery) ([]Journal, database.PaginationInformation) {
pagination := database.PaginationInformation{
Expand Down
Loading