Compare commits

...

17 Commits

Author SHA1 Message Date
30c552cf6c change postgres to docker 2024-07-30 20:03:44 +02:00
3eab2a3c9f add todo 2024-07-29 22:09:06 +02:00
5d225f0186 Use redis to store the sessions 2024-07-29 21:54:37 +02:00
52c3679158 Use cmd flags 2024-07-29 14:20:51 +02:00
aca8605870 fix a make reservation error 2024-07-29 14:04:43 +02:00
6631288843 add more tests 2024-07-29 13:52:10 +02:00
99c2eec759 add some tests 2024-07-28 15:48:03 +02:00
623291541a Fixing redirects 2024-07-28 15:23:12 +02:00
d7f7a2d8d9 Handling Calendar changes 2024-07-28 14:43:39 +02:00
14828fb901 Reservation calendar 4 2024-07-28 13:59:54 +02:00
7894a05daf Reservation Calendar 3 2024-07-27 22:39:23 +02:00
b97c6cba5c reservation calendar 2 2024-07-26 13:40:56 +02:00
a7cf9fe4f0 Showing the reservation calendar 2024-07-26 10:18:27 +02:00
5987fadb03 Deleting a reservation 2024-07-25 14:09:42 +02:00
fcd29cc082 Marking a resercation as processed 2024-07-25 13:51:31 +02:00
ca1e72c676 Editing a reservation 2024-07-25 13:29:57 +02:00
d4cf44bb22 Database functions for editing a reservation 2024-07-25 13:13:16 +02:00
20 changed files with 833 additions and 64 deletions

View File

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/gob" "encoding/gob"
"flag"
"fmt" "fmt"
"go-udemy-web-1/internal/config" "go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/driver" "go-udemy-web-1/internal/driver"
@ -14,7 +15,9 @@ import (
"os" "os"
"time" "time"
"github.com/alexedwards/scs/redisstore"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
"github.com/gomodule/redigo/redis"
) )
const portNumber = ":8080" const portNumber = ":8080"
@ -53,6 +56,23 @@ func run() (*driver.DB, error) {
gob.Register(models.User{}) gob.Register(models.User{})
gob.Register(models.Room{}) gob.Register(models.Room{})
gob.Register(models.Restriction{}) gob.Register(models.Restriction{})
gob.Register(map[string]int{})
// read flags
inProduction := flag.Bool("production", true, "Application is in production")
useCache := flag.Bool("cache", true, "Use template cache")
dbHost := flag.String("dbhost", "localhost", "Database host")
dbName := flag.String("dbname", "bookings", "Database name")
dbUser := flag.String("dbuser", os.Getenv("PGUSER"), "Database user")
dbPass := flag.String("dbpass", os.Getenv("PGPWD"), "Database password")
dbPort := flag.String("dbport", "5432", "Database port")
dbSSL := flag.String("dbssl", "disable", "Database ssl settings (disable, prefer, require)")
flag.Parse()
if *dbUser == "" {
fmt.Println("Missing required flags")
os.Exit(1)
}
mailChan := make(chan models.MailData) mailChan := make(chan models.MailData)
app.MailChan = mailChan app.MailChan = mailChan
@ -60,9 +80,18 @@ func run() (*driver.DB, error) {
listenForMail() listenForMail()
// change this to true when in production // change this to true when in production
app.InProduction = false app.InProduction = *inProduction
// Establish connection pool to Redis.
pool := &redis.Pool{
MaxIdle: 10,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", "localhost:6379")
},
}
session = scs.New() session = scs.New()
session.Store = redisstore.New(pool)
session.Lifetime = 24 * time.Hour session.Lifetime = 24 * time.Hour
session.Cookie.Persist = true session.Cookie.Persist = true
session.Cookie.SameSite = http.SameSiteLaxMode session.Cookie.SameSite = http.SameSiteLaxMode
@ -72,7 +101,8 @@ func run() (*driver.DB, error) {
// connect to database // connect to database
log.Println("Connecting to database...") log.Println("Connecting to database...")
dsn := fmt.Sprintf("host=localhost port=5432 dbname=bookings user=%s password=%s", os.Getenv("PGUSER"), os.Getenv("PGPWD")) dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
*dbHost, *dbPort, *dbName, *dbUser, *dbPass, *dbSSL)
db, err := driver.ConnectSQL(dsn) db, err := driver.ConnectSQL(dsn)
if err != nil { if err != nil {
log.Fatal("Cannot connect to database! Dying...") log.Fatal("Cannot connect to database! Dying...")
@ -85,7 +115,7 @@ func run() (*driver.DB, error) {
return nil, err return nil, err
} }
app.TemplateCahce = tc app.TemplateCahce = tc
app.UseCache = false app.UseCache = *useCache
infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
app.InfoLog = infoLog app.InfoLog = infoLog

View File

@ -38,14 +38,20 @@ func routes(app *config.AppConfig) http.Handler {
mux.Handle("/static/*", http.StripPrefix("/static", fileServer)) mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
mux.Route("/admin", func(mux chi.Router) { mux.Route("/admin", func(mux chi.Router) {
// mux.Use(Auth) if app.InProduction {
mux.Use(Auth)
}
mux.Get("/dashboard", handlers.Repo.AdminDashboard) mux.Get("/dashboard", handlers.Repo.AdminDashboard)
mux.Get("/reservations-new", handlers.Repo.AdminNewReservations) mux.Get("/reservations-new", handlers.Repo.AdminNewReservations)
mux.Get("/reservations-all", handlers.Repo.AdminAllReservations) mux.Get("/reservations-all", handlers.Repo.AdminAllReservations)
mux.Get("/reservations-calendar", handlers.Repo.AdminReservationsCalendar) mux.Get("/reservations-calendar", handlers.Repo.AdminReservationsCalendar)
mux.Post("/reservations-calendar", handlers.Repo.AdminPostReservationsCalendar)
mux.Get("/process-reservation/{src}/{id}/do", handlers.Repo.AdminProcessReservation)
mux.Get("/delete-reservation/{src}/{id}/do", handlers.Repo.AdminDeleteReservation)
mux.Get("/reservations/{src}/{id}", handlers.Repo.AdminShowReservation) mux.Get("/reservations/{src}/{id}/show", handlers.Repo.AdminShowReservation)
mux.Post("/reservations/{src}/{id}", handlers.Repo.AdminPostShowReservation)
}) })
return mux return mux

View File

@ -0,0 +1,32 @@
# Use postgres/postgres user/password credentials
services:
db:
image: postgres
restart: always
# set shared memory limit when using docker-compose
shm_size: 128mb
# or set shared memory limit when deploy via swarm stack
#volumes:
# - type: tmpfs
# target: /dev/shm
# tmpfs:
# size: 134217728 # 128*2^20 bytes = 128Mb
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
adminer:
image: adminer
restart: always
ports:
- 8081:8080
cache:
image: redis
restart: always
ports:
- 6379:6379
command: redis-server --save 60 1 --loglevel warning
volumes:
- cache:/data

View File

@ -23,3 +23,9 @@ services:
ports: ports:
- 8081:8080 - 8081:8080
cache:
image: redis
restart: always
ports:
- 6379:6379
command: redis-server --save 60 1 --loglevel warning

7
go.mod
View File

@ -2,11 +2,12 @@ module go-udemy-web-1
go 1.21.0 go 1.21.0
// github.com/CloudyKit/jet --> Check this later // Template: github.com/CloudyKit/jet --> Check this later
// //
// ORM packages: // ORM packages:
// upper/db https://github.com/upper/db // upper/db https://github.com/upper/db
// gorm.io/gorm // gorm.io/gorm
// TODO Checkout sqlc and sqlx
// TODO Use default http package to rewrite the project with go 1.22.1 // TODO Use default http package to rewrite the project with go 1.22.1
@ -17,10 +18,13 @@ require github.com/justinas/nosurf v1.1.1
require github.com/alexedwards/scs/v2 v2.8.0 require github.com/alexedwards/scs/v2 v2.8.0
require ( require (
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/gomodule/redigo v1.9.2
github.com/jackc/pgconn v1.14.3 github.com/jackc/pgconn v1.14.3
github.com/jackc/pgx/v5 v5.6.0 github.com/jackc/pgx/v5 v5.6.0
github.com/xhit/go-simple-mail v2.2.2+incompatible github.com/xhit/go-simple-mail v2.2.2+incompatible
golang.org/x/crypto v0.25.0
) )
require ( require (
@ -30,7 +34,6 @@ require (
github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.16.0 // indirect
) )

10
go.sum
View File

@ -1,3 +1,5 @@
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885 h1:UdHeICe7BgRbDq5yjA/yjCyJnohROtyD8PpJjhdAvF8=
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885/go.mod h1:ceKFatoD+hfHWWeHOAYue1J+XgOJjE7dw8l3JtIRTGY=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@ -7,6 +9,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/gomodule/redigo v1.8.0/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@ -33,9 +38,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74= github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74=
github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y= github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=

View File

@ -16,6 +16,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/go-chi/chi/v5"
) )
// Repo the repository used by the handlers // Repo the repository used by the handlers
@ -81,14 +83,14 @@ func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) {
res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation) res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
if !ok { if !ok {
m.App.Session.Put(r.Context(), "error", "can't get reservation from session") m.App.Session.Put(r.Context(), "error", "can't get reservation from session")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
room, err := m.DB.GetRoomById(res.RoomID) room, err := m.DB.GetRoomById(res.RoomID)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "can't find room") m.App.Session.Put(r.Context(), "error", "can't find room")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -116,13 +118,13 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
reservation, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation) reservation, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
if !ok { if !ok {
m.App.Session.Put(r.Context(), "error", "can't get reservation from session") m.App.Session.Put(r.Context(), "error", "can't get reservation from session")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
m.App.Session.Put(r.Context(), "error", "can't parse form") m.App.Session.Put(r.Context(), "error", "can't parse form")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -137,15 +139,20 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
reservation.Email = form.Get("email") reservation.Email = form.Get("email")
reservation.Phone = form.Get("phone") reservation.Phone = form.Get("phone")
// TODO: Should I check the validity of reservation.StartDate / EndDate?
if !form.Valid() { if !form.Valid() {
data := make(map[string]interface{}) data := make(map[string]interface{})
data["reservation"] = reservation data["reservation"] = reservation
sd := reservation.StartDate.Format("2006-01-02")
ed := reservation.EndDate.Format("2006-01-02")
stringMap := make(map[string]string)
stringMap["start_date"] = sd
stringMap["end_date"] = ed
render.Template(w, r, "make-reservation.page.tmpl", &models.TemplateData{ render.Template(w, r, "make-reservation.page.tmpl", &models.TemplateData{
Data: data, Data: data,
Form: form, Form: form,
StringMap: stringMap,
}) })
return return
} }
@ -153,7 +160,7 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
newReservationID, err := m.DB.InsertReservation(reservation) newReservationID, err := m.DB.InsertReservation(reservation)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "can't insert reservation into database") m.App.Session.Put(r.Context(), "error", "can't insert reservation into database")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -171,7 +178,7 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
err = m.DB.InsertRoomRestriction(restriction) err = m.DB.InsertRoomRestriction(restriction)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "can't insert room restriction into database") m.App.Session.Put(r.Context(), "error", "can't insert room restriction into database")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -222,7 +229,7 @@ func (m *Repository) ReservationSummary(w http.ResponseWriter, r *http.Request)
if !ok { if !ok {
m.App.ErrorLog.Println("connot get item from session") m.App.ErrorLog.Println("connot get item from session")
m.App.Session.Put(r.Context(), "error", "Can't get reservation from session") m.App.Session.Put(r.Context(), "error", "Can't get reservation from session")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
m.App.Session.Remove(r.Context(), "reservation") m.App.Session.Remove(r.Context(), "reservation")
@ -252,7 +259,7 @@ func (m *Repository) Availability(w http.ResponseWriter, r *http.Request) {
func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) { func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
m.App.Session.Put(r.Context(), "error", "can't parse form") m.App.Session.Put(r.Context(), "error", "can't parse form")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
start := r.Form.Get("start") start := r.Form.Get("start")
@ -262,20 +269,20 @@ func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
startDate, err := time.Parse(layout, start) startDate, err := time.Parse(layout, start)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't parse start date") m.App.Session.Put(r.Context(), "error", "Can't parse start date")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
endDate, err := time.Parse(layout, end) endDate, err := time.Parse(layout, end)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't parse end date") m.App.Session.Put(r.Context(), "error", "Can't parse end date")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
rooms, err := m.DB.SearchAvailabilityForAllRooms(startDate, endDate) rooms, err := m.DB.SearchAvailabilityForAllRooms(startDate, endDate)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't connect to database") m.App.Session.Put(r.Context(), "error", "Can't connect to database")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
@ -307,11 +314,11 @@ func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
} }
type jsonResponse struct { type jsonResponse struct {
OK bool `json:"ok"`
Message string `json:"message"` Message string `json:"message"`
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
StartDate string `json:"start_date"` StartDate string `json:"start_date"`
EndDate string `json:"end_date"` EndDate string `json:"end_date"`
OK bool `json:"ok"`
} }
// AvailabilityJSON is the search for availability page handler // AvailabilityJSON is the search for availability page handler
@ -400,7 +407,7 @@ func (m *Repository) ChooseRoom(w http.ResponseWriter, r *http.Request) {
roomID, err := strconv.Atoi(exploded[2]) roomID, err := strconv.Atoi(exploded[2])
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't parse roomID") m.App.Session.Put(r.Context(), "error", "Can't parse roomID")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
m.App.Session.Get(r.Context(), "reservation") m.App.Session.Get(r.Context(), "reservation")
@ -408,7 +415,7 @@ func (m *Repository) ChooseRoom(w http.ResponseWriter, r *http.Request) {
res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation) res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
if !ok { if !ok {
m.App.Session.Put(r.Context(), "error", "Can't get reservation from session") m.App.Session.Put(r.Context(), "error", "Can't get reservation from session")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
@ -430,19 +437,19 @@ func (m *Repository) BookRoom(w http.ResponseWriter, r *http.Request) {
startDate, err := time.Parse(layout, sd) startDate, err := time.Parse(layout, sd)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't parse start date") m.App.Session.Put(r.Context(), "error", "Can't parse start date")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
endDate, err := time.Parse(layout, ed) endDate, err := time.Parse(layout, ed)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't parse end date") m.App.Session.Put(r.Context(), "error", "Can't parse end date")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
room, err := m.DB.GetRoomById(roomID) room, err := m.DB.GetRoomById(roomID)
if err != nil { if err != nil {
m.App.Session.Put(r.Context(), "error", "Can't parse roomId") m.App.Session.Put(r.Context(), "error", "Can't parse roomId")
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect) http.Redirect(w, r, "/availability", http.StatusSeeOther)
return return
} }
res.RoomID = roomID res.RoomID = roomID
@ -499,7 +506,6 @@ func (m *Repository) PostShowLogin(w http.ResponseWriter, r *http.Request) {
// Logout logs a user out // Logout logs a user out
func (m *Repository) Logout(w http.ResponseWriter, r *http.Request) { func (m *Repository) Logout(w http.ResponseWriter, r *http.Request) {
// TODO Use Redis to store the session. Check the documentation of scs package
m.App.Session.Destroy(r.Context()) m.App.Session.Destroy(r.Context())
m.App.Session.RenewToken(r.Context()) m.App.Session.RenewToken(r.Context())
http.Redirect(w, r, "/user/login", http.StatusSeeOther) http.Redirect(w, r, "/user/login", http.StatusSeeOther)
@ -545,16 +551,24 @@ func (m *Repository) AdminShowReservation(w http.ResponseWriter, r *http.Request
id, err := strconv.Atoi(exploded[4]) id, err := strconv.Atoi(exploded[4])
if err != nil { if err != nil {
helpers.ServerError(w, err) helpers.ServerError(w, err)
return
} }
src := exploded[3] src := exploded[3]
stringMap := make(map[string]string) stringMap := make(map[string]string)
stringMap["src"] = src stringMap["src"] = src
year := r.URL.Query().Get("y")
month := r.URL.Query().Get("m")
stringMap["month"] = month
stringMap["year"] = year
// get reservation from the database // get reservation from the database
res, err := m.DB.GetReservationByID(id) res, err := m.DB.GetReservationByID(id)
if err != nil { if err != nil {
helpers.ServerError(w, err) helpers.ServerError(w, err)
return
} }
data := make(map[string]interface{}) data := make(map[string]interface{})
@ -568,7 +582,240 @@ func (m *Repository) AdminShowReservation(w http.ResponseWriter, r *http.Request
}) })
} }
// AdminShowReservation shows the detail of a reservation
func (m *Repository) AdminPostShowReservation(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
helpers.ServerError(w, err)
return
}
exploded := strings.Split(r.RequestURI, "/")
id, err := strconv.Atoi(exploded[4])
if err != nil {
helpers.ServerError(w, err)
return
}
src := exploded[3]
stringMap := make(map[string]string)
stringMap["src"] = src
// get reservation from the database
res, err := m.DB.GetReservationByID(id)
if err != nil {
helpers.ServerError(w, err)
return
}
res.FirstName = r.Form.Get("first_name")
res.LastName = r.Form.Get("last_name")
res.Email = r.Form.Get("email")
res.Phone = r.Form.Get("phone")
// TODO error checking
err = m.DB.UpdateReservation(res)
if err != nil {
helpers.ServerError(w, err)
return
}
month := r.Form.Get("month")
year := r.Form.Get("year")
m.App.Session.Put(r.Context(), "flash", "Changes saved")
if year == "" {
log.Println(year, month)
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-%s", src), http.StatusSeeOther)
} else {
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%s&m=%s", year, month), http.StatusSeeOther)
}
}
// AdminReservationsCalendar displays the reservation calendar // AdminReservationsCalendar displays the reservation calendar
func (m *Repository) AdminReservationsCalendar(w http.ResponseWriter, r *http.Request) { func (m *Repository) AdminReservationsCalendar(w http.ResponseWriter, r *http.Request) {
render.Template(w, r, "admin-reservations-calendar.page.tmpl", &models.TemplateData{}) now := time.Now()
if r.URL.Query().Get("y") != "" {
year, _ := strconv.Atoi(r.URL.Query().Get("y"))
month, _ := strconv.Atoi(r.URL.Query().Get("m"))
now = time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
}
data := make(map[string]interface{})
data["now"] = now
next := now.AddDate(0, 1, 0)
last := now.AddDate(0, -1, 0)
nextMonth := next.Format("01")
nextMonthYear := next.Format("2006")
lastMonth := last.Format("01")
lastMonthYear := last.Format("2006")
stringMap := make(map[string]string)
stringMap["next_month"] = nextMonth
stringMap["next_month_year"] = nextMonthYear
stringMap["last_month"] = lastMonth
stringMap["last_month_year"] = lastMonthYear
stringMap["this_month"] = now.Format("01")
stringMap["this_month_year"] = now.Format("2006")
// get the first and last days from the month
currentYear, currentMonth, _ := now.Date()
currentLocation := now.Location()
firstOfMonth := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation)
lastOfMonth := firstOfMonth.AddDate(0, 1, -1)
intMap := make(map[string]int)
intMap["days_in_month"] = lastOfMonth.Day()
rooms, err := m.DB.AllRooms()
if err != nil {
helpers.ServerError(w, err)
return
}
data["rooms"] = rooms
for _, x := range rooms {
// create maps
reservationMap := make(map[string]int)
blockMap := make(map[string]int)
for d := firstOfMonth; !d.After(lastOfMonth); d = d.AddDate(0, 0, 1) {
reservationMap[d.Format("2006-01-2")] = 0
blockMap[d.Format("2006-01-2")] = 0
}
// get all the restrictions for the current room
restrictions, err := m.DB.GetRestrictionsForRoomByDate(x.ID, firstOfMonth, lastOfMonth)
if err != nil {
helpers.ServerError(w, err)
return
}
for _, y := range restrictions {
if y.ReservationID > 0 {
// it's a reservation
for d := y.StartDate; !d.After(y.EndDate); d = d.AddDate(0, 0, 1) {
reservationMap[d.Format("2006-01-2")] = y.ReservationID
}
} else {
// it's a block.
// NOTE:A block can only be set day by day
blockMap[y.StartDate.Format("2006-01-2")] = y.ID
}
}
data[fmt.Sprintf("reservation_map_%d", x.ID)] = reservationMap
data[fmt.Sprintf("block_map_%d", x.ID)] = blockMap
m.App.Session.Put(r.Context(), fmt.Sprintf("block_map_%d", x.ID), blockMap)
}
render.Template(w, r, "admin-reservations-calendar.page.tmpl",
&models.TemplateData{
StringMap: stringMap,
Data: data,
IntMap: intMap,
})
}
// AdminProcessReservation marks a reservation as processed
func (m *Repository) AdminProcessReservation(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
src := chi.URLParam(r, "src")
_ = m.DB.UpdateProcessedForReservation(id, 1)
year := r.URL.Query().Get("y")
month := r.URL.Query().Get("m")
m.App.Session.Put(r.Context(), "flash", "Reservation marked as processed")
if year == "" {
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-%s", src), http.StatusSeeOther)
} else {
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%s&m=%s", year, month), http.StatusSeeOther)
}
}
// AdminDeleteReservation deletes a reservation
func (m *Repository) AdminDeleteReservation(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
src := chi.URLParam(r, "src")
_ = m.DB.DeleteReservation(id)
year := r.URL.Query().Get("y")
month := r.URL.Query().Get("m")
m.App.Session.Put(r.Context(), "flash", fmt.Sprintf("Reservation %d deleted", id))
if year == "" {
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-%s", src), http.StatusSeeOther)
} else {
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%s&m=%s", year, month), http.StatusSeeOther)
}
}
// AdminPostReservationsCalendar handles post of reservation calendar
func (m *Repository) AdminPostReservationsCalendar(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
helpers.ServerError(w, err)
return
}
year, _ := strconv.Atoi(r.Form.Get("y"))
month, _ := strconv.Atoi(r.Form.Get("m"))
// process blocks
rooms, err := m.DB.AllRooms()
if err != nil {
helpers.ServerError(w, err)
return
}
form := forms.New(r.PostForm)
for _, x := range rooms {
// Get the block map from the session. Loop through entire map, if we
// have an entry in the map taht does not exist in our posted data,
// and if the restriction id > 0, then it is a block we need to remove.
curMap, _ := m.App.Session.Get(r.Context(), fmt.Sprintf("block_map_%d", x.ID)).(map[string]int)
// TODO check session get ok
for name, value := range curMap {
// ok will be false if the value is not in the map
if val, ok := curMap[name]; ok {
// only pay attention to values > 0, and that are not in the form post
// the rest are just placeholders for days without blocks
if val > 0 {
if !form.Has(fmt.Sprintf("remove_block_%d_%s", x.ID, name)) {
err := m.DB.DeleteBlockByID(value)
if err != nil {
helpers.ServerError(w, err)
return
}
}
}
}
}
}
// handle new blocks
for name := range r.PostForm {
if strings.HasPrefix(name, "add_block") {
exploded := strings.Split(name, "_")
roomID, _ := strconv.Atoi(exploded[2])
startDate, _ := time.Parse("2006-01-2", exploded[3])
// insert a new block
err := m.DB.InsertBlockForRoom(roomID, startDate)
if err != nil {
helpers.ServerError(w, err)
return
}
}
}
m.App.Session.Put(r.Context(), "flash", "Changes saved")
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%d&m=%d", year, month), http.StatusSeeOther)
} }

