diff options
author | Matthew Lemon <y@yulqen.org> | 2024-04-18 11:06:32 +0100 |
---|---|---|
committer | Matthew Lemon <y@yulqen.org> | 2024-04-18 11:06:32 +0100 |
commit | 20560419614e22fbb58567cdb0b88b54caf679f4 (patch) | |
tree | 163df44f07907d865b363db3bc198287ebd3d25f /cmd/web | |
parent | 435742cede199e3c85b5e2eb5a42ccbee4906a05 (diff) |
Adds code from ded-go-core - no database
D2S test app code removed (nginx error).
Adds all go code from `ded-go-core` using the basic Gov.UK UI for a test
page for DED.
Diffstat (limited to 'cmd/web')
-rw-r--r-- | cmd/web/handlers.go | 144 | ||||
-rw-r--r-- | cmd/web/helpers.go | 52 | ||||
-rw-r--r-- | cmd/web/main.go | 82 | ||||
-rw-r--r-- | cmd/web/routes.go | 18 | ||||
-rw-r--r-- | cmd/web/templates.go | 48 |
5 files changed, 344 insertions, 0 deletions
diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go new file mode 100644 index 0000000..0827ea1 --- /dev/null +++ b/cmd/web/handlers.go @@ -0,0 +1,144 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/defencedigital/ded-web/internal/models" +) + +func (app *application) listPersons(w http.ResponseWriter, r *http.Request) { + ps, err := app.persons.ListAll() + if err != nil { + app.serverError(w, r, err) + return + } + + app.render(w, r, http.StatusOK, "persons_list.tmpl.html", templateData{ + Persons: ps, + }) +} + +func (app *application) listOperations(w http.ResponseWriter, r *http.Request) { + + ops, err := app.operations.ListAll() + if err != nil { + app.serverError(w, r, err) + return + } + + var newOps []models.Operation + + for _, op := range ops { + esses, err := app.engagementStrategies.GetForOperation(op.ID) + // TODO: Check what kind of error this is, don't just continue + if err != nil { + continue + } + if len(esses) > 0 { + op.EngagementStrategies = esses + newOps = append(newOps, op) + } else { + newOps = append(newOps, op) + } + } + + app.render(w, r, http.StatusOK, "operations_list.tmpl.html", templateData{ + Operations: newOps, + }) +} + +func (app *application) listOrganisations(w http.ResponseWriter, r *http.Request) { + + latest, err := app.organisations.Latest() + if err != nil { + app.serverError(w, r, err) + return + } + + // We are putting latest in the Organisations field of templateData and leaving + // templateData.Operations as the nil value because we don't need Operations in this + // page, of course. + app.render(w, r, http.StatusOK, "organisations_list.tmpl.html", templateData{ + Organisations: latest, + }) +} + +func (app *application) home(w http.ResponseWriter, r *http.Request) { + // Check if the current request URL path exactly matches "/". If it doesn't, use + // the http.NotFound() function to send a 404 response to the client. + // Importantly, we then return from the handler. If we don't return the handler + // would keep executing. + if r.URL.Path != "/" { + app.notFound(w) + return + } + + // organisations, err := app.organisations.Latest() + // if err != nil { + // app.serverError(w, r, err) + // return + // } + + // for _, organisation := range organisations { + // fmt.Fprintf(w, "%+v\n", organisation) + // } + + // files := []string{ + // "./ui/html/base.tmpl.html", + // "./ui/html/pages/home.tmpl.html", + // "./ui/html/partials/nav.tmpl.html", + // } + + // ts, err := template.ParseFiles(files...) + // if err != nil { + // app.serverError(w, r, err) // Use the serverError() helper + // return + // } + + // err = ts.ExecuteTemplate(w, "base", nil) + // if err != nil { + // app.serverError(w, r, err) // Use the serverError() helper + // } + app.render(w, r, http.StatusOK, "home.tmpl.html", templateData{}) +} + +func (app *application) organisationView(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(r.URL.Query().Get("id")) + if err != nil || id < 1 { + app.notFound(w) + return + } + + o, err := app.organisations.Get(id) + if err != nil { + if errors.Is(err, models.ErrNoRecord) { + app.notFound(w) + } else { + app.serverError(w, r, err) + } + return + } + fmt.Fprintf(w, "%+v", o) +} + +func (app *application) organisationCreate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.Header().Set("Allow", http.MethodPost) + app.clientError(w, http.StatusMethodNotAllowed) + return + } + + // Dummy data + name := "Test Organisation" + + id, err := app.organisations.Insert(name) + if err != nil { + app.serverError(w, r, err) + return + } + + http.Redirect(w, r, fmt.Sprintf("/organisation/view?id=%d", id), http.StatusSeeOther) +} diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go new file mode 100644 index 0000000..fc9b8f8 --- /dev/null +++ b/cmd/web/helpers.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "runtime/debug" +) + +func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) { + var ( + method = r.Method + uri = r.URL.RequestURI() + trace = string(debug.Stack()) + ) + + app.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace) + log.Printf("Crash! %s %s %s", method, uri, trace) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +// The clientError helper sends a specific status code and corresponding description +// to the user. We'll use this later in the book to send responses like 400 "Bad +// Request" when there's a problem with the request that the user sent. +func (app *application) clientError(w http.ResponseWriter, status int) { + http.Error(w, http.StatusText(status), status) +} + +// notFound helper is a convenience wrapper around clientError which sends a 404 Not Found response +func (app *application) notFound(w http.ResponseWriter) { + app.clientError(w, http.StatusNotFound) +} + +func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) { + // retrieve the appropriate template set from the cache based on the page name. + // If no entry exists in the cache with the provided name, create a new error + // and call serverError() helper that we made earlier and return. + ts, ok := app.templateCache[page] + if !ok { + err := fmt.Errorf("the template %s does not exist", page) + app.serverError(w, r, err) + return + } + + // Write out the provided HTTP status code('200 OK', '400 Bad Request', etc). + w.WriteHeader(status) + + err := ts.ExecuteTemplate(w, "base", data) + if err != nil { + app.serverError(w, r, err) + } +} diff --git a/cmd/web/main.go b/cmd/web/main.go new file mode 100644 index 0000000..5f4bfdf --- /dev/null +++ b/cmd/web/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "database/sql" + "flag" + "html/template" + "log/slog" + "net/http" + "os" + + // _ "github.com/go-sql-driver/mysql" + "github.com/defencedigital/ded-web/internal/models" + // _ "github.com/lib/pq" +) + +type application struct { + logger *slog.Logger + operations *models.OperationModel + organisations *models.OrganisationModel + persons *models.PersonModel + engagementStrategies *models.EngagementStrategyModel + templateCache map[string]*template.Template +} + +func main() { + addr := flag.String("addr", ":4000", "HTTP network port") + // This uses "ded-db" in the connection string; this should be the name of the container running postgres. + // If not running this app inside a container, use "localhost". + //dsn := flag.String("dsn", "postgresql://postgres:secret@ded-db?sslmode=disable", "PostgreSQL data source name") + flag.Parse() + + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Database connection + //db, err := openDB(*dsn) + //if err != nil { + // logger.Error(err.Error()) + // os.Exit(1) + //} + // + //defer db.Close() + + // initialise the new template cache... + templateCache, err := newTemplateCache() + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + app := &application{ + logger: logger, + //operations: &models.OperationModel{DB: db}, + //organisations: &models.OrganisationModel{DB: db}, + //persons: &models.PersonModel{DB: db}, + //engagementStrategies: &models.EngagementStrategyModel{DB: db}, + templateCache: templateCache, + } + + // mux := http.NewServeMux() + // mux.HandleFunc("/", home) + // log.Print("starting server on :4000") + logger.Info("starting server", "addr", *addr) + err = http.ListenAndServe(*addr, app.routes()) + logger.Error(err.Error()) + os.Exit(1) +} + +// openDB wraps sql.Open() and returns a sql.DB connection pool +// for a given DSN. +func openDB(dsn string) (*sql.DB, error) { + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err + } + + err = db.Ping() + if err != nil { + db.Close() + return nil, err + } + return db, nil +} diff --git a/cmd/web/routes.go b/cmd/web/routes.go new file mode 100644 index 0000000..58a351a --- /dev/null +++ b/cmd/web/routes.go @@ -0,0 +1,18 @@ +package main + +import "net/http" + +func (app *application) routes() *http.ServeMux { + mux := http.NewServeMux() + + fileServer := http.FileServer(http.Dir("./ui/static/")) + mux.Handle("/static/", http.StripPrefix("/static", fileServer)) + + mux.HandleFunc("/", app.home) + mux.HandleFunc("/organisation/list", app.listOrganisations) + mux.HandleFunc("/organisation/view", app.organisationView) + mux.HandleFunc("/organisation/create", app.organisationCreate) + mux.HandleFunc("/operation/list", app.listOperations) + mux.HandleFunc("/person/list", app.listPersons) + return mux +} diff --git a/cmd/web/templates.go b/cmd/web/templates.go new file mode 100644 index 0000000..845fe25 --- /dev/null +++ b/cmd/web/templates.go @@ -0,0 +1,48 @@ +package main + +import ( + "html/template" + "path/filepath" + + "github.com/defencedigital/ded-web/internal/models" +) + +// TODO: At the moment we are using a struct will have fields for all the data +// we will need. Not sure whether this is good or bad at the moment. +type templateData struct { + Operations []models.Operation + Organisations []models.Organisation + Persons []models.Person +} + +func newTemplateCache() (map[string]*template.Template, error) { + cache := map[string]*template.Template{} + + // get a slice of filepaths match the pattern for our page templates + // e.g. [ui/html/pages/home.tmpl.html ui/html/pages/view.tmpl.html] + pages, err := filepath.Glob("./ui/html/pages/*.html") + if err != nil { + return nil, err + } + + // loop through the page filepaths one by one + for _, page := range pages { + // extract "home.tmp.html" from the full filepath. + name := filepath.Base(page) + + files := []string{ + "./ui/html/base.tmpl.html", + "./ui/html/partials/nav.tmpl.html", + page, + } + + ts, err := template.ParseFiles(files...) + if err != nil { + return nil, err + } + + cache[name] = ts + } + + return cache, nil +} |