Compare commits

..

117 Commits

Author SHA1 Message Date
dd49a1e687 Finish adding sqlc and fix some bugs 2024-08-01 16:49:59 +02:00
459a4e5c7d 1. change db to sqlc, 2.use repo for db drivers 2024-08-01 10:04:50 +02:00
cec183b416 finish pgc impl 2024-07-31 22:15:37 +02:00
f20f256313 Use sqlc 2024-07-30 22:02:26 +02:00
30c552cf6c change postgres to docker 2024-07-30 20:03:44 +02:00
3eab2a3c9f add todo 2024-07-29 22:09:06 +02:00
5d225f0186 Use redis to store the sessions 2024-07-29 21:54:37 +02:00
52c3679158 Use cmd flags 2024-07-29 14:20:51 +02:00
aca8605870 fix a make reservation error 2024-07-29 14:04:43 +02:00
6631288843 add more tests 2024-07-29 13:52:10 +02:00
99c2eec759 add some tests 2024-07-28 15:48:03 +02:00
623291541a Fixing redirects 2024-07-28 15:23:12 +02:00
d7f7a2d8d9 Handling Calendar changes 2024-07-28 14:43:39 +02:00
14828fb901 Reservation calendar 4 2024-07-28 13:59:54 +02:00
7894a05daf Reservation Calendar 3 2024-07-27 22:39:23 +02:00
b97c6cba5c reservation calendar 2 2024-07-26 13:40:56 +02:00
a7cf9fe4f0 Showing the reservation calendar 2024-07-26 10:18:27 +02:00
5987fadb03 Deleting a reservation 2024-07-25 14:09:42 +02:00
fcd29cc082 Marking a resercation as processed 2024-07-25 13:51:31 +02:00
ca1e72c676 Editing a reservation 2024-07-25 13:29:57 +02:00
d4cf44bb22 Database functions for editing a reservation 2024-07-25 13:13:16 +02:00
845f3a5836 Showing one reservation 2024-07-24 22:23:31 +02:00
dafd6f21c0 Listing new reservations 2024-07-24 14:20:30 +02:00
f93388a8e9 Listing all reservations 2024-07-22 22:14:48 +02:00
b34c217d98 create stub handlers for admin functionnality 2024-07-22 14:05:20 +02:00
ea796999d4 convert admin template into go template 2024-07-22 13:54:59 +02:00
51e3c74950 add admin dropdown 2024-07-22 13:10:17 +02:00
dedab91898 Protecting our routes with auth middleware 2024-07-22 10:13:50 +02:00
68d9110cd2 change flash to success 2024-07-22 10:04:08 +02:00
96f81418ec Checking to see if a user is logged in, and logging a user our 2024-07-22 10:00:17 +02:00
a0853cf880 Writing auth middleware 2024-07-19 18:40:19 +02:00
a6dca00199 Creating handler to log in 2024-07-19 10:02:58 +02:00
d32fd432d2 Creating the auth and user database functions 2024-07-19 09:44:27 +02:00
3e70a5727f Creating the auth handlers for the login screen 2024-07-19 09:23:45 +02:00
447bfc4599 Create the login screen 2024-07-19 09:23:24 +02:00
c88974788f mock the mail server using a channel to read 2024-07-17 22:37:56 +02:00
b1d3095c89 Sending mails using Foundation templates 2024-07-17 22:34:03 +02:00
d40233b4ba Installing a mailer package and setting up a mail channel 2024-07-16 14:31:54 +02:00
bf61fb7c18 sending mails using standard lib 2024-07-16 13:35:10 +02:00
998850b708 finish handler tests 2024-07-16 13:22:46 +02:00
191b010b8e use RequestURI instead of chi.URLParam for testing purpose 2024-07-15 22:42:05 +02:00
6853a1a483 add tests for postavailability and reservation summary 2024-07-15 22:38:29 +02:00
e8390cc51d Use url.Values for post params 2024-07-15 21:36:02 +02:00
4debe0e14e finish availabilityjson test 2024-07-15 21:24:10 +02:00
3f1dfeabc2 add docker compose for pg 2024-07-15 15:13:21 +02:00
4ec8c5d9a1 Fix seed sqls
Should use '' instead of "".
2024-07-15 15:11:06 +02:00
904ee95d63 add tests for availabilityJson 2024-07-14 14:51:12 +02:00
413bfc1685 user postData instead 2024-07-14 11:59:43 +02:00
d76070c21d fix bugs and add tests for PostMakeReservation handler 2024-07-14 11:49:42 +02:00
262b48161d add comment for nosurf 2024-07-13 18:19:51 +02:00
5d670c1dc5 fix availabilityJson post not working error: should pass CSRFToken into the function 2024-07-13 18:19:23 +02:00
0f17b3405e Improving tests by handling multiple test cases 2024-07-13 16:47:23 +02:00
f67aed4942 Updating existing tests to handle sessions 2024-07-13 16:35:17 +02:00
1c46c5a64b Creating a test database repository 2024-07-12 22:43:16 +02:00
32250d92c4 Adding migrations for seeding rooms and restrictions 2024-07-11 23:47:14 +02:00
e2e03307b7 Extrating js module 2024-07-11 23:30:17 +02:00
9775b67a2d Connecting the rooms page to make reservation page 2024-07-11 23:01:10 +02:00
fed901ce25 Providing feedback when searching by room, and connecting to the reservation page 2024-07-10 23:32:49 +02:00
5eb9284b6f Searching for availability by Room 2024-07-10 23:09:45 +02:00
e0a29d21c3 Cleaning up the reservation summary page and improbing validation 2024-07-10 22:55:54 +02:00
95cddfc950 Cleaning up our make reservation page and testing everything 2024-07-10 22:34:27 +02:00
5a646a2b27 Connection search availability to the make reservation page 2024-07-10 22:08:23 +02:00
ab02f8e635 Connecting handlers to new database functions 2024-07-10 12:35:16 +02:00
5f649d6d8e add todo 2024-07-10 11:30:04 +02:00
b9f974cd1f searching for availability for all rooms 2024-07-09 23:50:54 +02:00
42a88034bf Searching for availability by room 2024-07-09 23:08:31 +02:00
74e9fee942 Inserting Room Restrictions 2024-07-09 22:57:07 +02:00
bb7fcbccb7 fix wrong dbname 2024-07-08 23:16:13 +02:00
2ae5d9a2c2 Setting up database functions: inserting a reservation 2024-07-08 23:04:17 +02:00
f5f89c2c03 add comment about orm packages 2024-07-07 22:50:46 +02:00
18c82ba19f Cleaning up our code 2024-07-07 22:37:48 +02:00
44385fbbce Setting up models 2024-07-07 22:32:32 +02:00
c4b41d305d Connecting to the datavase and adding the sql connection to our Repository 2024-07-06 22:55:25 +02:00
07c84fc414 Creating a Driver package 2024-07-06 22:26:41 +02:00
4986434e46 How to connect a Go application to a database 2024-07-06 22:03:02 +02:00
a497798d2f Designing the database using soda migration 2024-07-06 16:51:08 +02:00
9fc6c05d38 Centralizing error handling to a helpers package 2024-07-03 10:03:25 +02:00
0c0159734e Add run script 2024-07-02 21:52:22 +02:00
d87d8ed594 Writing tests for the Forms package 2024-07-02 21:48:00 +02:00
875be55076 Writing tests for Render package 2024-07-02 13:57:45 +02:00
70996c6f60 Writing tests for POST handlers 2024-07-02 13:16:27 +02:00
21478b20ae Writing tests for main package and for GET handlers 2024-07-01 22:34:16 +02:00
dc7ece7d12 update go mod 2024-07-01 14:34:15 +02:00
d8d2abcde3 Finishing up our response to user, and adding alerts 2024-07-01 14:28:00 +02:00
3ad57c754f Displaying a response to user after posting form data 2024-07-01 14:19:38 +02:00
6826634a01 Server Side form validation 4: check email 2024-07-01 13:47:52 +02:00
87dfd26268 Server Side form validation 3 2024-07-01 13:37:03 +02:00
8be6ba7119 Server side form validation 2 2024-06-30 19:35:59 +02:00
8394832428 Server-side form validation 1 2024-06-30 17:08:47 +02:00
7294254e13 Refactoring to use internal packages 2024-06-30 16:41:46 +02:00
eca62e2e7b Sending AJAX post and generalizing the custom function 2024-06-30 16:37:57 +02:00
e18849331c Sending & processing an AJAX request 2024-06-30 16:23:12 +02:00
97b9898dfc rename reservation to availability 2024-06-30 10:51:29 +02:00
a8b56b50db Creating a handler that return JSON 2024-06-30 10:42:57 +02:00
76bee566cd Creating handlers for forms & adding CSRF Protection 2024-06-30 10:31:15 +02:00
592d5241d1 Add check-avalability js 2024-06-29 21:42:15 +02:00
0ca6de59fd Converting pages to templates 2024-06-29 21:30:06 +02:00
b3eb96845a Add CSS file and fix datepicker display error 2024-06-29 17:50:33 +02:00
310f61539c Using JS module on the book button 2024-06-29 17:01:26 +02:00
e5e5a93565 Custom alerts using Notie 2024-06-29 15:57:00 +02:00
bd094d019e Making a better date picker 2024-06-29 15:38:55 +02:00
52d4d40140 javascript init 2024-06-29 15:21:09 +02:00
d7f978a884 Creating the reservation page 2024-06-29 15:02:15 +02:00
bfe66073f1 Improving our form 2024-06-29 14:54:21 +02:00
2351db7178 Creating a page for each room && Adding a form to search for availability 2024-06-29 11:53:45 +02:00
2e9391d502 Creating a landing page 2024-06-28 23:20:01 +02:00
6e6b003e05 Enabling static files 2024-06-28 22:30:19 +02:00
c15e4da93c Experimenting with sessions 2024-06-28 15:41:25 +02:00
8ec31267c6 Installing and setting up a sessions package 2024-06-28 15:30:00 +02:00
9911144aff Developping our own middleware 2024-06-28 13:33:43 +02:00
e1b990dfd3 Using chi for routing 2024-06-28 13:14:05 +02:00
daef31f070 Using pat for routing 2024-06-28 12:59:47 +02:00
b9c8c2592d Sharing data with templates: correct import cycle 2024-06-28 10:48:52 +02:00
668e88e578 Sharing data with templates: cycle import error 2024-06-28 10:40:47 +02:00
1dd22ba8db Optimizing template cache by using an application config *** 2024-06-27 14:03:43 +02:00
2391f5a160 Building a more complex template cache 2024-06-27 13:19:38 +02:00
3eb7a210b2 Building a simple template cache 2024-06-26 22:50:04 +02:00
252 changed files with 48122 additions and 45 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/c.out
**/c.html
bookings
database.yml
postgres/docker-compose.yml

