move routes in seperate files; better error handling for enpoint functions

This commit is contained in:
2025-01-02 15:05:20 +01:00
parent b1719704e4
commit 59222160af
5 changed files with 118 additions and 88 deletions

View File

@@ -1,99 +1,19 @@
package main
import (
"crowsnest/internal/data"
"crowsnest/internal/model"
"crowsnest/internal/viewmodel"
"html/template"
"crowsnest/internal/routes"
"net/http"
"regexp"
"strings"
)
type articleDateOrder struct {}
func (ord articleDateOrder) Weight(a *model.Article) int {
return int(a.PublishDate.Unix())
}
type articleTermFrequency struct {
terms []string
}
func (ord articleTermFrequency) Weight(a *model.Article) int {
score := 0
for _, term := range ord.terms {
score += strings.Count(a.Content, term)
}
return score
}
func index(w http.ResponseWriter, req *http.Request) {
// retrieve from repo
fds, err := data.NewFileDatastore("./persistence/spiegel100.json")
if err != nil { panic(err) }
repo, err := data.NewDefaultRepository[*model.Article](fds, "article")
if err != nil { panic(err) }
articles, err := repo.GetByCriteria(articleDateOrder{})
if err != nil { panic(err) }
// convert to viewmodel
articleVMs := make([]*viewmodel.ArticleViewModel, 0, len(articles))
for _, a := range articles {
articleVMs = append(articleVMs, viewmodel.NewArticleViewModel(a))
}
// render template
t := template.Must(template.ParseFiles("templates/article.html", "templates/layout.html"))
err = t.ExecuteTemplate(w, "base", articleVMs[:10])
if err != nil { panic(err) }
}
func search(w http.ResponseWriter, req *http.Request) {
// parse the form data
err := req.ParseForm()
if err != nil {
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
// collect search terms
p := regexp.MustCompile("\\s+")
searchTerms := make([]string, 0)
for _, elem := range p.Split(req.FormValue("search"), -1) {
elem = strings.TrimSpace(elem)
if elem == "" { continue }
searchTerms = append(searchTerms, elem)
}
// retrieve from repo
fds, _ := data.NewFileDatastore("./persistence/spiegel100.json")
repo, _ := data.NewDefaultRepository[*model.Article](fds, "article")
var articles []*model.Article
if len(searchTerms) != 0 {
articles, _ = repo.GetByCriteria(articleTermFrequency{ terms: searchTerms })
} else {
articles, _ = repo.GetAll()
}
// convert to viewmodel
articleVMs := make([]*viewmodel.ArticleViewModel, 0, len(articles))
for _, a := range articles {
articleVMs = append(articleVMs, viewmodel.NewArticleViewModel(a))
}
t := template.Must(template.ParseFiles("templates/article.html", "templates/layout.html"))
err = t.ExecuteTemplate(w, "base", articleVMs)
if err != nil { panic(err) }
}
func main() {
// routes
http.HandleFunc("/", index)
http.HandleFunc("/search", search)
http.HandleFunc("GET /", routes.Index)
http.HandleFunc("POST /up/search", routes.UpSearch)
// serve files from the "static" directory
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static", http.StripPrefix("/", fs))
http.Handle("GET /static/", http.StripPrefix("/", fs))
// start server
http.ListenAndServe(":8080", nil)

42
internal/routes/Index.go Normal file
View File

@@ -0,0 +1,42 @@
package routes
import (
"crowsnest/internal/data"
"crowsnest/internal/model"
"crowsnest/internal/viewmodel"
"html/template"
"net/http"
)
// sort criteria
type articleDateOrder struct {}
func (ord articleDateOrder) Weight(a *model.Article) int {
return int(a.PublishDate.Unix())
}
// List the latest articles using the base template.
func Index(w http.ResponseWriter, req *http.Request) {
// retrieve from repo
fds, err := data.NewFileDatastore("./persistence/spiegel100.json")
if err != nil { http.Error(w, "Failed to load datastore", http.StatusInternalServerError); return; }
repo, err := data.NewDefaultRepository[*model.Article](fds, "article")
if err != nil { http.Error(w, "Failed to create repository", http.StatusInternalServerError); return; }
articles, err := repo.GetByCriteria(articleDateOrder{})
if err != nil { http.Error(w, "Failed to get articles", http.StatusInternalServerError); return; }
// truncate
if len(articles) > 10 {
articles = articles[:10]
}
// convert to viewmodel
articleVMs := make([]*viewmodel.ArticleViewModel, 0, len(articles))
for _, a := range articles {
articleVMs = append(articleVMs, viewmodel.NewArticleViewModel(a))
}
// render template
t := template.Must(template.ParseFiles("templates/article.html", "templates/layout.html"))
err = t.ExecuteTemplate(w, "base", articleVMs)
if err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError); return; }
}

