Compare commits
62 Commits
ab02f8e635
...
sqlc
Author | SHA1 | Date | |
---|---|---|---|
dd49a1e687 | |||
459a4e5c7d | |||
cec183b416 | |||
f20f256313 | |||
30c552cf6c | |||
3eab2a3c9f | |||
5d225f0186 | |||
52c3679158 | |||
aca8605870 | |||
6631288843 | |||
99c2eec759 | |||
623291541a | |||
d7f7a2d8d9 | |||
14828fb901 | |||
7894a05daf | |||
b97c6cba5c | |||
a7cf9fe4f0 | |||
5987fadb03 | |||
fcd29cc082 | |||
ca1e72c676 | |||
d4cf44bb22 | |||
845f3a5836 | |||
dafd6f21c0 | |||
f93388a8e9 | |||
b34c217d98 | |||
ea796999d4 | |||
51e3c74950 | |||
dedab91898 | |||
68d9110cd2 | |||
96f81418ec | |||
a0853cf880 | |||
a6dca00199 | |||
d32fd432d2 | |||
3e70a5727f | |||
447bfc4599 | |||
c88974788f | |||
b1d3095c89 | |||
d40233b4ba | |||
bf61fb7c18 | |||
998850b708 | |||
191b010b8e | |||
6853a1a483 | |||
e8390cc51d | |||
4debe0e14e | |||
3f1dfeabc2 | |||
4ec8c5d9a1 | |||
904ee95d63 | |||
413bfc1685 | |||
d76070c21d | |||
262b48161d | |||
5d670c1dc5 | |||
0f17b3405e | |||
f67aed4942 | |||
1c46c5a64b | |||
32250d92c4 | |||
e2e03307b7 | |||
9775b67a2d | |||
fed901ce25 | |||
5eb9284b6f | |||
e0a29d21c3 | |||
95cddfc950 | |||
5a646a2b27 |
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
**/c.html
|
||||
bookings
|
||||
database.yml
|
||||
postgres/docker-compose.yml
|
||||
|
@ -2,19 +2,23 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/driver"
|
||||
"go-udemy-web-1/internal/handlers"
|
||||
"go-udemy-web-1/internal/helpers"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"go-udemy-web-1/internal/render"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
"go-udemy-web-1/internal/repository/driverrepo"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/redisstore"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const portNumber = ":8080"
|
||||
@ -33,7 +37,9 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.SQL.Close()
|
||||
defer close(app.MailChan)
|
||||
|
||||
fmt.Println("Starting mail listener...")
|
||||
fmt.Printf("Starting application on port %s\n", portNumber)
|
||||
|
||||
srv := &http.Server{
|
||||
@ -45,17 +51,48 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
func run() (*driver.DB, error) {
|
||||
func run() (*repository.DB, error) {
|
||||
// what am I going to put in the session
|
||||
gob.Register(models.Reservation{})
|
||||
gob.Register(models.User{})
|
||||
gob.Register(models.Room{})
|
||||
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)
|
||||
app.MailChan = mailChan
|
||||
|
||||
listenForMail()
|
||||
|
||||
// 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.Store = redisstore.New(pool)
|
||||
session.Lifetime = 24 * time.Hour
|
||||
session.Cookie.Persist = true
|
||||
session.Cookie.SameSite = http.SameSiteLaxMode
|
||||
@ -65,8 +102,10 @@ func run() (*driver.DB, error) {
|
||||
|
||||
// connect 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"))
|
||||
db, err := driver.ConnectSQL(dsn)
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
|
||||
*dbHost, *dbPort, *dbName, *dbUser, *dbPass, *dbSSL)
|
||||
dbdriver := driverrepo.NewSqlcRepo()
|
||||
db, err := dbdriver.ConnectSQL(dsn)
|
||||
if err != nil {
|
||||
log.Fatal("Cannot connect to database! Dying...")
|
||||
}
|
||||
@ -78,7 +117,7 @@ func run() (*driver.DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
app.TemplateCahce = tc
|
||||
app.UseCache = false
|
||||
app.UseCache = *useCache
|
||||
|
||||
infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
|
||||
app.InfoLog = infoLog
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/helpers"
|
||||
"net/http"
|
||||
|
||||
"github.com/justinas/nosurf"
|
||||
@ -34,3 +35,15 @@ func NoSurf(next http.Handler) http.Handler {
|
||||
func SessionLoad(next http.Handler) http.Handler {
|
||||
return session.LoadAndSave(next)
|
||||
}
|
||||
|
||||
func Auth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !helpers.IsAuthenticated(r) {
|
||||
session.Put(r.Context(), "error", "Log in first!")
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ func routes(app *config.AppConfig) http.Handler {
|
||||
|
||||
mux.Use(middleware.Recoverer)
|
||||
mux.Use(WriteToConsole)
|
||||
mux.Use(NoSurf)
|
||||
mux.Use(NoSurf) // XXX: Maybe this middleware makes that I don't have to r.ParseForm before Get the elements.
|
||||
mux.Use(SessionLoad)
|
||||
|
||||
mux.Get("/", handlers.Repo.Home)
|
||||
@ -28,9 +28,31 @@ func routes(app *config.AppConfig) http.Handler {
|
||||
mux.Get("/make-reservation", handlers.Repo.MakeReservation)
|
||||
mux.Post("/make-reservation", handlers.Repo.PostMakeReservation)
|
||||
mux.Get("/reservation-summary", handlers.Repo.ReservationSummary)
|
||||
mux.Get("/choose-room/{id}", handlers.Repo.ChooseRoom)
|
||||
mux.Get("/book-room", handlers.Repo.BookRoom)
|
||||
mux.Get("/user/login", handlers.Repo.ShowLogin)
|
||||
mux.Post("/user/login", handlers.Repo.PostShowLogin)
|
||||
mux.Get("/user/logout", handlers.Repo.Logout)
|
||||
|
||||
fileServer := http.FileServer(http.Dir("./static/"))
|
||||
mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
|
||||
|
||||
mux.Route("/admin", func(mux chi.Router) {
|
||||
if app.InProduction {
|
||||
mux.Use(Auth)
|
||||
}
|
||||
mux.Get("/dashboard", handlers.Repo.AdminDashboard)
|
||||
|
||||
mux.Get("/reservations-new", handlers.Repo.AdminNewReservations)
|
||||
mux.Get("/reservations-all", handlers.Repo.AdminAllReservations)
|
||||
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}/show", handlers.Repo.AdminShowReservation)
|
||||
mux.Post("/reservations/{src}/{id}", handlers.Repo.AdminPostShowReservation)
|
||||
})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
58
cmd/web/send-mail.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mail "github.com/xhit/go-simple-mail"
|
||||
)
|
||||
|
||||
func listenForMail() {
|
||||
go func() {
|
||||
for {
|
||||
msg := <-app.MailChan
|
||||
sendMsg(msg)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func sendMsg(m models.MailData) {
|
||||
server := mail.NewSMTPClient()
|
||||
server.Host = "localhost"
|
||||
server.Port = 1025 // fake port
|
||||
server.KeepAlive = false
|
||||
server.ConnectTimeout = 10 * time.Second
|
||||
server.SendTimeout = 10 * time.Second
|
||||
|
||||
client, err := server.Connect()
|
||||
if err != nil {
|
||||
errorLog.Println(err)
|
||||
}
|
||||
|
||||
email := mail.NewMSG()
|
||||
email.SetFrom(m.From).AddTo(m.To).SetSubject(m.Subject)
|
||||
if m.Template == "" {
|
||||
email.SetBody(mail.TextHTML, m.Content)
|
||||
} else {
|
||||
data, err := os.ReadFile(fmt.Sprintf("./email-templates/%s", m.Template))
|
||||
if err != nil {
|
||||
app.ErrorLog.Println(err)
|
||||
email.SetBody(mail.TextHTML, m.Content)
|
||||
} else {
|
||||
mailTemplate := string(data)
|
||||
msgToSend := strings.Replace(mailTemplate, "[%body%]", m.Content, 1)
|
||||
email.SetBody(mail.TextHTML, msgToSend)
|
||||
}
|
||||
}
|
||||
|
||||
err = email.Send(client)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
log.Println("Email sent")
|
||||
}
|
||||
}
|
32
docker/docker-compose-example.yml
Normal 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
|
31
docker/docker-compose.yml
Normal file
@ -0,0 +1,31 @@
|
||||
# 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
|
1524
email-templates/drip.html
Normal file
8
go.mod
@ -2,11 +2,12 @@ module go-udemy-web-1
|
||||
|
||||
go 1.21.0
|
||||
|
||||
// github.com/CloudyKit/jet --> Check this later
|
||||
// Template: github.com/CloudyKit/jet --> Check this later
|
||||
//
|
||||
// ORM packages:
|
||||
// upper/db https://github.com/upper/db
|
||||
// gorm.io/gorm
|
||||
// TODO Checkout sqlc and sqlx
|
||||
|
||||
// TODO Use default http package to rewrite the project with go 1.22.1
|
||||
|
||||
@ -17,9 +18,13 @@ require github.com/justinas/nosurf v1.1.1
|
||||
require github.com/alexedwards/scs/v2 v2.8.0
|
||||
|
||||
require (
|
||||
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885
|
||||
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/pgx/v5 v5.6.0
|
||||
github.com/xhit/go-simple-mail v2.2.2+incompatible
|
||||
golang.org/x/crypto v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -29,7 +34,6 @@ require (
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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/text v0.16.0 // indirect
|
||||
)
|
||||
|
12
go.sum
@ -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/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||
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/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/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.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
@ -33,9 +38,12 @@ 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/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.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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
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/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/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
|
@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/internal/models"
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
@ -15,4 +16,5 @@ type AppConfig struct {
|
||||
InfoLog *log.Logger
|
||||
ErrorLog *log.Logger
|
||||
Session *scs.SessionManager
|
||||
MailChan chan models.MailData
|
||||
}
|
||||
|
@ -2,17 +2,21 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/driver"
|
||||
"go-udemy-web-1/internal/forms"
|
||||
"go-udemy-web-1/internal/helpers"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"go-udemy-web-1/internal/render"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
"go-udemy-web-1/internal/repository/dbrepo"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// Repo the repository used by the handlers
|
||||
@ -25,10 +29,18 @@ type Repository struct {
|
||||
}
|
||||
|
||||
// NewRepo creates a new repository
|
||||
func NewRepo(a *config.AppConfig, db *driver.DB) *Repository {
|
||||
func NewRepo(a *config.AppConfig, db *repository.DB) *Repository {
|
||||
return &Repository{
|
||||
App: a,
|
||||
DB: dbrepo.NewPostgresRepo(db.SQL, a),
|
||||
DB: dbrepo.NewPgcRepo(db.PG, a),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestRepo creates a new testing repository
|
||||
func NewTestRepo(a *config.AppConfig) *Repository {
|
||||
return &Repository{
|
||||
App: a,
|
||||
DB: dbrepo.NewTestingRepo(a),
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,56 +79,53 @@ func (m *Repository) Majors(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) {
|
||||
// For the first time render emptyReservation so that this object is
|
||||
// filled with the info when sent back.
|
||||
var emptyReservation models.Reservation
|
||||
data := make(map[string]interface{})
|
||||
res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
if !ok {
|
||||
m.App.Session.Put(r.Context(), "error", "can't get reservation from session")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
data["reservation"] = emptyReservation
|
||||
room, err := m.DB.GetRoomById(res.RoomID)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't find room")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
res.Room.RoomName = room.RoomName
|
||||
m.App.Session.Put(r.Context(), "reservation", res)
|
||||
|
||||
sd := res.StartDate.Format("2006-01-02")
|
||||
ed := res.EndDate.Format("2006-01-02")
|
||||
stringMap := make(map[string]string)
|
||||
stringMap["start_date"] = sd
|
||||
stringMap["end_date"] = ed
|
||||
|
||||
data := make(map[string]interface{})
|
||||
data["reservation"] = res
|
||||
|
||||
render.Template(w, r, "make-reservation.page.tmpl", &models.TemplateData{
|
||||
Form: forms.New(nil),
|
||||
Data: data,
|
||||
StringMap: stringMap,
|
||||
})
|
||||
}
|
||||
|
||||
// PostMakeReservation is the make reservation page post handler
|
||||
func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
reservation, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
if !ok {
|
||||
m.App.Session.Put(r.Context(), "error", "can't get reservation from session")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
sd := r.Form.Get("start_date")
|
||||
ed := r.Form.Get("end_date")
|
||||
|
||||
// 2024-07-08 -- 01/02 03:04:05PM '06 -0700
|
||||
layout := "2006-01-02"
|
||||
startDate, err := time.Parse(layout, sd)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't parse form")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(layout, ed)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
roomId, err := strconv.Atoi(r.Form.Get("room_id"))
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
reservation := models.Reservation{
|
||||
FirstName: r.Form.Get("first_name"),
|
||||
LastName: r.Form.Get("last_name"),
|
||||
Email: r.Form.Get("email"),
|
||||
Phone: r.Form.Get("phone"),
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
RoomID: roomId,
|
||||
}
|
||||
|
||||
form := forms.New(r.PostForm)
|
||||
|
||||
@ -124,38 +133,90 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
|
||||
form.MinLength("first_name", 2)
|
||||
form.IsEmail("email")
|
||||
|
||||
reservation.FirstName = form.Get("first_name")
|
||||
reservation.LastName = form.Get("last_name")
|
||||
reservation.Email = form.Get("email")
|
||||
reservation.Phone = form.Get("phone")
|
||||
|
||||
if !form.Valid() {
|
||||
data := make(map[string]interface{})
|
||||
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{
|
||||
Data: data,
|
||||
Form: form,
|
||||
StringMap: stringMap,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
newReservationID, err := m.DB.InsertReservation(reservation)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
m.App.Session.Put(r.Context(), "error", "can't insert reservation into database")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
m.App.Session.Put(r.Context(), "reservation", reservation)
|
||||
|
||||
restriction := models.RoomRestriction{
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
ID: 0,
|
||||
RoomID: roomId,
|
||||
StartDate: reservation.StartDate,
|
||||
EndDate: reservation.EndDate,
|
||||
ID: reservation.ID,
|
||||
RoomID: reservation.RoomID,
|
||||
ReservationID: newReservationID,
|
||||
RestrictionID: 1,
|
||||
}
|
||||
|
||||
err = m.DB.InsertRoomRestriction(restriction)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
m.App.Session.Put(r.Context(), "error", "can't insert room restriction into database")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// send notif to guest
|
||||
htmlMessage := fmt.Sprintf(`
|
||||
<strong>Reservation Confirmation</strong><br>
|
||||
Dear %s: <br>
|
||||
This is to confirm your reservation from %s to %s.
|
||||
|
||||
`, reservation.FirstName, reservation.StartDate.Format("2006-01-02"),
|
||||
reservation.EndDate.Format("2006-01-02"))
|
||||
|
||||
msg := models.MailData{
|
||||
To: reservation.Email,
|
||||
From: "me@here.com",
|
||||
Subject: "Reservation Confirmation",
|
||||
Content: htmlMessage,
|
||||
Template: "drip.html",
|
||||
}
|
||||
|
||||
m.App.MailChan <- msg
|
||||
|
||||
// send notif to property owner
|
||||
htmlMessage = fmt.Sprintf(`
|
||||
<strong>Reservation Notification</strong><br>
|
||||
A reservation has been made for %s from %s to %s.
|
||||
|
||||
`, reservation.Room.RoomName, reservation.StartDate.Format("2006-01-02"),
|
||||
reservation.EndDate.Format("2006-01-02"))
|
||||
|
||||
msg = models.MailData{
|
||||
To: "me@here.com",
|
||||
From: "me@here.com",
|
||||
Subject: "Reservation Notification",
|
||||
Content: htmlMessage,
|
||||
}
|
||||
|
||||
m.App.MailChan <- msg
|
||||
|
||||
m.App.Session.Put(r.Context(), "reservation", reservation)
|
||||
|
||||
http.Redirect(w, r, "/reservation-summary", http.StatusSeeOther)
|
||||
@ -167,7 +228,7 @@ func (m *Repository) ReservationSummary(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
m.App.ErrorLog.Println("connot get item 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")
|
||||
@ -175,8 +236,16 @@ func (m *Repository) ReservationSummary(w http.ResponseWriter, r *http.Request)
|
||||
data := make(map[string]interface{})
|
||||
data["reservation"] = reservation
|
||||
|
||||
sd := reservation.StartDate.Format("2006-01-02")
|
||||
ed := reservation.EndDate.Format("2006-01-02")
|
||||
stringMap := map[string]string{
|
||||
"start_date": sd,
|
||||
"end_date": ed,
|
||||
}
|
||||
|
||||
render.Template(w, r, "reservation-summary.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
StringMap: stringMap,
|
||||
})
|
||||
}
|
||||
|
||||
@ -187,24 +256,32 @@ func (m *Repository) Availability(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// PostAvailability is the search for availability page handler
|
||||
func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't parse form")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
start := r.Form.Get("start")
|
||||
end := r.Form.Get("end")
|
||||
|
||||
layout := "2006-01-02"
|
||||
startDate, err := time.Parse(layout, start)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse start date")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(layout, end)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse end date")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
rooms, err := m.DB.SearchAvailabilityForAllRooms(startDate, endDate)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
m.App.Session.Put(r.Context(), "error", "Can't connect to database")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -216,7 +293,8 @@ func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
// No availability
|
||||
m.App.InfoLog.Println("No availability")
|
||||
m.App.Session.Put(r.Context(), "error", "No availability")
|
||||
http.Redirect(w, r, "/search-availability", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
@ -234,25 +312,509 @@ func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
type responseJSON struct {
|
||||
OK string `json:"ok"`
|
||||
type jsonResponse struct {
|
||||
Message string `json:"message"`
|
||||
RoomID string `json:"room_id"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
// AvailabilityJSON is the search for availability page handler
|
||||
func (m *Repository) AvailabilityJSON(w http.ResponseWriter, r *http.Request) {
|
||||
resp := responseJSON{
|
||||
OK: "true",
|
||||
Message: "Available!",
|
||||
if err := r.ParseForm(); err != nil {
|
||||
// can't parse form, so return appropriate json
|
||||
resp := jsonResponse{
|
||||
OK: false,
|
||||
Message: "Internal server error",
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(resp, "", " ")
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(out)
|
||||
return
|
||||
}
|
||||
sd := r.Form.Get("start")
|
||||
ed := r.Form.Get("end")
|
||||
|
||||
layout := "2006-01-02"
|
||||
startDate, err := time.Parse(layout, sd)
|
||||
if err != nil {
|
||||
resp := jsonResponse{
|
||||
OK: false,
|
||||
Message: "Wrong startDate",
|
||||
}
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(out)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(layout, ed)
|
||||
if err != nil {
|
||||
resp := jsonResponse{
|
||||
OK: false,
|
||||
Message: "Wrong endDate",
|
||||
}
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(out)
|
||||
return
|
||||
}
|
||||
|
||||
roomID, err := strconv.Atoi(r.Form.Get("room_id"))
|
||||
if err != nil {
|
||||
resp := jsonResponse{
|
||||
OK: false,
|
||||
Message: "Wrong roomID",
|
||||
}
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(out)
|
||||
return
|
||||
}
|
||||
|
||||
available, err := m.DB.SearchAvailabilityByDatesByRoomID(startDate, endDate, roomID)
|
||||
if err != nil {
|
||||
resp := jsonResponse{
|
||||
OK: false,
|
||||
Message: "Error connecting to database",
|
||||
}
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(out)
|
||||
return
|
||||
}
|
||||
|
||||
resp := jsonResponse{
|
||||
OK: available,
|
||||
Message: "",
|
||||
StartDate: sd,
|
||||
EndDate: ed,
|
||||
RoomID: strconv.Itoa(roomID),
|
||||
}
|
||||
|
||||
// No error check because all aspects of the json are handled
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(out)
|
||||
}
|
||||
|
||||
// ChooseRoom displays list of available rooms
|
||||
func (m *Repository) ChooseRoom(w http.ResponseWriter, r *http.Request) {
|
||||
exploded := strings.Split(r.URL.RequestURI(), "/")
|
||||
roomID, err := strconv.Atoi(exploded[2])
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse roomID")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
m.App.Session.Get(r.Context(), "reservation")
|
||||
|
||||
res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
if !ok {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't get reservation from session")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
res.RoomID = roomID
|
||||
m.App.Session.Put(r.Context(), "reservation", res)
|
||||
|
||||
http.Redirect(w, r, "/make-reservation", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// BookRoom takes URL parameters, builds a sessional variable, and takes user to make reservation
|
||||
func (m *Repository) BookRoom(w http.ResponseWriter, r *http.Request) {
|
||||
roomID, _ := strconv.Atoi(r.URL.Query().Get("id"))
|
||||
sd := r.URL.Query().Get("s")
|
||||
ed := r.URL.Query().Get("e")
|
||||
|
||||
var res models.Reservation
|
||||
|
||||
layout := "2006-01-02"
|
||||
startDate, err := time.Parse(layout, sd)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse start date")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(layout, ed)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse end date")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
room, err := m.DB.GetRoomById(roomID)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse roomId")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
res.RoomID = roomID
|
||||
res.StartDate = startDate
|
||||
res.EndDate = endDate
|
||||
res.Room.RoomName = room.RoomName
|
||||
|
||||
m.App.Session.Put(r.Context(), "reservation", res)
|
||||
http.Redirect(w, r, "/make-reservation", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ShowLogin shows the login screen
|
||||
func (m *Repository) ShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "login.page.tmpl", &models.TemplateData{
|
||||
Form: forms.New(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// PostShowLogin handles logging the user in
|
||||
func (m *Repository) PostShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
_ = m.App.Session.RenewToken(r.Context())
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse form")
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
email := r.Form.Get("email")
|
||||
password := r.Form.Get("password")
|
||||
form := forms.New(r.PostForm)
|
||||
form.Required("email", "password")
|
||||
form.IsEmail("email")
|
||||
if !form.Valid() {
|
||||
render.Template(w, r, "login.page.tmpl", &models.TemplateData{
|
||||
Form: form,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id, _, err := m.DB.Authenticate(email, password)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
m.App.Session.Put(r.Context(), "error", "Invalid login credentials")
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
m.App.Session.Put(r.Context(), "user_id", id)
|
||||
m.App.Session.Put(r.Context(), "flash", "Logged in successfully")
|
||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// Logout logs a user out
|
||||
func (m *Repository) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
m.App.Session.Destroy(r.Context())
|
||||
m.App.Session.RenewToken(r.Context())
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (m *Repository) AdminDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "admin-dashboard.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// AdminNewReservations shows all new reservations in admin tool
|
||||
func (m *Repository) AdminNewReservations(w http.ResponseWriter, r *http.Request) {
|
||||
reservations, err := m.DB.AllNewReservations()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
data["reservations"] = reservations
|
||||
|
||||
render.Template(w, r, "admin-new-reservations.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminNewReservations shows all reservations in admin tool
|
||||
func (m *Repository) AdminAllReservations(w http.ResponseWriter, r *http.Request) {
|
||||
reservations, err := m.DB.AllReservations()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
data["reservations"] = reservations
|
||||
|
||||
render.Template(w, r, "admin-all-reservations.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminShowReservation shows the detail of a reservation
|
||||
func (m *Repository) AdminShowReservation(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
|
||||
year := r.URL.Query().Get("y")
|
||||
month := r.URL.Query().Get("m")
|
||||
|
||||
stringMap["month"] = month
|
||||
stringMap["year"] = year
|
||||
|
||||
// get reservation from the database
|
||||
res, err := m.DB.GetReservationByID(id)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["reservation"] = res
|
||||
|
||||
render.Template(w, r, "admin-reservations-show.page.tmpl", &models.TemplateData{
|
||||
StringMap: stringMap,
|
||||
Data: data,
|
||||
Form: forms.New(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (m *Repository) AdminReservationsCalendar(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
@ -1,10 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postData struct {
|
||||
@ -12,34 +21,39 @@ type postData struct {
|
||||
value string
|
||||
}
|
||||
|
||||
// {{{ Test NewRepo
|
||||
|
||||
func Test_NewRepo(t *testing.T) {
|
||||
var db repository.DB
|
||||
repo := NewRepo(&app, &db)
|
||||
|
||||
if reflect.TypeOf(repo).String() != "*handlers.Repository" {
|
||||
t.Errorf("repo is of type %s instead of *handlers.Repository", reflect.TypeOf(repo).String())
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Simple get tests
|
||||
|
||||
var theTests = []struct {
|
||||
name string
|
||||
url string
|
||||
method string
|
||||
params []postData
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"home", "/", "GET", []postData{}, http.StatusOK},
|
||||
{"about", "/about", "GET", []postData{}, http.StatusOK},
|
||||
{"gq", "/generals-quarters", "GET", []postData{}, http.StatusOK},
|
||||
{"ms", "/majors-suite", "GET", []postData{}, http.StatusOK},
|
||||
{"sa", "/availability", "GET", []postData{}, http.StatusOK},
|
||||
{"contact", "/contact", "GET", []postData{}, http.StatusOK},
|
||||
{"ma", "/make-reservation", "GET", []postData{}, http.StatusOK},
|
||||
{"post-search-avail", "/availability", "POST", []postData{
|
||||
{key: "start", value: "2020-01-01"},
|
||||
{key: "end", value: "2020-01-02"},
|
||||
}, http.StatusOK},
|
||||
{"post-search-avail-json", "/availability-json", "POST", []postData{
|
||||
{key: "start", value: "2020-01-01"},
|
||||
{key: "end", value: "2020-01-02"},
|
||||
}, http.StatusOK},
|
||||
{"make-reservation", "/make-reservation", "POST", []postData{
|
||||
{key: "first_name", value: "John"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "me@here.com"},
|
||||
{key: "phone", value: "12345"},
|
||||
}, http.StatusOK},
|
||||
{"home", "/", "GET", http.StatusOK},
|
||||
{"about", "/about", "GET", http.StatusOK},
|
||||
{"gq", "/generals-quarters", "GET", http.StatusOK},
|
||||
{"ms", "/majors-suite", "GET", http.StatusOK},
|
||||
{"sa", "/availability", "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) {
|
||||
@ -48,28 +62,586 @@ func TestHandlers(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
for _, e := range theTests {
|
||||
if e.method == "GET" {
|
||||
resp, err := ts.Client().Get(ts.URL + e.url)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != e.expectedStatusCode {
|
||||
t.Errorf("for %s, expected %d but got %d", e.name, e.expectedStatusCode, resp.StatusCode)
|
||||
t.Errorf("for %s, expected %d but got %d\n", e.name, e.expectedStatusCode, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Make Reservation Tests
|
||||
|
||||
var makeReservationTests = []struct {
|
||||
name string
|
||||
roomID int
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", 1, http.StatusOK},
|
||||
{"no session", 0, http.StatusSeeOther},
|
||||
{"non-existant room", 100, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func TestRepository_MakeReservation(t *testing.T) {
|
||||
for _, test := range makeReservationTests {
|
||||
reservation := models.Reservation{
|
||||
RoomID: test.roomID,
|
||||
Room: models.Room{
|
||||
ID: test.roomID,
|
||||
RoomName: "Room name",
|
||||
},
|
||||
}
|
||||
req, _ := http.NewRequest("GET", "/make-reservation", nil)
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
rr := httptest.NewRecorder()
|
||||
if test.roomID == 0 {
|
||||
session.Put(ctx, "reservation", nil)
|
||||
} else {
|
||||
values := url.Values{}
|
||||
for _, x := range e.params {
|
||||
values.Add(x.key, x.value)
|
||||
session.Put(ctx, "reservation", reservation)
|
||||
}
|
||||
resp, err := ts.Client().PostForm(ts.URL+e.url, values)
|
||||
handler := http.HandlerFunc(Repo.MakeReservation)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != test.expectedStatusCode {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ PostMakeReservation tests
|
||||
|
||||
var postMakeReservationTests = []struct {
|
||||
name string
|
||||
reservationInfo []postData
|
||||
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"ok",
|
||||
[]postData{
|
||||
{key: "first_name", value: "John"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith.com"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "1"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
{
|
||||
"no_session",
|
||||
[]postData{
|
||||
{key: "first_name", value: "John"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith.com"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "0"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
{"no_post_data", []postData{}, http.StatusSeeOther},
|
||||
{
|
||||
"missing first name",
|
||||
[]postData{
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith.com"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "1"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusOK,
|
||||
},
|
||||
{
|
||||
"wrong first name",
|
||||
[]postData{
|
||||
{key: "first_name", value: "J"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith.com"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "1"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusOK,
|
||||
},
|
||||
{
|
||||
"wrong email",
|
||||
[]postData{
|
||||
{key: "first_name", value: "John"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "1"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusOK,
|
||||
},
|
||||
{
|
||||
"insert reservation error",
|
||||
[]postData{
|
||||
{key: "first_name", value: "John"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith.com"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "2"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
{
|
||||
"insert room restriction error",
|
||||
[]postData{
|
||||
{key: "first_name", value: "John"},
|
||||
{key: "last_name", value: "Smith"},
|
||||
{key: "email", value: "john@smith.com"},
|
||||
{key: "phone", value: "1234"},
|
||||
{key: "room_id", value: "100"},
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
}
|
||||
|
||||
func TestRepository_PostMakeReservation(t *testing.T) {
|
||||
for _, test := range postMakeReservationTests {
|
||||
roomID := 1
|
||||
reqBody := url.Values{}
|
||||
var req *http.Request
|
||||
if len(test.reservationInfo) > 0 {
|
||||
for _, element := range test.reservationInfo {
|
||||
reqBody.Add(element.key, element.value)
|
||||
if element.key == "room_id" {
|
||||
roomID, _ = strconv.Atoi(element.value)
|
||||
}
|
||||
}
|
||||
req, _ = http.NewRequest("POST", "/make-reservation", strings.NewReader(reqBody.Encode()))
|
||||
} else {
|
||||
req, _ = http.NewRequest("POST", "/make-reservation", nil)
|
||||
}
|
||||
layout := "2006-01-02"
|
||||
sd, _ := time.Parse(layout, "2050-01-01")
|
||||
ed, _ := time.Parse(layout, "2050-01-02")
|
||||
reservation := models.Reservation{
|
||||
RoomID: roomID,
|
||||
StartDate: sd,
|
||||
EndDate: ed,
|
||||
}
|
||||
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
if roomID == 0 {
|
||||
session.Put(ctx, "reservation", nil)
|
||||
} else {
|
||||
session.Put(ctx, "reservation", reservation)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(Repo.PostMakeReservation)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != test.expectedStatusCode {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, test.expectedStatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Test AvailabilityJSON
|
||||
|
||||
var availabilityJSONTests = []struct {
|
||||
name string
|
||||
queryInfo []postData
|
||||
expectedStatusJSON jsonResponse
|
||||
}{
|
||||
{"ok", []postData{
|
||||
{key: "start", value: "2050-01-01"},
|
||||
{key: "end", value: "2050-01-02"},
|
||||
{key: "room_id", value: "1"},
|
||||
}, jsonResponse{
|
||||
OK: true,
|
||||
Message: "",
|
||||
StartDate: "2050-01-01",
|
||||
EndDate: "2050-01-02",
|
||||
RoomID: "1",
|
||||
}},
|
||||
{"not available", []postData{
|
||||
{key: "start", value: "2050-01-01"},
|
||||
{key: "end", value: "2050-01-02"},
|
||||
{key: "room_id", value: "2"},
|
||||
}, jsonResponse{
|
||||
OK: false,
|
||||
Message: "",
|
||||
StartDate: "2050-01-01",
|
||||
EndDate: "2050-01-02",
|
||||
RoomID: "2",
|
||||
}},
|
||||
{"wrong start date", []postData{
|
||||
{key: "start", value: "2050-01"},
|
||||
{key: "end", value: "2050-01-02"},
|
||||
{key: "room_id", value: "1"},
|
||||
}, jsonResponse{
|
||||
OK: false,
|
||||
Message: "Wrong startDate",
|
||||
}},
|
||||
{"wrong end date", []postData{
|
||||
{key: "start", value: "2050-01-01"},
|
||||
{key: "end", value: "wrong"},
|
||||
{key: "room_id", value: "1"},
|
||||
}, jsonResponse{
|
||||
OK: false,
|
||||
Message: "Wrong endDate",
|
||||
}},
|
||||
{"wrong room id", []postData{
|
||||
{key: "start", value: "2050-01-01"},
|
||||
{key: "end", value: "2050-01-02"},
|
||||
{key: "room_id", value: "x"},
|
||||
}, jsonResponse{
|
||||
OK: false,
|
||||
Message: "Wrong roomID",
|
||||
}},
|
||||
{"not available", []postData{
|
||||
{key: "start", value: "2050-01-01"},
|
||||
{key: "end", value: "2050-01-02"},
|
||||
{key: "room_id", value: "100"},
|
||||
}, jsonResponse{
|
||||
OK: false,
|
||||
Message: "Error connecting to database",
|
||||
}},
|
||||
{"no form", []postData{}, jsonResponse{
|
||||
OK: false,
|
||||
Message: "Internal server error",
|
||||
}},
|
||||
}
|
||||
|
||||
func Test_AvailabilityJSON(t *testing.T) {
|
||||
for _, test := range availabilityJSONTests {
|
||||
var req *http.Request
|
||||
reqBody := url.Values{}
|
||||
if len(test.queryInfo) > 0 {
|
||||
for _, element := range test.queryInfo {
|
||||
reqBody.Add(element.key, element.value)
|
||||
}
|
||||
|
||||
req, _ = http.NewRequest("POST", "/make-reservation", strings.NewReader(reqBody.Encode()))
|
||||
} else {
|
||||
req, _ = http.NewRequest("POST", "/make-reservation", nil)
|
||||
}
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(Repo.AvailabilityJSON)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var j jsonResponse
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &j)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fatal(err)
|
||||
t.Errorf("for %s, failed to parse json", test.name)
|
||||
}
|
||||
if resp.StatusCode != e.expectedStatusCode {
|
||||
t.Errorf("for %s, expected %d but got %d", e.name, e.expectedStatusCode, resp.StatusCode)
|
||||
if j != test.expectedStatusJSON {
|
||||
expected, _ := json.MarshalIndent(test.expectedStatusJSON, "", " ")
|
||||
t.Errorf("for %s, returned json is wrong, expected: %s, returned: %s",
|
||||
test.name, expected, rr.Body.String())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Test ReservationSummary
|
||||
|
||||
var reservationSummaryTests = []struct {
|
||||
name string
|
||||
haveSession bool
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", true, http.StatusOK},
|
||||
{"nok", false, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_ReservationSummary(t *testing.T) {
|
||||
for _, test := range reservationSummaryTests {
|
||||
req, _ := http.NewRequest("GET", "/reservation-summary", nil)
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
layout := "2006-01-02"
|
||||
sd, _ := time.Parse(layout, "2050-01-01")
|
||||
ed, _ := time.Parse(layout, "2050-01-02")
|
||||
reservation := models.Reservation{
|
||||
StartDate: sd,
|
||||
EndDate: ed,
|
||||
FirstName: "John",
|
||||
LastName: "Smith",
|
||||
Email: "john@smith.com",
|
||||
RoomID: 1,
|
||||
}
|
||||
|
||||
if test.haveSession {
|
||||
session.Put(req.Context(), "reservation", reservation)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(Repo.ReservationSummary)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != test.expectedStatusCode {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, test.expectedStatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Test PostAvailability
|
||||
|
||||
var postAvailabilityTests = []struct {
|
||||
name string
|
||||
queryInfo []postData
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", []postData{
|
||||
{key: "start", value: "2050-01-01"},
|
||||
{key: "end", value: "2050-01-02"},
|
||||
}, http.StatusOK},
|
||||
{"database error", []postData{
|
||||
{key: "start", value: "2050-01-03"},
|
||||
{key: "end", value: "2050-01-04"},
|
||||
}, http.StatusSeeOther},
|
||||
{"no availability", []postData{
|
||||
{key: "start", value: "2050-01-05"},
|
||||
{key: "end", value: "2050-01-06"},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong start date", []postData{
|
||||
{key: "start", value: "2050-05"},
|
||||
{key: "end", value: "2050-01-06"},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong end date", []postData{
|
||||
{key: "start", value: "2050-01-05"},
|
||||
{key: "end", value: "01-06"},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong end date", []postData{}, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_PostAvailability(t *testing.T) {
|
||||
for _, test := range postAvailabilityTests {
|
||||
var req *http.Request
|
||||
if len(test.queryInfo) > 0 {
|
||||
reqBody := url.Values{}
|
||||
for _, element := range test.queryInfo {
|
||||
reqBody.Add(element.key, element.value)
|
||||
}
|
||||
|
||||
req, _ = http.NewRequest("POST", "/availability", strings.NewReader(reqBody.Encode()))
|
||||
} else {
|
||||
req, _ = http.NewRequest("POST", "/availability", nil)
|
||||
}
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(Repo.PostAvailability)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != test.expectedStatusCode {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, test.expectedStatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Test ChooseRoom
|
||||
|
||||
var chooseRoomTests = []struct {
|
||||
name string
|
||||
request string
|
||||
haveSession bool
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", "/choose-room/1", true, http.StatusSeeOther},
|
||||
{"wrong room id", "/choose-room/1wrong", true, http.StatusSeeOther},
|
||||
{"no session", "/choose-room/1", false, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_ChooseRoom(t *testing.T) {
|
||||
for _, test := range chooseRoomTests {
|
||||
req, _ := http.NewRequest("GET", test.request, nil)
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
layout := "2006-01-02"
|
||||
sd, _ := time.Parse(layout, "2050-01-01")
|
||||
ed, _ := time.Parse(layout, "2050-01-02")
|
||||
reservation := models.Reservation{
|
||||
StartDate: sd,
|
||||
EndDate: ed,
|
||||
}
|
||||
|
||||
if test.haveSession {
|
||||
session.Put(req.Context(), "reservation", reservation)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(Repo.ChooseRoom)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != test.expectedStatusCode {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, test.expectedStatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Test BookRoom
|
||||
|
||||
var bookRoomTests = []struct {
|
||||
name string
|
||||
queryInfo []postData
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", []postData{
|
||||
{key: "id", value: "1"},
|
||||
{key: "s", value: "2050-01-01"},
|
||||
{key: "e", value: "2050-01-02"},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong start date", []postData{
|
||||
{key: "id", value: "1"},
|
||||
{key: "s", value: "20-01-01"},
|
||||
{key: "e", value: "2050-01-02"},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong end date", []postData{
|
||||
{key: "id", value: "1"},
|
||||
{key: "s", value: "2050-01-01"},
|
||||
{key: "e", value: "2050-0-02"},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong room id", []postData{
|
||||
{key: "id", value: "w"},
|
||||
{key: "s", value: "2050-01-01"},
|
||||
{key: "e", value: "2050-01-02"},
|
||||
}, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_BookRoom(t *testing.T) {
|
||||
for _, test := range bookRoomTests {
|
||||
request := "/book-room?"
|
||||
for _, element := range test.queryInfo {
|
||||
request += element.key + "=" + element.value + "&"
|
||||
}
|
||||
request = request[:len(request)-1]
|
||||
req, _ := http.NewRequest("GET", request, nil)
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(Repo.BookRoom)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != test.expectedStatusCode {
|
||||
t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
|
||||
test.name, rr.Code, test.expectedStatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ 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
|
||||
|
||||
func getCtx(req *http.Request) context.Context {
|
||||
ctx, err := session.Load(req.Context(), req.Header.Get("X-Session"))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
@ -11,23 +11,32 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
var functions = template.FuncMap{}
|
||||
var functions = template.FuncMap{
|
||||
"humanDate": render.HumanDate,
|
||||
"formatDate": render.FormatDate,
|
||||
"iterate": render.Iterate,
|
||||
"add": render.Add,
|
||||
}
|
||||
|
||||
var (
|
||||
app config.AppConfig
|
||||
session *scs.SessionManager
|
||||
)
|
||||
|
||||
func getRoutes() http.Handler {
|
||||
func TestMain(m *testing.M) {
|
||||
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
|
||||
app.InProduction = false
|
||||
|
||||
@ -39,6 +48,12 @@ func getRoutes() http.Handler {
|
||||
|
||||
app.Session = session
|
||||
|
||||
mailChan := make(chan models.MailData)
|
||||
app.MailChan = mailChan
|
||||
defer close(mailChan)
|
||||
|
||||
listenForMail()
|
||||
|
||||
tc, err := CreateTestTemplateCache()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create template cache: %s", err)
|
||||
@ -52,15 +67,19 @@ func getRoutes() http.Handler {
|
||||
errorLog := log.New(os.Stdout, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
app.ErrorLog = errorLog
|
||||
|
||||
repo := NewRepo(&app)
|
||||
repo := NewTestRepo(&app)
|
||||
NewHandlers(repo)
|
||||
|
||||
render.NewRenderer(&app)
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func getRoutes() http.Handler {
|
||||
mux := chi.NewMux()
|
||||
|
||||
mux.Use(middleware.Recoverer)
|
||||
// mux.Use(NoSurf) // No need to test
|
||||
mux.Use(WriteToConsole)
|
||||
mux.Use(SessionLoad)
|
||||
|
||||
mux.Get("/", Repo.Home)
|
||||
@ -75,6 +94,25 @@ func getRoutes() http.Handler {
|
||||
mux.Post("/make-reservation", Repo.PostMakeReservation)
|
||||
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/"))
|
||||
mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
|
||||
|
||||
@ -90,20 +128,6 @@ func WriteToConsole(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// NoSurf adds CSRF protection to all POST requests
|
||||
func NoSurf(next http.Handler) http.Handler {
|
||||
csrfHandler := nosurf.New(next)
|
||||
|
||||
csrfHandler.SetBaseCookie(http.Cookie{
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: app.InProduction,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
return csrfHandler
|
||||
}
|
||||
|
||||
// SessionLoad loads and saves the session on every request
|
||||
func SessionLoad(next http.Handler) http.Handler {
|
||||
return session.LoadAndSave(next)
|
||||
@ -145,3 +169,11 @@ func CreateTestTemplateCache() (map[string]*template.Template, error) {
|
||||
|
||||
return myCache, nil
|
||||
}
|
||||
|
||||
func listenForMail() {
|
||||
go func() {
|
||||
for {
|
||||
<-app.MailChan
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -24,3 +24,7 @@ func ServerError(w http.ResponseWriter, err error) {
|
||||
app.ErrorLog.Println(trace)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func IsAuthenticated(r *http.Request) bool {
|
||||
return app.Session.Exists(r.Context(), "user_id")
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User is the user model
|
||||
type User struct {
|
||||
@ -43,6 +45,7 @@ type Reservation struct {
|
||||
Room Room
|
||||
ID int
|
||||
RoomID int
|
||||
Processed int
|
||||
}
|
||||
|
||||
// RoomRestriction is the room restriction model
|
||||
@ -58,3 +61,12 @@ type RoomRestriction struct {
|
||||
ReservationID int
|
||||
RestrictionID int
|
||||
}
|
||||
|
||||
// MailData holds an email message
|
||||
type MailData struct {
|
||||
To string
|
||||
From string
|
||||
Subject string
|
||||
Content string
|
||||
Template string
|
||||
}
|
||||
|
@ -8,9 +8,10 @@ type TemplateData struct {
|
||||
IntMap map[string]int
|
||||
FloatMap map[string]float32
|
||||
Data map[string]interface{}
|
||||
Form *forms.Form
|
||||
CSRFToken string
|
||||
Flash string
|
||||
Warning string
|
||||
Error string
|
||||
Form *forms.Form
|
||||
IsAuthenticated int
|
||||
}
|
||||
|
@ -10,11 +10,17 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
var functions = template.FuncMap{}
|
||||
var functions = template.FuncMap{
|
||||
"humanDate": HumanDate,
|
||||
"formatDate": FormatDate,
|
||||
"iterate": Iterate,
|
||||
"add": Add,
|
||||
}
|
||||
|
||||
var (
|
||||
app *config.AppConfig
|
||||
@ -26,12 +32,38 @@ func NewRenderer(a *config.AppConfig) {
|
||||
app = a
|
||||
}
|
||||
|
||||
// HumanDate returns formatted time
|
||||
func HumanDate(t time.Time) string {
|
||||
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
|
||||
func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData {
|
||||
td.Flash = app.Session.PopString(r.Context(), "flash")
|
||||
td.Warning = app.Session.PopString(r.Context(), "warning")
|
||||
td.Error = app.Session.PopString(r.Context(), "error")
|
||||
td.CSRFToken = nosurf.Token(r)
|
||||
if app.Session.Exists(r.Context(), "user_id") {
|
||||
td.IsAuthenticated = 1
|
||||
}
|
||||
return td
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,9 @@ import (
|
||||
"database/sql"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
"go-udemy-web-1/internal/repository/sqlc"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type postgresDBRepo struct {
|
||||
@ -11,9 +14,33 @@ type postgresDBRepo struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
type pgcDBRepo struct {
|
||||
App *config.AppConfig
|
||||
Q *sqlc.Queries
|
||||
}
|
||||
|
||||
type testDBRepo struct {
|
||||
App *config.AppConfig
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgresRepo(conn *sql.DB, a *config.AppConfig) repository.DatabaseRepo {
|
||||
return &postgresDBRepo{
|
||||
App: a,
|
||||
DB: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTestingRepo(a *config.AppConfig) repository.DatabaseRepo {
|
||||
return &testDBRepo{
|
||||
App: a,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPgcRepo(conn *pgx.Conn, a *config.AppConfig) repository.DatabaseRepo {
|
||||
q := sqlc.New(conn)
|
||||
return &pgcDBRepo{
|
||||
App: a,
|
||||
Q: q,
|
||||
}
|
||||
}
|
||||
|
416
internal/repository/dbrepo/pgc.go
Normal file
@ -0,0 +1,416 @@
|
||||
package dbrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"go-udemy-web-1/internal/repository/sqlc"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (m *pgcDBRepo) AllUsers() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// InsertReservation inserts a reservation into the database
|
||||
func (m *pgcDBRepo) InsertReservation(res models.Reservation) (int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var newId int32
|
||||
newId, err := m.Q.InsertReservation(ctx, sqlc.InsertReservationParams{
|
||||
FirstName: res.FirstName,
|
||||
LastName: res.LastName,
|
||||
Email: res.Email,
|
||||
Phone: res.Phone,
|
||||
StartDate: pgtype.Date{Time: res.StartDate, Valid: true},
|
||||
EndDate: pgtype.Date{Time: res.EndDate, Valid: true},
|
||||
RoomID: int32(res.RoomID),
|
||||
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
m.App.ErrorLog.Println(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(newId), nil
|
||||
}
|
||||
|
||||
// InsertRoomRestriction inserts a room restriction into the database
|
||||
func (m *pgcDBRepo) InsertRoomRestriction(r models.RoomRestriction) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.InsertRoomRestriction(ctx, sqlc.InsertRoomRestrictionParams{
|
||||
StartDate: pgtype.Date{Time: r.StartDate, Valid: true},
|
||||
EndDate: pgtype.Date{Time: r.EndDate, Valid: true},
|
||||
RoomID: int32(r.RoomID),
|
||||
ReservationID: pgtype.Int4{Int32: int32(r.ReservationID), Valid: true},
|
||||
RestrictionID: int32(r.RestrictionID),
|
||||
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
m.App.ErrorLog.Println(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchAvailabilityByDatesByRoomID returns true if availability exists for roomID, and false if no availability
|
||||
func (m *pgcDBRepo) SearchAvailabilityByDatesByRoomID(start, end time.Time, roomID int) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
numRows, err := m.Q.SearchAvailabilityByDatesByRoomID(ctx, sqlc.SearchAvailabilityByDatesByRoomIDParams{
|
||||
RoomID: int32(roomID),
|
||||
EndDate: pgtype.Date{Time: start, Valid: true},
|
||||
StartDate: pgtype.Date{Time: end, Valid: false},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if numRows == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SearchAvailabilityForAllRooms returns a slice of rooms, if any, for given date range
|
||||
func (m *pgcDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]models.Room, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var rooms []models.Room
|
||||
|
||||
rows, err := m.Q.SearchAvailabilityForAllRooms(ctx, sqlc.SearchAvailabilityForAllRoomsParams{
|
||||
EndDate: pgtype.Date{Time: start, Valid: true},
|
||||
StartDate: pgtype.Date{Time: start, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
room := models.Room{
|
||||
RoomName: row.RoomName,
|
||||
ID: int(row.ID),
|
||||
}
|
||||
rooms = append(rooms, room)
|
||||
}
|
||||
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// GetRoomById gets a room by id
|
||||
func (m *pgcDBRepo) GetRoomById(id int) (models.Room, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var room models.Room
|
||||
|
||||
row, err := m.Q.GetRoomById(ctx, int32(id))
|
||||
if err != nil {
|
||||
return room, err
|
||||
}
|
||||
room = models.Room{
|
||||
ID: int(row.ID),
|
||||
RoomName: row.RoomName,
|
||||
}
|
||||
|
||||
return room, nil
|
||||
}
|
||||
|
||||
// GetUserByID gets a user by id
|
||||
func (m *pgcDBRepo) GetUserByID(id int) (models.User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var u models.User
|
||||
|
||||
row, err := m.Q.GetUserByID(ctx, int32(id))
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
|
||||
u = models.User{
|
||||
ID: int(row.ID),
|
||||
FirstName: row.FirstName,
|
||||
LastName: row.LastName,
|
||||
Email: row.Email,
|
||||
Password: row.Password,
|
||||
AccessLevel: int(row.AccessLevel),
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates a user in the database
|
||||
func (m *pgcDBRepo) UpdateUser(u models.User) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.UpdateUser(ctx, sqlc.UpdateUserParams{
|
||||
FirstName: u.FirstName,
|
||||
LastName: u.LastName,
|
||||
Email: u.Email,
|
||||
AccessLevel: int32(u.AccessLevel),
|
||||
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate authenticates a user
|
||||
func (m *pgcDBRepo) Authenticate(email, testPassword string) (int, string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var id int
|
||||
var hashedPassword string
|
||||
|
||||
row, err := m.Q.GetUserCred(ctx, email)
|
||||
if err != nil {
|
||||
return id, "", err
|
||||
}
|
||||
id = int(row.ID)
|
||||
hashedPassword = row.Password
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(testPassword))
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return 0, "", errors.New("incorrect password")
|
||||
} else if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return id, hashedPassword, nil
|
||||
}
|
||||
|
||||
// AllReservations returns a slice of all reservations
|
||||
func (m *pgcDBRepo) AllReservations() ([]models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var reservations []models.Reservation
|
||||
|
||||
rows, err := m.Q.AllReservations(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
r := models.Reservation{
|
||||
StartDate: row.StartDate.Time,
|
||||
EndDate: row.EndDate.Time,
|
||||
FirstName: row.FirstName,
|
||||
LastName: row.LastName,
|
||||
Email: row.Email,
|
||||
Phone: row.Phone,
|
||||
Room: models.Room{ID: int(row.RoomID), RoomName: row.RoomName.String},
|
||||
ID: int(row.ID),
|
||||
RoomID: int(row.RoomID),
|
||||
Processed: int(row.Processed),
|
||||
}
|
||||
|
||||
reservations = append(reservations, r)
|
||||
}
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// AllNewReservations returns a slice of all new reservations
|
||||
func (m *pgcDBRepo) AllNewReservations() ([]models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var reservations []models.Reservation
|
||||
|
||||
rows, err := m.Q.AllNewReservations(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
r := models.Reservation{
|
||||
StartDate: row.StartDate.Time,
|
||||
EndDate: row.EndDate.Time,
|
||||
FirstName: row.FirstName,
|
||||
LastName: row.LastName,
|
||||
Email: row.Email,
|
||||
Phone: row.Phone,
|
||||
Room: models.Room{ID: int(row.RoomID), RoomName: row.RoomName.String},
|
||||
ID: int(row.ID),
|
||||
RoomID: int(row.RoomID),
|
||||
Processed: int(row.Processed),
|
||||
}
|
||||
|
||||
reservations = append(reservations, r)
|
||||
}
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// GetReservationByID returns one reservation by ID
|
||||
func (m *pgcDBRepo) GetReservationByID(id int) (models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var res models.Reservation
|
||||
|
||||
row, err := m.Q.GetReservationByID(ctx, int32(id))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res = models.Reservation{
|
||||
StartDate: row.StartDate.Time,
|
||||
EndDate: row.EndDate.Time,
|
||||
FirstName: row.FirstName,
|
||||
LastName: row.LastName,
|
||||
Email: row.Email,
|
||||
Phone: row.Phone,
|
||||
Room: models.Room{ID: int(row.RoomID), RoomName: row.RoomName.String},
|
||||
ID: int(row.ID),
|
||||
RoomID: int(row.RoomID),
|
||||
Processed: int(row.Processed),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateReservation updates a user in the database
|
||||
func (m *pgcDBRepo) UpdateReservation(r models.Reservation) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.UpdateReservation(ctx, sqlc.UpdateReservationParams{
|
||||
ID: int32(r.ID),
|
||||
FirstName: r.FirstName,
|
||||
LastName: r.LastName,
|
||||
Email: r.Email,
|
||||
Phone: r.Phone,
|
||||
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *pgcDBRepo) DeleteReservation(id int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.DeleteReservation(ctx, int32(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProcessedForReservation set processed for a reservation
|
||||
func (m *pgcDBRepo) UpdateProcessedForReservation(id, processed int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.UpdateProcessedForReservation(ctx, sqlc.UpdateProcessedForReservationParams{
|
||||
Processed: int32(processed),
|
||||
ID: int32(id),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *pgcDBRepo) AllRooms() ([]models.Room, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var rooms []models.Room
|
||||
|
||||
rows, err := m.Q.AllRooms(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
room := models.Room{
|
||||
RoomName: row.RoomName,
|
||||
ID: int(row.ID),
|
||||
}
|
||||
rooms = append(rooms, room)
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// GetRestrictionsForRoomByDate returns restrictions for a room by date range
|
||||
func (m *pgcDBRepo) 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
|
||||
|
||||
rows, err := m.Q.GetRestrictionsForRoomByDate(ctx, sqlc.GetRestrictionsForRoomByDateParams{
|
||||
EndDate: pgtype.Date{Time: end, Valid: true},
|
||||
StartDate: pgtype.Date{Time: start, Valid: true},
|
||||
RoomID: int32(roomId),
|
||||
})
|
||||
if err != nil {
|
||||
return restrictions, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
r := models.RoomRestriction{
|
||||
StartDate: row.StartDate.Time,
|
||||
EndDate: row.EndDate.Time,
|
||||
ID: int(row.ID),
|
||||
RoomID: int(row.RoomID),
|
||||
ReservationID: int(row.ReservationID),
|
||||
RestrictionID: int(row.RestrictionID),
|
||||
}
|
||||
restrictions = append(restrictions, r)
|
||||
}
|
||||
|
||||
return restrictions, nil
|
||||
}
|
||||
|
||||
// InsertBlockForRoom inserts a room restriction
|
||||
func (m *pgcDBRepo) InsertBlockForRoom(id int, startDate time.Time) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.InsertBlockForRoom(ctx, sqlc.InsertBlockForRoomParams{
|
||||
StartDate: pgtype.Date{Time: startDate, Valid: true},
|
||||
EndDate: pgtype.Date{Time: startDate.AddDate(0, 0, 1)},
|
||||
RoomID: int32(id),
|
||||
RestrictionID: 2,
|
||||
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBlockByID deletes a block by ID
|
||||
func (m *pgcDBRepo) DeleteBlockByID(id int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.Q.DeleteBlockByID(ctx, int32(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -2,8 +2,12 @@ package dbrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (m *postgresDBRepo) AllUsers() bool {
|
||||
@ -100,7 +104,7 @@ func (m *postgresDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]
|
||||
rooms r
|
||||
where r.id not in
|
||||
(select
|
||||
roomId
|
||||
room_id
|
||||
from
|
||||
room_restrictions rr
|
||||
where
|
||||
@ -110,6 +114,8 @@ func (m *postgresDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var room models.Room
|
||||
|
||||
@ -126,3 +132,363 @@ func (m *postgresDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]
|
||||
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// GetRoomById gets a room by id
|
||||
func (m *postgresDBRepo) GetRoomById(id int) (models.Room, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var room models.Room
|
||||
stmt := `select
|
||||
id, room_name, created_at, updated_at
|
||||
from
|
||||
rooms
|
||||
where id = $1`
|
||||
|
||||
row := m.DB.QueryRowContext(ctx, stmt, id)
|
||||
err := row.Scan(&room.ID, &room.RoomName, &room.CreatedAt, &room.UpdatedAt)
|
||||
if err != nil {
|
||||
return room, err
|
||||
}
|
||||
return room, nil
|
||||
}
|
||||
|
||||
// GetUserByID gets a user by id
|
||||
func (m *postgresDBRepo) GetUserByID(id int) (models.User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, first_name, last_name, email, password, access_level, created_at, updated_at
|
||||
from users where id = $1`
|
||||
|
||||
row := m.DB.QueryRowContext(ctx, query, id)
|
||||
var u models.User
|
||||
err := row.Scan(
|
||||
&u.ID,
|
||||
&u.FirstName,
|
||||
&u.LastName,
|
||||
&u.Email,
|
||||
&u.Password,
|
||||
&u.AccessLevel,
|
||||
&u.CreatedAt,
|
||||
&u.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates a user in the database
|
||||
func (m *postgresDBRepo) UpdateUser(u models.User) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `update users set first_name = $1, last_name = $2, email = $3, access_level = $4, updated_at = $5`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, u.FirstName, u.LastName, u.Email, u.AccessLevel, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate authenticates a user
|
||||
func (m *postgresDBRepo) Authenticate(email, testPassword string) (int, string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var id int
|
||||
var hashedPassword string
|
||||
row := m.DB.QueryRowContext(ctx, "select id, password from users where email = $1", email)
|
||||
err := row.Scan(&id, &hashedPassword)
|
||||
if err != nil {
|
||||
return id, "", err
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(testPassword))
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return 0, "", errors.New("incorrect password")
|
||||
} else if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return id, hashedPassword, nil
|
||||
}
|
||||
|
||||
// AllReservations returns a slice of all reservations
|
||||
func (m *postgresDBRepo) AllReservations() ([]models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var reservations []models.Reservation
|
||||
|
||||
query := `select r.id, r.first_name, r.last_name, r.email, r.phone,
|
||||
r.start_date, r.end_date, r.room_id, r.created_at,
|
||||
r.updated_at, r.processed, rm.id, rm.room_name
|
||||
from reservations r
|
||||
left join rooms rm on (r.room_id = rm.id)
|
||||
order by r.start_date asc`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
defer rows.Close() // To avoid memory leak
|
||||
|
||||
for rows.Next() {
|
||||
var i models.Reservation
|
||||
err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.Room.ID,
|
||||
&i.Room.RoomName,
|
||||
)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
reservations = append(reservations, i)
|
||||
}
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// AllNewReservations returns a slice of all new reservations
|
||||
func (m *postgresDBRepo) AllNewReservations() ([]models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var reservations []models.Reservation
|
||||
|
||||
query := `select r.id, r.first_name, r.last_name, r.email, r.phone,
|
||||
r.start_date, r.end_date, r.room_id, r.created_at,
|
||||
r.updated_at, r.processed, rm.id, rm.room_name
|
||||
from reservations r
|
||||
left join rooms rm on (r.room_id = rm.id)
|
||||
where r.processed = 0
|
||||
order by r.start_date asc`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
defer rows.Close() // To avoid memory leak
|
||||
|
||||
for rows.Next() {
|
||||
var i models.Reservation
|
||||
err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.Room.ID,
|
||||
&i.Room.RoomName,
|
||||
)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
reservations = append(reservations, i)
|
||||
}
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// GetReservationByID returns one reservation by ID
|
||||
func (m *postgresDBRepo) GetReservationByID(id int) (models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var res models.Reservation
|
||||
|
||||
query := `select r.id, r.first_name, r.last_name, r.email, r.phone,
|
||||
r.start_date, r.end_date, r.room_id, r.created_at,
|
||||
r.updated_at, r.processed, rm.id, rm.room_name
|
||||
from reservations r
|
||||
left join rooms rm on (r.room_id = rm.id)
|
||||
where r.id = $1`
|
||||
|
||||
row := m.DB.QueryRowContext(ctx, query, id)
|
||||
err := row.Scan(
|
||||
&res.ID,
|
||||
&res.FirstName,
|
||||
&res.LastName,
|
||||
&res.Email,
|
||||
&res.Phone,
|
||||
&res.StartDate,
|
||||
&res.EndDate,
|
||||
&res.RoomID,
|
||||
&res.CreatedAt,
|
||||
&res.UpdatedAt,
|
||||
&res.Processed,
|
||||
&res.Room.ID,
|
||||
&res.Room.RoomName,
|
||||
)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
143
internal/repository/dbrepo/test-repo.go
Normal file
@ -0,0 +1,143 @@
|
||||
package dbrepo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (m *testDBRepo) AllUsers() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// InsertReservation inserts a reservation into the database
|
||||
func (m *testDBRepo) InsertReservation(res models.Reservation) (int, error) {
|
||||
// if the room id is 2, then fail; otherwise, pass
|
||||
if res.RoomID == 2 {
|
||||
return 0, errors.New("deliberate error")
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// InsertRoomRestriction inserts a room restriction into the database
|
||||
func (m *testDBRepo) InsertRoomRestriction(r models.RoomRestriction) error {
|
||||
if r.RoomID == 100 {
|
||||
return errors.New("deliberate error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchAvailabilityByDatesByRoomID returns true if availability exists for roomID, and false if no availability
|
||||
func (m *testDBRepo) SearchAvailabilityByDatesByRoomID(start, end time.Time, roomID int) (bool, error) {
|
||||
if roomID == 2 {
|
||||
return false, nil
|
||||
}
|
||||
if roomID == 100 {
|
||||
return false, errors.New("deliberate error")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SearchAvailabilityForAllRooms returns a slice of rooms, if any, for given date range
|
||||
func (m *testDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]models.Room, error) {
|
||||
var rooms []models.Room
|
||||
if start.Format("2006-01-02") == "2050-01-01" && end.Format("2006-01-02") == "2050-01-02" {
|
||||
room := models.Room{
|
||||
RoomName: "room",
|
||||
ID: 1,
|
||||
}
|
||||
rooms = append(rooms, room)
|
||||
return rooms, nil
|
||||
}
|
||||
if start.Format("2006-01-02") == "2050-01-03" && end.Format("2006-01-02") == "2050-01-04" {
|
||||
return rooms, errors.New("deliberate error")
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// GetRoomById gets a room by id
|
||||
func (m *testDBRepo) GetRoomById(id int) (models.Room, error) {
|
||||
var room models.Room
|
||||
|
||||
if id > 2 || id <= 0 {
|
||||
return room, errors.New("deliberate error")
|
||||
}
|
||||
return room, nil
|
||||
}
|
||||
|
||||
// GetUserByID gets a user by id
|
||||
func (m *testDBRepo) GetUserByID(id int) (models.User, error) {
|
||||
var u models.User
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates a user in the database
|
||||
func (m *testDBRepo) UpdateUser(u models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate authenticates a user
|
||||
func (m *testDBRepo) Authenticate(email, testPassword string) (int, string, error) {
|
||||
if email == "a@b.c" {
|
||||
return 1, "", nil
|
||||
}
|
||||
return 0, "", errors.New("deliberate errors")
|
||||
}
|
||||
|
||||
// AllReservations returns a slice of all reservations
|
||||
func (m *testDBRepo) AllReservations() ([]models.Reservation, error) {
|
||||
var reservations []models.Reservation
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// AllNewReservations returns a slice of all new reservations
|
||||
func (m *testDBRepo) AllNewReservations() ([]models.Reservation, error) {
|
||||
var reservations []models.Reservation
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// GetReservationByID returns one reservation by ID
|
||||
func (m *testDBRepo) GetReservationByID(id int) (models.Reservation, error) {
|
||||
var res models.Reservation
|
||||
|
||||
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
|
||||
}
|
20
internal/repository/driverrepo/driverrepo.go
Normal file
@ -0,0 +1,20 @@
|
||||
package driverrepo
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/internal/repository"
|
||||
)
|
||||
|
||||
type (
|
||||
sqlDriverRepo struct{}
|
||||
sqlcDriverRepo struct{}
|
||||
)
|
||||
|
||||
var dbConn = &repository.DB{}
|
||||
|
||||
func NewSqlRepo() repository.DatabaseDriverRepo {
|
||||
return &sqlDriverRepo{}
|
||||
}
|
||||
|
||||
func NewSqlcRepo() repository.DatabaseDriverRepo {
|
||||
return &sqlcDriverRepo{}
|
||||
}
|
48
internal/repository/driverrepo/sqlcdriver.go
Normal file
@ -0,0 +1,48 @@
|
||||
package driverrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *sqlcDriverRepo) ConnectSQL(dsn string) (*repository.DB, error) {
|
||||
ctx := context.Background()
|
||||
c, err := s.newConn(ctx, dsn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dbConn.PG = c
|
||||
|
||||
err = s.testDB(ctx, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbConn, nil
|
||||
}
|
||||
|
||||
// testDB tries to ping the database
|
||||
func (s *sqlcDriverRepo) testDB(ctx context.Context, c *pgx.Conn) error {
|
||||
err := c.Ping(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newDatabase creates a new database for the application
|
||||
func (s *sqlcDriverRepo) newConn(ctx context.Context, dsn string) (*pgx.Conn, error) {
|
||||
conn, err := pgx.Connect(ctx, dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ping test the conn
|
||||
if err = conn.Ping(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, err
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
package driver
|
||||
package driverrepo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgconn"
|
||||
@ -9,13 +10,6 @@ import (
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
// DB holds the database connection pool
|
||||
type DB struct {
|
||||
SQL *sql.DB
|
||||
}
|
||||
|
||||
var dbConn = &DB{}
|
||||
|
||||
const (
|
||||
maxOpenDbConn = 10
|
||||
maxIdleDbConn = 5
|
||||
@ -23,8 +17,8 @@ const (
|
||||
)
|
||||
|
||||
// ConnectSQL creates SQL pool for Postgres
|
||||
func ConnectSQL(dsn string) (*DB, error) {
|
||||
d, err := NewDatabase(dsn)
|
||||
func (s *sqlDriverRepo) ConnectSQL(dsn string) (*repository.DB, error) {
|
||||
d, err := s.newDatabase(dsn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -35,7 +29,7 @@ func ConnectSQL(dsn string) (*DB, error) {
|
||||
|
||||
dbConn.SQL = d
|
||||
|
||||
err = testDB(d)
|
||||
err = s.testDB(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -43,7 +37,7 @@ func ConnectSQL(dsn string) (*DB, error) {
|
||||
}
|
||||
|
||||
// testDB tries to ping the database
|
||||
func testDB(d *sql.DB) error {
|
||||
func (s *sqlDriverRepo) testDB(d *sql.DB) error {
|
||||
err := d.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -51,8 +45,8 @@ func testDB(d *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDatabase creates a new datavase for the application
|
||||
func NewDatabase(dsn string) (*sql.DB, error) {
|
||||
// newDatabase creates a new database for the application
|
||||
func (s *sqlDriverRepo) newDatabase(dsn string) (*sql.DB, error) {
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
@ -1,8 +1,11 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type DatabaseRepo interface {
|
||||
@ -12,4 +15,28 @@ type DatabaseRepo interface {
|
||||
InsertRoomRestriction(res models.RoomRestriction) error
|
||||
SearchAvailabilityByDatesByRoomID(start, end time.Time, roomID int) (bool, error)
|
||||
SearchAvailabilityForAllRooms(start, end time.Time) ([]models.Room, error)
|
||||
GetRoomById(id int) (models.Room, error)
|
||||
GetUserByID(id int) (models.User, error)
|
||||
UpdateUser(u models.User) error
|
||||
Authenticate(email, testPassword string) (int, string, error)
|
||||
AllReservations() ([]models.Reservation, error)
|
||||
AllNewReservations() ([]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
|
||||
}
|
||||
|
||||
// DB holds the database connection pool
|
||||
type DB struct {
|
||||
SQL *sql.DB
|
||||
PG *pgx.Conn
|
||||
}
|
||||
|
||||
type DatabaseDriverRepo interface {
|
||||
ConnectSQL(dsn string) (*DB, error)
|
||||
}
|
||||
|
32
internal/repository/sqlc/db.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
63
internal/repository/sqlc/models.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Reservation struct {
|
||||
ID int32
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
Processed int32
|
||||
}
|
||||
|
||||
type Restriction struct {
|
||||
ID int32
|
||||
RestrictionName string
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
ID int32
|
||||
RoomName string
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
type RoomRestriction struct {
|
||||
ID int32
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
ReservationID pgtype.Int4
|
||||
RestrictionID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
type SchemaMigration struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int32
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Password string
|
||||
AccessLevel int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
685
internal/repository/sqlc/query.sql.go
Normal file
@ -0,0 +1,685 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// source: query.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const allNewReservations = `-- name: AllNewReservations :many
|
||||
SELECT
|
||||
r.id,
|
||||
r.first_name,
|
||||
r.last_name,
|
||||
r.email,
|
||||
r.phone,
|
||||
r.start_date,
|
||||
r.end_date,
|
||||
r.room_id,
|
||||
r.created_at,
|
||||
r.updated_at,
|
||||
r.processed,
|
||||
rm.id,
|
||||
rm.room_name
|
||||
FROM
|
||||
reservations r
|
||||
LEFT JOIN rooms rm ON (r.room_id = rm.id)
|
||||
WHERE
|
||||
r.processed = 0
|
||||
ORDER BY
|
||||
r.start_date ASC
|
||||
`
|
||||
|
||||
type AllNewReservationsRow struct {
|
||||
ID int32
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
Processed int32
|
||||
ID_2 pgtype.Int4
|
||||
RoomName pgtype.Text
|
||||
}
|
||||
|
||||
func (q *Queries) AllNewReservations(ctx context.Context) ([]AllNewReservationsRow, error) {
|
||||
rows, err := q.db.Query(ctx, allNewReservations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AllNewReservationsRow
|
||||
for rows.Next() {
|
||||
var i AllNewReservationsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.ID_2,
|
||||
&i.RoomName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const allReservations = `-- name: AllReservations :many
|
||||
SELECT
|
||||
r.id,
|
||||
r.first_name,
|
||||
r.last_name,
|
||||
r.email,
|
||||
r.phone,
|
||||
r.start_date,
|
||||
r.end_date,
|
||||
r.room_id,
|
||||
r.created_at,
|
||||
r.updated_at,
|
||||
r.processed,
|
||||
rm.id,
|
||||
rm.room_name
|
||||
FROM
|
||||
reservations r
|
||||
LEFT JOIN rooms rm ON (r.room_id = rm.id)
|
||||
ORDER BY
|
||||
r.start_date ASC
|
||||
`
|
||||
|
||||
type AllReservationsRow struct {
|
||||
ID int32
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
Processed int32
|
||||
ID_2 pgtype.Int4
|
||||
RoomName pgtype.Text
|
||||
}
|
||||
|
||||
func (q *Queries) AllReservations(ctx context.Context) ([]AllReservationsRow, error) {
|
||||
rows, err := q.db.Query(ctx, allReservations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AllReservationsRow
|
||||
for rows.Next() {
|
||||
var i AllReservationsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.ID_2,
|
||||
&i.RoomName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const allRooms = `-- name: AllRooms :many
|
||||
SELECT
|
||||
id,
|
||||
room_name,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
rooms
|
||||
ORDER BY
|
||||
room_name
|
||||
`
|
||||
|
||||
func (q *Queries) AllRooms(ctx context.Context) ([]Room, error) {
|
||||
rows, err := q.db.Query(ctx, allRooms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Room
|
||||
for rows.Next() {
|
||||
var i Room
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.RoomName,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const deleteBlockByID = `-- name: DeleteBlockByID :exec
|
||||
DELETE FROM room_restrictions
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteBlockByID(ctx context.Context, id int32) error {
|
||||
_, err := q.db.Exec(ctx, deleteBlockByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteReservation = `-- name: DeleteReservation :exec
|
||||
DELETE FROM reservations
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteReservation(ctx context.Context, id int32) error {
|
||||
_, err := q.db.Exec(ctx, deleteReservation, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getReservationByID = `-- name: GetReservationByID :one
|
||||
SELECT
|
||||
r.id,
|
||||
r.first_name,
|
||||
r.last_name,
|
||||
r.email,
|
||||
r.phone,
|
||||
r.start_date,
|
||||
r.end_date,
|
||||
r.room_id,
|
||||
r.created_at,
|
||||
r.updated_at,
|
||||
r.processed,
|
||||
rm.id,
|
||||
rm.room_name
|
||||
FROM
|
||||
reservations r
|
||||
LEFT JOIN rooms rm ON (r.room_id = rm.id)
|
||||
WHERE
|
||||
r.id = $1
|
||||
`
|
||||
|
||||
type GetReservationByIDRow struct {
|
||||
ID int32
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
Processed int32
|
||||
ID_2 pgtype.Int4
|
||||
RoomName pgtype.Text
|
||||
}
|
||||
|
||||
func (q *Queries) GetReservationByID(ctx context.Context, id int32) (GetReservationByIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, getReservationByID, id)
|
||||
var i GetReservationByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.ID_2,
|
||||
&i.RoomName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getRestrictionsForRoomByDate = `-- name: GetRestrictionsForRoomByDate :many
|
||||
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
|
||||
`
|
||||
|
||||
type GetRestrictionsForRoomByDateParams struct {
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
}
|
||||
|
||||
type GetRestrictionsForRoomByDateRow struct {
|
||||
ID int32
|
||||
ReservationID int32
|
||||
RestrictionID int32
|
||||
RoomID int32
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
}
|
||||
|
||||
func (q *Queries) GetRestrictionsForRoomByDate(ctx context.Context, arg GetRestrictionsForRoomByDateParams) ([]GetRestrictionsForRoomByDateRow, error) {
|
||||
rows, err := q.db.Query(ctx, getRestrictionsForRoomByDate, arg.StartDate, arg.EndDate, arg.RoomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetRestrictionsForRoomByDateRow
|
||||
for rows.Next() {
|
||||
var i GetRestrictionsForRoomByDateRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ReservationID,
|
||||
&i.RestrictionID,
|
||||
&i.RoomID,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getRoomById = `-- name: GetRoomById :one
|
||||
SELECT
|
||||
id,
|
||||
room_name,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
rooms
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetRoomById(ctx context.Context, id int32) (Room, error) {
|
||||
row := q.db.QueryRow(ctx, getRoomById, id)
|
||||
var i Room
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.RoomName,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
password,
|
||||
access_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Password,
|
||||
&i.AccessLevel,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserCred = `-- name: GetUserCred :one
|
||||
SELECT
|
||||
id,
|
||||
password
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = $1
|
||||
`
|
||||
|
||||
type GetUserCredRow struct {
|
||||
ID int32
|
||||
Password string
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserCred(ctx context.Context, email string) (GetUserCredRow, error) {
|
||||
row := q.db.QueryRow(ctx, getUserCred, email)
|
||||
var i GetUserCredRow
|
||||
err := row.Scan(&i.ID, &i.Password)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertBlockForRoom = `-- name: InsertBlockForRoom :exec
|
||||
INSERT INTO
|
||||
room_restrictions (
|
||||
start_date,
|
||||
end_date,
|
||||
room_id,
|
||||
restriction_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
|
||||
type InsertBlockForRoomParams struct {
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
RestrictionID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
func (q *Queries) InsertBlockForRoom(ctx context.Context, arg InsertBlockForRoomParams) error {
|
||||
_, err := q.db.Exec(ctx, insertBlockForRoom,
|
||||
arg.StartDate,
|
||||
arg.EndDate,
|
||||
arg.RoomID,
|
||||
arg.RestrictionID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const insertReservation = `-- name: InsertReservation :one
|
||||
INSERT INTO
|
||||
reservations (
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone,
|
||||
start_date,
|
||||
end_date,
|
||||
room_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING
|
||||
id
|
||||
`
|
||||
|
||||
type InsertReservationParams struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
func (q *Queries) InsertReservation(ctx context.Context, arg InsertReservationParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, insertReservation,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.Phone,
|
||||
arg.StartDate,
|
||||
arg.EndDate,
|
||||
arg.RoomID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var id int32
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const insertRoomRestriction = `-- name: InsertRoomRestriction :exec
|
||||
INSERT INTO
|
||||
room_restrictions (
|
||||
start_date,
|
||||
end_date,
|
||||
room_id,
|
||||
reservation_id,
|
||||
restriction_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7)
|
||||
`
|
||||
|
||||
type InsertRoomRestrictionParams struct {
|
||||
StartDate pgtype.Date
|
||||
EndDate pgtype.Date
|
||||
RoomID int32
|
||||
ReservationID pgtype.Int4
|
||||
RestrictionID int32
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
func (q *Queries) InsertRoomRestriction(ctx context.Context, arg InsertRoomRestrictionParams) error {
|
||||
_, err := q.db.Exec(ctx, insertRoomRestriction,
|
||||
arg.StartDate,
|
||||
arg.EndDate,
|
||||
arg.RoomID,
|
||||
arg.ReservationID,
|
||||
arg.RestrictionID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const searchAvailabilityByDatesByRoomID = `-- name: SearchAvailabilityByDatesByRoomID :one
|
||||
SELECT
|
||||
count(id)
|
||||
FROM
|
||||
room_restrictions
|
||||
WHERE
|
||||
room_id = $1
|
||||
AND $2 < end_date
|
||||
AND $3 > start_date
|
||||
`
|
||||
|
||||
type SearchAvailabilityByDatesByRoomIDParams struct {
|
||||
RoomID int32
|
||||
EndDate pgtype.Date
|
||||
StartDate pgtype.Date
|
||||
}
|
||||
|
||||
func (q *Queries) SearchAvailabilityByDatesByRoomID(ctx context.Context, arg SearchAvailabilityByDatesByRoomIDParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, searchAvailabilityByDatesByRoomID, arg.RoomID, arg.EndDate, arg.StartDate)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const searchAvailabilityForAllRooms = `-- name: SearchAvailabilityForAllRooms :many
|
||||
SELECT
|
||||
r.id,
|
||||
r.room_name
|
||||
FROM
|
||||
rooms r
|
||||
WHERE
|
||||
r.id NOT IN (
|
||||
SELECT
|
||||
room_id
|
||||
FROM
|
||||
room_restrictions rr
|
||||
WHERE
|
||||
$1 < rr.end_date
|
||||
AND $2 > rr.start_date
|
||||
)
|
||||
`
|
||||
|
||||
type SearchAvailabilityForAllRoomsParams struct {
|
||||
EndDate pgtype.Date
|
||||
StartDate pgtype.Date
|
||||
}
|
||||
|
||||
type SearchAvailabilityForAllRoomsRow struct {
|
||||
ID int32
|
||||
RoomName string
|
||||
}
|
||||
|
||||
func (q *Queries) SearchAvailabilityForAllRooms(ctx context.Context, arg SearchAvailabilityForAllRoomsParams) ([]SearchAvailabilityForAllRoomsRow, error) {
|
||||
rows, err := q.db.Query(ctx, searchAvailabilityForAllRooms, arg.EndDate, arg.StartDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SearchAvailabilityForAllRoomsRow
|
||||
for rows.Next() {
|
||||
var i SearchAvailabilityForAllRoomsRow
|
||||
if err := rows.Scan(&i.ID, &i.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateProcessedForReservation = `-- name: UpdateProcessedForReservation :exec
|
||||
UPDATE reservations
|
||||
SET
|
||||
processed = $1
|
||||
WHERE
|
||||
id = $2
|
||||
`
|
||||
|
||||
type UpdateProcessedForReservationParams struct {
|
||||
Processed int32
|
||||
ID int32
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateProcessedForReservation(ctx context.Context, arg UpdateProcessedForReservationParams) error {
|
||||
_, err := q.db.Exec(ctx, updateProcessedForReservation, arg.Processed, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateReservation = `-- name: UpdateReservation :exec
|
||||
UPDATE reservations
|
||||
SET
|
||||
first_name = $1,
|
||||
last_name = $2,
|
||||
email = $3,
|
||||
phone = $4,
|
||||
updated_at = $5
|
||||
WHERE
|
||||
id = $6
|
||||
`
|
||||
|
||||
type UpdateReservationParams struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
UpdatedAt pgtype.Timestamp
|
||||
ID int32
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateReservation(ctx context.Context, arg UpdateReservationParams) error {
|
||||
_, err := q.db.Exec(ctx, updateReservation,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.Phone,
|
||||
arg.UpdatedAt,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
first_name = $1,
|
||||
last_name = $2,
|
||||
email = $3,
|
||||
access_level = $4,
|
||||
updated_at = $5
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
AccessLevel int32
|
||||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||
_, err := q.db.Exec(ctx, updateUser,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.AccessLevel,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
@ -0,0 +1 @@
|
||||
delete from rooms
|
@ -0,0 +1,3 @@
|
||||
INSERT INTO rooms (room_name, created_at, updated_at) VALUES
|
||||
('General''s Quarters', '2024-07-08 00:00:00', '2024-07-08 00:00:00'),
|
||||
('Major''s Suite', '2024-07-08 00:00:00', '2024-07-08 00:00:00');
|
@ -0,0 +1 @@
|
||||
delete from restrictions;
|
@ -0,0 +1,4 @@
|
||||
INSERT INTO public.restrictions (restriction_name, created_at, updated_at) VALUES
|
||||
('Reservation','2024-07-08 00:00:00','2024-07-08 00:00:00'),
|
||||
('Owner block','2024-07-08 00:00:00','2024-07-08 00:00:00');
|
||||
|
@ -0,0 +1 @@
|
||||
drop_column("reservations", "processed")
|
@ -0,0 +1 @@
|
||||
add_column("reservations", "processed", "integer", {"default": 0})
|
@ -1,395 +0,0 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 16.3 (Ubuntu 16.3-0ubuntu0.24.04.1)
|
||||
-- Dumped by pg_dump version 16.3 (Ubuntu 16.3-0ubuntu0.24.04.1)
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- Name: reservations; Type: TABLE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE TABLE public.reservations (
|
||||
id integer NOT NULL,
|
||||
first_name character varying(255) DEFAULT ''::character varying NOT NULL,
|
||||
last_name character varying(255) DEFAULT ''::character varying NOT NULL,
|
||||
email character varying(255) NOT NULL,
|
||||
phone character varying(255) DEFAULT ''::character varying NOT NULL,
|
||||
start_date date NOT NULL,
|
||||
end_date date NOT NULL,
|
||||
room_id integer NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.reservations OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: reservations_id_seq; Type: SEQUENCE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.reservations_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE public.reservations_id_seq OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: reservations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: restrictions; Type: TABLE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE TABLE public.restrictions (
|
||||
id integer NOT NULL,
|
||||
restriction_name character varying(255) NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.restrictions OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: restrictions_id_seq; Type: SEQUENCE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.restrictions_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE public.restrictions_id_seq OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: restrictions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.restrictions_id_seq OWNED BY public.restrictions.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions; Type: TABLE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE TABLE public.room_restrictions (
|
||||
id integer NOT NULL,
|
||||
start_date date NOT NULL,
|
||||
end_date date NOT NULL,
|
||||
room_id integer NOT NULL,
|
||||
reservation_id integer,
|
||||
restriction_id integer NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.room_restrictions OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: room_restrictions_id_seq; Type: SEQUENCE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.room_restrictions_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE public.room_restrictions_id_seq OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: room_restrictions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.room_restrictions_id_seq OWNED BY public.room_restrictions.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: rooms; Type: TABLE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE TABLE public.rooms (
|
||||
id integer NOT NULL,
|
||||
room_name character varying(255) NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.rooms OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: rooms_id_seq; Type: SEQUENCE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.rooms_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE public.rooms_id_seq OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: rooms_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.rooms_id_seq OWNED BY public.rooms.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: schema_migration; Type: TABLE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE TABLE public.schema_migration (
|
||||
version character varying(14) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.schema_migration OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: users; Type: TABLE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE TABLE public.users (
|
||||
id integer NOT NULL,
|
||||
first_name character varying(255) DEFAULT ''::character varying NOT NULL,
|
||||
last_name character varying(255) DEFAULT ''::character varying NOT NULL,
|
||||
email character varying(255) NOT NULL,
|
||||
password character varying(60) NOT NULL,
|
||||
access_level integer DEFAULT 1 NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.users OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.users_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE public.users_id_seq OWNER TO jing;
|
||||
|
||||
--
|
||||
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations id; Type: DEFAULT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservations ALTER COLUMN id SET DEFAULT nextval('public.reservations_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: restrictions id; Type: DEFAULT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.restrictions ALTER COLUMN id SET DEFAULT nextval('public.restrictions_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions id; Type: DEFAULT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.room_restrictions ALTER COLUMN id SET DEFAULT nextval('public.room_restrictions_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: rooms id; Type: DEFAULT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.rooms ALTER COLUMN id SET DEFAULT nextval('public.rooms_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: users id; Type: DEFAULT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations reservations_pkey; Type: CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservations
|
||||
ADD CONSTRAINT reservations_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: restrictions restrictions_pkey; Type: CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.restrictions
|
||||
ADD CONSTRAINT restrictions_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions room_restrictions_pkey; Type: CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.room_restrictions
|
||||
ADD CONSTRAINT room_restrictions_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: rooms rooms_pkey; Type: CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.rooms
|
||||
ADD CONSTRAINT rooms_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: schema_migration schema_migration_pkey; Type: CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.schema_migration
|
||||
ADD CONSTRAINT schema_migration_pkey PRIMARY KEY (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations_email_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE INDEX reservations_email_idx ON public.reservations USING btree (email);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations_last_name_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE INDEX reservations_last_name_idx ON public.reservations USING btree (last_name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions_reservation_id_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE INDEX room_restrictions_reservation_id_idx ON public.room_restrictions USING btree (reservation_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions_room_id_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE INDEX room_restrictions_room_id_idx ON public.room_restrictions USING btree (room_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions_start_date_end_date_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE INDEX room_restrictions_start_date_end_date_idx ON public.room_restrictions USING btree (start_date, end_date);
|
||||
|
||||
|
||||
--
|
||||
-- Name: schema_migration_version_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX schema_migration_version_idx ON public.schema_migration USING btree (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: users_email_idx; Type: INDEX; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX users_email_idx ON public.users USING btree (email);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations reservations_rooms_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservations
|
||||
ADD CONSTRAINT reservations_rooms_id_fk FOREIGN KEY (room_id) REFERENCES public.rooms(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions room_restrictions_reservations_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.room_restrictions
|
||||
ADD CONSTRAINT room_restrictions_reservations_id_fk FOREIGN KEY (reservation_id) REFERENCES public.reservations(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions room_restrictions_restrictions_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.room_restrictions
|
||||
ADD CONSTRAINT room_restrictions_restrictions_id_fk FOREIGN KEY (restriction_id) REFERENCES public.restrictions(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- Name: room_restrictions room_restrictions_rooms_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: jing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.room_restrictions
|
||||
ADD CONSTRAINT room_restrictions_rooms_id_fk FOREIGN KEY (room_id) REFERENCES public.rooms(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
2
run.sh
@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
go build -o bookings cmd/web/*go && ./bookings
|
||||
go build -o bookings cmd/web/*go && ./bookings -cache=false -production=false
|
||||
|
234
sql/query.sql
Normal file
@ -0,0 +1,234 @@
|
||||
-- name: InsertReservation :one
|
||||
INSERT INTO
|
||||
reservations (
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone,
|
||||
start_date,
|
||||
end_date,
|
||||
room_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING
|
||||
id;
|
||||
|
||||
-- name: InsertRoomRestriction :exec
|
||||
INSERT INTO
|
||||
room_restrictions (
|
||||
start_date,
|
||||
end_date,
|
||||
room_id,
|
||||
reservation_id,
|
||||
restriction_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7);
|
||||
|
||||
-- name: SearchAvailabilityByDatesByRoomID :one
|
||||
SELECT
|
||||
count(id)
|
||||
FROM
|
||||
room_restrictions
|
||||
WHERE
|
||||
room_id = $1
|
||||
AND $2 < end_date
|
||||
AND $3 > start_date;
|
||||
|
||||
-- name: SearchAvailabilityForAllRooms :many
|
||||
SELECT
|
||||
r.id,
|
||||
r.room_name
|
||||
FROM
|
||||
rooms r
|
||||
WHERE
|
||||
r.id NOT IN (
|
||||
SELECT
|
||||
room_id
|
||||
FROM
|
||||
room_restrictions rr
|
||||
WHERE
|
||||
$1 < rr.end_date
|
||||
AND $2 > rr.start_date
|
||||
);
|
||||
|
||||
-- name: GetRoomById :one
|
||||
SELECT
|
||||
id,
|
||||
room_name,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
rooms
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
password,
|
||||
access_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
first_name = $1,
|
||||
last_name = $2,
|
||||
email = $3,
|
||||
access_level = $4,
|
||||
updated_at = $5;
|
||||
|
||||
-- name: GetUserCred :one
|
||||
SELECT
|
||||
id,
|
||||
password
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = $1;
|
||||
|
||||
-- name: AllReservations :many
|
||||
SELECT
|
||||
r.id,
|
||||
r.first_name,
|
||||
r.last_name,
|
||||
r.email,
|
||||
r.phone,
|
||||
r.start_date,
|
||||
r.end_date,
|
||||
r.room_id,
|
||||
r.created_at,
|
||||
r.updated_at,
|
||||
r.processed,
|
||||
rm.id,
|
||||
rm.room_name
|
||||
FROM
|
||||
reservations r
|
||||
LEFT JOIN rooms rm ON (r.room_id = rm.id)
|
||||
ORDER BY
|
||||
r.start_date ASC;
|
||||
|
||||
-- name: AllNewReservations :many
|
||||
SELECT
|
||||
r.id,
|
||||
r.first_name,
|
||||
r.last_name,
|
||||
r.email,
|
||||
r.phone,
|
||||
r.start_date,
|
||||
r.end_date,
|
||||
r.room_id,
|
||||
r.created_at,
|
||||
r.updated_at,
|
||||
r.processed,
|
||||
rm.id,
|
||||
rm.room_name
|
||||
FROM
|
||||
reservations r
|
||||
LEFT JOIN rooms rm ON (r.room_id = rm.id)
|
||||
WHERE
|
||||
r.processed = 0
|
||||
ORDER BY
|
||||
r.start_date ASC;
|
||||
|
||||
-- name: GetReservationByID :one
|
||||
SELECT
|
||||
r.id,
|
||||
r.first_name,
|
||||
r.last_name,
|
||||
r.email,
|
||||
r.phone,
|
||||
r.start_date,
|
||||
r.end_date,
|
||||
r.room_id,
|
||||
r.created_at,
|
||||
r.updated_at,
|
||||
r.processed,
|
||||
rm.id,
|
||||
rm.room_name
|
||||
FROM
|
||||
reservations r
|
||||
LEFT JOIN rooms rm ON (r.room_id = rm.id)
|
||||
WHERE
|
||||
r.id = $1;
|
||||
|
||||
-- name: UpdateReservation :exec
|
||||
UPDATE reservations
|
||||
SET
|
||||
first_name = $1,
|
||||
last_name = $2,
|
||||
email = $3,
|
||||
phone = $4,
|
||||
updated_at = $5
|
||||
WHERE
|
||||
id = $6;
|
||||
|
||||
-- name: DeleteReservation :exec
|
||||
DELETE FROM reservations
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateProcessedForReservation :exec
|
||||
UPDATE reservations
|
||||
SET
|
||||
processed = $1
|
||||
WHERE
|
||||
id = $2;
|
||||
|
||||
-- name: AllRooms :many
|
||||
SELECT
|
||||
id,
|
||||
room_name,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
rooms
|
||||
ORDER BY
|
||||
room_name;
|
||||
|
||||
-- name: GetRestrictionsForRoomByDate :many
|
||||
SELECT
|
||||
id,
|
||||
coalesce(reservation_id, 0),
|
||||
restriction_id,
|
||||
room_id,
|
||||
start_date,
|
||||
end_date
|
||||
FROM
|
||||
room_restrictions
|
||||
WHERE
|
||||
@start_date < end_date
|
||||
AND @end_date >= start_date
|
||||
AND room_id = @room_id;
|
||||
|
||||
-- name: InsertBlockForRoom :exec
|
||||
INSERT INTO
|
||||
room_restrictions (
|
||||
start_date,
|
||||
end_date,
|
||||
room_id,
|
||||
restriction_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6);
|
||||
|
||||
-- name: DeleteBlockByID :exec
|
||||
DELETE FROM room_restrictions
|
||||
WHERE
|
||||
id = $1;
|
96
sql/schema.sql
Normal file
@ -0,0 +1,96 @@
|
||||
-- Adminer 4.8.1 PostgreSQL 16.3 (Debian 16.3-1.pgdg120+1) dump
|
||||
|
||||
CREATE SEQUENCE reservations_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."reservations" (
|
||||
"id" integer DEFAULT nextval('reservations_id_seq') NOT NULL,
|
||||
"first_name" character varying(255) DEFAULT '' NOT NULL,
|
||||
"last_name" character varying(255) DEFAULT '' NOT NULL,
|
||||
"email" character varying(255) NOT NULL,
|
||||
"phone" character varying(255) DEFAULT '' NOT NULL,
|
||||
"start_date" date NOT NULL,
|
||||
"end_date" date NOT NULL,
|
||||
"room_id" integer NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
"processed" integer DEFAULT '0' NOT NULL,
|
||||
CONSTRAINT "reservations_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
CREATE INDEX "reservations_email_idx" ON "public"."reservations" USING btree ("email");
|
||||
|
||||
CREATE INDEX "reservations_last_name_idx" ON "public"."reservations" USING btree ("last_name");
|
||||
|
||||
|
||||
CREATE SEQUENCE restrictions_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."restrictions" (
|
||||
"id" integer DEFAULT nextval('restrictions_id_seq') NOT NULL,
|
||||
"restriction_name" character varying(255) NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
CONSTRAINT "restrictions_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE SEQUENCE room_restrictions_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."room_restrictions" (
|
||||
"id" integer DEFAULT nextval('room_restrictions_id_seq') NOT NULL,
|
||||
"start_date" date NOT NULL,
|
||||
"end_date" date NOT NULL,
|
||||
"room_id" integer NOT NULL,
|
||||
"reservation_id" integer,
|
||||
"restriction_id" integer NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
CONSTRAINT "room_restrictions_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
CREATE INDEX "room_restrictions_reservation_id_idx" ON "public"."room_restrictions" USING btree ("reservation_id");
|
||||
|
||||
CREATE INDEX "room_restrictions_room_id_idx" ON "public"."room_restrictions" USING btree ("room_id");
|
||||
|
||||
CREATE INDEX "room_restrictions_start_date_end_date_idx" ON "public"."room_restrictions" USING btree ("start_date", "end_date");
|
||||
|
||||
|
||||
CREATE SEQUENCE rooms_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."rooms" (
|
||||
"id" integer DEFAULT nextval('rooms_id_seq') NOT NULL,
|
||||
"room_name" character varying(255) NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
CONSTRAINT "rooms_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE TABLE "public"."schema_migration" (
|
||||
"version" character varying(14) NOT NULL,
|
||||
CONSTRAINT "schema_migration_pkey" PRIMARY KEY ("version"),
|
||||
CONSTRAINT "schema_migration_version_idx" UNIQUE ("version")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE SEQUENCE users_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."users" (
|
||||
"id" integer DEFAULT nextval('users_id_seq') NOT NULL,
|
||||
"first_name" character varying(255) DEFAULT '' NOT NULL,
|
||||
"last_name" character varying(255) DEFAULT '' NOT NULL,
|
||||
"email" character varying(255) NOT NULL,
|
||||
"password" character varying(60) NOT NULL,
|
||||
"access_level" integer DEFAULT '1' NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
CONSTRAINT "users_email_idx" UNIQUE ("email"),
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."reservations" ADD CONSTRAINT "reservations_rooms_id_fk" FOREIGN KEY (room_id) REFERENCES rooms(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
|
||||
|
||||
ALTER TABLE ONLY "public"."room_restrictions" ADD CONSTRAINT "room_restrictions_reservations_id_fk" FOREIGN KEY (reservation_id) REFERENCES reservations(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
|
||||
ALTER TABLE ONLY "public"."room_restrictions" ADD CONSTRAINT "room_restrictions_restrictions_id_fk" FOREIGN KEY (restriction_id) REFERENCES restrictions(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
|
||||
ALTER TABLE ONLY "public"."room_restrictions" ADD CONSTRAINT "room_restrictions_rooms_id_fk" FOREIGN KEY (room_id) REFERENCES rooms(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
|
||||
|
10
sqlc.yml
Normal file
@ -0,0 +1,10 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
queries: "sql/query.sql"
|
||||
schema: "sql/schema.sql"
|
||||
gen:
|
||||
go:
|
||||
package: "sqlc"
|
||||
out: "internal/repository/sqlc"
|
||||
sql_package: "pgx/v5"
|
BIN
static/admin/css/.DS_Store
vendored
Normal file
1
static/admin/css/maps/style.css.map
Normal file
22631
static/admin/css/style.css
Normal file
BIN
static/admin/documentation/.DS_Store
vendored
Normal file
137
static/admin/documentation/documentation.html
Normal file
@ -0,0 +1,137 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>RoyalUI Admin Dashboard</title>
|
||||
<!-- plugins:css -->
|
||||
<link rel="stylesheet" href="../vendors/ti-icons/css/themify-icons.css">
|
||||
<link rel="stylesheet" href="../vendors/base/vendor.bundle.base.css">
|
||||
<!-- endinject -->
|
||||
<!-- plugin css for this page -->
|
||||
<!-- End plugin css for this page -->
|
||||
<!-- inject:css -->
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
<!-- endinject -->
|
||||
<link rel="shortcut icon" href="../images/favicon.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-scroller">
|
||||
<div class="container-fluid page-body-wrapper full-page-wrapper">
|
||||
<div class="main-panel w-100 documentation">
|
||||
<div class="content-wrapper">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 doc-header">
|
||||
<a class="btn btn-success" href="../index.html"><i class="mdi mdi-home me-2"></i>Back to home</a>
|
||||
<h1 class="text-primary mt-4">Documentation</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row doc-content">
|
||||
<div class="col-12 col-md-10 offset-md-1">
|
||||
<div class="col-12 grid-margin" id="doc-intro">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="mb-4 mt-4">Introduction</h3>
|
||||
<p>RoyalUI Admin is a responsive HTML template that is based on the CSS framework Bootstrap 5 and it is built with Sass. Sass compiler makes it easier to code and customize. If you are unfamiliar with Bootstrap or Sass, visit their
|
||||
website and read through the documentation. All of Bootstrap components have been modified to fit the style of RoyalUI Admin and provide a consistent look throughout the template.</p>
|
||||
<p>Before you start working with the template, we suggest you go through the pages that are bundled with the theme. Most of the template example pages contain quick tips on how to create or use a component which can
|
||||
be really helpful when you need to create something on the fly.</p>
|
||||
<p class="d-inline"><strong>Note</strong>: We are trying our best to document how to use the template. If you think that something is missing from the documentation, please do not hesitate to tell us about it. If you have any questions or issues regarding this theme please email us at <a class="d-inline text-info" href="mailto:info@bootstrapdash.com">info@bootstrapdash.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 grid-margin" id="doc-started">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="mb-4">Getting started</h3>
|
||||
<p>You can directly use the compiled and ready-to-use the version of the template. But in case you plan to customize the template extensively the template allows you to do so.</p>
|
||||
<p>Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations:</p>
|
||||
<pre>
|
||||
RoyalUI/
|
||||
├── template/
|
||||
├── css/
|
||||
├── fonts/
|
||||
├── images/
|
||||
├── js/
|
||||
├── pages/
|
||||
├── partials/
|
||||
├── index.html
|
||||
├── scss/
|
||||
├── vendors/
|
||||
├── gulpfile.js
|
||||
├── package.json
|
||||
├── documentation/
|
||||
├── CHANGELOG.md</pre>
|
||||
<p class="mt-1">Note: The root folder denoted further in this documentation refers to the 'template' folder inside the downloaded folder</p>
|
||||
<div class="alert alert-success mt-4 d-flex align-items-center" role="alert">
|
||||
<i class="ti-info-alt me-4"></i>
|
||||
<p>We have bundled up the vendor files needed for demo purpose into a folder 'vendors', you may not need all those vendors or may need to add more vendors in your application. If you want to make any change in the vendor package files, you can either change the src path for related tasks in the file gulpfile.js and run the task <code> bundleVendors </code> to rebuild the vendor files or manually edit the vendor folder.</p>
|
||||
</div>
|
||||
<hr class="mt-5">
|
||||
<h4 class="mt-4">Installation</h4>
|
||||
<p class="mb-0">
|
||||
You need to install package files/Dependencies for this project if you want to customize it. To do this, you must have <span class="font-weight-bold">node and npm</span> installed in your computer.
|
||||
</p>
|
||||
<p class="mb-0">Installation guide of the node can be found <a href="https://nodejs.org/en/">here</a>. As npm comes bundled with a node, a separate installation of npm is not needed.</p>
|
||||
<p>
|
||||
If you have installed them, just go to the root folder and run the following command in your command prompt or terminal (for the mac users).
|
||||
</p>
|
||||
<pre class="shell-mode">
|
||||
npm install</pre>
|
||||
<p class="mt-4">
|
||||
This will install the dev dependencies in the local <span class="font-weight-bold">node_modules</span> folder in your root directory.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Then you will need to install <span class="font-weight-bold">Gulp</span>. We use the Gulp task manager for the development processes. Gulp will watch for changes to the SCSS files and automatically compile the files to CSS.
|
||||
</p>
|
||||
<p>Getting started with Gulp is pretty simple. The <a href="https://gulpjs.com/" target="_blank">Gulp</a> site is a great place to get information on installing Gulp if you need more information. You need to first install Gulp-cli in your machine using the below command.</p>
|
||||
<pre class="shell-mode">
|
||||
npm install -g gulp-cli</pre>
|
||||
<p class="mt-4">This installs Gulp-cli globally to your machine. The other thing that Gulp requires, which, is really what does all the work, is the gulpfile.js. In this file, you set up all of your tasks that you will run.</p>
|
||||
<p>Don't worry. We have this file already created for you!</p>
|
||||
<p>To run this project in development mode enter the following command below. This will start the file watch by gulp and whenever a file is modified, the SCSS files will be compiled to create the CSS file.</p>
|
||||
<pre class="shell-mode">
|
||||
gulp serve</pre>
|
||||
<div class="alert alert-warning mt-4" role="alert">
|
||||
<i class="ti-info-alt-outline"></i>It is important to run <code>gulp serve</code> command from the directory where the gulpfile.js is located.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- partial:../partials/_footer.html -->
|
||||
|
||||
<footer class="footer">
|
||||
<div class="d-sm-flex justify-content-center justify-content-sm-between">
|
||||
<span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © 2018 <a href="https://www.bootstrapdash.com/" target="_blank">Bootstrapdash</a>. All rights reserved.</span>
|
||||
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Hand-crafted & made with <i class="ti-heart text-danger ms-1"></i></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- partial -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- plugins:js -->
|
||||
<script src="../vendors/base/vendor.bundle.base.js"></script>
|
||||
<!-- endinject -->
|
||||
<!-- inject:js -->
|
||||
<script src="../js/off-canvas.js"></script>
|
||||
<script src="../js/hoverable-collapse.js"></script>
|
||||
<script src="../js/template.js"></script>
|
||||
<script src="../js/todolist.js"></script>
|
||||
<!-- endinject -->
|
||||
<!-- Custom js for this page-->
|
||||
<script src="../js/documentation.js"></script>
|
||||
<!-- End custom js for this page-->
|
||||
</body>
|
||||
|
||||
</html>
|
BIN
static/admin/fonts/.DS_Store
vendored
Normal file
BIN
static/admin/fonts/Roboto/.DS_Store
vendored
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff2
Normal file
176
static/admin/gulpfile.js
Normal file
@ -0,0 +1,176 @@
|
||||
'use strict'
|
||||
|
||||
var gulp = require('gulp');
|
||||
var browserSync = require('browser-sync').create();
|
||||
var sass = require('gulp-sass');
|
||||
var del = require('del');
|
||||
var replace = require('gulp-replace');
|
||||
var injectPartials = require('gulp-inject-partials');
|
||||
var inject = require('gulp-inject');
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
var concat = require('gulp-concat');
|
||||
var merge = require('merge-stream');
|
||||
|
||||
gulp.paths = {
|
||||
dist: 'dist',
|
||||
};
|
||||
|
||||
var paths = gulp.paths;
|
||||
|
||||
gulp.task('sass', function () {
|
||||
return gulp.src('./scss/**/style.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError))
|
||||
.pipe(sourcemaps.write('./maps'))
|
||||
.pipe(gulp.dest('./css'))
|
||||
.pipe(browserSync.stream());
|
||||
});
|
||||
|
||||
// Static Server + watching scss/html files
|
||||
gulp.task('serve', gulp.series('sass', function() {
|
||||
|
||||
browserSync.init({
|
||||
port: 3100,
|
||||
server: "./",
|
||||
ghostMode: false,
|
||||
notify: false
|
||||
});
|
||||
|
||||
gulp.watch('scss/**/*.scss', gulp.series('sass'));
|
||||
gulp.watch('**/*.html').on('change', browserSync.reload);
|
||||
gulp.watch('js/**/*.js').on('change', browserSync.reload);
|
||||
|
||||
}));
|
||||
|
||||
|
||||
|
||||
// Static Server without watching scss files
|
||||
gulp.task('serve:lite', function() {
|
||||
|
||||
browserSync.init({
|
||||
server: "./",
|
||||
ghostMode: false,
|
||||
notify: false
|
||||
});
|
||||
|
||||
gulp.watch('**/*.css').on('change', browserSync.reload);
|
||||
gulp.watch('**/*.html').on('change', browserSync.reload);
|
||||
gulp.watch('js/**/*.js').on('change', browserSync.reload);
|
||||
|
||||
});
|
||||
|
||||
|
||||
gulp.task('sass:watch', function () {
|
||||
gulp.watch('./scss/**/*.scss');
|
||||
});
|
||||
|
||||
|
||||
/* inject partials like sidebar and navbar */
|
||||
gulp.task('injectPartial', function () {
|
||||
var injPartial1 = gulp.src("./pages/**/*.html", { base: "./" })
|
||||
.pipe(injectPartials())
|
||||
.pipe(gulp.dest("."));
|
||||
var injPartial2 = gulp.src("./*.html", { base: "./" })
|
||||
.pipe(injectPartials())
|
||||
.pipe(gulp.dest("."));
|
||||
return merge(injPartial1, injPartial2);
|
||||
});
|
||||
|
||||
|
||||
/* inject Js and CCS assets into HTML */
|
||||
gulp.task('injectCommonAssets', function () {
|
||||
return gulp.src('./**/*.html')
|
||||
.pipe(inject(gulp.src([
|
||||
'./vendors/ti-icons/css/themify-icons.css',
|
||||
'./vendors/base/vendor.bundle.base.css',
|
||||
'./vendors/base/vendor.bundle.base.js'
|
||||
], {read: false}), {name: 'plugins', relative: true}))
|
||||
.pipe(inject(gulp.src([
|
||||
'./css/*.css',
|
||||
'./js/off-canvas.js',
|
||||
'./js/hoverable-collapse.js',
|
||||
'./js/template.js',
|
||||
'./js/todolist.js'
|
||||
], {read: false}), {relative: true}))
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
/* inject Js and CCS assets into HTML */
|
||||
gulp.task('injectLayoutStyles', function () {
|
||||
return gulp.src('./**/*.html')
|
||||
.pipe(inject(gulp.src([
|
||||
'./css/style.css',
|
||||
], {read: false}), {relative: true}))
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
/*replace image path and linking after injection*/
|
||||
gulp.task('replacePath', function(){
|
||||
var replacePath1 = gulp.src(['./pages/*/*.html'], { base: "./" })
|
||||
.pipe(replace('="images/', '="../../images/'))
|
||||
.pipe(replace('href="pages/', 'href="../../pages/'))
|
||||
.pipe(replace('href="documentation/', 'href="../../documentation/'))
|
||||
.pipe(replace('href="index.html"', 'href="../../index.html"'))
|
||||
.pipe(gulp.dest('.'));
|
||||
var replacePath2 = gulp.src(['./pages/*.html'], { base: "./" })
|
||||
.pipe(replace('="images/', '="../images/'))
|
||||
.pipe(replace('"pages/', '"../pages/'))
|
||||
.pipe(replace('href="index.html"', 'href="../index.html"'))
|
||||
.pipe(gulp.dest('.'));
|
||||
var replacePath3 = gulp.src(['./index.html'], { base: "./" })
|
||||
.pipe(replace('="images/', '="images/'))
|
||||
.pipe(gulp.dest('.'));
|
||||
return merge(replacePath1, replacePath2, replacePath3);
|
||||
});
|
||||
|
||||
/*sequence for injecting partials and replacing paths*/
|
||||
gulp.task('inject', gulp.series('injectPartial' , 'injectCommonAssets' , 'injectLayoutStyles', 'replacePath'));
|
||||
|
||||
|
||||
gulp.task('clean:vendors', function () {
|
||||
return del([
|
||||
'vendors/**/*'
|
||||
]);
|
||||
});
|
||||
|
||||
/*Building vendor scripts needed for basic template rendering*/
|
||||
gulp.task('buildBaseVendorScripts', function() {
|
||||
return gulp.src([
|
||||
'./node_modules/jquery/dist/jquery.min.js',
|
||||
// './node_modules/popper.js/dist/umd/popper.min.js',
|
||||
'./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js',
|
||||
'./node_modules/perfect-scrollbar/dist/perfect-scrollbar.min.js'
|
||||
])
|
||||
.pipe(concat('vendor.bundle.base.js'))
|
||||
.pipe(gulp.dest('./vendors/base'));
|
||||
});
|
||||
|
||||
/*Building vendor styles needed for basic template rendering*/
|
||||
gulp.task('buildBaseVendorStyles', function() {
|
||||
return gulp.src(['./node_modules/perfect-scrollbar/css/perfect-scrollbar.css'])
|
||||
.pipe(concat('vendor.bundle.base.css'))
|
||||
.pipe(gulp.dest('./vendors/base'));
|
||||
});
|
||||
|
||||
/* Scripts for addons */
|
||||
gulp.task('copyRecursiveVendorFiles', function () {
|
||||
var chartJs = gulp.src(['./node_modules/chart.js/dist/Chart.min.js'])
|
||||
.pipe(gulp.dest('./vendors/chart.js'));
|
||||
var ti = gulp.src(['./node_modules/ti-icons/css/themify-icons.css'])
|
||||
.pipe(gulp.dest('./vendors/ti-icons/css'));
|
||||
var tiFonts = gulp.src(['./node_modules/ti-icons/fonts/*'])
|
||||
.pipe(gulp.dest('./vendors/ti-icons/fonts'));
|
||||
return merge(chartJs, ti, tiFonts);
|
||||
});
|
||||
|
||||
//Copy essential map files
|
||||
gulp.task('copyMapFiles', function() {
|
||||
var map1 = gulp.src('node_modules/bootstrap/dist/js/bootstrap.min.js.map')
|
||||
.pipe(gulp.dest('./vendors/base'));
|
||||
return merge(map1);
|
||||
});
|
||||
|
||||
/*sequence for building vendor scripts and styles*/
|
||||
gulp.task('bundleVendors', gulp.series('clean:vendors', 'buildBaseVendorStyles','buildBaseVendorScripts','copyRecursiveVendorFiles','copyMapFiles'));
|
||||
|
||||
gulp.task('default', gulp.series('serve'));
|
BIN
static/admin/images/.DS_Store
vendored
Normal file
BIN
static/admin/images/auth/.DS_Store
vendored
Normal file
BIN
static/admin/images/auth/lockscreen-bg.jpg
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
static/admin/images/auth/login-bg.jpg
Executable file
After Width: | Height: | Size: 162 KiB |
BIN
static/admin/images/auth/register-bg.jpg
Executable file
After Width: | Height: | Size: 269 KiB |
3
static/admin/images/dashboard/shape-1.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="39" viewBox="0 0 56 39">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M6.18 19.87A15.62 15.62 0 0 1 6 17.5C6 8.94 12.94 2 21.5 2 30.06 2 37 8.94 37 17.5c0 1.582-.237 3.108-.677 4.545a5 5 0 1 1-4.047 6.596A15.449 15.449 0 0 1 21.5 33c-.867 0-1.717-.071-2.544-.208A7.001 7.001 0 1 1 6.17 28.123 4.995 4.995 0 0 1 4 24c0-1.715.864-3.229 2.18-4.13zM39 7a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM2 20a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm53 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM34 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM21 37a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM36.5 1a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 626 B |
3
static/admin/images/dashboard/shape-2.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="39" viewBox="0 0 56 39">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M49.82 19.13c.118.772.18 1.564.18 2.37C50 30.06 43.06 37 34.5 37 25.94 37 19 30.06 19 21.5c0-1.582.237-3.108.677-4.545a5 5 0 1 1 4.047-6.596A15.449 15.449 0 0 1 34.5 6c.867 0 1.717.071 2.544.208a7.001 7.001 0 1 1 12.785 4.669A4.995 4.995 0 0 1 52 15a4.995 4.995 0 0 1-2.18 4.13zM17 32a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm37-13a2 2 0 1 1 0 4 2 2 0 0 1 0-4zM1 15a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm21 20a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM35 2a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM19.5 38a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 633 B |
3
static/admin/images/dashboard/shape-3.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="51" viewBox="0 0 48 51">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M14.41 7.005a15.62 15.62 0 0 1 1.89-1.442c7.179-4.662 16.778-2.622 21.44 4.558 4.663 7.18 2.622 16.779-4.557 21.441a15.498 15.498 0 0 1-4.18 1.908 5 5 0 1 1-7.736.198 15.449 15.449 0 0 1-9.525-6.663 15.608 15.608 0 0 1-1.212-2.248 7.001 7.001 0 1 1-3.047-13.265 4.995 4.995 0 0 1 2.276-4.066 4.995 4.995 0 0 1 4.65-.42zm28.668 20.516a2 2 0 1 1 3.355-2.179 2 2 0 0 1-3.355 2.179zM12.024 3.57a2 2 0 1 1 3.355-2.18 2 2 0 0 1-3.355 2.18zm25.511 46.627a1 1 0 1 1 1.678-1.09 1 1 0 0 1-1.678 1.09zm5.336-28.505a1 1 0 1 1 1.678-1.089 1 1 0 0 1-1.678 1.09zm-34.756 7.07a1 1 0 1 1 1.677-1.089 1 1 0 0 1-1.677 1.09zm38.634-6.607a.5.5 0 1 1 .838-.545.5.5 0 0 1-.838.545z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 801 B |
3
static/admin/images/dashboard/shape-4.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="93" height="99" viewBox="0 0 93 99">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M27.66 13.596a30.44 30.44 0 0 1 3.684-2.81c13.991-9.086 32.699-5.11 41.785 8.882 9.086 13.991 5.11 32.7-8.882 41.785a30.203 30.203 0 0 1-8.148 3.718c.284.33.55.683.793 1.059 2.931 4.513 1.649 10.548-2.865 13.479-4.513 2.93-10.548 1.648-13.479-2.865-2.32-3.574-2-8.101.475-11.286A30.107 30.107 0 0 1 22.462 52.57a30.417 30.417 0 0 1-2.361-4.379c-6.127 3.065-13.71 1.102-17.53-4.78-4.103-6.318-2.307-14.767 4.012-18.87a13.578 13.578 0 0 1 7.579-2.202 9.734 9.734 0 0 1 4.435-7.924 9.734 9.734 0 0 1 9.063-.82zm55.872 39.981a3.898 3.898 0 1 1 6.537-4.245 3.898 3.898 0 0 1-6.537 4.245zM23.012 6.902a3.898 3.898 0 1 1 6.538-4.246 3.898 3.898 0 0 1-6.538 4.246zm49.717 90.87a1.949 1.949 0 1 1 3.269-2.123 1.949 1.949 0 0 1-3.269 2.123zm10.399-55.551a1.949 1.949 0 1 1 3.269-2.123 1.949 1.949 0 0 1-3.269 2.123zM15.393 56a1.949 1.949 0 1 1 3.27-2.123A1.949 1.949 0 0 1 15.392 56zm75.292-12.877a.974.974 0 1 1 1.634-1.062.974.974 0 0 1-1.634 1.062z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/admin/images/faces/.DS_Store
vendored
Normal file
BIN
static/admin/images/faces/face1.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face10.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/faces/face11.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/admin/images/faces/face12.jpg
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
static/admin/images/faces/face13.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/admin/images/faces/face14.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/admin/images/faces/face15.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/admin/images/faces/face16.jpg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
static/admin/images/faces/face17.jpg
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
static/admin/images/faces/face18.jpg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
static/admin/images/faces/face19.jpg
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
static/admin/images/faces/face2.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face20.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/admin/images/faces/face21.jpg
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
static/admin/images/faces/face22.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/faces/face23.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face24.jpg
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
static/admin/images/faces/face25.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/admin/images/faces/face26.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face27.jpg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
static/admin/images/faces/face28.jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
static/admin/images/faces/face3.jpg
Normal file
After Width: | Height: | Size: 15 KiB |