View File

@ -1,19 +1,134 @@
package main
import (
"encoding/gob"
"flag"
"fmt"
"go-udemy-web-1/pkg/handlers"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/handlers"
"go-udemy-web-1/internal/helpers"
"go-udemy-web-1/internal/models"
"go-udemy-web-1/internal/render"
"go-udemy-web-1/internal/repository"
"go-udemy-web-1/internal/repository/driverrepo"
"log"
"net/http"
"os"
"time"
"github.com/alexedwards/scs/redisstore"
"github.com/alexedwards/scs/v2"
"github.com/gomodule/redigo/redis"
)
const portNumber = ":8080"
var (
app config.AppConfig
session *scs.SessionManager
infoLog *log.Logger
errorLog *log.Logger
)
// main is the main application function
func main() {
http.HandleFunc("/", handlers.Home)
http.HandleFunc("/about", handlers.About)
db, err := run()
if err != nil {
log.Fatal(err)
}
defer db.SQL.Close()
defer close(app.MailChan)
fmt.Println("Starting mail listener...")
fmt.Printf("Starting application on port %s\n", portNumber)
_ = http.ListenAndServe(portNumber, nil)
srv := &http.Server{
Addr: portNumber,
Handler: routes(&app),
}
err = srv.ListenAndServe()
log.Fatal(err)
}
func run() (*repository.DB, error) {
// what am I going to put in the session
gob.Register(models.Reservation{})
gob.Register(models.User{})
gob.Register(models.Room{})
gob.Register(models.Restriction{})
gob.Register(map[string]int{})
// read flags
inProduction := flag.Bool("production", true, "Application is in production")
useCache := flag.Bool("cache", true, "Use template cache")
dbHost := flag.String("dbhost", "localhost", "Database host")
dbName := flag.String("dbname", "bookings", "Database name")
dbUser := flag.String("dbuser", os.Getenv("PGUSER"), "Database user")
dbPass := flag.String("dbpass", os.Getenv("PGPWD"), "Database password")
dbPort := flag.String("dbport", "5432", "Database port")
dbSSL := flag.String("dbssl", "disable", "Database ssl settings (disable, prefer, require)")
flag.Parse()
if *dbUser == "" {
fmt.Println("Missing required flags")
os.Exit(1)
}
mailChan := make(chan models.MailData)
app.MailChan = mailChan
listenForMail()
// change this to true when in production
app.InProduction = *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)
dbdriver := driverrepo.NewSqlcRepo()
db, err := dbdriver.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
View File

