Compare commits
103 Commits
c15e4da93c
...
main
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
ab02f8e635 | |||
5f649d6d8e | |||
b9f974cd1f | |||
42a88034bf | |||
74e9fee942 | |||
bb7fcbccb7 | |||
2ae5d9a2c2 | |||
f5f89c2c03 | |||
18c82ba19f | |||
44385fbbce | |||
c4b41d305d | |||
07c84fc414 | |||
4986434e46 | |||
a497798d2f | |||
9fc6c05d38 | |||
0c0159734e | |||
d87d8ed594 | |||
875be55076 | |||
70996c6f60 | |||
21478b20ae | |||
dc7ece7d12 | |||
d8d2abcde3 | |||
3ad57c754f | |||
6826634a01 | |||
87dfd26268 | |||
8be6ba7119 | |||
8394832428 | |||
7294254e13 | |||
eca62e2e7b | |||
e18849331c | |||
97b9898dfc | |||
a8b56b50db | |||
76bee566cd | |||
592d5241d1 | |||
0ca6de59fd | |||
b3eb96845a | |||
310f61539c | |||
e5e5a93565 | |||
bd094d019e | |||
52d4d40140 | |||
d7f978a884 | |||
bfe66073f1 | |||
2351db7178 | |||
2e9391d502 | |||
6e6b003e05 |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
**/c.out
|
||||
**/c.html
|
||||
bookings
|
||||
database.yml
|
||||
postgres/docker-compose.yml
|
126
cmd/web/main.go
126
cmd/web/main.go
@ -1,49 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go-udemy-web-1/pkg/config"
|
||||
"go-udemy-web-1/pkg/handlers"
|
||||
"go-udemy-web-1/pkg/render"
|
||||
"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"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/redisstore"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const portNumber = ":8080"
|
||||
|
||||
var (
|
||||
app config.AppConfig
|
||||
session *scs.SessionManager
|
||||
app config.AppConfig
|
||||
session *scs.SessionManager
|
||||
infoLog *log.Logger
|
||||
errorLog *log.Logger
|
||||
)
|
||||
|
||||
// main is the main application function
|
||||
func main() {
|
||||
// change this to true when in production
|
||||
app.InProduction = false
|
||||
|
||||
session = scs.New()
|
||||
session.Lifetime = 24 * time.Hour
|
||||
session.Cookie.Persist = true
|
||||
session.Cookie.SameSite = http.SameSiteLaxMode
|
||||
session.Cookie.Secure = app.InProduction
|
||||
|
||||
app.Session = session
|
||||
|
||||
tc, err := render.CreateTemplateCache()
|
||||
db, err := run()
|
||||
if err != nil {
|
||||
log.Fatal("cannot create template cache")
|
||||
log.Fatal(err)
|
||||
}
|
||||
app.TemplateCahce = tc
|
||||
app.UseCache = false
|
||||
|
||||
repo := handlers.NewRepo(&app)
|
||||
handlers.NewHandlers(repo)
|
||||
|
||||
render.NewTemplates(&app)
|
||||
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{
|
||||
@ -54,3 +49,84 @@ func main() {
|
||||
err = srv.ListenAndServe()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
func run() (*driver.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 = *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
|
||||
session.Cookie.Secure = app.InProduction
|
||||
|
||||
app.Session = session
|
||||
|
||||
// connect to database
|
||||
log.Println("Connecting to database...")
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
|
||||
*dbHost, *dbPort, *dbName, *dbUser, *dbPass, *dbSSL)
|
||||
db, err := driver.ConnectSQL(dsn)
|
||||
if err != nil {
|
||||
log.Fatal("Cannot connect to database! Dying...")
|
||||
}
|
||||
log.Println("Connected to database")
|
||||
|
||||
tc, err := render.CreateTemplateCache()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create template cache: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
app.TemplateCahce = tc
|
||||
app.UseCache = *useCache
|
||||
|
||||
infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
|
||||
app.InfoLog = infoLog
|
||||
|
||||
errorLog = log.New(os.Stdout, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
app.ErrorLog = errorLog
|
||||
|
||||
repo := handlers.NewRepo(&app, db)
|
||||
handlers.NewHandlers(repo)
|
||||
helpers.NewHelpers(&app)
|
||||
|
||||
render.NewRenderer(&app)
|
||||
return db, nil
|
||||
}
|
||||
|
10
cmd/web/main_test.go
Normal file
10
cmd/web/main_test.go
Normal file
@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
_, err := run()
|
||||
if err != nil {
|
||||
t.Error("failed run()")
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
45
cmd/web/middleware_test.go
Normal file
45
cmd/web/middleware_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoSurf(t *testing.T) {
|
||||
var myH myHandler
|
||||
|
||||
h := NoSurf(&myH)
|
||||
|
||||
switch v := h.(type) {
|
||||
case http.Handler:
|
||||
// do nothing
|
||||
default:
|
||||
t.Errorf("type is not http.Handler, but is %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionLoad(t *testing.T) {
|
||||
var myH myHandler
|
||||
|
||||
s := SessionLoad(&myH)
|
||||
|
||||
switch v := s.(type) {
|
||||
case http.Handler:
|
||||
// do nothing
|
||||
default:
|
||||
t.Errorf("type is not http.Handler, but is %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteToConsole(t *testing.T) {
|
||||
var myH myHandler
|
||||
|
||||
w := WriteToConsole(&myH)
|
||||
|
||||
switch v := w.(type) {
|
||||
case http.Handler:
|
||||
// do nothing
|
||||
default:
|
||||
t.Errorf("type is not http.Handler, but is %T", v)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/pkg/config"
|
||||
"go-udemy-web-1/pkg/handlers"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/handlers"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -14,11 +14,45 @@ 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)
|
||||
mux.Get("/about", handlers.Repo.About)
|
||||
mux.Get("/contact", handlers.Repo.Contact)
|
||||
mux.Get("/generals-quarters", handlers.Repo.Generals)
|
||||
mux.Get("/majors-suite", handlers.Repo.Majors)
|
||||
mux.Get("/availability", handlers.Repo.Availability)
|
||||
mux.Post("/availability", handlers.Repo.PostAvailability)
|
||||
mux.Post("/availability-json", handlers.Repo.AvailabilityJSON)
|
||||
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
|
||||
}
|
||||
|
21
cmd/web/routes_test.go
Normal file
21
cmd/web/routes_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/internal/config"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func TestRoutes(t *testing.T) {
|
||||
var app config.AppConfig
|
||||
|
||||
mux := routes(&app)
|
||||
|
||||
switch v := mux.(type) {
|
||||
case *chi.Mux:
|
||||
// do nothing
|
||||
default:
|
||||
t.Errorf("type is not *chi.Mux, is %T", v)
|
||||
}
|
||||
}
|
58
cmd/web/send-mail.go
Normal file
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")
|
||||
}
|
||||
}
|
16
cmd/web/setup_test.go
Normal file
16
cmd/web/setup_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
type myHandler struct{}
|
||||
|
||||
func (mh *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
14
database-template.yml
Normal file
14
database-template.yml
Normal file
@ -0,0 +1,14 @@
|
||||
development:
|
||||
dialect: postgres
|
||||
database: bookings
|
||||
user:
|
||||
password:
|
||||
host: 127.0.0.1
|
||||
pool: 5
|
||||
|
||||
test:
|
||||
url: {{envOr "TEST_DATABASE_URL" "postgres://postgres:postgres@127.0.0.1:5432/myapp_test"}}
|
||||
|
||||
production:
|
||||
url: {{envOr "DATABASE_URL" "postgres://postgres:postgres@127.0.0.1:5432/myapp_production"}}
|
||||
|
32
docker/docker-compose-example.yml
Normal file
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
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
1524
email-templates/drip.html
Normal file
File diff suppressed because it is too large
Load Diff
30
go.mod
30
go.mod
@ -2,8 +2,38 @@ module go-udemy-web-1
|
||||
|
||||
go 1.21.0
|
||||
|
||||
// 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
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.0.14
|
||||
|
||||
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 (
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
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/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
)
|
||||
|
51
go.sum
51
go.sum
@ -1,6 +1,57 @@
|
||||
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=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
|
||||
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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.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=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -1,7 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/internal/models"
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
)
|
||||
@ -11,5 +13,8 @@ type AppConfig struct {
|
||||
TemplateCahce map[string]*template.Template
|
||||
UseCache bool
|
||||
InProduction bool
|
||||
InfoLog *log.Logger
|
||||
ErrorLog *log.Logger
|
||||
Session *scs.SessionManager
|
||||
MailChan chan models.MailData
|
||||
}
|
67
internal/driver/driver.go
Normal file
67
internal/driver/driver.go
Normal file
@ -0,0 +1,67 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgconn"
|
||||
_ "github.com/jackc/pgx/v5"
|
||||
_ "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
|
||||
maxDbLifetime = 5 * time.Minute
|
||||
)
|
||||
|
||||
// ConnectSQL creates SQL pool for Postgres
|
||||
func ConnectSQL(dsn string) (*DB, error) {
|
||||
d, err := NewDatabase(dsn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
d.SetMaxOpenConns(maxOpenDbConn)
|
||||
d.SetMaxIdleConns(maxIdleDbConn)
|
||||
d.SetConnMaxLifetime(maxDbLifetime)
|
||||
|
||||
dbConn.SQL = d
|
||||
|
||||
err = testDB(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbConn, nil
|
||||
}
|
||||
|
||||
// testDB tries to ping the database
|
||||
func testDB(d *sql.DB) error {
|
||||
err := d.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDatabase creates a new datavase for the application
|
||||
func NewDatabase(dsn string) (*sql.DB, error) {
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ping test the conn
|
||||
if err = db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, err
|
||||
}
|
18
internal/forms/errors.go
Normal file
18
internal/forms/errors.go
Normal file
@ -0,0 +1,18 @@
|
||||
package forms
|
||||
|
||||
type errors map[string][]string
|
||||
|
||||
// Add adds an error message for a given form field
|
||||
func (e errors) Add(field, message string) {
|
||||
e[field] = append(e[field], message)
|
||||
}
|
||||
|
||||
// Get returns the first error message
|
||||
func (e errors) Get(field string) string {
|
||||
es := e[field]
|
||||
|
||||
if len(es) == 0 {
|
||||
return ""
|
||||
}
|
||||
return es[0]
|
||||
}
|
62
internal/forms/forms.go
Normal file
62
internal/forms/forms.go
Normal file
@ -0,0 +1,62 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
// Form creates a custom form struct, embeds a url.Values object
|
||||
type Form struct {
|
||||
url.Values
|
||||
Errors errors
|
||||
}
|
||||
|
||||
// New initializes a form struct
|
||||
func New(data url.Values) *Form {
|
||||
return &Form{
|
||||
data,
|
||||
errors(map[string][]string{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Required checks required fields
|
||||
func (f *Form) Required(fields ...string) {
|
||||
for _, field := range fields {
|
||||
value := f.Get(field)
|
||||
if strings.TrimSpace(value) == "" {
|
||||
f.Errors.Add(field, "This field cannot be blank")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Has checks if form field is in post and not emtpy
|
||||
func (f *Form) Has(field string) bool {
|
||||
x := f.Get(field)
|
||||
return x != ""
|
||||
}
|
||||
|
||||
// Valid returns true if there are no errors, otherwise false
|
||||
func (f *Form) Valid() bool {
|
||||
return len(f.Errors) == 0
|
||||
}
|
||||
|
||||
// MinLength checks for string minimum length
|
||||
func (f *Form) MinLength(field string, length int) bool {
|
||||
value := f.Get(field)
|
||||
if len(value) < length {
|
||||
f.Errors.Add(field, fmt.Sprintf("This field must have at least %d letters", length))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEmail checks the email address
|
||||
func (f *Form) IsEmail(field string) {
|
||||
value := f.Get(field)
|
||||
if !govalidator.IsEmail(value) {
|
||||
f.Errors.Add(field, "Invalid email address")
|
||||
}
|
||||
}
|
131
internal/forms/forms_test.go
Normal file
131
internal/forms/forms_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestForms_Valid(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/test-url", nil)
|
||||
form := New(r.PostForm)
|
||||
|
||||
isValid := form.Valid()
|
||||
if !isValid {
|
||||
t.Error("got invalid when should have been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForms_Required(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/test-url", nil)
|
||||
form := New(r.PostForm)
|
||||
|
||||
form.Required("a", "b", "c")
|
||||
if form.Valid() {
|
||||
t.Error("required fields are not given, should be invalid")
|
||||
}
|
||||
|
||||
postData := url.Values{}
|
||||
|
||||
postData.Add("a", "a")
|
||||
postData.Add("b", "a")
|
||||
postData.Add("c", "a")
|
||||
|
||||
r = httptest.NewRequest("POST", "/test-url", nil)
|
||||
r.PostForm = postData
|
||||
|
||||
form = New(r.PostForm)
|
||||
|
||||
form.Required("a", "b", "c")
|
||||
if !form.Valid() {
|
||||
t.Error("required fields are given, should be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForms_Has(t *testing.T) {
|
||||
postData := url.Values{}
|
||||
form := New(postData)
|
||||
|
||||
if form.Has("a") {
|
||||
t.Error("the field should not exist")
|
||||
}
|
||||
|
||||
postData = url.Values{}
|
||||
postData.Add("a", "a")
|
||||
form = New(postData)
|
||||
|
||||
if !form.Has("a") {
|
||||
t.Error("the field should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForms_MinLength(t *testing.T) {
|
||||
postData := url.Values{}
|
||||
form := New(postData)
|
||||
|
||||
if form.MinLength("a", 3) {
|
||||
t.Error("the field should not exist")
|
||||
}
|
||||
|
||||
errMsg := form.Errors.Get("a")
|
||||
if errMsg != "This field must have at least 3 letters" {
|
||||
t.Error("should have an errMsg")
|
||||
}
|
||||
|
||||
if form.Valid() {
|
||||
t.Error("should be invalid")
|
||||
}
|
||||
|
||||
postData = url.Values{}
|
||||
postData.Add("a", "ab")
|
||||
postData.Add("b", "abc")
|
||||
form = New(postData)
|
||||
|
||||
if form.MinLength("a", 3) {
|
||||
t.Error("the field is shorter than 3 chars")
|
||||
}
|
||||
|
||||
if !form.MinLength("b", 3) {
|
||||
t.Error("the field is equal to 3 chars")
|
||||
}
|
||||
|
||||
if form.Valid() {
|
||||
t.Error("should be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForms_IsEmail(t *testing.T) {
|
||||
postData := url.Values{}
|
||||
form := New(postData)
|
||||
|
||||
form.IsEmail("a")
|
||||
|
||||
if form.Valid() {
|
||||
t.Error("email should not exit")
|
||||
}
|
||||
|
||||
postData = url.Values{}
|
||||
postData.Add("a", "a@a.com")
|
||||
form = New(postData)
|
||||
|
||||
form.IsEmail("a")
|
||||
|
||||
errMsg := form.Errors.Get("a")
|
||||
if errMsg != "" {
|
||||
t.Error("should not have an errMsg")
|
||||
}
|
||||
|
||||
if !form.Valid() {
|
||||
t.Error("should be a valid email")
|
||||
}
|
||||
|
||||
postData = url.Values{}
|
||||
postData.Add("a", "a@.com")
|
||||
form = New(postData)
|
||||
|
||||
form.IsEmail("a")
|
||||
|
||||
if form.Valid() {
|
||||
t.Error("Should not be a valid email")
|
||||
}
|
||||
}
|
821
internal/handlers/handlers.go
Normal file
821
internal/handlers/handlers.go
Normal file
@ -0,0 +1,821 @@
|
||||
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
|
||||
var Repo *Repository
|
||||
|
||||
// Repository is the repository type
|
||||
type Repository struct {
|
||||
App *config.AppConfig
|
||||
DB repository.DatabaseRepo
|
||||
}
|
||||
|
||||
// NewRepo creates a new repository
|
||||
func NewRepo(a *config.AppConfig, db *driver.DB) *Repository {
|
||||
return &Repository{
|
||||
App: a,
|
||||
DB: dbrepo.NewPostgresRepo(db.SQL, a),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestRepo creates a new testing repository
|
||||
func NewTestRepo(a *config.AppConfig) *Repository {
|
||||
return &Repository{
|
||||
App: a,
|
||||
DB: dbrepo.NewTestingRepo(a),
|
||||
}
|
||||
}
|
||||
|
||||
// NewHandlers sets the repository for the handlers
|
||||
func NewHandlers(r *Repository) {
|
||||
Repo = r
|
||||
}
|
||||
|
||||
// Home is the home page handler
|
||||
func (m *Repository) Home(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "home.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// About is the about page handler
|
||||
func (m *Repository) About(w http.ResponseWriter, r *http.Request) {
|
||||
// send the data to the template
|
||||
render.Template(w, r, "about.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// Contact is the contact page handler
|
||||
func (m *Repository) Contact(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "contact.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// Generals is the General's Quarters page handler
|
||||
func (m *Repository) Generals(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "generals.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// Majors is the Major's Suite page handler
|
||||
func (m *Repository) Majors(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "majors.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// MakeReservation is the make reservation page handler
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't parse form")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
form := forms.New(r.PostForm)
|
||||
|
||||
form.Required("first_name", "last_name", "email")
|
||||
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 {
|
||||
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: reservation.StartDate,
|
||||
EndDate: reservation.EndDate,
|
||||
ID: reservation.ID,
|
||||
RoomID: reservation.RoomID,
|
||||
ReservationID: newReservationID,
|
||||
RestrictionID: 1,
|
||||
}
|
||||
|
||||
err = m.DB.InsertRoomRestriction(restriction)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// ReservationSummary is the reservation summary page handler
|
||||
func (m *Repository) ReservationSummary(w http.ResponseWriter, r *http.Request) {
|
||||
reservation, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
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.StatusSeeOther)
|
||||
}
|
||||
|
||||
m.App.Session.Remove(r.Context(), "reservation")
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// Availability is the search for availability page handler
|
||||
func (m *Repository) Availability(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "availability.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't connect to database")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
for _, i := range rooms {
|
||||
m.App.InfoLog.Println("ROOM:", i.ID, i.RoomName)
|
||||
}
|
||||
|
||||
if len(rooms) == 0 {
|
||||
// No availability
|
||||
m.App.InfoLog.Println("No availability")
|
||||
m.App.Session.Put(r.Context(), "error", "No availability")
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
data["rooms"] = rooms
|
||||
|
||||
res := models.Reservation{
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
}
|
||||
|
||||
m.App.Session.Put(r.Context(), "reservation", res)
|
||||
|
||||
render.Template(w, r, "choose-room.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
// can't parse form, so return appropriate json
|
||||
resp := jsonResponse{
|
||||
OK: false,
|
||||
Message: "Internal server error",
|
||||
}
|
||||
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)
|
||||
}
|
647
internal/handlers/handlers_test.go
Normal file
647
internal/handlers/handlers_test.go
Normal file
@ -0,0 +1,647 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-udemy-web-1/internal/driver"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postData struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
// {{{ Test NewRepo
|
||||
|
||||
func Test_NewRepo(t *testing.T) {
|
||||
var db driver.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
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"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) {
|
||||
routes := getRoutes()
|
||||
ts := httptest.NewTLSServer(routes)
|
||||
defer ts.Close()
|
||||
|
||||
for _, e := range theTests {
|
||||
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\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 {
|
||||
session.Put(ctx, "reservation", reservation)
|
||||
}
|
||||
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.Errorf("for %s, failed to parse json", test.name)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// }}}
|
179
internal/handlers/setup_test.go
Normal file
179
internal/handlers/setup_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"go-udemy-web-1/internal/render"
|
||||
"html/template"
|
||||
"log"
|
||||
"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"
|
||||
)
|
||||
|
||||
var functions = template.FuncMap{
|
||||
"humanDate": render.HumanDate,
|
||||
"formatDate": render.FormatDate,
|
||||
"iterate": render.Iterate,
|
||||
"add": render.Add,
|
||||
}
|
||||
|
||||
var (
|
||||
app config.AppConfig
|
||||
session *scs.SessionManager
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
session = scs.New()
|
||||
session.Lifetime = 24 * time.Hour
|
||||
session.Cookie.Persist = true
|
||||
session.Cookie.SameSite = http.SameSiteLaxMode
|
||||
session.Cookie.Secure = app.InProduction
|
||||
|
||||
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)
|
||||
}
|
||||
app.TemplateCahce = tc
|
||||
app.UseCache = true // Not to use ./templates
|
||||
|
||||
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
|
||||
app.InfoLog = infoLog
|
||||
|
||||
errorLog := log.New(os.Stdout, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
app.ErrorLog = errorLog
|
||||
|
||||
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(WriteToConsole)
|
||||
mux.Use(SessionLoad)
|
||||
|
||||
mux.Get("/", Repo.Home)
|
||||
mux.Get("/about", Repo.About)
|
||||
mux.Get("/contact", Repo.Contact)
|
||||
mux.Get("/generals-quarters", Repo.Generals)
|
||||
mux.Get("/majors-suite", Repo.Majors)
|
||||
mux.Get("/availability", Repo.Availability)
|
||||
mux.Post("/availability", Repo.PostAvailability)
|
||||
mux.Post("/availability-json", Repo.AvailabilityJSON)
|
||||
mux.Get("/make-reservation", Repo.MakeReservation)
|
||||
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))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// WriteToConsole writes a log when user hits a page
|
||||
func WriteToConsole(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("Hit the page %s\n", r.URL.String())
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// SessionLoad loads and saves the session on every request
|
||||
func SessionLoad(next http.Handler) http.Handler {
|
||||
return session.LoadAndSave(next)
|
||||
}
|
||||
|
||||
var pathToTemplates = "../../templates"
|
||||
|
||||
func CreateTestTemplateCache() (map[string]*template.Template, error) {
|
||||
myCache := map[string]*template.Template{}
|
||||
|
||||
// get all of the files named *.page.tmpl from templates
|
||||
pages, err := filepath.Glob(fmt.Sprintf("%s/*.page.tmpl", pathToTemplates))
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
// range through all files ending with *page.tmpl
|
||||
for _, page := range pages {
|
||||
name := filepath.Base(page)
|
||||
ts, err := template.New(name).Funcs(functions).ParseFiles(page)
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob(fmt.Sprintf("%s/*.layout.tmpl", pathToTemplates))
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
ts, err = ts.ParseGlob(fmt.Sprintf("%s/*.layout.tmpl", pathToTemplates))
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
}
|
||||
|
||||
myCache[name] = ts
|
||||
}
|
||||
|
||||
return myCache, nil
|
||||
}
|
||||
|
||||
func listenForMail() {
|
||||
go func() {
|
||||
for {
|
||||
<-app.MailChan
|
||||
}
|
||||
}()
|
||||
}
|
30
internal/helpers/helpers.go
Normal file
30
internal/helpers/helpers.go
Normal file
@ -0,0 +1,30 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var app *config.AppConfig
|
||||
|
||||
// NewHelpers sets up app config for helpers
|
||||
func NewHelpers(a *config.AppConfig) {
|
||||
app = a
|
||||
}
|
||||
|
||||
func ClientError(w http.ResponseWriter, status int) {
|
||||
app.InfoLog.Println("Client error with status of", status)
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
func ServerError(w http.ResponseWriter, err error) {
|
||||
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
|
||||
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")
|
||||
}
|
72
internal/models/models.go
Normal file
72
internal/models/models.go
Normal file
@ -0,0 +1,72 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User is the user model
|
||||
type User struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Password string
|
||||
ID int
|
||||
AccessLevel int
|
||||
}
|
||||
|
||||
// Room is the room model
|
||||
type Room struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
RoomName string
|
||||
ID int
|
||||
}
|
||||
|
||||
// Restriction is the restriction model
|
||||
type Restriction struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
RestrictionName string
|
||||
ID int
|
||||
}
|
||||
|
||||
// Reservation is the reservation model
|
||||
type Reservation struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
Phone string
|
||||
Room Room
|
||||
ID int
|
||||
RoomID int
|
||||
Processed int
|
||||
}
|
||||
|
||||
// RoomRestriction is the room restriction model
|
||||
type RoomRestriction struct {
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Room Room
|
||||
Restriction Restriction
|
||||
ID int
|
||||
RoomID int
|
||||
ReservationID int
|
||||
RestrictionID int
|
||||
}
|
||||
|
||||
// MailData holds an email message
|
||||
type MailData struct {
|
||||
To string
|
||||
From string
|
||||
Subject string
|
||||
Content string
|
||||
Template string
|
||||
}
|
17
internal/models/templatedata.go
Normal file
17
internal/models/templatedata.go
Normal file
@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "go-udemy-web-1/internal/forms"
|
||||
|
||||
// TemplateData holds data sent from handlers to templates
|
||||
type TemplateData struct {
|
||||
StringMap map[string]string
|
||||
IntMap map[string]int
|
||||
FloatMap map[string]float32
|
||||
Data map[string]interface{}
|
||||
Form *forms.Form
|
||||
CSRFToken string
|
||||
Flash string
|
||||
Warning string
|
||||
Error string
|
||||
IsAuthenticated int
|
||||
}
|
141
internal/render/render.go
Normal file
141
internal/render/render.go
Normal file
@ -0,0 +1,141 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/helpers"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
var functions = template.FuncMap{
|
||||
"humanDate": HumanDate,
|
||||
"formatDate": FormatDate,
|
||||
"iterate": Iterate,
|
||||
"add": Add,
|
||||
}
|
||||
|
||||
var (
|
||||
app *config.AppConfig
|
||||
pathToTemplates = "./templates"
|
||||
)
|
||||
|
||||
// NewRenderer sets the config for the template package
|
||||
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
|
||||
}
|
||||
|
||||
// Template renders a HTML template file
|
||||
func Template(w http.ResponseWriter, r *http.Request, tmpl string, td *models.TemplateData) error {
|
||||
var tc map[string]*template.Template
|
||||
if app.UseCache {
|
||||
// get the template cache from the app config
|
||||
tc = app.TemplateCahce
|
||||
} else {
|
||||
tc, _ = CreateTemplateCache()
|
||||
}
|
||||
|
||||
// get requested template from cache
|
||||
t, ok := tc[tmpl]
|
||||
if !ok {
|
||||
return errors.New("could not get template from template cache")
|
||||
}
|
||||
|
||||
// Write to a buffer to make sure that the template can be read and
|
||||
// written successfully
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
td = AddDefaultData(td, r)
|
||||
|
||||
err := t.Execute(buf, td)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// render the template
|
||||
_, err = buf.WriteTo(w)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateTemplateCache() (map[string]*template.Template, error) {
|
||||
myCache := map[string]*template.Template{}
|
||||
|
||||
// get all of the files named *.page.tmpl from templates
|
||||
pages, err := filepath.Glob(fmt.Sprintf("%s/*.page.tmpl", pathToTemplates))
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
// range through all files ending with *page.tmpl
|
||||
for _, page := range pages {
|
||||
name := filepath.Base(page)
|
||||
ts, err := template.New(name).Funcs(functions).ParseFiles(page)
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob(fmt.Sprintf("%s/*.layout.tmpl", pathToTemplates))
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
ts, err = ts.ParseGlob(fmt.Sprintf("%s/*.layout.tmpl", pathToTemplates))
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
}
|
||||
|
||||
myCache[name] = ts
|
||||
}
|
||||
|
||||
return myCache, nil
|
||||
}
|
76
internal/render/render_test.go
Normal file
76
internal/render/render_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/internal/models"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddDefaultData(t *testing.T) {
|
||||
var td models.TemplateData
|
||||
|
||||
r, err := getSession()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
session.Put(r.Context(), "flash", "123")
|
||||
|
||||
result := AddDefaultData(&td, r)
|
||||
|
||||
if result.Flash != "123" {
|
||||
t.Error("flash value of 123 not found in session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTemplate(t *testing.T) {
|
||||
pathToTemplates = "../../templates"
|
||||
tc, err := CreateTemplateCache()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
app.TemplateCahce = tc
|
||||
|
||||
r, err := getSession()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var ww myWriter
|
||||
|
||||
err = Template(&ww, r, "home.page.tmpl", &models.TemplateData{})
|
||||
if err != nil {
|
||||
t.Error("error writiing template to browser")
|
||||
}
|
||||
|
||||
err = Template(&ww, r, "non-existent.page.html", &models.TemplateData{})
|
||||
if err == nil {
|
||||
t.Error("rendered template that doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
func getSession() (*http.Request, error) {
|
||||
r, err := http.NewRequest("GET", "/some-url", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx, _ = session.Load(ctx, r.Header.Get("X-Session"))
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func TestNewTemplate(t *testing.T) {
|
||||
NewRenderer(app)
|
||||
}
|
||||
|
||||
func TestCreateTemplateCache(t *testing.T) {
|
||||
pathToTemplates = "../../templates"
|
||||
_, err := CreateTemplateCache()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
59
internal/render/setup_test.go
Normal file
59
internal/render/setup_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
session *scs.SessionManager
|
||||
testApp config.AppConfig
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// what am I going to put in the session
|
||||
gob.Register(models.Reservation{})
|
||||
|
||||
// change this to true when in production
|
||||
testApp.InProduction = false
|
||||
|
||||
session = scs.New()
|
||||
session.Lifetime = 24 * time.Hour
|
||||
session.Cookie.Persist = true
|
||||
session.Cookie.SameSite = http.SameSiteLaxMode
|
||||
session.Cookie.Secure = testApp.InProduction
|
||||
|
||||
testApp.Session = session
|
||||
|
||||
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
|
||||
testApp.InfoLog = infoLog
|
||||
|
||||
errorLog := log.New(os.Stdout, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
testApp.ErrorLog = errorLog
|
||||
|
||||
app = &testApp
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
type myWriter struct{}
|
||||
|
||||
func (tw *myWriter) Header() http.Header {
|
||||
var h http.Header
|
||||
return h
|
||||
}
|
||||
|
||||
func (tw *myWriter) WriteHeader(i int) {}
|
||||
|
||||
func (tw *myWriter) Write(b []byte) (int, error) {
|
||||
length := len(b)
|
||||
return length, nil
|
||||
}
|
30
internal/repository/dbrepo/dbrepo.go
Normal file
30
internal/repository/dbrepo/dbrepo.go
Normal file
@ -0,0 +1,30 @@
|
||||
package dbrepo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
)
|
||||
|
||||
type postgresDBRepo struct {
|
||||
App *config.AppConfig
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
494
internal/repository/dbrepo/postgres.go
Normal file
494
internal/repository/dbrepo/postgres.go
Normal file
@ -0,0 +1,494 @@
|
||||
package dbrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (m *postgresDBRepo) AllUsers() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// InsertReservation inserts a reservation into the database
|
||||
func (m *postgresDBRepo) InsertReservation(res models.Reservation) (int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var newId int
|
||||
// statement
|
||||
stmt := `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`
|
||||
|
||||
row := m.DB.QueryRowContext(ctx, stmt,
|
||||
res.FirstName,
|
||||
res.LastName,
|
||||
res.Email,
|
||||
res.Phone,
|
||||
res.StartDate,
|
||||
res.EndDate,
|
||||
res.RoomID,
|
||||
time.Now(),
|
||||
time.Now())
|
||||
|
||||
err := row.Scan(&newId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newId, nil
|
||||
}
|
||||
|
||||
// InsertRoomRestriction inserts a room restriction into the database
|
||||
func (m *postgresDBRepo) InsertRoomRestriction(r models.RoomRestriction) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stmt := `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)`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, stmt,
|
||||
r.StartDate,
|
||||
r.EndDate,
|
||||
r.RoomID,
|
||||
r.ReservationID,
|
||||
r.RestrictionID,
|
||||
time.Now(),
|
||||
time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchAvailabilityByDatesByRoomID returns true if availability exists for roomID, and false if no availability
|
||||
func (m *postgresDBRepo) SearchAvailabilityByDatesByRoomID(start, end time.Time, roomID int) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var numRows int
|
||||
stmt := `select count(id) from room_restrictions
|
||||
where room_id = $1 and
|
||||
$2 < end_date and $3> start_date`
|
||||
|
||||
row := m.DB.QueryRowContext(ctx, stmt, roomID, start, end)
|
||||
err := row.Scan(&numRows)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if numRows == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SearchAvailabilityForAllRooms returns a slice of rooms, if any, for given date range
|
||||
func (m *postgresDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]models.Room, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var rooms []models.Room
|
||||
stmt := `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)`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, stmt, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var room models.Room
|
||||
|
||||
err := rows.Scan(&room.ID, &room.RoomName)
|
||||
if err != nil {
|
||||
return rooms, err
|
||||
}
|
||||
rooms = append(rooms, room)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return rooms, err
|
||||
}
|
||||
|
||||
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
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
|
||||
}
|
29
internal/repository/repository.go
Normal file
29
internal/repository/repository.go
Normal file
@ -0,0 +1,29 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/internal/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DatabaseRepo interface {
|
||||
AllUsers() bool
|
||||
|
||||
InsertReservation(res models.Reservation) (int, error)
|
||||
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
|
||||
}
|
1
migrations/20240706133130_create_user_table.down.fizz
Normal file
1
migrations/20240706133130_create_user_table.down.fizz
Normal file
@ -0,0 +1 @@
|
||||
sql("DROP TABLE users")
|
8
migrations/20240706133130_create_user_table.up.fizz
Normal file
8
migrations/20240706133130_create_user_table.up.fizz
Normal file
@ -0,0 +1,8 @@
|
||||
create_table("users") {
|
||||
t.Column("id", "integer", {primary: true})
|
||||
t.Column("first_name", "string", {"default": ""})
|
||||
t.Column("last_name", "string", {"default": ""})
|
||||
t.Column("email", "string", {})
|
||||
t.Column("password", "string", {"size": 60})
|
||||
t.Column("access_level", "integer", {"default": 1})
|
||||
}
|
@ -0,0 +1 @@
|
||||
drop_table("reservations")
|
11
migrations/20240706135509_create_reservations_table.up.fizz
Normal file
11
migrations/20240706135509_create_reservations_table.up.fizz
Normal file
@ -0,0 +1,11 @@
|
||||
create_table("reservations") {
|
||||
t.Column("id", "integer", {primary: true})
|
||||
t.Column("first_name", "string", {"default": ""})
|
||||
t.Column("last_name", "string", {"default": ""})
|
||||
t.Column("email", "string", {})
|
||||
t.Column("phone", "string", {"default": ""})
|
||||
t.Column("start_date", "date", {})
|
||||
t.Column("end_date", "date", {})
|
||||
t.Column("room_id", "integer", {})
|
||||
}
|
||||
|
1
migrations/20240706135731_create_rooms_table.down.fizz
Normal file
1
migrations/20240706135731_create_rooms_table.down.fizz
Normal file
@ -0,0 +1 @@
|
||||
drop_table("rooms")
|
5
migrations/20240706135731_create_rooms_table.up.fizz
Normal file
5
migrations/20240706135731_create_rooms_table.up.fizz
Normal file
@ -0,0 +1,5 @@
|
||||
create_table("rooms") {
|
||||
t.Column("id", "integer", {primary: true})
|
||||
t.Column("room_name", "string", {})
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
drop_table("restrictions")
|
@ -0,0 +1,5 @@
|
||||
create_table("restrictions") {
|
||||
t.Column("id", "integer", {primary: true})
|
||||
t.Column("restriction_name", "string", {})
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
drop_table("room_restrictions")
|
||||
|
@ -0,0 +1,9 @@
|
||||
create_table("room_restrictions") {
|
||||
t.Column("id", "integer", {primary: true})
|
||||
t.Column("start_date", "date", {})
|
||||
t.Column("end_date", "date", {})
|
||||
t.Column("room_id", "integer", {})
|
||||
t.Column("reservation_id", "integer", {})
|
||||
t.Column("restriction_id", "integer", {})
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
drop_foreign_key("reservations", "reservations_rooms_id_fk")
|
@ -0,0 +1,4 @@
|
||||
add_foreign_key("reservations", "room_id", {"rooms": ["id"]}, {
|
||||
"on_delete": "cascade",
|
||||
"on_update": "cascade",
|
||||
})
|
@ -0,0 +1,3 @@
|
||||
drop_foreign_key("room_restrictions", "room_restrictions_rooms_id_fk")
|
||||
drop_foreign_key("room_restrictions", "room_restrictions_restrictions_id_fk")
|
||||
|
@ -0,0 +1,9 @@
|
||||
add_foreign_key("room_restrictions", "room_id", {"rooms": ["id"]}, {
|
||||
"on_delete": "cascade",
|
||||
"on_update": "cascade",
|
||||
})
|
||||
|
||||
add_foreign_key("room_restrictions", "restriction_id", {"restrictions": ["id"]}, {
|
||||
"on_delete": "cascade",
|
||||
"on_update": "cascade",
|
||||
})
|
@ -0,0 +1,2 @@
|
||||
drop_index("users", "users_email_idx")
|
||||
|
@ -0,0 +1 @@
|
||||
add_index("users", "email", {"unique": true})
|
@ -0,0 +1,4 @@
|
||||
drop_index("room_restrictions", "room_restrictions_reservation_id_idx")
|
||||
drop_index("room_restrictions", "room_restrictions_room_id_idx")
|
||||
drop_index("room_restrictions", "room_restrictions_start_date_end_date_idx")
|
||||
|
@ -0,0 +1,3 @@
|
||||
add_index("room_restrictions", ["start_date", "end_date"], {})
|
||||
add_index("room_restrictions", "room_id", {})
|
||||
add_index("room_restrictions", "reservation_id", {})
|
@ -0,0 +1,4 @@
|
||||
drop_foreign_key("room_restrictions", "room_restrictions_reservations_id_fk")
|
||||
drop_index("room_restrictions", "reservations_email_idx")
|
||||
drop_index("room_restrictions", "reservations_last_name_idx")
|
||||
|
@ -0,0 +1,9 @@
|
||||
add_foreign_key("room_restrictions", "reservation_id", {"reservations": ["id"]}, {
|
||||
"on_delete": "cascade",
|
||||
"on_update": "cascade",
|
||||
})
|
||||
|
||||
add_index("reservations", "email", {})
|
||||
add_index("reservations", "last_name", {})
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
change_column("room_restrictions", "reservation_id", "integer", {"null": false})
|
||||
|
@ -0,0 +1 @@
|
||||
change_column("room_restrictions", "reservation_id", "integer", {"null": true})
|
@ -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,49 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"go-udemy-web-1/pkg/config"
|
||||
"go-udemy-web-1/pkg/models"
|
||||
"go-udemy-web-1/pkg/render"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Repo the repository used by the handlers
|
||||
var Repo *Repository
|
||||
|
||||
// Repository is the repository type
|
||||
type Repository struct {
|
||||
App *config.AppConfig
|
||||
}
|
||||
|
||||
// NewRepo creates a new repository
|
||||
func NewRepo(a *config.AppConfig) *Repository {
|
||||
return &Repository{
|
||||
App: a,
|
||||
}
|
||||
}
|
||||
|
||||
// NewHandlers sets the repository for the handlers
|
||||
func NewHandlers(r *Repository) {
|
||||
Repo = r
|
||||
}
|
||||
|
||||
// Home is the about page handler
|
||||
func (m *Repository) Home(w http.ResponseWriter, r *http.Request) {
|
||||
remoteIP := r.RemoteAddr
|
||||
m.App.Session.Put(r.Context(), "remote_ip", remoteIP)
|
||||
|
||||
render.RenderTemplate(w, "home.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// About is the about page handler
|
||||
func (m *Repository) About(w http.ResponseWriter, r *http.Request) {
|
||||
// perform some logic
|
||||
stringMap := make(map[string]string)
|
||||
stringMap["test"] = "Hello world!"
|
||||
|
||||
remoteIP := m.App.Session.GetString(r.Context(), "remote_ip")
|
||||
stringMap["remote_ip"] = remoteIP
|
||||
|
||||
// send the data to the template
|
||||
render.RenderTemplate(w, "about.page.tmpl", &models.TemplateData{StringMap: stringMap})
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package models
|
||||
|
||||
// TemplateData holds data sent from handlers to templates
|
||||
type TemplateData struct {
|
||||
StringMap map[string]string
|
||||
IntMap map[string]int
|
||||
FloatMap map[string]float32
|
||||
Data map[string]interface{}
|
||||
CSRFToken string
|
||||
Flash string
|
||||
Warning string
|
||||
Error string
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go-udemy-web-1/pkg/config"
|
||||
"go-udemy-web-1/pkg/models"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var app *config.AppConfig
|
||||
|
||||
// NewTemplates sets the config for the template package
|
||||
func NewTemplates(a *config.AppConfig) {
|
||||
app = a
|
||||
}
|
||||
|
||||
// AddDefaultData adds default template data
|
||||
func AddDefaultData(td *models.TemplateData) *models.TemplateData {
|
||||
return td
|
||||
}
|
||||
|
||||
// RenderTemplate renders a HTML template file
|
||||
func RenderTemplate(w http.ResponseWriter, tmpl string, td *models.TemplateData) {
|
||||
var tc map[string]*template.Template
|
||||
if app.UseCache {
|
||||
// get the template cache from the app config
|
||||
tc = app.TemplateCahce
|
||||
} else {
|
||||
tc, _ = CreateTemplateCache()
|
||||
}
|
||||
|
||||
// get requested template from cache
|
||||
t, ok := tc[tmpl]
|
||||
if !ok {
|
||||
log.Fatal("Could not get template from template cache")
|
||||
}
|
||||
|
||||
// Write to a buffer to make sure that the template can be read and
|
||||
// written successfully
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
td = AddDefaultData(td)
|
||||
|
||||
err := t.Execute(buf, td)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
// render the template
|
||||
_, err = buf.WriteTo(w)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTemplateCache() (map[string]*template.Template, error) {
|
||||
myCache := map[string]*template.Template{}
|
||||
|
||||
// get all of the files named *.page.tmpl from ./templates
|
||||
pages, err := filepath.Glob("./templates/*.page.tmpl")
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
// range through all files ending with *page.tmpl
|
||||
for _, page := range pages {
|
||||
name := filepath.Base(page)
|
||||
ts, err := template.New(name).ParseFiles(page)
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob("./templates/*.layout.tmpl")
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
ts, err = ts.ParseGlob("./templates/*.layout.tmpl")
|
||||
if err != nil {
|
||||
return myCache, err
|
||||
}
|
||||
}
|
||||
|
||||
myCache[name] = ts
|
||||
}
|
||||
|
||||
return myCache, nil
|
||||
}
|
3
run.sh
Executable file
3
run.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
go build -o bookings cmd/web/*go && ./bookings -cache=false -production=false
|
BIN
static/admin/css/.DS_Store
vendored
Normal file
BIN
static/admin/css/.DS_Store
vendored
Normal file
Binary file not shown.
1
static/admin/css/maps/style.css.map
Normal file
1
static/admin/css/maps/style.css.map
Normal file
File diff suppressed because one or more lines are too long
22631
static/admin/css/style.css
Normal file
22631
static/admin/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/admin/documentation/.DS_Store
vendored
Normal file
BIN
static/admin/documentation/.DS_Store
vendored
Normal file
Binary file not shown.
137
static/admin/documentation/documentation.html
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/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/.DS_Store
vendored
Normal file
BIN
static/admin/fonts/Roboto/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Black.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.eot
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Black.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.woff
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Black.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.woff2
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Bold.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.eot
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff2
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Light.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.eot
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Light.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.woff
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Light.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.woff2
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Medium.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.eot
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff2
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Regular.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.eot
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff
Normal file
Binary file not shown.
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff2
Normal file
Binary file not shown.
176
static/admin/gulpfile.js
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/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/admin/images/auth/.DS_Store
vendored
Normal file
BIN
static/admin/images/auth/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/admin/images/auth/lockscreen-bg.jpg
Normal file
BIN
static/admin/images/auth/lockscreen-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 208 KiB |
BIN
static/admin/images/auth/login-bg.jpg
Executable file
BIN
static/admin/images/auth/login-bg.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
BIN
static/admin/images/auth/register-bg.jpg
Executable file
BIN
static/admin/images/auth/register-bg.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 269 KiB |
3
static/admin/images/dashboard/shape-1.svg
Normal file
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
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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user