This commit is contained in:
Victor Lacasse-Beaudoin 2024-12-30 15:00:42 -05:00
parent cc8c8ef967
commit 947ee56596
10 changed files with 319 additions and 32 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

15
compose.yaml Normal file
View file

@ -0,0 +1,15 @@
name: 'agendas-db'
services:
db:
image: 'postgres:16'
environment:
POSTGRES_USER: "${DB_USER:?}"
POSTGRES_PASSWORD: "${DB_PASSWORD:?}"
POSTGRES_DATABASE: "${DB_DATABASE:?}"
restart: 'unless-stopped'
ports:
- '5432:5432'
volumes:
- 'db-data:/var/lib/postgresql/data/'
volumes:
db-data:

53
db.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
type DBClient struct {
Pool *pgxpool.Pool
}
func (d DBClient) Ping(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
if d.Pool == nil {
return fmt.Errorf("db: cannot check health using nil connection pool")
}
return d.Pool.Ping(ctx)
}
}
func (d DBClient) Init(ctx context.Context) error {
//TODO check context is not closed
//TODO check *DB is not nil
//TODO Init
return fmt.Errorf("db: Init not implemented")
}
func (d DBClient) CreateTransaction(ctx context.Context, transaction Transaction) error {
//TODO check context is not closed
//TODO check *DB is not nil
//TODO CreateTransaction
return fmt.Errorf("db: CreateTransaction not implemented")
}
func (d DBClient) ReadTransaction(ctx context.Context, membreID string, isPerpetual bool) error {
//TODO check context is not closed
//TODO check *DB is not nil
//TODO ReadTransaction
return fmt.Errorf("db: ReadTransaction not implemented")
}
func (d DBClient) CountTransactions(ctx context.Context) (membresSansAgenda, membresAvecPerpetuel, membresAvecNonPerpetuel, membresAvecTout int, err error) {
//TODO check context is not closed
//TODO check *DB is not nil
//TODO CountTransactions
return 0, 0, 0, 0, fmt.Errorf("db: CountTransactions not implemented")
}

9
entity.go Normal file
View file

@ -0,0 +1,9 @@
package main
import "time"
type Transaction struct {
MembreID string `db:"membre_id"`
GivenAt *time.Time `db:"given_at"`
IsPerpetual bool `db:"is_perpetual"`
}

View file

@ -11,11 +11,12 @@ import (
) )
// Handling // Handling
func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFunc { func UIIndex(ctx context.Context, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
type data struct { type data struct {
Error string Error string
BottinHealthResponse bottin.ReadHealthResponse BottinHealthResponse bottin.ReadHealthResponse
IsDBUp bool
} }
return c.Render(http.StatusOK, "index", func() (d data) { return c.Render(http.StatusOK, "index", func() (d data) {
@ -36,6 +37,16 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFu
} }
d.BottinHealthResponse = bottinHealthResponse d.BottinHealthResponse = bottinHealthResponse
// Check db health
if dbClient == nil {
return fmt.Errorf("Impossible de contacter la base de données, le client DB est nil")
}
if err := dbClient.Ping(ctx); err != nil {
return err
}
d.IsDBUp = true
// No errors // No errors
return nil return nil
} }
@ -91,3 +102,66 @@ func UIReadMembre(ctx context.Context, bottinClient *bottin.APIClient) echo.Hand
}()) }())
} }
} }
/*
UICreateTransaction gère la création des transactions
TODO:
- Fully implement
- Check context is not closed
*/
func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc {
return func(c echo.Context) error {
type data struct {
BottinHealth bottin.ReadHealthResponse
Error string
Result string
}
return c.Render(http.StatusOK, "index", func() (d data) {
if err := func() error {
if bottinClient == nil {
return fmt.Errorf("Cannot operate on nil *bottin.APIClient")
}
bottinReadHealthResponse, err := bottinClient.ReadHealth(ctx)
if err != nil {
return err
}
d.BottinHealth = bottinReadHealthResponse
isPerpetual := c.FormValue("is_perpetual") == "on"
membreID := c.FormValue("membre_id")
if membreID == "" {
return fmt.Errorf("👎 Aucun numéro étudiant sélectionné. Assurez-vous de cliquer sur la case 'Numéro étudiant:' avant de scanner.")
}
// dbclient.CreateTransaction
if err := dbClient.CreateTransaction(ctx, Transaction{
MembreID: membreID,
IsPerpetual: isPerpetual,
}); err != nil {
return err
}
// Prepare result message
var typeAgenda string
if isPerpetual {
typeAgenda = "perpétuel"
} else {
typeAgenda = "non-perpétuel"
}
d.Result = fmt.Sprintf("👍 Membre %s peut recevoir son agenda %s", membreID, typeAgenda)
return fmt.Errorf("UIIndexPOST not fully implemented")
}(); err != nil {
d.Error = err.Error()
log.Println("err:", d.Error)
}
return
}())
}
}

12
main.go
View file