@ -0,0 +1,10 @@
package main
import "testing"
func TestRun(t *testing.T) {
_, err := run()
if err != nil {
t.Error("failed run()")
}
}

49
cmd/web/middleware.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"fmt"
"go-udemy-web-1/internal/helpers"
"net/http"
"github.com/justinas/nosurf"
)
// 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)
})
}
// NoSurf adds CSRF protection to all POST requests
func NoSurf(next http.Handler) http.Handler {
csrfHandler := nosurf.New(next)
csrfHandler.SetBaseCookie(http.Cookie{
HttpOnly: true,
Path: "/",
Secure: app.InProduction,
SameSite: http.SameSiteLaxMode,
})
return csrfHandler
}
// SessionLoad loads and saves the session on every request
func SessionLoad(next http.Handler) http.Handler {
return session.LoadAndSave(next)
}
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)
})
}

View 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)
}
}

58
cmd/web/routes.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/handlers"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func routes(app *config.AppConfig) http.Handler {
mux := chi.NewMux()
mux.Use(middleware.Recoverer)
mux.Use(WriteToConsole)
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
View 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
View 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
View 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
View 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"}}

View File

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

31
docker/docker-compose.yml Normal file
View 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

File diff suppressed because it is too large Load Diff

36
go.mod
View File

@ -1,3 +1,39 @@
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
)

57
go.sum Normal file
View File

@ -0,0 +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=

20
internal/config/config.go Normal file
View File

@ -0,0 +1,20 @@
package config
import (
"go-udemy-web-1/internal/models"
"html/template"
"log"
"github.com/alexedwards/scs/v2"
)
// AppConfig holds the application config
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
}

18
internal/forms/errors.go Normal file
View 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
View 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")
}
}

View 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")
}
}

View File

@ -0,0 +1,820 @@
package handlers
import (
"encoding/json"
"fmt"
"go-udemy-web-1/internal/config"
"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 *repository.DB) *Repository {
return &Repository{
App: a,
DB: dbrepo.NewPgcRepo(db.PG, a),
}
}
// NewTestRepo creates a new testing repository
func NewTestRepo(a *config.AppConfig) *Repository {
return &Repository{
App: a,
DB: dbrepo.NewTestingRepo(a),
}
}
// 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)
}

View File

@ -0,0 +1,647 @@
package handlers
import (
"context"
"encoding/json"
"go-udemy-web-1/internal/models"
"go-udemy-web-1/internal/repository"
"log"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"time"
)
type postData struct {
key string
value string
}
// {{{ Test NewRepo
func Test_NewRepo(t *testing.T) {
var db repository.DB
repo := NewRepo(&app, &db)
if reflect.TypeOf(repo).String() != "*handlers.Repository" {
t.Errorf("repo is of type %s instead of *handlers.Repository", reflect.TypeOf(repo).String())
}
}
// }}}
// {{{ Simple get tests
var theTests = []struct {
name string
url string
method string
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
}
// }}}

View 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
}
}()
}

View 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
View 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
}

View 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
View 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
}

View 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)
}
}

View 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
}

View File

@ -0,0 +1,46 @@
package dbrepo
import (
"database/sql"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/repository"
"go-udemy-web-1/internal/repository/sqlc"
"github.com/jackc/pgx/v5"
)
type postgresDBRepo struct {
App *config.AppConfig
DB *sql.DB
}
type pgcDBRepo struct {
App *config.AppConfig
Q *sqlc.Queries
}
type testDBRepo struct {
App *config.AppConfig
DB *sql.DB
}
func NewPostgresRepo(conn *sql.DB, a *config.AppConfig) repository.DatabaseRepo {
return &postgresDBRepo{
App: a,
DB: conn,
}
}
func NewTestingRepo(a *config.AppConfig) repository.DatabaseRepo {
return &testDBRepo{
App: a,
}
}
func NewPgcRepo(conn *pgx.Conn, a *config.AppConfig) repository.DatabaseRepo {
q := sqlc.New(conn)
return &pgcDBRepo{
App: a,
Q: q,
}
}

View File

