Version 7 #53

Merged
vlbeaudoin merged 33 commits from rewrite/v7 into main 2024-09-03 11:17:26 -04:00
9 changed files with 168 additions and 11 deletions
Showing only changes of commit e847f693e0 - Show all commits

View file

@ -41,3 +41,20 @@ func (c APIClient) InsertProgrammes(programmes ...Programme) (amountInserted int
return response.Data.ProgrammesInserted, nil return response.Data.ProgrammesInserted, nil
} }
func (c APIClient) InsertMembres(membres ...Membre) (amountInserted int64, err error) {
var request MembresPOSTRequest
request.Data.Membres = membres
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.MembresInserted, nil
}

View file

@ -40,7 +40,6 @@ func TestAPI(t *testing.T) {
}) })
//TODO create or replace schema //TODO create or replace schema
//TODO insert programmes
t.Run("insert programmes", t.Run("insert programmes",
func(t *testing.T) { func(t *testing.T) {
programmes := []Programme{ programmes := []Programme{
@ -54,6 +53,28 @@ func TestAPI(t *testing.T) {
} }
}) })
//TODO insert membres //TODO insert membres
t.Run("insert membres",
func(t *testing.T) {
membres := []Membre{
{
ID: "0000000",
FirstName: "Test",
LastName: "User",
ProgrammeID: "404.42",
},
{
ID: "1234567",
FirstName: "Deadname",
LastName: "User",
PreferedName: "User, Test-Name",
ProgrammeID: "200.10",
},
}
_, err := apiClient.InsertMembres(membres...)
if err != nil {
t.Error(err)
}
})
//TODO get membre //TODO get membre
//TODO update membre prefered name //TODO update membre prefered name
//TODO get membres //TODO get membres

4
cmd.go
View file

@ -85,6 +85,10 @@ var apiCmd = &cobra.Command{
log.Fatal("create or replace schema:", err) log.Fatal("create or replace schema:", err)
} }
if err := db.CreateOrReplaceViews(); err != nil {
log.Fatal("create or replace views:", err)
}
// Routes // Routes
if err := addRoutes(e, db); err != nil { if err := addRoutes(e, db); err != nil {
log.Fatal("add routes:", err) log.Fatal("add routes:", err)

14
db.go
View file

@ -11,6 +11,9 @@ import (
//go:embed sql/schema.sql //go:embed sql/schema.sql
var sqlSchema string var sqlSchema string
//go:embed sql/views.sql
var sqlViews string
type PostgresClient struct { type PostgresClient struct {
//TODO move context out of client //TODO move context out of client
Ctx context.Context Ctx context.Context
@ -22,8 +25,13 @@ func (db *PostgresClient) CreateOrReplaceSchema() error {
return err return err
} }
func (db *PostgresClient) CreateOrReplaceViews() error {
_, err := db.Pool.Exec(db.Ctx, sqlViews)
return err
}
// InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered // InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
func (d *PostgresClient) InsertMembres(membres []Membre) (inserted int64, err error) { func (d *PostgresClient) InsertMembres(membres ...Membre) (inserted int64, err error) {
select { select {
case <-d.Ctx.Done(): case <-d.Ctx.Done():
return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err()) return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
@ -84,10 +92,10 @@ func (d *PostgresClient) InsertProgrammes(programmes ...Programme) (inserted int
result, err := tx.Exec(d.Ctx, ` result, err := tx.Exec(d.Ctx, `
INSERT INTO programmes INSERT INTO programmes
(id, titre) (id, name)
VALUES ($1, $2) ON CONFLICT DO NOTHING;`, VALUES ($1, $2) ON CONFLICT DO NOTHING;`,
programme.ID, programme.ID,
programme.Titre) programme.Name)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View file

@ -2,7 +2,7 @@ package main
type Programme struct { type Programme struct {
ID string `db:"id" json:"programme_id" csv:"programme_id"` ID string `db:"id" json:"programme_id" csv:"programme_id"`
Titre string `db:"titre" json:"nom_programme" csv:"nom_programme"` Name string `db:"name" json:"nom_programme" csv:"nom_programme"`
} }
type Membre struct { type Membre struct {

View file

@ -79,3 +79,44 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP
return return
} }
var _ voki.Requester[MembresPOSTResponse] = MembresPOSTRequest{}
type MembresPOSTRequest struct {
Data struct {
Membres []Membre
}
}
func (request MembresPOSTRequest) Complete() bool {
return len(request.Data.Membres) != 0
}
func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresPOSTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPost,
"/api/membres/",
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}

View file

@ -48,7 +48,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
apiPath, apiPath,
http.MethodPost, http.MethodPost,
"/programmes/", "/programmes/",
"Get registered programmes", "Insert programmes",
"ProgrammesPOST", func(c echo.Context) error { "ProgrammesPOST", func(c echo.Context) error {
var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{} var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
@ -84,5 +84,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
}); err != nil { }); err != nil {
return err return err
} }
if err := pave.EchoRegister[MembresPOSTRequest](
apiGroup,
&p,
apiPath,
http.MethodPost,
"/membres/",
"Insert membres",
"MembresPOST", func(c echo.Context) error {
var request, response = MembresPOSTRequest{}, MembresPOSTResponse{}
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresPOST request received"
return c.JSON(response.StatusCode(), response)
}
amountInserted, err := db.InsertMembres(request.Data.Membres...)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.MembresInserted = amountInserted
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
return nil return nil
} }

View file

@ -1,12 +1,12 @@
CREATE TABLE IF NOT EXISTS programmes ( CREATE TABLE IF NOT EXISTS programmes (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
titre TEXT name TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS membres ( CREATE TABLE IF NOT EXISTS membres (
id VARCHAR(7) PRIMARY KEY, id VARCHAR(7) PRIMARY KEY,
last_name TEXT, last_name TEXT NOT NULL,
first_name TEXT, first_name TEXT NOT NULL,
prefered_name TEXT, prefered_name TEXT,
programme_id TEXT REFERENCES programmes(id) programme_id TEXT REFERENCES programmes(id) NOT NULL
); );

23
sql/views.sql Normal file
View file

@ -0,0 +1,23 @@
-- membres_for_display affiche le numéro étudiant, nom complet OU prefered_name, et titre du programme.
--
-- Utilisé par l'application web pour rechercher et afficher les informations des membres
CREATE OR REPLACE VIEW
"membres_for_display"
AS (
SELECT
"membres".id,
CASE
WHEN
"membres".prefered_name != '' AND "membres".prefered_name IS NOT NULL
THEN
"membres".prefered_name
ELSE
CONCAT("membres".last_name, ', ', "membres".first_name)
END AS name,
"programmes".id AS programme_id,
"programmes".name AS programme_name
FROM
"membres"
INNER JOIN
"programmes" ON "programmes".id = "membres".programme_id
);