View File

@ -47,6 +47,13 @@ var theTests = []struct {
{"ms", "/majors-suite", "GET", http.StatusOK}, {"ms", "/majors-suite", "GET", http.StatusOK},
{"sa", "/availability", "GET", http.StatusOK}, {"sa", "/availability", "GET", http.StatusOK},
{"contact", "/contact", "GET", http.StatusOK}, {"contact", "/contact", "GET", http.StatusOK},
{"non-existant", "/some/link", "GET", http.StatusNotFound},
{"login", "/user/login", "GET", http.StatusOK},
{"logout", "/user/logout", "GET", http.StatusOK},
{"dashboard", "/admin/dashboard", "GET", http.StatusOK},
{"all reservations", "/admin/reservations-all", "GET", http.StatusOK},
{"new reservations", "/admin/reservations-new", "GET", http.StatusOK},
{"show reservation", "/admin/reservations/new/1/show", "GET", http.StatusOK},
} }
func TestHandlers(t *testing.T) { func TestHandlers(t *testing.T) {
@ -75,8 +82,8 @@ var makeReservationTests = []struct {
expectedStatusCode int expectedStatusCode int
}{ }{
{"ok", 1, http.StatusOK}, {"ok", 1, http.StatusOK},
{"no session", 0, http.StatusTemporaryRedirect}, {"no session", 0, http.StatusSeeOther},
{"non-existant room", 100, http.StatusTemporaryRedirect}, {"non-existant room", 100, http.StatusSeeOther},
} }
func TestRepository_MakeReservation(t *testing.T) { func TestRepository_MakeReservation(t *testing.T) {
@ -141,9 +148,9 @@ var postMakeReservationTests = []struct {
{key: "start_date", value: "2050-01-01"}, {key: "start_date", value: "2050-01-01"},
{key: "end_date", value: "2050-01-02"}, {key: "end_date", value: "2050-01-02"},
}, },
http.StatusTemporaryRedirect, http.StatusSeeOther,
}, },
{"no_post_data", []postData{}, http.StatusTemporaryRedirect}, {"no_post_data", []postData{}, http.StatusSeeOther},
{ {
"missing first name", "missing first name",
[]postData{ []postData{
@ -193,7 +200,7 @@ var postMakeReservationTests = []struct {
{key: "start_date", value: "2050-01-01"}, {key: "start_date", value: "2050-01-01"},
{key: "end_date", value: "2050-01-02"}, {key: "end_date", value: "2050-01-02"},
}, },
http.StatusTemporaryRedirect, http.StatusSeeOther,
}, },
{ {
"insert room restriction error", "insert room restriction error",
@ -206,7 +213,7 @@ var postMakeReservationTests = []struct {
{key: "start_date", value: "2050-01-01"}, {key: "start_date", value: "2050-01-01"},
{key: "end_date", value: "2050-01-02"}, {key: "end_date", value: "2050-01-02"},
}, },
http.StatusTemporaryRedirect, http.StatusSeeOther,
}, },
} }
@ -374,7 +381,7 @@ var reservationSummaryTests = []struct {
expectedStatusCode int expectedStatusCode int
}{ }{
{"ok", true, http.StatusOK}, {"ok", true, http.StatusOK},
{"nok", false, http.StatusTemporaryRedirect}, {"nok", false, http.StatusSeeOther},
} }
func Test_ReservationSummary(t *testing.T) { func Test_ReservationSummary(t *testing.T) {
@ -426,7 +433,7 @@ var postAvailabilityTests = []struct {
{"database error", []postData{ {"database error", []postData{
{key: "start", value: "2050-01-03"}, {key: "start", value: "2050-01-03"},
{key: "end", value: "2050-01-04"}, {key: "end", value: "2050-01-04"},
}, http.StatusTemporaryRedirect}, }, http.StatusSeeOther},
{"no availability", []postData{ {"no availability", []postData{
{key: "start", value: "2050-01-05"}, {key: "start", value: "2050-01-05"},
{key: "end", value: "2050-01-06"}, {key: "end", value: "2050-01-06"},
@ -434,12 +441,12 @@ var postAvailabilityTests = []struct {
{"wrong start date", []postData{ {"wrong start date", []postData{
{key: "start", value: "2050-05"}, {key: "start", value: "2050-05"},
{key: "end", value: "2050-01-06"}, {key: "end", value: "2050-01-06"},
}, http.StatusTemporaryRedirect}, }, http.StatusSeeOther},
{"wrong end date", []postData{ {"wrong end date", []postData{
{key: "start", value: "2050-01-05"}, {key: "start", value: "2050-01-05"},
{key: "end", value: "01-06"}, {key: "end", value: "01-06"},
}, http.StatusTemporaryRedirect}, }, http.StatusSeeOther},
{"wrong end date", []postData{}, http.StatusTemporaryRedirect}, {"wrong end date", []postData{}, http.StatusSeeOther},
} }
func Test_PostAvailability(t *testing.T) { func Test_PostAvailability(t *testing.T) {
@ -481,8 +488,8 @@ var chooseRoomTests = []struct {
expectedStatusCode int expectedStatusCode int
}{ }{
{"ok", "/choose-room/1", true, http.StatusSeeOther}, {"ok", "/choose-room/1", true, http.StatusSeeOther},
{"wrong room id", "/choose-room/1wrong", true, http.StatusTemporaryRedirect}, {"wrong room id", "/choose-room/1wrong", true, http.StatusSeeOther},
{"no session", "/choose-room/1", false, http.StatusTemporaryRedirect}, {"no session", "/choose-room/1", false, http.StatusSeeOther},
} }
func Test_ChooseRoom(t *testing.T) { func Test_ChooseRoom(t *testing.T) {
@ -531,17 +538,17 @@ var bookRoomTests = []struct {
{key: "id", value: "1"}, {key: "id", value: "1"},
{key: "s", value: "20-01-01"}, {key: "s", value: "20-01-01"},
{key: "e", value: "2050-01-02"}, {key: "e", value: "2050-01-02"},
}, http.StatusTemporaryRedirect}, }, http.StatusSeeOther},
{"wrong end date", []postData{ {"wrong end date", []postData{
{key: "id", value: "1"}, {key: "id", value: "1"},
{key: "s", value: "2050-01-01"}, {key: "s", value: "2050-01-01"},
{key: "e", value: "2050-0-02"}, {key: "e", value: "2050-0-02"},
}, http.StatusTemporaryRedirect}, }, http.StatusSeeOther},
{"wrong room id", []postData{ {"wrong room id", []postData{
{key: "id", value: "w"}, {key: "id", value: "w"},
{key: "s", value: "2050-01-01"}, {key: "s", value: "2050-01-01"},
{key: "e", value: "2050-01-02"}, {key: "e", value: "2050-01-02"},
}, http.StatusTemporaryRedirect}, }, http.StatusSeeOther},
} }
func Test_BookRoom(t *testing.T) { func Test_BookRoom(t *testing.T) {
@ -567,6 +574,64 @@ func Test_BookRoom(t *testing.T) {
} }
} }
// }}}
// {{{ Login
var loginTests = []struct {
name string
email string
expectedStatusCode int
expectedHTML string
expectedLocation string
}{
{"valid-credentials", "a@b.c", http.StatusSeeOther, "", "/admin/dashboard"},
{"invalid-credentials", "invalid@b.c", http.StatusSeeOther, "", "/user/login"},
{"invalid-data", "a@b", http.StatusOK, `action="/user/login"`, ""},
}
func TestLogin(t *testing.T) {
// range through all tests
for _, e := range loginTests {
postedData := url.Values{}
postedData.Add("email", e.email)
postedData.Add("password", "password")
// create request
req, _ := http.NewRequest("POST", "/user/login", strings.NewReader(postedData.Encode()))
ctx := getCtx(req)
req = req.WithContext(ctx)
// set the header
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
// call the handler
handler := http.HandlerFunc(Repo.PostShowLogin)
handler.ServeHTTP(rr, req)
if rr.Code != e.expectedStatusCode {
t.Errorf("failed %s: expected code %d, but got %d", e.name, e.expectedStatusCode, rr.Code)
}
if e.expectedLocation != "" {
// get the URL from test
actualLoc, _ := rr.Result().Location()
if actualLoc.String() != e.expectedLocation {
t.Errorf("failed %s: expected location %s, but got location %s", e.name, e.expectedLocation, actualLoc.String())
}
}
// checking for expected values in HTML
if e.expectedHTML != "" {
// read the response body into a string
html := rr.Body.String()
if !strings.Contains(html, e.expectedHTML) {
t.Errorf("failed %s: expected html contains %s, but got html %s", e.name, e.expectedHTML, html)
}
}
}
}
// }}} // }}}
// {{{ Test Helpers // {{{ Test Helpers

View File

@ -19,7 +19,12 @@ import (
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
) )
var functions = template.FuncMap{} var functions = template.FuncMap{
"humanDate": render.HumanDate,
"formatDate": render.FormatDate,
"iterate": render.Iterate,
"add": render.Add,
}
var ( var (
app config.AppConfig app config.AppConfig
@ -28,6 +33,10 @@ var (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
gob.Register(models.Reservation{}) gob.Register(models.Reservation{})
gob.Register(models.User{})
gob.Register(models.Room{})
gob.Register(models.Restriction{})
gob.Register(map[string]int{})
// change this to true when in production // change this to true when in production
app.InProduction = false app.InProduction = false
@ -85,6 +94,25 @@ func getRoutes() http.Handler {
mux.Post("/make-reservation", Repo.PostMakeReservation) mux.Post("/make-reservation", Repo.PostMakeReservation)
mux.Get("/reservation-summary", Repo.ReservationSummary) mux.Get("/reservation-summary", Repo.ReservationSummary)
mux.Get("/choose-room/{id}", Repo.ChooseRoom)
mux.Get("/book-room", Repo.BookRoom)
mux.Get("/user/login", Repo.ShowLogin)
mux.Post("/user/login", Repo.PostShowLogin)
mux.Get("/user/logout", Repo.Logout)
mux.Get("/admin/dashboard", Repo.AdminDashboard)
mux.Get("/admin/reservations-new", Repo.AdminNewReservations)
mux.Get("/admin/reservations-all", Repo.AdminAllReservations)
mux.Get("/admin/reservations-calendar", Repo.AdminReservationsCalendar)
mux.Post("/admin/reservations-calendar", Repo.AdminPostReservationsCalendar)
mux.Get("/admin/process-reservation/{src}/{id}/do", Repo.AdminProcessReservation)
mux.Get("/admin/delete-reservation/{src}/{id}/do", Repo.AdminDeleteReservation)
mux.Get("/admin/reservations/{src}/{id}/show", Repo.AdminShowReservation)
mux.Post("/admin/reservations/{src}/{id}", Repo.AdminPostShowReservation)
fileServer := http.FileServer(http.Dir("./static/")) fileServer := http.FileServer(http.Dir("./static/"))
mux.Handle("/static/*", http.StripPrefix("/static", fileServer)) mux.Handle("/static/*", http.StripPrefix("/static", fileServer))

View File

@ -16,7 +16,10 @@ import (
) )
var functions = template.FuncMap{ var functions = template.FuncMap{
"humanDate": HumanDate, "humanDate": HumanDate,
"formatDate": FormatDate,
"iterate": Iterate,
"add": Add,
} }
var ( var (
@ -34,6 +37,24 @@ func HumanDate(t time.Time) string {
return t.Format("2006-01-02") return t.Format("2006-01-02")
} }
func FormatDate(t time.Time, f string) string {
return t.Format(f)
}
// Iterate returns a slice of ints, starting at 1, going to count
func Iterate(count int) []int {
var i int
var items []int
for i = 0; i < count; i++ {
items = append(items, i)
}
return items
}
func Add(a, b int) int {
return a + b
}
// AddDefaultData adds default template data // AddDefaultData adds default template data
func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData { func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData {
td.Flash = app.Session.PopString(r.Context(), "flash") td.Flash = app.Session.PopString(r.Context(), "flash")

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"go-udemy-web-1/internal/models" "go-udemy-web-1/internal/models"
"log"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -342,3 +343,152 @@ func (m *postgresDBRepo) GetReservationByID(id int) (models.Reservation, error)
} }
return res, nil return res, nil
} }
// UpdateReservation updates a user in the database
func (m *postgresDBRepo) UpdateReservation(r models.Reservation) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `update reservations set first_name = $1, last_name = $2, email = $3, phone = $4, updated_at = $5
where id = $6`
_, err := m.DB.ExecContext(ctx, query, r.FirstName, r.LastName, r.Email,
r.Phone, time.Now(), r.ID)
if err != nil {
return err
}
return nil
}
// DeleteReservation deletes one reservation by ID
func (m *postgresDBRepo) DeleteReservation(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `delete from reservations where id = $1`
_, err := m.DB.ExecContext(ctx, query, id)
if err != nil {
return err
}
return nil
}
// UpdateProcessedForReservation set processed for a reservation
func (m *postgresDBRepo) UpdateProcessedForReservation(id, processed int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `update reservations set processed = $1 where id = $2`
_, err := m.DB.ExecContext(ctx, query, processed, id)
if err != nil {
return err
}
return nil
}
// AllRooms gets all rooms
func (m *postgresDBRepo) AllRooms() ([]models.Room, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var rooms []models.Room
query := `select id, room_name, created_at, updated_at from rooms order by room_name`
rows, err := m.DB.QueryContext(ctx, query)
if err != nil {
return rooms, err
}
defer rows.Close() // To avoid memory leak
for rows.Next() {
var rm models.Room
err := rows.Scan(
&rm.ID,
&rm.RoomName,
&rm.CreatedAt,
&rm.UpdatedAt,
)
if err != nil {
return rooms, err
}
rooms = append(rooms, rm)
}
if err = rows.Err(); err != nil {
return rooms, err
}
return rooms, nil
}
// GetRestrictionsForRoomByDate returns restrictions for a room by date range
func (m *postgresDBRepo) GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var restrictions []models.RoomRestriction
// coalesce use 0 if null
query := `select id, coalesce(reservation_id, 0), restriction_id, room_id, start_date, end_date
from room_restrictions where $1 < end_date and $2 >= start_date
and room_id = $3`
rows, err := m.DB.QueryContext(ctx, query, start, end, roomId)
if err != nil {
return restrictions, err
}
defer rows.Close() // To avoid memory leak
for rows.Next() {
var r models.RoomRestriction
err := rows.Scan(
&r.ID,
&r.ReservationID,
&r.RestrictionID,
&r.RoomID,
&r.StartDate,
&r.EndDate,
)
if err != nil {
return restrictions, err
}
restrictions = append(restrictions, r)
}
if err = rows.Err(); err != nil {
return restrictions, err
}
return restrictions, nil
}
// InsertBlockForRoom inserts a room restriction
func (m *postgresDBRepo) InsertBlockForRoom(id int, startDate time.Time) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `insert into room_restrictions (start_date, end_date, room_id, restriction_id,
created_at, updated_at)
values ($1, $2, $3, $4, $5, $6)`
_, err := m.DB.ExecContext(ctx, query, startDate, startDate.AddDate(0, 0, 1), id, 2, time.Now(), time.Now())
if err != nil {
log.Println(err)
return err
}
return nil
}
// DeleteBlockByID deletes a block by ID
func (m *postgresDBRepo) DeleteBlockByID(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `delete from room_restrictions where id = $1`
_, err := m.DB.ExecContext(ctx, query, id)
if err != nil {
log.Println(err)
return err
}
return nil
}

View File

@ -79,7 +79,10 @@ func (m *testDBRepo) UpdateUser(u models.User) error {
// Authenticate authenticates a user // Authenticate authenticates a user
func (m *testDBRepo) Authenticate(email, testPassword string) (int, string, error) { func (m *testDBRepo) Authenticate(email, testPassword string) (int, string, error) {
return 1, "", nil if email == "a@b.c" {
return 1, "", nil
}
return 0, "", errors.New("deliberate errors")
} }
// AllReservations returns a slice of all reservations // AllReservations returns a slice of all reservations
@ -102,3 +105,39 @@ func (m *testDBRepo) GetReservationByID(id int) (models.Reservation, error) {
return res, nil return res, nil
} }
// UpdateReservation updates a user in the database
func (m *testDBRepo) UpdateReservation(r models.Reservation) error {
return nil
}
func (m *testDBRepo) DeleteReservation(id int) error {
return nil
}
// UpdateProcessedForReservation set processed for a reservation
func (m *testDBRepo) UpdateProcessedForReservation(id, processed int) error {
return nil
}
func (m *testDBRepo) AllRooms() ([]models.Room, error) {
var rooms []models.Room
return rooms, nil
}
// GetRestrictionsForRoomByDate returns restrictions for a room by date range
func (m *testDBRepo) GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error) {
var restrictions []models.RoomRestriction
return restrictions, nil
}
// InsertBlockForRoom inserts a room restriction
func (m *testDBRepo) InsertBlockForRoom(id int, startDate time.Time) error {
return nil
}
// DeleteBlockByID deletes a block by ID
func (m *testDBRepo) DeleteBlockByID(id int) error {
return nil
}