@ -0,0 +1,416 @@
package dbrepo
import (
"context"
"errors"
"go-udemy-web-1/internal/models"
"go-udemy-web-1/internal/repository/sqlc"
"time"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt"
)
func (m *pgcDBRepo) AllUsers() bool {
return true
}
// InsertReservation inserts a reservation into the database
func (m *pgcDBRepo) InsertReservation(res models.Reservation) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var newId int32
newId, err := m.Q.InsertReservation(ctx, sqlc.InsertReservationParams{
FirstName: res.FirstName,
LastName: res.LastName,
Email: res.Email,
Phone: res.Phone,
StartDate: pgtype.Date{Time: res.StartDate, Valid: true},
EndDate: pgtype.Date{Time: res.EndDate, Valid: true},
RoomID: int32(res.RoomID),
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
m.App.ErrorLog.Println(err)
return 0, err
}
return int(newId), nil
}
// InsertRoomRestriction inserts a room restriction into the database
func (m *pgcDBRepo) InsertRoomRestriction(r models.RoomRestriction) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.InsertRoomRestriction(ctx, sqlc.InsertRoomRestrictionParams{
StartDate: pgtype.Date{Time: r.StartDate, Valid: true},
EndDate: pgtype.Date{Time: r.EndDate, Valid: true},
RoomID: int32(r.RoomID),
ReservationID: pgtype.Int4{Int32: int32(r.ReservationID), Valid: true},
RestrictionID: int32(r.RestrictionID),
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
m.App.ErrorLog.Println(err)
return err
}
return nil
}
// SearchAvailabilityByDatesByRoomID returns true if availability exists for roomID, and false if no availability
func (m *pgcDBRepo) SearchAvailabilityByDatesByRoomID(start, end time.Time, roomID int) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
numRows, err := m.Q.SearchAvailabilityByDatesByRoomID(ctx, sqlc.SearchAvailabilityByDatesByRoomIDParams{
RoomID: int32(roomID),
EndDate: pgtype.Date{Time: start, Valid: true},
StartDate: pgtype.Date{Time: end, Valid: false},
})
if err != nil {
return false, err
}
if numRows == 0 {
return false, nil
}
return true, nil
}
// SearchAvailabilityForAllRooms returns a slice of rooms, if any, for given date range
func (m *pgcDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]models.Room, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var rooms []models.Room
rows, err := m.Q.SearchAvailabilityForAllRooms(ctx, sqlc.SearchAvailabilityForAllRoomsParams{
EndDate: pgtype.Date{Time: start, Valid: true},
StartDate: pgtype.Date{Time: start, Valid: true},
})
if err != nil {
return nil, err
}
for _, row := range rows {
room := models.Room{
RoomName: row.RoomName,
ID: int(row.ID),
}
rooms = append(rooms, room)
}
return rooms, nil
}
// GetRoomById gets a room by id
func (m *pgcDBRepo) GetRoomById(id int) (models.Room, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var room models.Room
row, err := m.Q.GetRoomById(ctx, int32(id))
if err != nil {
return room, err
}
room = models.Room{
ID: int(row.ID),
RoomName: row.RoomName,
}
return room, nil
}
// GetUserByID gets a user by id
func (m *pgcDBRepo) GetUserByID(id int) (models.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var u models.User
row, err := m.Q.GetUserByID(ctx, int32(id))
if err != nil {
return u, err
}
u = models.User{
ID: int(row.ID),
FirstName: row.FirstName,
LastName: row.LastName,
Email: row.Email,
Password: row.Password,
AccessLevel: int(row.AccessLevel),
}
return u, nil
}
// UpdateUser updates a user in the database
func (m *pgcDBRepo) UpdateUser(u models.User) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.UpdateUser(ctx, sqlc.UpdateUserParams{
FirstName: u.FirstName,
LastName: u.LastName,
Email: u.Email,
AccessLevel: int32(u.AccessLevel),
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
return err
}
return nil
}
// Authenticate authenticates a user
func (m *pgcDBRepo) Authenticate(email, testPassword string) (int, string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var id int
var hashedPassword string
row, err := m.Q.GetUserCred(ctx, email)
if err != nil {
return id, "", err
}
id = int(row.ID)
hashedPassword = row.Password
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(testPassword))
if err == bcrypt.ErrMismatchedHashAndPassword {
return 0, "", errors.New("incorrect password")
} else if err != nil {
return 0, "", err
}
return id, hashedPassword, nil
}
// AllReservations returns a slice of all reservations
func (m *pgcDBRepo) AllReservations() ([]models.Reservation, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var reservations []models.Reservation
rows, err := m.Q.AllReservations(ctx)
if err != nil {
return nil, err
}
for _, row := range rows {
r := models.Reservation{
StartDate: row.StartDate.Time,
EndDate: row.EndDate.Time,
FirstName: row.FirstName,
LastName: row.LastName,
Email: row.Email,
Phone: row.Phone,
Room: models.Room{ID: int(row.RoomID), RoomName: row.RoomName.String},
ID: int(row.ID),
RoomID: int(row.RoomID),
Processed: int(row.Processed),
}
reservations = append(reservations, r)
}
return reservations, nil
}
// AllNewReservations returns a slice of all new reservations
func (m *pgcDBRepo) AllNewReservations() ([]models.Reservation, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var reservations []models.Reservation
rows, err := m.Q.AllNewReservations(ctx)
if err != nil {
return nil, err
}
for _, row := range rows {
r := models.Reservation{
StartDate: row.StartDate.Time,
EndDate: row.EndDate.Time,
FirstName: row.FirstName,
LastName: row.LastName,
Email: row.Email,
Phone: row.Phone,
Room: models.Room{ID: int(row.RoomID), RoomName: row.RoomName.String},
ID: int(row.ID),
RoomID: int(row.RoomID),
Processed: int(row.Processed),
}
reservations = append(reservations, r)
}
return reservations, nil
}
// GetReservationByID returns one reservation by ID
func (m *pgcDBRepo) GetReservationByID(id int) (models.Reservation, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var res models.Reservation
row, err := m.Q.GetReservationByID(ctx, int32(id))
if err != nil {
return res, err
}
res = models.Reservation{
StartDate: row.StartDate.Time,
EndDate: row.EndDate.Time,
FirstName: row.FirstName,
LastName: row.LastName,
Email: row.Email,
Phone: row.Phone,
Room: models.Room{ID: int(row.RoomID), RoomName: row.RoomName.String},
ID: int(row.ID),
RoomID: int(row.RoomID),
Processed: int(row.Processed),
}
return res, nil
}
// UpdateReservation updates a user in the database
func (m *pgcDBRepo) UpdateReservation(r models.Reservation) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.UpdateReservation(ctx, sqlc.UpdateReservationParams{
ID: int32(r.ID),
FirstName: r.FirstName,
LastName: r.LastName,
Email: r.Email,
Phone: r.Phone,
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
return err
}
return nil
}
func (m *pgcDBRepo) DeleteReservation(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.DeleteReservation(ctx, int32(id))
if err != nil {
return err
}
return nil
}
// UpdateProcessedForReservation set processed for a reservation
func (m *pgcDBRepo) UpdateProcessedForReservation(id, processed int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.UpdateProcessedForReservation(ctx, sqlc.UpdateProcessedForReservationParams{
Processed: int32(processed),
ID: int32(id),
})
if err != nil {
return err
}
return nil
}
func (m *pgcDBRepo) AllRooms() ([]models.Room, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var rooms []models.Room
rows, err := m.Q.AllRooms(ctx)
if err != nil {
return nil, err
}
for _, row := range rows {
room := models.Room{
RoomName: row.RoomName,
ID: int(row.ID),
}
rooms = append(rooms, room)
}
return rooms, nil
}
// GetRestrictionsForRoomByDate returns restrictions for a room by date range
func (m *pgcDBRepo) GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var restrictions []models.RoomRestriction
rows, err := m.Q.GetRestrictionsForRoomByDate(ctx, sqlc.GetRestrictionsForRoomByDateParams{
EndDate: pgtype.Date{Time: end, Valid: true},
StartDate: pgtype.Date{Time: start, Valid: true},
RoomID: int32(roomId),
})
if err != nil {
return restrictions, err
}
for _, row := range rows {
r := models.RoomRestriction{
StartDate: row.StartDate.Time,
EndDate: row.EndDate.Time,
ID: int(row.ID),
RoomID: int(row.RoomID),
ReservationID: int(row.ReservationID),
RestrictionID: int(row.RestrictionID),
}
restrictions = append(restrictions, r)
}
return restrictions, nil
}
// InsertBlockForRoom inserts a room restriction
func (m *pgcDBRepo) InsertBlockForRoom(id int, startDate time.Time) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.InsertBlockForRoom(ctx, sqlc.InsertBlockForRoomParams{
StartDate: pgtype.Date{Time: startDate, Valid: true},
EndDate: pgtype.Date{Time: startDate.AddDate(0, 0, 1)},
RoomID: int32(id),
RestrictionID: 2,
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
return err
}
return nil
}
// DeleteBlockByID deletes a block by ID
func (m *pgcDBRepo) DeleteBlockByID(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := m.Q.DeleteBlockByID(ctx, int32(id))
if err != nil {
return err
}
return nil
}

