app.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "html/template"
  7. "io"
  8. "log"
  9. "net/http"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. "github.com/Masterminds/sprig/v3"
  14. )
  15. const (
  16. App = "Contacts.app"
  17. SecretKey = "hypermedia rocks"
  18. )
  19. func MakeTemplate(first string, others ...string) *template.Template {
  20. all := append([]string{first}, others...)
  21. paths := make([]string, len(all))
  22. for i, name := range all {
  23. paths[i] = fmt.Sprintf("./templates/%s.gohtml", name)
  24. }
  25. // All templates are named like "whatever.html" to match the Python version.
  26. tpl := template.Must(template.New(first + ".html").
  27. Funcs(sprig.FuncMap()).
  28. Funcs(template.FuncMap{
  29. "get_flashed_messages": func() []string {
  30. return []string{"some message"}
  31. },
  32. }).
  33. ParseFiles(paths...),
  34. )
  35. return tpl
  36. }
  37. var (
  38. /*
  39. @app.route("/")
  40. def index():
  41. */
  42. RouteFront = http.RedirectHandler("/contacts", http.StatusFound)
  43. )
  44. /*
  45. @app.route("/contacts")
  46. def contacts():
  47. */
  48. func MakeRouteContacts() http.HandlerFunc {
  49. tpl := MakeTemplate(
  50. "layout",
  51. "index",
  52. "archive_ui",
  53. "rows",
  54. )
  55. return func(w http.ResponseWriter, r *http.Request) {
  56. search := r.URL.Query().Get("q")
  57. page, err := strconv.Atoi(r.URL.Query().Get("page"))
  58. if err != nil || page == 0 {
  59. page = 1
  60. }
  61. var contactsSet []Contact
  62. if search != "" {
  63. contactsSet = (&Contact{}).Search(search)
  64. if r.Header.Get("HX-Trigger") == "search" {
  65. MakeTemplate(
  66. "layout",
  67. "rows",
  68. ).Execute(w, anyMap{"contacts": contactsSet})
  69. return
  70. }
  71. } else {
  72. contactsSet = (&Contact{}).All(page)
  73. }
  74. if err := tpl.Execute(w, anyMap{
  75. "contacts": contactsSet,
  76. "search": search,
  77. "page": page,
  78. "archiver": GetArchiver(),
  79. }); err != nil {
  80. http.Error(w, err.Error(), http.StatusInternalServerError)
  81. return
  82. }
  83. }
  84. }
  85. /*
  86. @app.route("/contacts/archive", methods=["POST"])
  87. def start_archive():
  88. */
  89. func MakeStartArchive() http.HandlerFunc {
  90. tpl := MakeTemplate(
  91. "archive_ui",
  92. )
  93. return func(w http.ResponseWriter, r *http.Request) {
  94. archiver := GetArchiver()
  95. archiver.Run()
  96. tpl.Execute(w, anyMap{"archiver": archiver})
  97. }
  98. }
  99. /*
  100. @app.route("/contacts/archive", methods=["GET"])
  101. def archive_status():
  102. */
  103. func MakeArchiveStatus() http.HandlerFunc {
  104. tpl := MakeTemplate(
  105. "archive_ui",
  106. )
  107. return func(w http.ResponseWriter, r *http.Request) {
  108. archiver := GetArchiver()
  109. tpl.Execute(w, anyMap{"archiver": archiver})
  110. }
  111. }
  112. /*
  113. @app.route("/contacts/archive/file", methods=["GET"])
  114. def archive_content():
  115. */
  116. func ArchiveContent(w http.ResponseWriter, r *http.Request) {
  117. archiver := GetArchiver()
  118. w.Header().Set("Content-Disposition", `attachement; filename="archive.json"`)
  119. http.ServeFile(w, r, archiver.ArchiveFile())
  120. }
  121. /*
  122. @app.route("/contacts/archive", methods=["DELETE"])
  123. def reset_archive():
  124. */
  125. func MakeResetArchive() http.HandlerFunc {
  126. tpl := MakeTemplate(
  127. "archive_ui",
  128. )
  129. return func(w http.ResponseWriter, r *http.Request) {
  130. archiver := GetArchiver()
  131. archiver.Reset()
  132. tpl.Execute(w, anyMap{"archiver": archiver})
  133. }
  134. }
  135. /*
  136. @app.route("/contacts/count")
  137. def contacts_count():
  138. */
  139. func ContactsCount(w http.ResponseWriter, r *http.Request) {
  140. count := (&Contact{}).Count()
  141. _, _ = fmt.Fprintf(w, "(%d total Contacts)", count)
  142. }
  143. /*
  144. @app.route("/contacts/new", methods=['GET'])
  145. def contacts_new_get():
  146. */
  147. func MakeContactsNewGet() http.HandlerFunc {
  148. tpl := MakeTemplate(
  149. "layout",
  150. "new",
  151. )
  152. return func(w http.ResponseWriter, r *http.Request) {
  153. if err := tpl.Execute(w, anyMap{"contact": NewContact(nil)}); err != nil {
  154. http.Error(w, err.Error(), http.StatusInternalServerError)
  155. return
  156. }
  157. }
  158. }
  159. /*
  160. @app.route("/contacts/new", methods=['POST'])
  161. def contacts_new():
  162. */
  163. func MakeContactsNew() http.HandlerFunc {
  164. tpl := MakeTemplate(
  165. "layout",
  166. "new",
  167. )
  168. return func(w http.ResponseWriter, r *http.Request) {
  169. c := NewContact(anyMap{
  170. "first": r.FormValue("first_name"),
  171. "last": r.FormValue("last_name"),
  172. "email": r.FormValue("phone"),
  173. "phone": r.FormValue("email"),
  174. })
  175. if c.Save() {
  176. flash("Created New Contact!")
  177. http.Redirect(w, r, "/contacts", http.StatusFound)
  178. return
  179. } else {
  180. tpl.Execute(w, anyMap{"contact": c})
  181. }
  182. }
  183. }
  184. /*
  185. @app.route("/contacts/<contact_id>")
  186. def contacts_view(contact_id=0):
  187. */
  188. func MakeContactsView() http.HandlerFunc {
  189. tpl := MakeTemplate(
  190. "layout",
  191. "show",
  192. )
  193. return func(w http.ResponseWriter, r *http.Request) {
  194. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  195. contact := (&Contact{}).Find(uint64(contact_id))
  196. if err := tpl.Execute(w, anyMap{"contact": contact}); err != nil {
  197. http.Error(w, err.Error(), http.StatusInternalServerError)
  198. return
  199. }
  200. }
  201. }
  202. /*
  203. @app.route("/contacts/<contact_id>/edit", methods=["GET"])
  204. def contacts_edit_get(contact_id=0):
  205. */
  206. func MakeContactsEditGet() http.HandlerFunc {
  207. tpl := MakeTemplate(
  208. "layout",
  209. "edit",
  210. )
  211. return func(w http.ResponseWriter, r *http.Request) {
  212. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  213. contact := (&Contact{}).Find(uint64(contact_id))
  214. tpl.Execute(w, anyMap{"contact": contact})
  215. }
  216. }
  217. /*
  218. @app.route("/contacts/<contact_id>/edit", methods=["POST"])
  219. def contacts_edit_post(contact_id=0):
  220. */
  221. func MakeContactsEditPost() http.HandlerFunc {
  222. tpl := MakeTemplate(
  223. "layout",
  224. "edit",
  225. )
  226. return func(w http.ResponseWriter, request *http.Request) {
  227. contact_id, _ := strconv.Atoi(request.PathValue("contact_id"))
  228. c := (&Contact{}).Find(uint64(contact_id))
  229. c.Update(
  230. request.FormValue("first_name"),
  231. request.FormValue("last_name"),
  232. request.FormValue("phone"),
  233. request.FormValue("email"),
  234. )
  235. if c.Save() {
  236. flash("Updated Contact!")
  237. http.Redirect(w, request, fmt.Sprintf("/contacts/%d", contact_id), http.StatusFound)
  238. return
  239. } else {
  240. tpl.Execute(w, anyMap{"contact": c})
  241. }
  242. }
  243. }
  244. /*
  245. @app.route("/contacts/<contact_id>/email", methods=["GET"])
  246. def contacts_email_get(contact_id=0):
  247. */
  248. func ContactsEmailGet(w http.ResponseWriter, r *http.Request) {
  249. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  250. c := (&Contact{}).Find(uint64(contact_id))
  251. c.Email = r.FormValue("email")
  252. c.validate()
  253. // FIXME find what this is really expected to do in the original Python.
  254. for _, err := range c.Errors {
  255. upErr := strings.ToUpper(err.Error())
  256. if strings.Contains(upErr, "EMAIL") {
  257. fmt.Fprintln(w, err)
  258. }
  259. }
  260. }
  261. /*
  262. @app.route("/contacts/<contact_id>", methods=["DELETE"])
  263. def contacts_delete(contact_id=0):
  264. */
  265. func ContactsDelete(w http.ResponseWriter, r *http.Request) {
  266. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  267. contact := (&Contact{}).Find(uint64(contact_id))
  268. contact.Delete()
  269. if r.Header.Get("HX-Trigger") == "delete-btn" {
  270. flash("Deleted Contact!")
  271. http.Redirect(w, r, "/contacts", http.StatusSeeOther)
  272. return
  273. } else {
  274. return
  275. }
  276. }
  277. /*
  278. @app.route("/contacts/", methods=["DELETE"])
  279. def contacts_delete_all():
  280. */
  281. func MakeContactsDeleteAll() http.HandlerFunc {
  282. tpl := MakeTemplate(
  283. "layout",
  284. "index",
  285. "archive_ui",
  286. "rows",
  287. )
  288. return func(w http.ResponseWriter, r *http.Request) {
  289. // Per rfc7231#section-4.3.5 "A payload within a DELETE request message has no defined semantics"
  290. //
  291. // So Go does not parse forms on methods other than POST, PUT and PATCH,
  292. // but this handler is called with DELETE, so we need a manual parse.
  293. if r == nil || r.Body == nil {
  294. http.Error(w, "invalid void request", http.StatusBadRequest)
  295. return
  296. }
  297. body, err := io.ReadAll(r.Body)
  298. if err != nil {
  299. http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusBadRequest)
  300. return
  301. }
  302. values, err := url.ParseQuery(string(body))
  303. if err != nil {
  304. http.Error(w, fmt.Sprintf("parsing form data: %v", err), http.StatusBadRequest)
  305. return
  306. }
  307. contactIDs := values["selected_contact_ids"]
  308. for _, sContactID := range contactIDs {
  309. if contactID, err := strconv.Atoi(sContactID); contactID > 0 && err == nil {
  310. contact := (&Contact{}).Find(uint64(contactID))
  311. contact.Delete()
  312. }
  313. }
  314. flash("Deleted contacts!")
  315. contacts_set := (&Contact{}).All(1)
  316. if err := tpl.Execute(w, anyMap{"contacts": contacts_set}); err != nil {
  317. http.Error(w, err.Error(), http.StatusInternalServerError)
  318. }
  319. }
  320. }
  321. /*
  322. # ===========================================================
  323. # JSON Data API
  324. # ===========================================================
  325. */
  326. /*
  327. @app.route("/api/v1/contacts", methods=["GET"])
  328. def json_contacts():
  329. */
  330. func JSONContacts(w http.ResponseWriter, r *http.Request) {
  331. contacts_set := (&Contact{}).All(1)
  332. enc := json.NewEncoder(w)
  333. if err := enc.Encode(anyMap{"contacts": contacts_set}); err != nil {
  334. http.Error(w, err.Error(), http.StatusInternalServerError)
  335. return
  336. }
  337. }
  338. /*
  339. @app.route("/api/v1/contacts", methods=["POST"])
  340. def json_contacts_new():
  341. */
  342. func JSONContactsNew(w http.ResponseWriter, r *http.Request) {
  343. if r == nil || r.Body == nil {
  344. http.Error(w, "invalid void request", http.StatusBadRequest)
  345. return
  346. }
  347. body, err := io.ReadAll(r.Body)
  348. if err != nil {
  349. http.Error(w, fmt.Sprintf("error reading body: %v", err), http.StatusBadRequest)
  350. return
  351. }
  352. m := make(anyMap, 4)
  353. if err := json.Unmarshal(body, &m); err != nil {
  354. http.Error(w, fmt.Sprintf("parsing JSON request: %v", err), http.StatusBadRequest)
  355. return
  356. }
  357. c := NewContact(anyMap{
  358. "first": m["first_name"],
  359. "last": m["last_name"],
  360. "phone": m["phone"],
  361. "email": m["email"],
  362. })
  363. enc := json.NewEncoder(w)
  364. if c.Save() {
  365. if err := enc.Encode(c); err != nil {
  366. http.Error(w, err.Error(), http.StatusInternalServerError)
  367. return
  368. }
  369. } else {
  370. w.WriteHeader(http.StatusBadRequest)
  371. if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil {
  372. http.Error(w, err.Error(), http.StatusInternalServerError)
  373. return
  374. }
  375. }
  376. }
  377. /*
  378. @app.route("/api/v1/contacts/<contact_id>", methods=["GET"])
  379. def json_contacts_view(contact_id=0):
  380. */
  381. func JSONContactsView(w http.ResponseWriter, r *http.Request) {
  382. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  383. c := (&Contact{}).Find(uint64(contact_id))
  384. enc := json.NewEncoder(w)
  385. if err := enc.Encode(c); err != nil {
  386. http.Error(w, err.Error(), http.StatusInternalServerError)
  387. return
  388. }
  389. }
  390. /*
  391. @app.route("/api/v1/contacts/<contact_id>", methods=["PUT"])
  392. def json_contacts_edit(contact_id):
  393. */
  394. func JSONContactsEdit(w http.ResponseWriter, r *http.Request) {
  395. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  396. if r == nil || r.Body == nil {
  397. http.Error(w, "invalid void request", http.StatusBadRequest)
  398. return
  399. }
  400. body, err := io.ReadAll(r.Body)
  401. if err != nil {
  402. http.Error(w, fmt.Sprintf("error reading body: %v", err), http.StatusBadRequest)
  403. return
  404. }
  405. m := make(map[string]string, 4)
  406. if err := json.Unmarshal(body, &m); err != nil {
  407. http.Error(w, fmt.Sprintf("parsing JSON request: %v", err), http.StatusBadRequest)
  408. return
  409. }
  410. c := (&Contact{}).Find(uint64(contact_id))
  411. c.Update(
  412. m["first_name"],
  413. m["last_name"],
  414. m["phone"],
  415. m["email"],
  416. )
  417. enc := json.NewEncoder(w)
  418. if c.Save() {
  419. if err := enc.Encode(c); err != nil {
  420. http.Error(w, err.Error(), http.StatusInternalServerError)
  421. return
  422. }
  423. } else {
  424. w.WriteHeader(http.StatusBadRequest)
  425. if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil {
  426. http.Error(w, err.Error(), http.StatusInternalServerError)
  427. return
  428. }
  429. }
  430. }
  431. /*
  432. @app.route("/api/v1/contacts/<contact_id>", methods=["DELETE"])
  433. def json_contacts_delete(contact_id=0):
  434. */
  435. func JSONContactsDelete(w http.ResponseWriter, r *http.Request) {
  436. contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
  437. contact := (&Contact{}).Find(uint64(contact_id))
  438. contact.Delete()
  439. enc := json.NewEncoder(w)
  440. _ = enc.Encode(anyMap{"success": true})
  441. }
  442. /*
  443. *
  444. TODO implement me
  445. */
  446. func flash(message string) {
  447. }
  448. func setupRoutes(mux *http.ServeMux, templates *template.Template) {
  449. // OK
  450. mux.Handle("/", RouteFront)
  451. mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
  452. mux.HandleFunc("/contacts", MakeRouteContacts())
  453. mux.Handle("POST /contacts/archive", MakeStartArchive())
  454. mux.Handle("GET /contacts/archive", MakeArchiveStatus())
  455. mux.HandleFunc("GET /contacts/archive/file", ArchiveContent)
  456. mux.Handle("DELETE /contacts/archive", MakeResetArchive())
  457. mux.HandleFunc("GET /contacts/count", ContactsCount)
  458. mux.Handle("GET /contacts/new", MakeContactsNewGet())
  459. mux.Handle("POST /contacts/new", MakeContactsNew())
  460. mux.Handle("GET /contacts/{contact_id}", MakeContactsView())
  461. mux.Handle("GET /contacts/{contact_id}/edit", MakeContactsEditGet())
  462. mux.Handle("POST /contacts/{contact_id}/edit", MakeContactsEditPost())
  463. mux.HandleFunc("GET /contacts/{contact_id}/email", ContactsEmailGet)
  464. mux.HandleFunc("DELETE /contacts/{contact_id}", ContactsDelete)
  465. mux.Handle("DELETE /contacts/", MakeContactsDeleteAll())
  466. mux.HandleFunc("GET /api/v1/contacts", JSONContacts)
  467. mux.HandleFunc("POST /api/v1/contacts", JSONContactsNew)
  468. mux.HandleFunc("GET /api/v1/contacts/{contact_id}", JSONContactsView)
  469. mux.HandleFunc("PUT /api/v1/contacts/{contact_id}", JSONContactsEdit)
  470. mux.HandleFunc("DELETE /api/v1/contacts/{contact_id}", JSONContactsDelete)
  471. }
  472. func main() {
  473. mux := http.NewServeMux()
  474. templates := template.Must(
  475. template.
  476. New("htmx-app").
  477. Funcs(sprig.FuncMap()). // Arithmetic, etc.
  478. Funcs(template.FuncMap{
  479. "get_flashed_messages": func() string { return "some message" },
  480. }).
  481. ParseGlob("./templates/*.gohtml"),
  482. )
  483. setupRoutes(mux, templates)
  484. (&Contact{}).LoadDB()
  485. if err := http.ListenAndServe(":8080", mux); err != nil {
  486. if !errors.Is(err, http.ErrServerClosed) {
  487. log.Printf("Server error: %v\n", err)
  488. return
  489. }
  490. }
  491. log.Println("Server shutdown")
  492. }