From 47b9939ead1d34202a1bdaba603c734c43dcb96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20Rajkovi=C4=87?= Date: Sat, 5 Aug 2023 21:34:09 +0200 Subject: [PATCH] Added dynamic support for countries --- assets/style.css | 1 + db/dev/v1_0.sql | 30 +++++- handlers.go | 2 +- holiday/country_service.go | 40 ++++++++ holiday/{service.go => holiday_service.go} | 24 ++--- holiday/model.go | 6 ++ main.go | 101 +++++++++++++++++--- templates/admin_dashboard.gohtml | 30 +++--- templates/countries.gohtml | 60 ++++++++++++ templates/dialogs/add-holiday.gohtml | 7 +- templates/dialogs/check-is-a-holiday.gohtml | 7 +- templates/dialogs/delete-country.gohtml | 11 +++ templates/dialogs/edit-country.gohtml | 18 ++++ templates/dialogs/edit-holiday.gohtml | 7 +- templates/documentation.gohtml | 7 +- templates/index.gohtml | 7 +- templates/search.gohtml | 7 +- 17 files changed, 296 insertions(+), 69 deletions(-) create mode 100644 holiday/country_service.go rename holiday/{service.go => holiday_service.go} (76%) create mode 100644 templates/countries.gohtml create mode 100644 templates/dialogs/delete-country.gohtml create mode 100644 templates/dialogs/edit-country.gohtml diff --git a/assets/style.css b/assets/style.css index 155b8d4..5559fe2 100644 --- a/assets/style.css +++ b/assets/style.css @@ -229,6 +229,7 @@ section#results a, section#results button { color: #000; cursor: pointer; } + dialog { position: fixed; left: 50%; diff --git a/db/dev/v1_0.sql b/db/dev/v1_0.sql index 1a4f060..9bcee9f 100644 --- a/db/dev/v1_0.sql +++ b/db/dev/v1_0.sql @@ -1,14 +1,34 @@ +CREATE TABLE IF NOT EXISTS "country" +( + id uuid, + iso_name char(2) UNIQUE NOT NULL, + name varchar(45) NOT NULL, + + primary key (id) +); + CREATE TABLE IF NOT EXISTS "holiday" ( id uuid, - country char(2), - date date, - name varchar(64), + country char(2) NOT NULL, + date date NOT NULL, + name varchar(64) NOT NULL, description varchar(512), - is_state boolean, - is_religious boolean + is_state boolean NOT NULL, + is_religious boolean NOT NULL, + + primary key (id), + constraint fk_country_id foreign key (country) + references country(iso_name) on delete cascade on update cascade ); +INSERT INTO "country" (id, iso_name, name) +VALUES + ('096ca6c4-5c04-47a4-0063-4b4cc6f6b671', 'HR', 'Croatia'), + ('096ca6c4-5c04-47a4-0063-4b4cc6f6b672', 'US', 'USA'), + ('096ca6c4-5c04-47a4-0063-4b4cc6f6b673', 'FR', 'France'), + ('096ca6c4-5c04-47a4-0063-4b4cc6f6b674', 'GB', 'Great Britain'); + INSERT INTO "holiday" (id, country, date, name, description, is_state, is_religious) VALUES ('096ca6c4-5c04-47a4-b363-4b4cc6f6b671', 'HR', '2023-01-01', 'Nova godina', '', true, false), diff --git a/handlers.go b/handlers.go index 81f3785..947a682 100644 --- a/handlers.go +++ b/handlers.go @@ -10,7 +10,7 @@ import ( "time" ) -func getHolidays(service holiday.Service) gin.HandlerFunc { +func getHolidays(service holiday.HolidayService) gin.HandlerFunc { return func(c *gin.Context) { paging := holiday.Paging{PageSize: 50} if err := c.ShouldBindQuery(&paging); err != nil { diff --git a/holiday/country_service.go b/holiday/country_service.go new file mode 100644 index 0000000..d514900 --- /dev/null +++ b/holiday/country_service.go @@ -0,0 +1,40 @@ +package holiday + +import ( + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +type CountryService struct { + DB *sqlx.DB +} + +func (s *CountryService) FindById(id uuid.UUID) (Country, error) { + var country Country + return country, s.DB.Get(&country, `SELECT * FROM "country" WHERE "id" = $1;`, id) +} + +func (s *CountryService) Find() ([]Country, error) { + var countries []Country + return countries, s.DB.Select(&countries, `SELECT * FROM "country";`) +} + +func (s *CountryService) Update(country Country) (Country, error) { + _, err := s.DB.Exec(`UPDATE country SET "iso_name" = $2, "name" = $3 WHERE "id" = $1`, + &country.Id, &country.IsoName, &country.Name, + ) + return country, err +} + +func (s *CountryService) Create(country Country) (Country, error) { + country.Id = uuid.Must(uuid.NewRandom()) + _, err := s.DB.Exec(`INSERT INTO country (id, iso_name, name) values ($1, $2, $3)`, + &country.Id, &country.IsoName, &country.Name, + ) + return country, err +} + +func (s *CountryService) Delete(id uuid.UUID) error { + _, err := s.DB.Exec(`DELETE FROM country WHERE "id" = $1`, &id) + return err +} diff --git a/holiday/service.go b/holiday/holiday_service.go similarity index 76% rename from holiday/service.go rename to holiday/holiday_service.go index b083795..d818d25 100644 --- a/holiday/service.go +++ b/holiday/holiday_service.go @@ -8,11 +8,11 @@ import ( "time" ) -type Service struct { +type HolidayService struct { DB *sqlx.DB } -func (s *Service) Find(search Search, paging Paging) ([]Holiday, error) { +func (s *HolidayService) Find(search Search, paging Paging) ([]Holiday, error) { var holidays []Holiday var err error if search.Date != nil { @@ -30,32 +30,32 @@ func (s *Service) Find(search Search, paging Paging) ([]Holiday, error) { return s.paginate(holidays, paging), err } -func (s *Service) FindById(id uuid.UUID) (Holiday, error) { +func (s *HolidayService) FindById(id uuid.UUID) (Holiday, error) { var holiday Holiday return holiday, s.DB.Get(&holiday, `SELECT * FROM "holiday" WHERE "id" = $1;`, id) } -func (s *Service) findByDate(date time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) { +func (s *HolidayService) findByDate(date time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) { var holidays []Holiday return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" = $1 AND country = $2 `+s.filter(isState, isReligious)+";", date, country) } -func (s *Service) findForRange(rangeStart time.Time, rangeEnd time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) { +func (s *HolidayService) findForRange(rangeStart time.Time, rangeEnd time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) { var holidays []Holiday return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" BETWEEN $1 AND $2 AND country = $3`+s.filter(isState, isReligious)+";", rangeStart, rangeEnd, country) } -func (s *Service) findByYear(year int, isState *bool, isReligious *bool, country string) ([]Holiday, error) { +func (s *HolidayService) findByYear(year int, isState *bool, isReligious *bool, country string) ([]Holiday, error) { var holidays []Holiday return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE extract(year from "date") = $1 AND country = $2 `+s.filter(isState, isReligious)+";", year, country) } -func (s *Service) find(isState *bool, isReligious *bool, country string) ([]Holiday, error) { +func (s *HolidayService) find(isState *bool, isReligious *bool, country string) ([]Holiday, error) { var holidays []Holiday return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE country = $1 `+s.filter(isState, isReligious)+";", country) } -func (s *Service) paginate(holidays []Holiday, paging Paging) []Holiday { +func (s *HolidayService) paginate(holidays []Holiday, paging Paging) []Holiday { start := paging.Page * paging.PageSize end := (paging.Page + 1) * paging.PageSize if end < len(holidays) { @@ -67,7 +67,7 @@ func (s *Service) paginate(holidays []Holiday, paging Paging) []Holiday { } } -func (s *Service) filter(isState *bool, isReligious *bool) string { +func (s *HolidayService) filter(isState *bool, isReligious *bool) string { var filters []string if isState != nil { filters = append(filters, "is_state = "+strconv.FormatBool(*isState)) @@ -82,14 +82,14 @@ func (s *Service) filter(isState *bool, isReligious *bool) string { } } -func (s *Service) Update(holiday Holiday) (Holiday, error) { +func (s *HolidayService) Update(holiday Holiday) (Holiday, error) { _, err := s.DB.Exec(`UPDATE holiday SET "country" = $1, "name" = $2, "description" = $3, "date" = $4, "is_state"=$5, "is_religious"=$6 WHERE "id" = $7`, &holiday.Country, &holiday.Name, &holiday.Description, &holiday.Date, &holiday.IsStateHoliday, &holiday.IsReligiousHoliday, &holiday.Id, ) return holiday, err } -func (s *Service) Create(holiday Holiday) (Holiday, error) { +func (s *HolidayService) Create(holiday Holiday) (Holiday, error) { holiday.Id = uuid.Must(uuid.NewRandom()) _, err := s.DB.Exec(`INSERT INTO holiday (id, country, name, description, date, is_state, is_religious) values ($1, $2, $3, $4, $5, $6, $7)`, &holiday.Id, &holiday.Country, &holiday.Name, &holiday.Description, &holiday.Date, &holiday.IsStateHoliday, &holiday.IsReligiousHoliday, @@ -97,7 +97,7 @@ func (s *Service) Create(holiday Holiday) (Holiday, error) { return holiday, err } -func (s *Service) Delete(id uuid.UUID) error { +func (s *HolidayService) Delete(id uuid.UUID) error { _, err := s.DB.Exec(`DELETE FROM holiday WHERE "id" = $1`, &id) return err } diff --git a/holiday/model.go b/holiday/model.go index b2f3a4e..b4f42ef 100644 --- a/holiday/model.go +++ b/holiday/model.go @@ -15,6 +15,12 @@ type Holiday struct { IsReligiousHoliday bool `db:"is_religious"` } +type Country struct { + Id uuid.UUID `db:"id"` + IsoName string `db:"iso_name"` + Name string `db:"name"` +} + type Paging struct { PageSize int `form:"page_size" binging:"min=0"` Page int `form:"page" binging:"min=0"` diff --git a/main.go b/main.go index c80ca38..ccdd344 100644 --- a/main.go +++ b/main.go @@ -53,11 +53,12 @@ func main() { loadTemplates(g) - holidayService := holiday.Service{DB: client} + holidayService := holiday.HolidayService{DB: client} + countryService := holiday.CountryService{DB: client} g.GET("/api/v1/holidays", getHolidays(holidayService)) - setupAdminDashboard(g.Group("/admin"), holidayService) + setupAdminDashboard(g.Group("/admin"), holidayService, countryService) g.GET("/", func(c *gin.Context) { year := time.Now().Year() @@ -67,10 +68,12 @@ func main() { return } holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) - c.HTML(http.StatusOK, "index.gohtml", gin.H{"Search": search, "Holidays": mapHolidays(holidays).Holidays}) + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "index.gohtml", gin.H{"Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) }) g.GET("/documentation", func(c *gin.Context) { - c.HTML(http.StatusOK, "documentation.gohtml", nil) + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Countries": countries}) }) g.GET("/search", func(c *gin.Context) { request := holiday.Search{} @@ -80,10 +83,12 @@ func main() { } search := holiday.Search{Country: request.Country, Date: request.Date} holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) - c.HTML(http.StatusOK, "search.gohtml", gin.H{"Search": search, "Holidays": mapHolidays(holidays).Holidays}) + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "search.gohtml", gin.H{"Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) }) g.GET("/dialogs/check-is-a-holiday", func(c *gin.Context) { - c.HTML(http.StatusOK, "check-is-a-holiday.gohtml", gin.H{}) + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "check-is-a-holiday.gohtml", gin.H{"Countries": countries}) }) log.Fatal(http.ListenAndServe(":5281", g)) @@ -112,15 +117,19 @@ func loadTemplates(g *gin.Engine) { "templates/documentation.gohtml", "templates/admin_dashboard.gohtml", + "templates/countries.gohtml", "templates/dialogs/add-holiday.gohtml", "templates/dialogs/edit-holiday.gohtml", "templates/dialogs/delete-holiday.gohtml", "templates/dialogs/check-is-a-holiday.gohtml", + + "templates/dialogs/edit-country.gohtml", + "templates/dialogs/delete-country.gohtml", ) } -func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Service) { +func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService) { adminDashboard.Use(gin.BasicAuth(loadAuth())) adminDashboard.GET("/", func(c *gin.Context) { @@ -132,14 +141,15 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Servic } holidays, _ := service.Find(search, holiday.Paging{PageSize: 100}) holidayResponse := mapHolidays(holidays) + countries, _ := countryService.Find() response := map[string]any{} - response["holidays"] = holidayResponse - response["search"] = search + response["Holidays"] = holidayResponse + response["Search"] = search + response["Countries"] = countries c.HTML(http.StatusOK, "admin_dashboard.gohtml", response) }) - adminDashboard.POST("/holidays", func(c *gin.Context) { request := struct { Id *string `form:"id"` @@ -190,8 +200,56 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Servic } c.Redirect(http.StatusSeeOther, "/admin?country="+hol.Country+"&year="+strconv.FormatInt(int64(hol.Date.Year()), 10)) }) + adminDashboard.GET("/countries", func(c *gin.Context) { + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "countries.gohtml", gin.H{"Countries": countries}) + }) + adminDashboard.POST("/countries", func(c *gin.Context) { + request := struct { + Id *string `form:"id"` + IsoName string `form:"iso_name" binding:"required,min=2,max=2"` + Name string `form:"name" binding:"required,min=1,max=45"` + }{} + if err := c.ShouldBind(&request); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + country := holiday.Country{ + IsoName: request.IsoName, + Name: request.Name, + } + + var err error + if request.Id != nil { + country.Id = uuid.MustParse(*request.Id) + country, err = countryService.Update(country) + } else { + country, err = countryService.Create(country) + } + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } else { + c.Redirect(http.StatusSeeOther, "/admin/countries") + } + }) + adminDashboard.POST("/countries/:id/delete", func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + _, err := countryService.FindById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + if err := countryService.Delete(id); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.Redirect(http.StatusSeeOther, "/admin/countries") + }) + adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) { - c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{}) + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries}) }) adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) @@ -200,7 +258,17 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Servic c.AbortWithError(http.StatusNotFound, err) return } - c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Holiday": hol}) + countries, _ := countryService.Find() + c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol}) + }) + adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) { + id := uuid.MustParse(c.Query("id")) + country, err := countryService.FindById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.HTML(http.StatusOK, "edit-country.gohtml", gin.H{"Country": country}) }) adminDashboard.GET("/dialogs/delete-holiday", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) @@ -211,6 +279,15 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Servic } c.HTML(http.StatusOK, "delete-holiday.gohtml", gin.H{"Holiday": hol}) }) + adminDashboard.GET("/dialogs/delete-country", func(c *gin.Context) { + id := uuid.MustParse(c.Query("id")) + country, err := countryService.FindById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country}) + }) } func loadAuth() map[string]string { diff --git a/templates/admin_dashboard.gohtml b/templates/admin_dashboard.gohtml index f7b1e16..40dcac9 100644 --- a/templates/admin_dashboard.gohtml +++ b/templates/admin_dashboard.gohtml @@ -17,6 +17,7 @@ @@ -27,23 +28,22 @@
@@ -53,7 +53,7 @@ - +
@@ -62,7 +62,7 @@ - +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + {{range $entry := .Countries}} + + + + + + {{end}} + +
Iso nameName
{{$entry.IsoName}}{{$entry.Name}} + + +
+
+
+
+ + \ No newline at end of file diff --git a/templates/dialogs/add-holiday.gohtml b/templates/dialogs/add-holiday.gohtml index 9ed3728..bd0ff19 100644 --- a/templates/dialogs/add-holiday.gohtml +++ b/templates/dialogs/add-holiday.gohtml @@ -4,10 +4,9 @@
diff --git a/templates/dialogs/check-is-a-holiday.gohtml b/templates/dialogs/check-is-a-holiday.gohtml index ae9ed23..0be8c52 100644 --- a/templates/dialogs/check-is-a-holiday.gohtml +++ b/templates/dialogs/check-is-a-holiday.gohtml @@ -7,10 +7,9 @@
diff --git a/templates/dialogs/delete-country.gohtml b/templates/dialogs/delete-country.gohtml new file mode 100644 index 0000000..7adb5b0 --- /dev/null +++ b/templates/dialogs/delete-country.gohtml @@ -0,0 +1,11 @@ + + +

Delete country

+

Are you sure you want to delete "{{.Country.Name}}"?
All holidays for given country will be deleted!

+
+
+ + +
+
+
\ No newline at end of file diff --git a/templates/dialogs/edit-country.gohtml b/templates/dialogs/edit-country.gohtml new file mode 100644 index 0000000..692aa5c --- /dev/null +++ b/templates/dialogs/edit-country.gohtml @@ -0,0 +1,18 @@ + +

Edit country

+
+ +
+ + +
+
+ + +
+
+ + +
+
+
\ No newline at end of file diff --git a/templates/dialogs/edit-holiday.gohtml b/templates/dialogs/edit-holiday.gohtml index adaeca5..8d475d5 100644 --- a/templates/dialogs/edit-holiday.gohtml +++ b/templates/dialogs/edit-holiday.gohtml @@ -5,10 +5,9 @@
diff --git a/templates/documentation.gohtml b/templates/documentation.gohtml index 61f46a1..e907ae2 100644 --- a/templates/documentation.gohtml +++ b/templates/documentation.gohtml @@ -29,10 +29,9 @@
diff --git a/templates/index.gohtml b/templates/index.gohtml index 92c0ff3..dc7c300 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -28,10 +28,9 @@
diff --git a/templates/search.gohtml b/templates/search.gohtml index fe619dc..9f51ddc 100644 --- a/templates/search.gohtml +++ b/templates/search.gohtml @@ -29,10 +29,9 @@