View 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
}

View 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
}

View File

@ -0,0 +1,20 @@
package driverrepo
import (
"go-udemy-web-1/internal/repository"
)
type (
sqlDriverRepo struct{}
sqlcDriverRepo struct{}
)
var dbConn = &repository.DB{}
func NewSqlRepo() repository.DatabaseDriverRepo {
return &sqlDriverRepo{}
}
func NewSqlcRepo() repository.DatabaseDriverRepo {
return &sqlcDriverRepo{}
}

View File

@ -0,0 +1,48 @@
package driverrepo
import (
"context"
"go-udemy-web-1/internal/repository"
"github.com/jackc/pgx/v5"
)
func (s *sqlcDriverRepo) ConnectSQL(dsn string) (*repository.DB, error) {
ctx := context.Background()
c, err := s.newConn(ctx, dsn)
if err != nil {
panic(err)
}
dbConn.PG = c
err = s.testDB(ctx, c)
if err != nil {
return nil, err
}
return dbConn, nil
}
// testDB tries to ping the database
func (s *sqlcDriverRepo) testDB(ctx context.Context, c *pgx.Conn) error {
err := c.Ping(ctx)
if err != nil {
return err
}
return nil
}
// newDatabase creates a new database for the application
func (s *sqlcDriverRepo) newConn(ctx context.Context, dsn string) (*pgx.Conn, error) {
conn, err := pgx.Connect(ctx, dsn)
if err != nil {
return nil, err
}
// Ping test the conn
if err = conn.Ping(ctx); err != nil {
return nil, err
}
return conn, err
}

View File

@ -0,0 +1,61 @@
package driverrepo
import (
"database/sql"
"go-udemy-web-1/internal/repository"
"time"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v5"
_ "github.com/jackc/pgx/v5/stdlib"
)
const (
maxOpenDbConn = 10
maxIdleDbConn = 5
maxDbLifetime = 5 * time.Minute
)
// ConnectSQL creates SQL pool for Postgres
func (s *sqlDriverRepo) ConnectSQL(dsn string) (*repository.DB, error) {
d, err := s.newDatabase(dsn)
if err != nil {
panic(err)
}
d.SetMaxOpenConns(maxOpenDbConn)
d.SetMaxIdleConns(maxIdleDbConn)
d.SetConnMaxLifetime(maxDbLifetime)
dbConn.SQL = d
err = s.testDB(d)
if err != nil {
return nil, err
}
return dbConn, nil
}
// testDB tries to ping the database
func (s *sqlDriverRepo) testDB(d *sql.DB) error {
err := d.Ping()
if err != nil {
return err
}
return nil
}
// newDatabase creates a new database for the application
func (s *sqlDriverRepo) newDatabase(dsn string) (*sql.DB, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
// Ping test the conn
if err = db.Ping(); err != nil {
return nil, err
}
return db, err
}

View File

@ -0,0 +1,42 @@
package repository
import (
"database/sql"
"go-udemy-web-1/internal/models"
"time"
"github.com/jackc/pgx/v5"
)
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
}
// DB holds the database connection pool
type DB struct {
SQL *sql.DB
PG *pgx.Conn
}
type DatabaseDriverRepo interface {
ConnectSQL(dsn string) (*DB, error)
}