View File

@ -19,4 +19,11 @@ type DatabaseRepo interface {
AllReservations() ([]models.Reservation, error) AllReservations() ([]models.Reservation, error)
AllNewReservations() ([]models.Reservation, error) AllNewReservations() ([]models.Reservation, error)
GetReservationByID(id int) (models.Reservation, error) GetReservationByID(id int) (models.Reservation, error)
UpdateReservation(r models.Reservation) error
DeleteReservation(id int) error
UpdateProcessedForReservation(id, processed int) error
AllRooms() ([]models.Room, error)
GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error)
InsertBlockForRoom(id int, startDate time.Time) error
DeleteBlockByID(id int) error
} }

2
run.sh
View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
go build -o bookings cmd/web/*go && ./bookings go build -o bookings cmd/web/*go && ./bookings -cache=false -production=false

View File

@ -97,12 +97,6 @@ function Prompt() {
c.didOpen(); c.didOpen();
} }
}, },
preConfirm: () => {
return [
document.getElementById('start').value,
document.getElementById('end').value,
]
},
}) })
if (c.callback !== undefined) { if (c.callback !== undefined) {

View File

@ -26,7 +26,7 @@ All Reservations
<tr> <tr>
<td>{{ .ID}}</td> <td>{{ .ID}}</td>
<td> <td>
<a href="/admin/reservations/all/{{.ID}}"> <a href="/admin/reservations/all/{{.ID}}/show">
{{ .LastName}} {{ .LastName}}
</a> </a>
</td> </td>

View File

@ -26,7 +26,7 @@ New Reservations
<tr> <tr>
<td>{{ .ID}}</td> <td>{{ .ID}}</td>
<td> <td>
<a href="/admin/reservations/new/{{.ID}}"> <a href="/admin/reservations/new/{{.ID}}/show">
{{ .LastName}} {{ .LastName}}
</a> </a>
</td> </td>

View File

@ -5,7 +5,73 @@ Reservations Calendar
{{end}} {{end}}
{{define "content"}} {{define "content"}}
{{$now := index .Data "now"}}
{{$rooms := index .Data "rooms"}}
{{$dim := index .IntMap "days_in_month"}}
{{$curMonth := index .StringMap "this_month"}}
{{$curYear := index .StringMap "this_month_year"}}
<div class="col-md-12"> <div class="col-md-12">
Reservations calendar content <div class="text-center">
<h3>{{formatDate $now "January"}} {{formatDate $now "2006"}}</h3>
</div>
<div class="float-start">
<a class="btn btn-sm btn-outline-secondary"
href="/admin/reservations-calendar?y={{index .StringMap "last_month_year"}}&m={{index .StringMap "last_month"}}">&lt;&lt;</a>
</div>
<div class="float-end">
<a class="btn btn-sm btn-outline-secondary"
href="/admin/reservations-calendar?y={{index .StringMap "next_month_year"}}&m={{index .StringMap "next_month"}}">&gt;&gt;</a>
</div>
<div class="clearfix"></div>
<form method="post" action="/admin/reservations-calendar">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="m" value="{{index .StringMap "this_month"}}">
<input type="hidden" name="y" value="{{index .StringMap "this_month_year"}}">
{{range $rooms}}
{{$roomID := .ID}}
{{$blocks := index $.Data (printf "block_map_%d" .ID)}}
{{$reservations := index $.Data (printf "reservation_map_%d" .ID)}}
<h4 class="mt-4">{{.RoomName}}</h4>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<tr class="table-dark">
{{range $index := iterate $dim}}
<td class="text-center">
{{add $index 1}}
</td>
{{end}}
</tr>
<tr class="table-light">
{{range $index := iterate $dim}}
<td class="text-center">
{{if gt (index $reservations (printf "%s-%s-%d" $curYear $curMonth (add $index 1))) 0}}
<a href="/admin/reservations/cal/{{index $reservations (printf "%s-%s-%d" $curYear $curMonth (add $index 1))}}/show?y={{$curYear}}&m={{$curMonth}}">
<span class="text-danger">R</span>
</a>
{{else}}
<input
{{if gt (index $blocks (printf "%s-%s-%d" $curYear $curMonth (add $index 1))) 0}}
checked
name="remove_block_{{$roomID}}_{{printf "%s-%s-%d" $curYear $curMonth (add $index 1)}}"
value="{{index $blocks (printf "%s-%s-%d" $curYear $curMonth (add $index 1))}}"
{{else}}
name="add_block_{{$roomID}}_{{printf "%s-%s-%d" $curYear $curMonth (add $index 1)}}"
value="1"
{{end}}
type="checkbox">
{{end}}
</td>
{{end}}
</tr>
</table>
</div>
{{end}}
<hr>
<input type="submit" class="btn btn-primary" value="Save Changes">
</form>
</div> </div>
{{end}} {{end}}

View File

@ -15,6 +15,8 @@ Reservation
</p> </p>
<form method="post" action="/admin/reservations/{{$src}}/{{$res.ID}}" class="" novalidate> <form method="post" action="/admin/reservations/{{$src}}/{{$res.ID}}" class="" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="year" value="{{index .StringMap "year"}}">
<input type="hidden" name="month" value="{{index .StringMap "month"}}">
<div class="form-group mt-5"> <div class="form-group mt-5">
<label for="first_name">First name:</label> <label for="first_name">First name:</label>
@ -48,9 +50,54 @@ Reservation
<hr> <hr>
<input type="submit" class="btn btn-primary" value="Save"> <div class="float-start">
<a href="/admin/reservations-{{$src}}" class="btn btn-warning">Cancel</a> <input type="submit" class="btn btn-primary" value="Save">
{{if eq $src "cal"}}
<a href="#" onclick="window.history.go(-1)" class="btn btn-warning">Cancel</a>
{{else}}
<a href="/admin/reservations-{{$src}}" class="btn btn-warning">Cancel</a>
{{end}}
{{ if eq $res.Processed 0}}
<a href="#" class="btn btn-info" onclick="processRes({{$res.ID}})">Mark as Processed</a>
{{end}}
</div>
<div class="float-end">
<a href="#" class="btn btn-danger" onclick="deleteRes({{$res.ID}})">Delete</a>
</div>
<!-- clear the float -->
<div class="clearfix"></div>
</form> </form>
</div> </div>
{{end}} {{end}}
{{define "js"}}
{{$src := index .StringMap "src"}}
<script>
function processRes(id) {
attention.custom({
icon: 'warning',
msg: 'Are you sure?',
callback: function(result) {
if (result != false) {
window.location.href = "/admin/process-reservation/{{$src}}/" + id + "/do?y={{index .StringMap "year"}}&m={{index .StringMap "month"}}";
}
}
})
}
function deleteRes(id) {
attention.custom({
icon: 'error',
msg: 'Are you sure?',
callback: function(result) {
if (result != false) {
window.location.href = "/admin/delete-reservation/{{$src}}/" + id + "/do?y={{index .StringMap "year"}}&m={{index .StringMap "month"}}";
}
}
})
}
</script>
{{end}}

View File

@ -14,6 +14,7 @@
<!-- plugin css for this page --> <!-- plugin css for this page -->
<!-- End plugin css for this page --> <!-- End plugin css for this page -->
<!-- inject:css --> <!-- inject:css -->
<link rel="stylesheet" type="text/css" href="https://unpkg.com/notie/dist/notie.min.css">
<link rel="stylesheet" href="/static/admin/css/style.css"> <link rel="stylesheet" href="/static/admin/css/style.css">
<!-- endinject --> <!-- endinject -->
<link rel="shortcut icon" href="/static/admin/images/favicon.png" /> <link rel="shortcut icon" href="/static/admin/images/favicon.png" />
@ -25,6 +26,9 @@
label { label {
font-weight: bold; font-weight: bold;
} }
.notie-container {
z-index: 50000;
}
</style> </style>
{{block "css" .}} {{block "css" .}}
@ -58,7 +62,8 @@
<!-- partial --> <!-- partial -->
<div class="container-fluid page-body-wrapper"> <div class="container-fluid page-body-wrapper">
<!-- partial:partials/_sidebar.html --> <!-- partial:partials/_sidebar.html -->
<nav class="sidebar sidebar-offcanvas" id="sidebar"> <!-- TODO sidebar-offcanvas need to be repaired -->
<nav class="sidebar" id="sidebar">
<ul class="nav"> <ul class="nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/admin/dashboard"> <a class="nav-link" href="/admin/dashboard">
@ -132,8 +137,25 @@
<script src="/static/admin/js/todolist.js"></script> <script src="/static/admin/js/todolist.js"></script>
<!-- endinject --> <!-- endinject -->
<!-- Custom js for this page--> <!-- Custom js for this page-->
<script src="https://unpkg.com/notie"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="/static/js/app.js"></script>
<script src="/static/admin/js/dashboard.js"></script> <script src="/static/admin/js/dashboard.js"></script>
<!-- End custom js for this page--> <!-- End custom js for this page-->
<script>
let attention = Prompt()
{{with .Error}}
notify("{{.}}", "error")
{{end}}
{{with .Warning}}
notify("{{.}}", "warning")
{{end}}
{{with .Flash}}
notify("{{.}}", "success")
{{end}}
</script>
{{block "js" .}} {{block "js" .}}
{{end}} {{end}}