@ -8,6 +8,7 @@ import (
"codeberg.org/vlbeaudoin/voki/v3" "codeberg.org/vlbeaudoin/voki/v3"
"git.agecem.com/bottin/bottin/v10/pkg/bottin" "git.agecem.com/bottin/bottin/v10/pkg/bottin"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/term" "golang.org/x/term"
) )
@ -57,7 +58,16 @@ func run(ctx context.Context, cfg Config) error {
protocol, protocol,
)} )}
if err := RunServer(ctx, &bottinClient); err != nil && err != http.ErrServerClosed { // connect to db
dbPool, err := pgxpool.New(ctx, "postgres://agendas:agendas@localhost:5432/agendas")
if err != nil {
return err
}
defer dbPool.Close()
dbClient := DBClient{Pool: dbPool}
if err := RunServer(ctx, cfg, &bottinClient, &dbClient); err != nil && err != http.ErrServerClosed {
return err return err
} }

View file

@ -1,23 +0,0 @@
package main
import (
"io"
"text/template"
"git.agecem.com/bottin/agendas/ui"
"github.com/labstack/echo/v4"
)
type Renderer struct {
templates *template.Template
}
func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func NewRenderer() *Renderer {
return &Renderer{
templates: template.Must(template.ParseFS(ui.HTMLFS(), "*html")),
}
}

View file

@ -4,12 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"git.agecem.com/bottin/agendas/ui"
"git.agecem.com/bottin/bottin/v10/pkg/bottin" "git.agecem.com/bottin/bottin/v10/pkg/bottin"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
) )
func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
@ -18,15 +19,24 @@ func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error {
return fmt.Errorf("nil bottin client") return fmt.Errorf("nil bottin client")
} }
if dbClient == nil {
return fmt.Errorf("nil dbClient")
}
e := echo.New() e := echo.New()
e.Renderer = NewRenderer() e.Renderer = ui.NewRenderer()
e.Pre(middleware.AddTrailingSlash()) e.Pre(middleware.AddTrailingSlash())
e.GET("/", UIIndex(ctx, bottinClient)) //basicauth TODO
//e.Use(
e.GET("/membre/", UIReadMembre(ctx, bottinClient)) e.GET("/", UIIndex(ctx, bottinClient, dbClient))
//e.GET("/transaction/", UIReadTransaction
e.POST("/transaction/", UICreateTransaction(ctx, cfg, bottinClient, dbClient))
//e.GET("/membre/", UIReadMembre(ctx, bottinClient))
return e.Start(":3333") return e.Start(":3333")
} }

View file

@ -1,6 +1,12 @@
package ui package ui
import "embed" import (
"embed"
"io"
"text/template"
"github.com/labstack/echo/v4"
)
//go:embed *.html //go:embed *.html
var htmlFS embed.FS var htmlFS embed.FS
@ -8,3 +14,17 @@ var htmlFS embed.FS
func HTMLFS() embed.FS { func HTMLFS() embed.FS {
return htmlFS return htmlFS
} }
type Renderer struct {
templates *template.Template
}
func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func NewRenderer() *Renderer {
return &Renderer{
templates: template.Must(template.ParseFS(HTMLFS(), "*html")),
}
}

View file

@ -1,4 +1,122 @@
{{ define "index" }} {{ define "index" }}
hello world <!DOCTYPE html>
{{ . }} <html lang="fr">
<head>
<meta charset="utf-8">
<title>
AGECEM | Agenda
</title>
<style>
body {
/* Center the form on the page */
margin: 0 auto;
width: 400px;
/* Form outline */
padding: 1em;
border: 1px solid #ccc;
border-radius: 1em;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
form li + li {
margin-top: 1em;
}
label {
/* Uniform size & alignment */
display: inline-block;
width: 90px;
text-align: right;
}
input,
textarea {
/* To make sure that all text fields have the same font settings
By default, textareas have a monospace font */
font: 1em sans-serif;
/* Uniform text field size */
width: 300px;
box-sizing: border-box;
/* Match form field borders */
border: 1px solid #999;
}
input:focus,
textarea:focus {
/* Additional highlight for focused elements */
border-color: #000;
}
textarea {
/* Align multiline text fields with their labels */
vertical-align: top;
/* Provide space to type some text */
height: 5em;
}
.button {
/* Align buttons with the text fields */
padding-left: 90px; /* same size as the label elements */
}
button {
/* This extra margin represent roughly the same space as the space
between the labels and their text fields */
margin-left: 0.5em;
}
</style>
</head>
<body>
<h2>
Distribution d'agendas aux membres de l'AGECEM
</h2>
<p>
1) Si l'agenda demandé est perpétuel, cochez 'Perpétuel?:'<br>
<br>
2) Sélectionnez le champs 'Numéro étudiant:'<br>
<br>
3) Scannez la carte étudiante d'unE membre<br>
-ou-<br>
Entrez manuellement le code à 7 chiffres<br>
<br>
4) Si aucune erreur ne survient, la personne est libre de partir avec son agenda
</p>
<hr>
<form action="/transaction/" method="post">
<ul>
<li>
<label for="is_perpetual">Perpétuel?:</label>
<input type="checkbox" id="is_perpetual" name="is_perpetual"/>
</li>
<li>
<label for="membre_id">Numéro étudiant:</label>
<input type="text" id="membre_id" name="membre_id" autofocus/>
</li>
<li class="button">
<button type="submit">Scan</button>
</li>
</ul>
</form>
<!--<p class="result">{{ .Result }}</p>--!>
<!--<p>{{ .Error }}</p>--!>
<!--<details><summary>Détails de connexion au bottin</summary>{{ .BottinHealth }}</details>--!>
<p class="result">{{ . }}</p>
</body>
</html>
{{ end }} {{ end }}