Initial implementation
This commit is contained in:
parent
bb4f465321
commit
2172b8232e
|
@ -1,3 +1,3 @@
|
||||||
.idea/**
|
.idea/**
|
||||||
template
|
holiday-api
|
||||||
.env
|
.env
|
106
README.md
106
README.md
|
@ -1,22 +1,96 @@
|
||||||
# go-web-app-template
|
# Holiday api
|
||||||
|
|
||||||
Basic template for creating go applications
|
Simple api used for tracking holidays
|
||||||
|
|
||||||
Includes:
|
## Endpoints
|
||||||
* dotenv
|
|
||||||
* pq
|
|
||||||
* sqlx
|
|
||||||
* chi
|
|
||||||
* google.uuid
|
|
||||||
|
|
||||||
To start using make sure to setup either environment variables listed
|
Fetching is done via `GET /api/v1/holidays` endpoint
|
||||||
below, or create .env file and copy variables below
|
|
||||||
|
That endpoint accepts a list of required and optional parameters
|
||||||
|
|
||||||
|
### Required parameters
|
||||||
|
`country`
|
||||||
|
- determines for which country holidays are fetched, uses ISO 3166-1 Alpha-2 codes [more info here](https://en.wikipedia.org/wiki/ISO_3166-1)
|
||||||
|
- eg. `country=US`
|
||||||
|
|
||||||
|
### Optional parameters
|
||||||
|
`year`
|
||||||
|
- returns only holidays for given year
|
||||||
|
- eg. `year=2023`
|
||||||
|
- only used if no more important parameters are defined
|
||||||
|
|
||||||
|
`date`
|
||||||
|
- returns only holidays for given date
|
||||||
|
- date must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
|
||||||
|
- eg. `date=2021-12-25`
|
||||||
|
- if defined year and rangeStart|rangeEnd parameters are ignored
|
||||||
|
|
||||||
|
|
||||||
|
`rangeStart|rangeEnd`
|
||||||
|
- returns holidays in given range with both ends being inclusive
|
||||||
|
- if either limit isn't defined it is assumed to be up to or all from given limit (if rangeStart isn't defined all holidays before rangeEnd are returned and vice-verse)
|
||||||
|
- dates must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
|
||||||
|
- eg. `rangeStart=2021-12-25&rangeEnd=2023-01-23`, `rangeStart=2023-01-20`
|
||||||
|
- if defined year parameter is ignored
|
||||||
|
|
||||||
|
`stateHoliday`
|
||||||
|
- if set true only holidays that are tagged as state holidays are returned, similar for if set false, if not set all holidays are returned
|
||||||
|
- eg. `stateHoliday=true`, `stateHoliday=false`
|
||||||
|
|
||||||
|
`religiousHoliday`
|
||||||
|
- if set true only holidays that are tagged as religious holidays are returned, similar for if set false, if not set all holidays are returned
|
||||||
|
- eg. `religiousHoliday=true`, `religiousHoliday=false`
|
||||||
|
|
||||||
|
#### Paging
|
||||||
|
`pageSize`
|
||||||
|
- returns at most pageSize number of holidays
|
||||||
|
- eg. `pageSize=20`
|
||||||
|
- only applied if page is defined as well, by default set to 20
|
||||||
|
|
||||||
|
`page`
|
||||||
|
- returns nth page of holidays, paging starts at 0
|
||||||
|
- eg. `page=0`
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
By default, responses are returned as a json array
|
||||||
```
|
```
|
||||||
PSQL_HOST=localhost
|
[{
|
||||||
PSQL_PORT=5432
|
id: string;
|
||||||
PSQL_USER=template
|
date: string(ISO 8601);
|
||||||
PSQL_PASSWORD=templatePassword
|
name: string;
|
||||||
PSQL_DB=template
|
description: string;
|
||||||
|
isStateHoliday: boolean;
|
||||||
|
isReligiousHoliday: boolean;
|
||||||
|
},...]
|
||||||
|
```
|
||||||
|
eg.
|
||||||
|
```
|
||||||
|
[{
|
||||||
|
"id": "74a2a769-abf2-45d4-bdc4-442bbcc89138",
|
||||||
|
"date": "2023-12-25",
|
||||||
|
"name": "Christmas",
|
||||||
|
"description": "TBD",
|
||||||
|
"isStateHoliday": true,
|
||||||
|
"isReligiousHoliday": true
|
||||||
|
}]
|
||||||
```
|
```
|
||||||
|
|
||||||
Also, database is required for template to start, so you can start it with `docker compose up -d`
|
But can be returned as XML or CSV by setting appropriate `Accept` header (application/xml, text/xml or text/csv)
|
||||||
|
|
||||||
|
XML Response
|
||||||
|
```
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
|
<Holidays>
|
||||||
|
<Holiday id="74a2a769-abf2-45d4-bdc4-442bbcc89138" date="2023-12-25" isReligious="true" isState="true">
|
||||||
|
<name>Christmas</name>
|
||||||
|
<description>TBD</description>
|
||||||
|
</Holiday>
|
||||||
|
</Holidays>
|
||||||
|
```
|
||||||
|
|
||||||
|
CSV Response
|
||||||
|
```
|
||||||
|
id,date,name,description,is_state_holiday,is_religious_holiday
|
||||||
|
74a2a769-abf2-45d4-bdc4-442bbcc89138,2023-12-25,Christmas,TBD,true,true
|
||||||
|
```
|
|
@ -0,0 +1,30 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "holiday"
|
||||||
|
(
|
||||||
|
id uuid,
|
||||||
|
country char(2),
|
||||||
|
date date,
|
||||||
|
name varchar(64),
|
||||||
|
description varchar(512),
|
||||||
|
is_state boolean,
|
||||||
|
is_religious boolean
|
||||||
|
);
|
||||||
|
|
||||||
|
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),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b672', 'HR', '2023-01-06', 'Sveta tri kralja', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b673', 'HR', '2023-04-09', 'Uskrs', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b674', 'HR', '2023-04-10', 'Uskrsni ponedjeljak', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b675', 'HR', '2023-05-01', 'Praznik rada', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b676', 'HR', '2023-05-30', 'Dan državnosti', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b677', 'HR', '2023-06-08', 'Tijelovo', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b678', 'HR', '2023-06-22', 'Dan antifašističke borbe', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b679', 'HR', '2023-08-05', 'Dan pobjede i domovinske zahvalnost', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b680', 'HR', '2023-08-15', 'Velika gospa', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b681', 'HR', '2023-11-01', 'Dan svih svetih', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b682', 'HR', '2023-11-18', 'Dan sjećanja na žrtve Domovinskog rata', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b683', 'HR', '2023-12-25', 'Božić', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b684', 'HR', '2023-12-26', 'Štefanje', '', true, true);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@ version: '3.1'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: registry.bbr-dev.info/template/backend:latest
|
image: registry.bbr-dev.info/holiday-api/backend:latest
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
ports:
|
ports:
|
||||||
- "5281:5281"
|
- "5281:5281"
|
||||||
networks:
|
networks:
|
||||||
- template
|
- holiday
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker
|
- .env.docker
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=template
|
- POSTGRES_USER=holiday
|
||||||
- POSTGRES_PASSWORD=templatePassword
|
- POSTGRES_PASSWORD=holidayPassword
|
||||||
- POSTGRES_DB=template
|
- POSTGRES_DB=holiday
|
||||||
networks:
|
networks:
|
||||||
- template
|
- holiday
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
template:
|
holiday:
|
|
@ -6,6 +6,6 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=template
|
- POSTGRES_USER=holiday
|
||||||
- POSTGRES_PASSWORD=templatePassword
|
- POSTGRES_PASSWORD=holidayPassword
|
||||||
- POSTGRES_DB=template
|
- POSTGRES_DB=holiday
|
|
@ -10,10 +10,10 @@ COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
RUN go build -tags timetzdata template
|
RUN go build -tags timetzdata holiday-api
|
||||||
|
|
||||||
### Stage 2: Run ###
|
### Stage 2: Run ###
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
COPY --from=go-build /root/template /usr/bin/template
|
COPY --from=go-build /root/holiday-api /usr/bin/holiday-api
|
||||||
ENTRYPOINT ["template"]
|
ENTRYPOINT ["holiday-api"]
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"holiday-api/holiday"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getHolidays(service holiday.Service) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
log.Printf("Failed parsing params: %v", err)
|
||||||
|
}
|
||||||
|
paging := holiday.Paging{
|
||||||
|
PageSize: readInt(r, "pageSize", 1000),
|
||||||
|
Page: readInt(r, "page", 0),
|
||||||
|
}
|
||||||
|
search := holiday.Search{
|
||||||
|
Country: r.Form.Get("country"),
|
||||||
|
Year: readIntNullable(r, "year"),
|
||||||
|
Date: readDateNullable(r, "date"),
|
||||||
|
RangeStart: readDateNullable(r, "rangeStart"),
|
||||||
|
RangeEnd: readDateNullable(r, "rangeEnd"),
|
||||||
|
StateHoliday: readBoolNullable(r, "stateHoliday"),
|
||||||
|
ReligiousHoliday: readBoolNullable(r, "religiousHoliday"),
|
||||||
|
}
|
||||||
|
holidays, err := service.Find(search, paging)
|
||||||
|
if err != nil {
|
||||||
|
render(w, r, 404, ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"})
|
||||||
|
} else {
|
||||||
|
render(w, r, 200, mapHolidays(holidays))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateResponse struct{ time.Time }
|
||||||
|
|
||||||
|
func (d DateResponse) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte("\"" + d.String() + "\""), nil
|
||||||
|
}
|
||||||
|
func (d DateResponse) String() string {
|
||||||
|
return fmt.Sprintf(`%04d-%02d-%02d`, d.Year(), d.Month(), d.Day())
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
XMLName xml.Name `json:"-" xml:"Error"`
|
||||||
|
Created time.Time `json:"created" xml:"created"`
|
||||||
|
Message string `json:"message" xml:"message"`
|
||||||
|
Code int `json:"-" xml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HolidayResponse struct {
|
||||||
|
XMLName xml.Name `json:"-" xml:"Holidays"`
|
||||||
|
Holidays []HolidayItemResponse `json:"holidays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HolidayItemResponse struct {
|
||||||
|
XMLName xml.Name `json:"-" xml:"Holiday"`
|
||||||
|
Id uuid.UUID `json:"id" xml:"id,attr"`
|
||||||
|
Date DateResponse `json:"date" xml:"date,attr"`
|
||||||
|
Name string `json:"name" xml:"name"`
|
||||||
|
Description string `json:"description" xml:"description,omitempty"`
|
||||||
|
IsStateHoliday bool `json:"isStateHoliday" xml:"isState,attr"`
|
||||||
|
IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HolidaySingleResponse struct {
|
||||||
|
Id *uuid.UUID
|
||||||
|
Country string
|
||||||
|
Date DateResponse
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
IsStateHoliday bool
|
||||||
|
IsReligiousHoliday bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type HolidaySingleRequest struct {
|
||||||
|
Id *uuid.UUID
|
||||||
|
Country string
|
||||||
|
Date DateResponse
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
IsStateHoliday bool
|
||||||
|
IsReligiousHoliday bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapHolidays(holidays []holiday.Holiday) HolidayResponse {
|
||||||
|
var response = make([]HolidayItemResponse, 0, len(holidays))
|
||||||
|
for _, h := range holidays {
|
||||||
|
response = append(response, HolidayItemResponse{
|
||||||
|
Id: h.Id,
|
||||||
|
Date: DateResponse{h.Date},
|
||||||
|
Name: h.Name,
|
||||||
|
Description: h.Description,
|
||||||
|
IsStateHoliday: h.IsStateHoliday,
|
||||||
|
IsReligiousHoliday: h.IsReligiousHoliday,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return HolidayResponse{
|
||||||
|
Holidays: response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDateNullable(r *http.Request, param string) *time.Time {
|
||||||
|
if r.Form.Has(param) {
|
||||||
|
var year, month, date int
|
||||||
|
count, err := fmt.Sscanf(r.Form.Get(param), "%d-%d-%d", &year, &month, &date)
|
||||||
|
if count < 3 {
|
||||||
|
log.Printf("failed reading %s: %v", param, err)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
readDate := time.Date(year, time.Month(month), date, 0, 0, 0, 0, time.UTC)
|
||||||
|
return &readDate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIntNullable(r *http.Request, param string) *int {
|
||||||
|
if r.Form.Has(param) {
|
||||||
|
if value, err := strconv.Atoi(r.Form.Get(param)); err == nil {
|
||||||
|
return &value
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBoolNullable(r *http.Request, param string) *bool {
|
||||||
|
if r.Form.Has(param) {
|
||||||
|
if value, err := strconv.ParseBool(r.Form.Get(param)); err == nil {
|
||||||
|
return &value
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInt(r *http.Request, param string, defaultValue int) int {
|
||||||
|
if r.Form.Has(param) {
|
||||||
|
if value, err := strconv.Atoi(r.Form.Get(param)); err == nil {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package holiday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Holiday struct {
|
||||||
|
Id uuid.UUID `db:"id"`
|
||||||
|
Country string `db:"country"`
|
||||||
|
Date time.Time `db:"date"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Description string `db:"description"`
|
||||||
|
IsStateHoliday bool `db:"is_state"`
|
||||||
|
IsReligiousHoliday bool `db:"is_religious"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Paging struct {
|
||||||
|
PageSize int
|
||||||
|
Page int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Search struct {
|
||||||
|
Country string
|
||||||
|
Year *int
|
||||||
|
Date *time.Time
|
||||||
|
RangeStart *time.Time
|
||||||
|
RangeEnd *time.Time
|
||||||
|
StateHoliday *bool
|
||||||
|
ReligiousHoliday *bool
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package holiday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Find(search Search, paging Paging) ([]Holiday, error) {
|
||||||
|
var holidays []Holiday
|
||||||
|
var err error
|
||||||
|
if search.Date != nil {
|
||||||
|
holidays, err = s.findByDate(*search.Date, search.StateHoliday, search.ReligiousHoliday, search.Country)
|
||||||
|
} else if search.RangeStart != nil || search.RangeEnd != nil {
|
||||||
|
holidays, err = s.findForRange(getDateOrDefault(search.RangeStart, 1000), getDateOrDefault(search.RangeEnd, 3000), search.StateHoliday, search.ReligiousHoliday, search.Country)
|
||||||
|
} else if search.Year != nil {
|
||||||
|
holidays, err = s.findByYear(*search.Year, search.StateHoliday, search.ReligiousHoliday, search.Country)
|
||||||
|
} else {
|
||||||
|
holidays, err = s.find(search.StateHoliday, search.ReligiousHoliday, search.Country)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.paginate(holidays, paging), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) 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) {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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 {
|
||||||
|
start := paging.Page * paging.PageSize
|
||||||
|
end := (paging.Page + 1) * paging.PageSize
|
||||||
|
if end < len(holidays) {
|
||||||
|
return holidays[start:end]
|
||||||
|
} else if start < len(holidays) {
|
||||||
|
return holidays[start:]
|
||||||
|
} else {
|
||||||
|
return []Holiday{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) filter(isState *bool, isReligious *bool) string {
|
||||||
|
var filters []string
|
||||||
|
if isState != nil {
|
||||||
|
filters = append(filters, "is_state = "+strconv.FormatBool(*isState))
|
||||||
|
}
|
||||||
|
if isReligious != nil {
|
||||||
|
filters = append(filters, "is_religious = "+strconv.FormatBool(*isReligious))
|
||||||
|
}
|
||||||
|
if len(filters) > 0 {
|
||||||
|
return " AND " + strings.Join(filters, " AND ")
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) 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) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
return holiday, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(id uuid.UUID) error {
|
||||||
|
_, err := s.DB.Exec(`DELETE FROM holiday WHERE "id" = $1`, &id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDateOrDefault(date *time.Time, year int) time.Time {
|
||||||
|
if date == nil {
|
||||||
|
return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
} else {
|
||||||
|
return *date
|
||||||
|
}
|
||||||
|
}
|
206
main.go
206
main.go
|
@ -2,19 +2,36 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"holiday-api/holiday"
|
||||||
|
"holiday-api/migration"
|
||||||
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"template/migration"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed db/dev/*.sql
|
//go:embed db/dev/*.sql
|
||||||
var devMigrations embed.FS
|
var devMigrations embed.FS
|
||||||
|
|
||||||
|
//go:embed db/prod/*.sql
|
||||||
|
var prodMigrations embed.FS
|
||||||
|
|
||||||
|
var isDev = false
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
|
if strings.Contains(os.Getenv("PROFILE"), "dev") {
|
||||||
|
isDev = true
|
||||||
|
}
|
||||||
log.SetPrefix("")
|
log.SetPrefix("")
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
}
|
}
|
||||||
|
@ -24,7 +41,11 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("couldn't connect to db: %v", err)
|
log.Fatalf("couldn't connect to db: %v", err)
|
||||||
}
|
}
|
||||||
if err := migration.InitializeMigrations(client, devMigrations); err != nil {
|
migrationFolder := prodMigrations
|
||||||
|
if isDev {
|
||||||
|
migrationFolder = devMigrations
|
||||||
|
}
|
||||||
|
if err := migration.InitializeMigrations(client, migrationFolder); err != nil {
|
||||||
log.Fatalf("couldn't execute migrations: %v", err)
|
log.Fatalf("couldn't execute migrations: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,16 +55,183 @@ func main() {
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
r.Get("/", helloWorld)
|
holidayService := holiday.Service{client}
|
||||||
|
|
||||||
|
r.Get("/api/v1/holidays", getHolidays(holidayService))
|
||||||
|
|
||||||
|
r.Mount("/admin", adminDashboard(holidayService))
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":5281", r))
|
log.Fatal(http.ListenAndServe(":5281", r))
|
||||||
}
|
}
|
||||||
|
|
||||||
func helloWorld(w http.ResponseWriter, r *http.Request) {
|
func adminDashboard(service holiday.Service) http.Handler {
|
||||||
response := struct {
|
r := chi.NewRouter()
|
||||||
Title string `json:"title"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{Title: "hello world", Message: "this is an example of response"}
|
|
||||||
|
|
||||||
render(w, r, http.StatusOK, response)
|
credentials := loadCredentials()
|
||||||
|
templates, err := template.New("").Funcs(template.FuncMap{
|
||||||
|
"boolcmp": func(value *bool, expected string) bool {
|
||||||
|
if value == nil {
|
||||||
|
return expected == "nil"
|
||||||
|
} else {
|
||||||
|
return (*value && expected == "true") || (!(*value) && expected == "false")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deferint": func(value *int) int { return *value },
|
||||||
|
}).ParseFiles("templates/index.gohtml", "templates/holiday.gohtml", "templates/error.gohtml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("couldn't load templates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Use(middleware.BasicAuth("/admin", credentials))
|
||||||
|
|
||||||
|
r.Get("/holiday", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
id := r.FormValue("id")
|
||||||
|
|
||||||
|
response := HolidaySingleResponse{
|
||||||
|
Id: nil,
|
||||||
|
Country: "",
|
||||||
|
Date: DateResponse{time.Now()},
|
||||||
|
Name: "",
|
||||||
|
Description: "",
|
||||||
|
IsStateHoliday: true,
|
||||||
|
IsReligiousHoliday: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != "" {
|
||||||
|
if h, err := service.FindById(uuid.Must(uuid.Parse(id))); err == nil {
|
||||||
|
response = HolidaySingleResponse{
|
||||||
|
Id: &h.Id,
|
||||||
|
Country: h.Country,
|
||||||
|
Date: DateResponse{h.Date},
|
||||||
|
Name: h.Name,
|
||||||
|
Description: h.Description,
|
||||||
|
IsStateHoliday: h.IsStateHoliday,
|
||||||
|
IsReligiousHoliday: h.IsReligiousHoliday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderTemplate(w, r, 200, templates, "holiday.gohtml", response)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Post("/holiday/delete", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
var id uuid.UUID
|
||||||
|
if i := r.FormValue("id"); i != "" {
|
||||||
|
id = uuid.MustParse(i)
|
||||||
|
} else {
|
||||||
|
log.Printf("Failed deleting holiday: missing id", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("There was an error"))
|
||||||
|
}
|
||||||
|
err := service.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed deleting holiday: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("There was an error"))
|
||||||
|
} else {
|
||||||
|
w.Header().Add("Location", "/admin")
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
w.Write([]byte{})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Post("/holiday", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
var id *uuid.UUID = nil
|
||||||
|
if i := r.FormValue("id"); i != "" {
|
||||||
|
temp := uuid.MustParse(i)
|
||||||
|
id = &temp
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
|
description := strings.TrimSpace(r.FormValue("description"))
|
||||||
|
isStateHoliday := parseCheckbox(r.FormValue("stateHoliday"))
|
||||||
|
isReligiousHoliday := parseCheckbox(r.FormValue("religiousHoliday"))
|
||||||
|
date := parseDate(r.FormValue("date"))
|
||||||
|
country := r.FormValue("country")
|
||||||
|
|
||||||
|
hol := holiday.Holiday{
|
||||||
|
Country: country,
|
||||||
|
Date: date,
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
IsStateHoliday: isStateHoliday,
|
||||||
|
IsReligiousHoliday: isReligiousHoliday,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if id != nil {
|
||||||
|
hol.Id = *id
|
||||||
|
hol, err = service.Update(hol)
|
||||||
|
} else {
|
||||||
|
hol, err = service.Create(hol)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed updating or creating holiday: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("There was an error"))
|
||||||
|
} else {
|
||||||
|
w.Header().Add("Location", "/admin/holiday?id="+hol.Id.String())
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
w.Write([]byte{})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
country := r.FormValue("country")
|
||||||
|
if country == "" {
|
||||||
|
country = "HR"
|
||||||
|
}
|
||||||
|
var stateHoliday *bool = nil
|
||||||
|
if h := r.FormValue("stateHoliday"); h != "" {
|
||||||
|
if hol, err := strconv.ParseBool(h); err == nil {
|
||||||
|
stateHoliday = &hol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var religiousHoliday *bool = nil
|
||||||
|
if h := r.FormValue("religiousHoliday"); h != "" {
|
||||||
|
if hol, err := strconv.ParseBool(h); err == nil {
|
||||||
|
religiousHoliday = &hol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
year := time.Now().Year()
|
||||||
|
if y := r.FormValue("year"); y != "" {
|
||||||
|
if yr, err := strconv.ParseInt(y, 10, 64); err == nil {
|
||||||
|
year = int(yr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search := holiday.Search{Year: &year, Country: country, StateHoliday: stateHoliday, ReligiousHoliday: religiousHoliday}
|
||||||
|
|
||||||
|
holidays, _ := service.Find(search, holiday.Paging{PageSize: 100})
|
||||||
|
holidayResponse := mapHolidays(holidays)
|
||||||
|
|
||||||
|
response := map[string]any{}
|
||||||
|
response["holidays"] = holidayResponse
|
||||||
|
response["search"] = search
|
||||||
|
|
||||||
|
renderTemplate(w, r, 200, templates, "index.gohtml", response)
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDate(date string) time.Time {
|
||||||
|
var year, month, day int
|
||||||
|
fmt.Sscanf(date, "%d-%d-%d", &year, &month, &day)
|
||||||
|
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCheckbox(value string) bool {
|
||||||
|
if value != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCredentials() map[string]string {
|
||||||
|
credentials := envMustExist("AUTH_KEY")
|
||||||
|
values := strings.Split(credentials, ":")
|
||||||
|
return map[string]string{values[0]: values[1]}
|
||||||
}
|
}
|
||||||
|
|
18
makefile
18
makefile
|
@ -10,17 +10,17 @@ setup:
|
||||||
go get
|
go get
|
||||||
|
|
||||||
docker-dev:
|
docker-dev:
|
||||||
docker image build -t registry.bbr-dev.info/template/backend:$(VERSION)-dev .
|
docker image build -t registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev .
|
||||||
docker tag registry.bbr-dev.info/template/backend:$(VERSION)-dev registry.bbr-dev.info/template/backend:latest-dev
|
docker tag registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev registry.bbr-dev.info/holiday-api/backend:latest-dev
|
||||||
docker image push registry.bbr-dev.info/template/backend:$(VERSION)-dev
|
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev
|
||||||
docker image push registry.bbr-dev.info/template/backend:latest-dev
|
docker image push registry.bbr-dev.info/holiday-api/backend:latest-dev
|
||||||
|
|
||||||
|
|
||||||
docker-prod:
|
docker-prod:
|
||||||
docker image build -t registry.bbr-dev.info/template/backend:$(VERSION) .
|
docker image build -t registry.bbr-dev.info/holiday-api/backend:$(VERSION) .
|
||||||
docker tag registry.bbr-dev.info/template/backend:$(VERSION) registry.bbr-dev.info/template/backend:latest
|
docker tag registry.bbr-dev.info/holiday-api/backend:$(VERSION) registry.bbr-dev.info/holiday-api/backend:latest
|
||||||
docker image push registry.bbr-dev.info/template/backend:$(VERSION)
|
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)
|
||||||
docker image push registry.bbr-dev.info/template/backend:latest
|
docker image push registry.bbr-dev.info/holiday-api/backend:latest
|
||||||
|
|
||||||
release:
|
release:
|
||||||
git tag $(VERSION)
|
git tag $(VERSION)
|
||||||
|
@ -30,4 +30,4 @@ test:
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf template
|
rm -rf holiday-api
|
||||||
|
|
47
render.go
47
render.go
|
@ -1,21 +1,58 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func renderTemplate[T any](w http.ResponseWriter, r *http.Request, status int, templates *template.Template, template string, response T) error {
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
if err := templates.Lookup(template).Execute(&buffer, response); err == nil {
|
||||||
|
w.Header().Add("content-type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_, err = w.Write(buffer.Bytes())
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return onError(w, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onError(w http.ResponseWriter, err error) error {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("failed writing response: %v", err)
|
||||||
|
_, _ = w.Write([]byte{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func render[T any](w http.ResponseWriter, r *http.Request, status int, response T) error {
|
func render[T any](w http.ResponseWriter, r *http.Request, status int, response T) error {
|
||||||
|
switch r.Header.Get("accept") {
|
||||||
|
case "application/xml":
|
||||||
|
fallthrough
|
||||||
|
case "text/xml":
|
||||||
|
if body, err := xml.MarshalIndent(response, "", " "); err == nil {
|
||||||
|
// we return the same type as requested
|
||||||
|
w.Header().Add("content-type", r.Header.Get("accept")+"; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_, err = w.Write([]byte(xml.Header))
|
||||||
|
_, err = w.Write(body)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return onError(w, err)
|
||||||
|
}
|
||||||
|
case "application/json":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
if body, err := json.MarshalIndent(response, "", " "); err == nil {
|
if body, err := json.MarshalIndent(response, "", " "); err == nil {
|
||||||
w.Header().Add("content-type", "application/json")
|
w.Header().Add("content-type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_, err = w.Write(body)
|
_, err = w.Write(body)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
return onError(w, err)
|
||||||
log.Printf("couldn't parse response")
|
}
|
||||||
_, err = w.Write([]byte{})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Test for error</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Holiday-api holiday</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Holiday-api - {{if .Id}}Updating{{else}}Creating{{end}}</h1>
|
||||||
|
<p>Page for creating or editing holidays</p>
|
||||||
|
<a href="/admin">Back</a>
|
||||||
|
<form method="post" action="/admin/holiday">
|
||||||
|
{{if .Id}}
|
||||||
|
<input type="hidden" id="id" name="id" value="{{.Id}}">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<label for="country">Country</label>
|
||||||
|
<select id="country" name="country">
|
||||||
|
<option value="HR" {{if eq .Country "HR"}}selected{{end}}>Croatia</option>
|
||||||
|
<option value="US" {{if eq .Country "US"}}selected{{end}}>USA</option>
|
||||||
|
<option value="GB" {{if eq .Country "GB"}}selected{{end}}>Great Britain</option>
|
||||||
|
<option value="FR" {{if eq .Country "FR"}}selected{{end}}>France</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{.Name}}">
|
||||||
|
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description">{{.Description}}</textarea>
|
||||||
|
|
||||||
|
<label for="date">Date</label>
|
||||||
|
<input type="date" id="date" name="date" value="{{.Date.String}}">
|
||||||
|
|
||||||
|
<label for="state-holiday">State holiday</label>
|
||||||
|
<input type="checkbox" id="state-holiday" name="stateHoliday" {{if .IsStateHoliday}}checked{{end}}>
|
||||||
|
|
||||||
|
<label for="religious-holiday">Religious holiday</label>
|
||||||
|
<input type="checkbox" id="religious-holiday" name="religiousHoliday" {{if .IsReligiousHoliday}}checked{{end}}>
|
||||||
|
|
||||||
|
<button type="submit">{{if .Id}}Update{{else}}Create{{end}}</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,102 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Holiday-api</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Holiday-api</h1>
|
||||||
|
<p>Welcome to admin interface for holiday api</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/admin/holiday">Add a holiday</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form id="filter-holidays" action="/admin" method="get">
|
||||||
|
<label for="country">Country</label>
|
||||||
|
<select id="country" name="country">
|
||||||
|
<option value="HR" {{if eq .search.Country "HR"}}selected{{end}}>Croatia</option>
|
||||||
|
<option value="GB" {{if eq .search.Country "GB"}}selected{{end}}>Great Britain</option>
|
||||||
|
<option value="US" {{if eq .search.Country "US"}}selected{{end}}>USA</option>
|
||||||
|
<option value="FR" {{if eq .search.Country "FR"}}selected{{end}}>France</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
<label for="year">Year</label>
|
||||||
|
<select id="year" name="year">
|
||||||
|
<option value="2020" {{if eq (deferint .search.Year) 2020}}selected{{end}}>2020</option>
|
||||||
|
<option value="2021" {{if eq (deferint .search.Year) 2021}}selected{{end}}>2021</option>
|
||||||
|
<option value="2022" {{if eq (deferint .search.Year) 2022}}selected{{end}}>2022</option>
|
||||||
|
<option value="2023" {{if eq (deferint .search.Year) 2023}}selected{{end}}>2023</option>
|
||||||
|
<option value="2024" {{if eq (deferint .search.Year) 2024}}selected{{end}}>2024</option>
|
||||||
|
<option value="2025" {{if eq (deferint .search.Year) 2025}}selected{{end}}>2025</option>
|
||||||
|
<option value="2026" {{if eq (deferint .search.Year) 2026}}selected{{end}}>2026</option>
|
||||||
|
<option value="2027" {{if eq (deferint .search.Year) 2027}}selected{{end}}>2027</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>State holiday</label>
|
||||||
|
|
||||||
|
<label for="state-holiday-true">True</label>
|
||||||
|
<input type="radio" value="true" {{if boolcmp .search.StateHoliday "true"}}checked{{end}} id="state-holiday-true" name="stateHoliday">
|
||||||
|
|
||||||
|
<label for="state-holiday-false">False</label>
|
||||||
|
<input type="radio" value="false" {{if boolcmp .search.StateHoliday "false"}}checked{{end}} id="state-holiday-false" name="stateHoliday">
|
||||||
|
|
||||||
|
<label for="state-holiday-any">Any</label>
|
||||||
|
<input type="radio" value="" {{if boolcmp .search.StateHoliday "nil"}}checked{{end}} id="state-holiday-any" name="stateHoliday">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Religious holiday</label>
|
||||||
|
|
||||||
|
<label for="religious-holiday-true">True</label>
|
||||||
|
<input type="radio" value="true" {{if boolcmp .search.ReligiousHoliday "true"}}checked{{end}} id="religious-holiday-true" name="religiousHoliday">
|
||||||
|
|
||||||
|
<label for="religious-holiday-false">False</label>
|
||||||
|
<input type="radio" value="false" {{if boolcmp .search.ReligiousHoliday "false"}}checked{{end}} id="religious-holiday-false" name="religiousHoliday">
|
||||||
|
|
||||||
|
<label for="religious-holiday-any">Any</label>
|
||||||
|
<input type="radio" value="" {{if boolcmp .search.ReligiousHoliday "nil"}}checked{{end}} id="religious-holiday-any" name="religiousHoliday">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>State holiday</th>
|
||||||
|
<th>Religious holiday</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $entry := .holidays.Holidays}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$entry.Name}}</td>
|
||||||
|
<td>{{$entry.Description}}</td>
|
||||||
|
<td>{{$entry.Date.Format "2006-01-02"}}</td>
|
||||||
|
<td>{{$entry.IsStateHoliday}}</td>
|
||||||
|
<td>{{$entry.IsReligiousHoliday}}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/holiday?id={{$entry.Id}}">Edit</a>
|
||||||
|
<form action="/admin/holiday/delete" method="post">
|
||||||
|
<input id="id" type="hidden" name="id" value="{{$entry.Id}}">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue