hswaw/oodviewer: init

This brings oodviewer into k0.

oodviewer started as a py2/flask script running on q3k's personal infra,
which is now being turned down.

This is a rewrite of that script into similarly mediocre Go, conforming
to the exact same mediocre JSON API and spartan HTML interface.

This also deploys it into k0 in the oodviewer-prod namespace. It's
already running, but the 'oodviewer.q3k.me' TTL has to expire before it
begins handling traffic.

Change-Id: Ieef1b0f8f0c60e6fa5dbe7701e0a07a4257f99ce
diff --git a/hswaw/oodviewer/views.go b/hswaw/oodviewer/views.go
new file mode 100644
index 0000000..5ed4b5e
--- /dev/null
+++ b/hswaw/oodviewer/views.go
@@ -0,0 +1,138 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"math/rand"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/golang/glog"
+
+	"code.hackerspace.pl/hscloud/hswaw/oodviewer/templates"
+)
+
+var (
+	tplBase  = template.Must(template.New("base").Parse(string(templates.Data["base.html"])))
+	tplTerm  = template.Must(template.Must(tplBase.Clone()).Parse(string(templates.Data["term.html"])))
+	tplTerms = template.Must(template.Must(tplBase.Clone()).Parse(string(templates.Data["terms.html"])))
+)
+
+// handleTermsJson returns a JSON list of all terms.
+func (a *app) handleTermsJson(w http.ResponseWriter, r *http.Request) {
+	terms, err := a.getTerms(r.Context())
+	if err != nil {
+		glog.Errorf("getTerms: %v", err)
+		w.WriteHeader(500)
+		fmt.Fprintf(w, "internal error")
+		return
+	}
+	// Target API from old oodviewer, even if it's terrible.
+	var res [][]interface{}
+	for _, term := range terms {
+		res = append(res, []interface{}{
+			term.Name, term.Entries,
+		})
+	}
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(res)
+}
+
+// handleTerms renders a HTML page containing all terms.
+func (a *app) handleTerms(w http.ResponseWriter, r *http.Request) {
+	terms, err := a.getTerms(r.Context())
+	if err != nil {
+		glog.Errorf("getTerms: %v", err)
+		w.WriteHeader(500)
+		fmt.Fprintf(w, "internal error")
+		return
+	}
+
+	termsData := make([]struct {
+		URL   string
+		Name  string
+		Count uint64
+	}, len(terms))
+
+	for i, term := range terms {
+		termsData[i].URL = url.QueryEscape(term.Name)
+		termsData[i].Name = term.Name
+		termsData[i].Count = term.Entries
+	}
+
+	tplTerms.Execute(w, map[string]interface{}{
+		"Terms": termsData,
+	})
+}
+
+// handleTermJson returns a JSON list of all entries contained within a term.
+func (a *app) handleTermJson(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	name := parts[len(parts)-1]
+
+	entries, err := a.getEntries(r.Context(), name)
+	if err != nil {
+		glog.Errorf("getEntries: %v", err)
+		w.WriteHeader(500)
+		fmt.Fprintf(w, "internal error")
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(entries)
+}
+
+// handleRandomTermJson returns a JSON serialized randomly chosen entry from a
+// given term.
+func (a *app) handleRandomTermJson(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	name := parts[len(parts)-1]
+
+	entries, err := a.getEntries(r.Context(), name)
+	if err != nil {
+		glog.Errorf("getEntries: %v", err)
+		w.WriteHeader(500)
+		fmt.Fprintf(w, "internal error")
+		return
+	}
+	if len(entries) < 1 {
+		w.WriteHeader(404)
+		fmt.Fprintf(w, "no such entry")
+		return
+	}
+	entry := entries[rand.Intn(len(entries))]
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(entry)
+}
+
+// handleTerm renders an HTML page of all entries contained within a term.
+func (a *app) handleTerm(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	name := parts[len(parts)-1]
+
+	entries, err := a.getEntries(r.Context(), name)
+	if err != nil {
+		glog.Errorf("getEntries: %v", err)
+		w.WriteHeader(500)
+		fmt.Fprintf(w, "internal error")
+		return
+	}
+
+	entriesData := make([]struct {
+		Entry  string
+		Author string
+		Added  string
+	}, len(entries))
+	for i, entry := range entries {
+		entriesData[i].Entry = entry.Entry
+		entriesData[i].Author = entry.Author
+		entriesData[i].Added = time.Unix(entry.Added, 0).String()
+	}
+
+	tplTerm.Execute(w, map[string]interface{}{
+		"Name":    name,
+		"Entries": entriesData,
+	})
+}