app.go 13 KB

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