View File

@@ -0,0 +1,68 @@
package routes
import (
"crowsnest/internal/data"
"crowsnest/internal/model"
"crowsnest/internal/viewmodel"
"html/template"
"net/http"
"regexp"
"strings"
)
// sort criteria
type articleTermFrequency struct {
terms []string
}
func (ord articleTermFrequency) Weight(a *model.Article) int {
score := 0
for _, term := range ord.terms {
score += strings.Count(a.Content, term)
}
return score
}
// Enpoint that returns a list of articles given search terms in the post
// request of a search form. Uses the content template.
func UpSearch(w http.ResponseWriter, req *http.Request) {
// parse the form data
err := req.ParseForm()
if err != nil { http.Error(w, "Unable to parse form", http.StatusBadRequest); return; }
// collect search terms
p := regexp.MustCompile("\\s+")
searchTerms := make([]string, 0)
for _, elem := range p.Split(req.FormValue("search"), -1) {
elem = strings.TrimSpace(elem)
if elem == "" { continue }
searchTerms = append(searchTerms, elem)
}
// retrieve from repo
fds, _ := data.NewFileDatastore("./persistence/spiegel100.json")
if err != nil { http.Error(w, "Failed to read datastore", http.StatusInternalServerError); return; }
repo, _ := data.NewDefaultRepository[*model.Article](fds, "article")
if err != nil { http.Error(w, "Failed to create repository", http.StatusInternalServerError); return; }
var articles []*model.Article
if len(searchTerms) != 0 {
articles, err = repo.GetByCriteria(articleTermFrequency{ terms: searchTerms })
} else {
Index(w, req)
return
}
if err != nil { http.Error(w, "Failed to get articles from repo", http.StatusInternalServerError); return; }
// convert to viewmodel
articleVMs := make([]*viewmodel.ArticleViewModel, 0, len(articles))
for _, a := range articles {
articleVMs = append(articleVMs, viewmodel.NewArticleViewModel(a))
}
// render template
t := template.Must(template.ParseFiles("templates/article.html"))
err = t.ExecuteTemplate(w, "content", articleVMs)
if err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError); return; }
}

View File

@@ -13,8 +13,8 @@ type ArticleViewModel struct {
func NewArticleViewModel(a *model.Article) *ArticleViewModel {
summary := a.Content
if len(a.Content) > 200 {
summary = summary[:200]
if len(a.Content) > 300 {
summary = summary[:300]
}
return &ArticleViewModel{

View File

@@ -8,8 +8,8 @@
<nav class="navbar sticky-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand">crowsnest</a>
<form class="d-flex" role="search" method="post" action="/search" up-submit up-autosubmit up-target=".content">
<a class="navbar-brand text-light">crowsnest</a>
<form class="d-flex" role="search" method="post" action="/up/search" up-submit up-autosubmit up-target=".content">
<input name="search" class="form-control me-2" type="search" placeholder="Suche" aria-label="Suche">
<button class="btn btn-outline-success" type="submit">Suchen</button>
</form>