View File

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package sqlc
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,63 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package sqlc
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Reservation struct {
ID int32
FirstName string
LastName string
Email string
Phone string
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
Processed int32
}
type Restriction struct {
ID int32
RestrictionName string
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
type Room struct {
ID int32
RoomName string
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
type RoomRestriction struct {
ID int32
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
ReservationID pgtype.Int4
RestrictionID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
type SchemaMigration struct {
Version string
}
type User struct {
ID int32
FirstName string
LastName string
Email string
Password string
AccessLevel int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}

View File

@ -0,0 +1,685 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: query.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const allNewReservations = `-- name: AllNewReservations :many
SELECT
r.id,
r.first_name,
r.last_name,
r.email,
r.phone,
r.start_date,
r.end_date,
r.room_id,
r.created_at,
r.updated_at,
r.processed,
rm.id,
rm.room_name
FROM
reservations r
LEFT JOIN rooms rm ON (r.room_id = rm.id)
WHERE
r.processed = 0
ORDER BY
r.start_date ASC
`
type AllNewReservationsRow struct {
ID int32
FirstName string
LastName string
Email string
Phone string
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
Processed int32
ID_2 pgtype.Int4
RoomName pgtype.Text
}
func (q *Queries) AllNewReservations(ctx context.Context) ([]AllNewReservationsRow, error) {
rows, err := q.db.Query(ctx, allNewReservations)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AllNewReservationsRow
for rows.Next() {
var i AllNewReservationsRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Phone,
&i.StartDate,
&i.EndDate,
&i.RoomID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Processed,
&i.ID_2,
&i.RoomName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const allReservations = `-- name: AllReservations :many
SELECT
r.id,
r.first_name,
r.last_name,
r.email,
r.phone,
r.start_date,
r.end_date,
r.room_id,
r.created_at,
r.updated_at,
r.processed,
rm.id,
rm.room_name
FROM
reservations r
LEFT JOIN rooms rm ON (r.room_id = rm.id)
ORDER BY
r.start_date ASC
`
type AllReservationsRow struct {
ID int32
FirstName string
LastName string
Email string
Phone string
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
Processed int32
ID_2 pgtype.Int4
RoomName pgtype.Text
}
func (q *Queries) AllReservations(ctx context.Context) ([]AllReservationsRow, error) {
rows, err := q.db.Query(ctx, allReservations)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AllReservationsRow
for rows.Next() {
var i AllReservationsRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Phone,
&i.StartDate,
&i.EndDate,
&i.RoomID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Processed,
&i.ID_2,
&i.RoomName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const allRooms = `-- name: AllRooms :many
SELECT
id,
room_name,
created_at,
updated_at
FROM
rooms
ORDER BY
room_name
`
func (q *Queries) AllRooms(ctx context.Context) ([]Room, error) {
rows, err := q.db.Query(ctx, allRooms)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Room
for rows.Next() {
var i Room
if err := rows.Scan(
&i.ID,
&i.RoomName,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteBlockByID = `-- name: DeleteBlockByID :exec
DELETE FROM room_restrictions
WHERE
id = $1
`
func (q *Queries) DeleteBlockByID(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, deleteBlockByID, id)
return err
}
const deleteReservation = `-- name: DeleteReservation :exec
DELETE FROM reservations
WHERE
id = $1
`
func (q *Queries) DeleteReservation(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, deleteReservation, id)
return err
}
const getReservationByID = `-- name: GetReservationByID :one
SELECT
r.id,
r.first_name,
r.last_name,
r.email,
r.phone,
r.start_date,
r.end_date,
r.room_id,
r.created_at,
r.updated_at,
r.processed,
rm.id,
rm.room_name
FROM
reservations r
LEFT JOIN rooms rm ON (r.room_id = rm.id)
WHERE
r.id = $1
`
type GetReservationByIDRow struct {
ID int32
FirstName string
LastName string
Email string
Phone string
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
Processed int32
ID_2 pgtype.Int4
RoomName pgtype.Text
}
func (q *Queries) GetReservationByID(ctx context.Context, id int32) (GetReservationByIDRow, error) {
row := q.db.QueryRow(ctx, getReservationByID, id)
var i GetReservationByIDRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Phone,
&i.StartDate,
&i.EndDate,
&i.RoomID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Processed,
&i.ID_2,
&i.RoomName,
)
return i, err
}
const getRestrictionsForRoomByDate = `-- name: GetRestrictionsForRoomByDate :many
SELECT
id,
coalesce(reservation_id, 0),
restriction_id,
room_id,
start_date,
end_date
FROM
room_restrictions
WHERE
$1 < end_date
AND $2 >= start_date
AND room_id = $3
`
type GetRestrictionsForRoomByDateParams struct {
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
}
type GetRestrictionsForRoomByDateRow struct {
ID int32
ReservationID int32
RestrictionID int32
RoomID int32
StartDate pgtype.Date
EndDate pgtype.Date
}
func (q *Queries) GetRestrictionsForRoomByDate(ctx context.Context, arg GetRestrictionsForRoomByDateParams) ([]GetRestrictionsForRoomByDateRow, error) {
rows, err := q.db.Query(ctx, getRestrictionsForRoomByDate, arg.StartDate, arg.EndDate, arg.RoomID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRestrictionsForRoomByDateRow
for rows.Next() {
var i GetRestrictionsForRoomByDateRow
if err := rows.Scan(
&i.ID,
&i.ReservationID,
&i.RestrictionID,
&i.RoomID,
&i.StartDate,
&i.EndDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRoomById = `-- name: GetRoomById :one
SELECT
id,
room_name,
created_at,
updated_at
FROM
rooms
WHERE
id = $1
`
func (q *Queries) GetRoomById(ctx context.Context, id int32) (Room, error) {
row := q.db.QueryRow(ctx, getRoomById, id)
var i Room
err := row.Scan(
&i.ID,
&i.RoomName,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
id,
first_name,
last_name,
email,
password,
access_level,
created_at,
updated_at
FROM
users
WHERE
id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
row := q.db.QueryRow(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Password,
&i.AccessLevel,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserCred = `-- name: GetUserCred :one
SELECT
id,
password
FROM
users
WHERE
email = $1
`
type GetUserCredRow struct {
ID int32
Password string
}
func (q *Queries) GetUserCred(ctx context.Context, email string) (GetUserCredRow, error) {
row := q.db.QueryRow(ctx, getUserCred, email)
var i GetUserCredRow
err := row.Scan(&i.ID, &i.Password)
return i, err
}
const insertBlockForRoom = `-- name: InsertBlockForRoom :exec
INSERT INTO
room_restrictions (
start_date,
end_date,
room_id,
restriction_id,
created_at,
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6)
`
type InsertBlockForRoomParams struct {
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
RestrictionID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
func (q *Queries) InsertBlockForRoom(ctx context.Context, arg InsertBlockForRoomParams) error {
_, err := q.db.Exec(ctx, insertBlockForRoom,
arg.StartDate,
arg.EndDate,
arg.RoomID,
arg.RestrictionID,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const insertReservation = `-- name: InsertReservation :one
INSERT INTO
reservations (
first_name,
last_name,
email,
phone,
start_date,
end_date,
room_id,
created_at,
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING
id
`
type InsertReservationParams struct {
FirstName string
LastName string
Email string
Phone string
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
func (q *Queries) InsertReservation(ctx context.Context, arg InsertReservationParams) (int32, error) {
row := q.db.QueryRow(ctx, insertReservation,
arg.FirstName,
arg.LastName,
arg.Email,
arg.Phone,
arg.StartDate,
arg.EndDate,
arg.RoomID,
arg.CreatedAt,
arg.UpdatedAt,
)
var id int32
err := row.Scan(&id)
return id, err
}
const insertRoomRestriction = `-- name: InsertRoomRestriction :exec
INSERT INTO
room_restrictions (
start_date,
end_date,
room_id,
reservation_id,
restriction_id,
created_at,
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7)
`
type InsertRoomRestrictionParams struct {
StartDate pgtype.Date
EndDate pgtype.Date
RoomID int32
ReservationID pgtype.Int4
RestrictionID int32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
func (q *Queries) InsertRoomRestriction(ctx context.Context, arg InsertRoomRestrictionParams) error {
_, err := q.db.Exec(ctx, insertRoomRestriction,
arg.StartDate,
arg.EndDate,
arg.RoomID,
arg.ReservationID,
arg.RestrictionID,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const searchAvailabilityByDatesByRoomID = `-- name: SearchAvailabilityByDatesByRoomID :one
SELECT
count(id)
FROM
room_restrictions
WHERE
room_id = $1
AND $2 < end_date
AND $3 > start_date
`
type SearchAvailabilityByDatesByRoomIDParams struct {
RoomID int32
EndDate pgtype.Date
StartDate pgtype.Date
}
func (q *Queries) SearchAvailabilityByDatesByRoomID(ctx context.Context, arg SearchAvailabilityByDatesByRoomIDParams) (int64, error) {
row := q.db.QueryRow(ctx, searchAvailabilityByDatesByRoomID, arg.RoomID, arg.EndDate, arg.StartDate)
var count int64
err := row.Scan(&count)
return count, err
}
const searchAvailabilityForAllRooms = `-- name: SearchAvailabilityForAllRooms :many
SELECT
r.id,
r.room_name
FROM
rooms r
WHERE
r.id NOT IN (
SELECT
room_id
FROM
room_restrictions rr
WHERE
$1 < rr.end_date
AND $2 > rr.start_date
)
`
type SearchAvailabilityForAllRoomsParams struct {
EndDate pgtype.Date
StartDate pgtype.Date
}
type SearchAvailabilityForAllRoomsRow struct {
ID int32
RoomName string
}
func (q *Queries) SearchAvailabilityForAllRooms(ctx context.Context, arg SearchAvailabilityForAllRoomsParams) ([]SearchAvailabilityForAllRoomsRow, error) {
rows, err := q.db.Query(ctx, searchAvailabilityForAllRooms, arg.EndDate, arg.StartDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchAvailabilityForAllRoomsRow
for rows.Next() {
var i SearchAvailabilityForAllRoomsRow
if err := rows.Scan(&i.ID, &i.RoomName); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateProcessedForReservation = `-- name: UpdateProcessedForReservation :exec
UPDATE reservations
SET
processed = $1
WHERE
id = $2
`
type UpdateProcessedForReservationParams struct {
Processed int32
ID int32
}
func (q *Queries) UpdateProcessedForReservation(ctx context.Context, arg UpdateProcessedForReservationParams) error {
_, err := q.db.Exec(ctx, updateProcessedForReservation, arg.Processed, arg.ID)
return err
}
const updateReservation = `-- name: UpdateReservation :exec
UPDATE reservations
SET
first_name = $1,
last_name = $2,
email = $3,
phone = $4,
updated_at = $5
WHERE
id = $6
`
type UpdateReservationParams struct {
FirstName string
LastName string
Email string
Phone string
UpdatedAt pgtype.Timestamp
ID int32
}
func (q *Queries) UpdateReservation(ctx context.Context, arg UpdateReservationParams) error {
_, err := q.db.Exec(ctx, updateReservation,
arg.FirstName,
arg.LastName,
arg.Email,
arg.Phone,
arg.UpdatedAt,
arg.ID,
)
return err
}
const updateUser = `-- name: UpdateUser :exec
UPDATE users
SET
first_name = $1,
last_name = $2,
email = $3,
access_level = $4,
updated_at = $5
`
type UpdateUserParams struct {
FirstName string
LastName string
Email string
AccessLevel int32
UpdatedAt pgtype.Timestamp
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, updateUser,
arg.FirstName,
arg.LastName,
arg.Email,
arg.AccessLevel,
arg.UpdatedAt,
)
return err
}

View File

@ -0,0 +1 @@
sql("DROP TABLE users")

View 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})
}

View File

@ -0,0 +1 @@
drop_table("reservations")

View 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", {})
}

View File

@ -0,0 +1 @@
drop_table("rooms")

View File

@ -0,0 +1,5 @@
create_table("rooms") {
t.Column("id", "integer", {primary: true})
t.Column("room_name", "string", {})
}

View File

@ -0,0 +1 @@
drop_table("restrictions")

View File

@ -0,0 +1,5 @@
create_table("restrictions") {
t.Column("id", "integer", {primary: true})
t.Column("restriction_name", "string", {})
}

View File

@ -0,0 +1,2 @@
drop_table("room_restrictions")

View File

@ -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", {})
}

View File

@ -0,0 +1 @@
drop_foreign_key("reservations", "reservations_rooms_id_fk")

View File

@ -0,0 +1,4 @@
add_foreign_key("reservations", "room_id", {"rooms": ["id"]}, {
"on_delete": "cascade",
"on_update": "cascade",
})

View File

@ -0,0 +1,3 @@
drop_foreign_key("room_restrictions", "room_restrictions_rooms_id_fk")
drop_foreign_key("room_restrictions", "room_restrictions_restrictions_id_fk")

View File

@ -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",
})

View File

@ -0,0 +1,2 @@
drop_index("users", "users_email_idx")

View File

@ -0,0 +1 @@
add_index("users", "email", {"unique": true})

View File

@ -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")

View File

@ -0,0 +1,3 @@
add_index("room_restrictions", ["start_date", "end_date"], {})
add_index("room_restrictions", "room_id", {})
add_index("room_restrictions", "reservation_id", {})

View File

@ -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")

View File

@ -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", {})

View File

@ -0,0 +1,2 @@
change_column("room_restrictions", "reservation_id", "integer", {"null": false})

View File

@ -0,0 +1 @@
change_column("room_restrictions", "reservation_id", "integer", {"null": true})

View File

@ -0,0 +1 @@
delete from rooms

View File

@ -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');

View File

@ -0,0 +1 @@
delete from restrictions;

View File

@ -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');

View File

@ -0,0 +1 @@
drop_column("reservations", "processed")

View File

@ -0,0 +1 @@
add_column("reservations", "processed", "integer", {"default": 0})

View File

@ -1,16 +0,0 @@
package handlers
import (
"go-udemy-web-1/pkg/render"
"net/http"
)
// Home is the about page handler
func Home(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "home.page.tmpl")
}
// About is the about page handler
func About(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "about.page.tmpl")
}

View File

@ -1,16 +0,0 @@
package render
import (
"fmt"
"html/template"
"net/http"
)
// renderTemplate renders a HTML template file
func RenderTemplate(w http.ResponseWriter, tmpl string) {
parsedTemplate, _ := template.ParseFiles("./templates/"+tmpl, "./templates/base.layout.tmpl")
err := parsedTemplate.Execute(w, nil)
if err != nil {
fmt.Println("error parsing template:", err)
}
}

3
run.sh Executable file
View File

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

234
sql/query.sql Normal file
View File

@ -0,0 +1,234 @@
-- name: InsertReservation :one
INSERT INTO
reservations (
first_name,
last_name,
email,
phone,
start_date,
end_date,
room_id,
created_at,
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING
id;
-- name: InsertRoomRestriction :exec
INSERT INTO
room_restrictions (
start_date,
end_date,
room_id,
reservation_id,
restriction_id,
created_at,
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7);
-- name: SearchAvailabilityByDatesByRoomID :one
SELECT
count(id)
FROM
room_restrictions
WHERE
room_id = $1
AND $2 < end_date
AND $3 > start_date;
-- name: SearchAvailabilityForAllRooms :many
SELECT
r.id,
r.room_name
FROM
rooms r
WHERE
r.id NOT IN (
SELECT
room_id
FROM
room_restrictions rr
WHERE
$1 < rr.end_date
AND $2 > rr.start_date
);
-- name: GetRoomById :one
SELECT
id,
room_name,
created_at,
updated_at
FROM
rooms
WHERE
id = $1;
-- name: GetUserByID :one
SELECT
id,
first_name,
last_name,
email,
password,
access_level,
created_at,
updated_at
FROM
users
WHERE
id = $1;
-- name: UpdateUser :exec
UPDATE users
SET
first_name = $1,
last_name = $2,
email = $3,
access_level = $4,
updated_at = $5;
-- name: GetUserCred :one
SELECT
id,
password
FROM
users
WHERE
email = $1;
-- name: AllReservations :many
SELECT
r.id,
r.first_name,
r.last_name,
r.email,
r.phone,
r.start_date,
r.end_date,
r.room_id,
r.created_at,
r.updated_at,
r.processed,
rm.id,
rm.room_name
FROM
reservations r
LEFT JOIN rooms rm ON (r.room_id = rm.id)
ORDER BY
r.start_date ASC;
-- name: AllNewReservations :many
SELECT
r.id,
r.first_name,
r.last_name,
r.email,
r.phone,
r.start_date,
r.end_date,
r.room_id,
r.created_at,
r.updated_at,
r.processed,
rm.id,
rm.room_name
FROM
reservations r
LEFT JOIN rooms rm ON (r.room_id = rm.id)
WHERE
r.processed = 0
ORDER BY
r.start_date ASC;
-- name: GetReservationByID :one
SELECT
r.id,
r.first_name,
r.last_name,
r.email,
r.phone,
r.start_date,
r.end_date,
r.room_id,
r.created_at,
r.updated_at,
r.processed,
rm.id,
rm.room_name
FROM
reservations r
LEFT JOIN rooms rm ON (r.room_id = rm.id)
WHERE
r.id = $1;
-- name: UpdateReservation :exec
UPDATE reservations
SET
first_name = $1,
last_name = $2,
email = $3,
phone = $4,
updated_at = $5
WHERE
id = $6;
-- name: DeleteReservation :exec
DELETE FROM reservations
WHERE
id = $1;
-- name: UpdateProcessedForReservation :exec
UPDATE reservations
SET
processed = $1
WHERE
id = $2;
-- name: AllRooms :many
SELECT
id,
room_name,
created_at,
updated_at
FROM
rooms
ORDER BY
room_name;
-- name: GetRestrictionsForRoomByDate :many
SELECT
id,
coalesce(reservation_id, 0),
restriction_id,
room_id,
start_date,
end_date
FROM
room_restrictions
WHERE
@start_date < end_date
AND @end_date >= start_date
AND room_id = @room_id;
-- name: InsertBlockForRoom :exec
INSERT INTO
room_restrictions (
start_date,
end_date,
room_id,
restriction_id,
created_at,
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6);
-- name: DeleteBlockByID :exec
DELETE FROM room_restrictions
WHERE
id = $1;

96
sql/schema.sql Normal file
View File

@ -0,0 +1,96 @@
-- Adminer 4.8.1 PostgreSQL 16.3 (Debian 16.3-1.pgdg120+1) dump
CREATE SEQUENCE reservations_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."reservations" (
"id" integer DEFAULT nextval('reservations_id_seq') NOT NULL,
"first_name" character varying(255) DEFAULT '' NOT NULL,
"last_name" character varying(255) DEFAULT '' NOT NULL,
"email" character varying(255) NOT NULL,
"phone" character varying(255) DEFAULT '' NOT NULL,
"start_date" date NOT NULL,
"end_date" date NOT NULL,
"room_id" integer NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"processed" integer DEFAULT '0' NOT NULL,
CONSTRAINT "reservations_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE INDEX "reservations_email_idx" ON "public"."reservations" USING btree ("email");
CREATE INDEX "reservations_last_name_idx" ON "public"."reservations" USING btree ("last_name");
CREATE SEQUENCE restrictions_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."restrictions" (
"id" integer DEFAULT nextval('restrictions_id_seq') NOT NULL,
"restriction_name" character varying(255) NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "restrictions_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE SEQUENCE room_restrictions_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."room_restrictions" (
"id" integer DEFAULT nextval('room_restrictions_id_seq') NOT NULL,
"start_date" date NOT NULL,
"end_date" date NOT NULL,
"room_id" integer NOT NULL,
"reservation_id" integer,
"restriction_id" integer NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "room_restrictions_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE INDEX "room_restrictions_reservation_id_idx" ON "public"."room_restrictions" USING btree ("reservation_id");
CREATE INDEX "room_restrictions_room_id_idx" ON "public"."room_restrictions" USING btree ("room_id");
CREATE INDEX "room_restrictions_start_date_end_date_idx" ON "public"."room_restrictions" USING btree ("start_date", "end_date");
CREATE SEQUENCE rooms_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."rooms" (
"id" integer DEFAULT nextval('rooms_id_seq') NOT NULL,
"room_name" character varying(255) NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "rooms_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE TABLE "public"."schema_migration" (
"version" character varying(14) NOT NULL,
CONSTRAINT "schema_migration_pkey" PRIMARY KEY ("version"),
CONSTRAINT "schema_migration_version_idx" UNIQUE ("version")
) WITH (oids = false);
CREATE SEQUENCE users_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."users" (
"id" integer DEFAULT nextval('users_id_seq') NOT NULL,
"first_name" character varying(255) DEFAULT '' NOT NULL,
"last_name" character varying(255) DEFAULT '' NOT NULL,
"email" character varying(255) NOT NULL,
"password" character varying(60) NOT NULL,
"access_level" integer DEFAULT '1' NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "users_email_idx" UNIQUE ("email"),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
ALTER TABLE ONLY "public"."reservations" ADD CONSTRAINT "reservations_rooms_id_fk" FOREIGN KEY (room_id) REFERENCES rooms(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
ALTER TABLE ONLY "public"."room_restrictions" ADD CONSTRAINT "room_restrictions_reservations_id_fk" FOREIGN KEY (reservation_id) REFERENCES reservations(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
ALTER TABLE ONLY "public"."room_restrictions" ADD CONSTRAINT "room_restrictions_restrictions_id_fk" FOREIGN KEY (restriction_id) REFERENCES restrictions(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;
ALTER TABLE ONLY "public"."room_restrictions" ADD CONSTRAINT "room_restrictions_rooms_id_fk" FOREIGN KEY (room_id) REFERENCES rooms(id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE;

10
sqlc.yml Normal file
View File

@ -0,0 +1,10 @@
version: "2"
sql:
- engine: "postgresql"
queries: "sql/query.sql"
schema: "sql/schema.sql"
gen:
go:
package: "sqlc"
out: "internal/repository/sqlc"
sql_package: "pgx/v5"

BIN
static/admin/css/.DS_Store vendored Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

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

Binary file not shown.

View 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

Binary file not shown.

BIN
static/admin/fonts/Roboto/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More