From 4643585e01e5dc68d6a815be23017ffe396a107a Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Sun, 19 Dec 2021 20:17:27 -0500 Subject: feat(grfn/bbbg): Add attendee checks Change-Id: I7f96597ab3f0552cdecd0abac1ef50a68d3e0b7b Reviewed-on: https://cl.tvl.fyi/c/depot/+/4508 Tested-by: BuildkiteCI Reviewed-by: grfn Autosubmit: grfn --- users/grfn/bbbg/src/bbbg/attendee_check.clj | 4 + users/grfn/bbbg/src/bbbg/db.clj | 4 +- users/grfn/bbbg/src/bbbg/db/attendee.clj | 3 +- users/grfn/bbbg/src/bbbg/db/attendee_check.clj | 46 +++++++ .../bbbg/src/bbbg/handlers/attendee_checks.clj | 55 ++++++++ users/grfn/bbbg/src/bbbg/handlers/attendees.clj | 22 ++- users/grfn/bbbg/src/bbbg/util/display.clj | 17 +++ users/grfn/bbbg/src/bbbg/util/time.clj | 149 +++++++++++++++++++++ users/grfn/bbbg/src/bbbg/web.clj | 2 + 9 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 users/grfn/bbbg/src/bbbg/attendee_check.clj create mode 100644 users/grfn/bbbg/src/bbbg/db/attendee_check.clj create mode 100644 users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj create mode 100644 users/grfn/bbbg/src/bbbg/util/display.clj create mode 100644 users/grfn/bbbg/src/bbbg/util/time.clj (limited to 'users/grfn/bbbg/src') diff --git a/users/grfn/bbbg/src/bbbg/attendee_check.clj b/users/grfn/bbbg/src/bbbg/attendee_check.clj new file mode 100644 index 0000000000..f34c41198e --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/attendee_check.clj @@ -0,0 +1,4 @@ +(ns bbbg.attendee-check + (:require [clojure.spec.alpha :as s])) + +(s/def ::id uuid?) diff --git a/users/grfn/bbbg/src/bbbg/db.clj b/users/grfn/bbbg/src/bbbg/db.clj index c5820f982b..f10cae3b8f 100644 --- a/users/grfn/bbbg/src/bbbg/db.clj +++ b/users/grfn/bbbg/src/bbbg/db.clj @@ -355,8 +355,6 @@ (comment (def db (:db bbbg.core/system)) - (generate-migration db "init-schema") + (generate-migration db "add-attendee-checks") (migrate! db) - - ) diff --git a/users/grfn/bbbg/src/bbbg/db/attendee.clj b/users/grfn/bbbg/src/bbbg/db/attendee.clj index c8b1df613f..06f495c57e 100644 --- a/users/grfn/bbbg/src/bbbg/db/attendee.clj +++ b/users/grfn/bbbg/src/bbbg/db/attendee.clj @@ -55,5 +55,6 @@ (db/list db (with-stats)) - (map? db) + (db/insert! db :attendee {::attendee/meetup-name "Rando Guy" + ::attendee/discord-name "rando"}) ) diff --git a/users/grfn/bbbg/src/bbbg/db/attendee_check.clj b/users/grfn/bbbg/src/bbbg/db/attendee_check.clj new file mode 100644 index 0000000000..66090b16c6 --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/db/attendee_check.clj @@ -0,0 +1,46 @@ +(ns bbbg.db.attendee-check + (:require + [bbbg.attendee :as attendee] + [bbbg.attendee-check :as attendee-check] + [bbbg.db :as db] + [bbbg.user :as user] + [bbbg.util.core :as u])) + +(defn attendees-with-last-checks + [db attendees] + (let [ids (map ::attendee/id attendees) + checks + (db/list db {:select [:attendee-check.*] + :from [:attendee-check] + :join [[{:select [:%max.attendee-check.checked-at + :attendee-check.attendee-id] + :from [:attendee-check] + :group-by [:attendee-check.attendee-id] + :where [:in :attendee-check.attendee-id ids]} + :last-check] + [:= + :attendee-check.attendee-id + :last-check.attendee-id]]}) + users (u/key-by + ::user/id + (db/list db {:select [:public.user.*] + :from [:public.user] + :where [:in :id (map ::user/id checks)]})) + checks (map #(assoc % :user (users (::user/id %))) checks) + attendee-id->check (u/key-by ::attendee/id checks)] + (map #(assoc % :last-check (attendee-id->check (::attendee/id %))) + attendees))) + +(comment + (def db (:db bbbg.core/system)) + + (attendees-with-last-checks + db + (db/list db :attendee) + ) + + (db/insert! db :attendee-check + {::attendee/id #uuid "58bcd372-ff6e-49df-b280-23d24c5ba0f0" + ::user/id #uuid "303fb606-5ef0-4682-ad7d-6429c670cd78" + ::attendee-check/last-dose-at "2021-12-19"}) + ) diff --git a/users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj b/users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj new file mode 100644 index 0000000000..f04b94ba30 --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj @@ -0,0 +1,55 @@ +(ns bbbg.handlers.attendee-checks + (:require + [bbbg.attendee :as attendee] + [bbbg.db :as db] + [bbbg.handlers.core :refer [page-response wrap-auth-required]] + [bbbg.util.display :refer [format-date]] + [compojure.coercions :refer [as-uuid]] + [compojure.core :refer [context GET POST]] + [ring.util.response :refer [not-found]] + [bbbg.attendee-check :as attendee-check] + [bbbg.user :as user])) + +(defn- edit-attendee-checks-page [{:keys [existing-check] + attendee-id ::attendee/id}] + [:div + (when existing-check + [:p + "Already checked on " + (-> existing-check ::attendee-check/checked-at format-date) + " by " + (::user/username existing-check)]) + [:form {:method :post + :action (str "/attendees/" attendee-id "/checks")} + [:div.form-group + [:label + "Last Dose" + [:input {:type :date + :name :last-dose-at}]]] + [:div.form-group + [:input {:type :submit + :value "Mark Checked"}]]]]) + +(defn attendee-checks-routes [{:keys [db]}] + (wrap-auth-required + (context "/attendees/:attendee-id/checks" [attendee-id :<< as-uuid] + (GET "/edit" [] + (if (db/exists? db {:select [1] + :from [:attendee] + :where [:= :id attendee-id]}) + (let [existing-check (db/fetch + db + {:select [:attendee-check.* + :public.user.*] + :from [:attendee-check] + :join [:public.user + [:= + :attendee-check.user-id + :public.user.id]] + :where [:= :attendee-id attendee-id]})] + (page-response + (edit-attendee-checks-page + {:existing-check existing-check + ::attendee/id attendee-id}))) + (not-found "Attendee not found"))) + (POST "/" [])))) diff --git a/users/grfn/bbbg/src/bbbg/handlers/attendees.clj b/users/grfn/bbbg/src/bbbg/handlers/attendees.clj index 3a5b499004..366f4b0973 100644 --- a/users/grfn/bbbg/src/bbbg/handlers/attendees.clj +++ b/users/grfn/bbbg/src/bbbg/handlers/attendees.clj @@ -1,18 +1,23 @@ (ns bbbg.handlers.attendees (:require [bbbg.attendee :as attendee] + [bbbg.attendee-check :as attendee-check] [bbbg.db :as db] [bbbg.db.attendee :as db.attendee] + [bbbg.db.attendee-check :as db.attendee-check] [bbbg.db.event :as db.event] [bbbg.event :as event] [bbbg.handlers.core :refer [page-response wrap-auth-required]] + [bbbg.user :as user] + [bbbg.util.display :refer [format-date]] [bbbg.views.flash :as flash] [cheshire.core :as json] [compojure.coercions :refer [as-uuid]] [compojure.core :refer [GET POST routes]] [honeysql.helpers :refer [merge-where]] - [ring.util.response :refer [content-type redirect response not-found]]) - (:import java.util.UUID)) + [ring.util.response :refer [content-type not-found redirect response]]) + (:import + java.util.UUID)) (defn- attendees-page [{:keys [attendees q edit-notes]}] [:div @@ -30,6 +35,7 @@ [:th "Events RSVPd"] [:th "Events Attended"] [:th "No-Shows"] + [:th "Last Vaccination Check"] [:th "Notes"]]] [:tbody (for [attendee attendees @@ -40,6 +46,15 @@ [:td (:events-rsvpd attendee)] [:td (:events-attended attendee)] [:td (:no-shows attendee)] + (if-let [last-check (:last-check attendee)] + [:td (str (-> last-check + ::attendee-check/checked-at + format-date) + ", by " + (get-in last-check [:user ::user/username]))] + [:td "Not Checked" + [:a {:href (str "/attendees/" id "/checks/edit")} + "Edit"]]) (if (= edit-notes id) [:td [:form.organizer-notes {:method :post @@ -59,6 +74,9 @@ (GET "/attendees" [q edit-notes] (let [attendees (db/list db (cond-> (db.attendee/with-stats) q (db.attendee/search q))) + attendees (db.attendee-check/attendees-with-last-checks + db + attendees) edit-notes (some-> edit-notes UUID/fromString)] (page-response (attendees-page {:attendees attendees :q q diff --git a/users/grfn/bbbg/src/bbbg/util/display.clj b/users/grfn/bbbg/src/bbbg/util/display.clj new file mode 100644 index 0000000000..79bd980887 --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/util/display.clj @@ -0,0 +1,17 @@ +(ns bbbg.util.display + (:require + [bbbg.util.time :as t]) + (:import + [java.time.format DateTimeFormatter FormatStyle])) + +(defn format-date + ([d] (format-date d FormatStyle/MEDIUM)) + ([d ^FormatStyle format-style] + (let [formatter (DateTimeFormatter/ofLocalizedDate format-style)] + (.format (t/->LocalDate d) formatter)))) + +(comment + (format-date #inst "2021-12-19T05:00:00.000-00:00") + (format-date #inst "2021-12-19T05:00:00.000-00:00" + FormatStyle/SHORT) + ) diff --git a/users/grfn/bbbg/src/bbbg/util/time.clj b/users/grfn/bbbg/src/bbbg/util/time.clj new file mode 100644 index 0000000000..46506fe32b --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/util/time.clj @@ -0,0 +1,149 @@ +(ns bbbg.util.time + "Utilities for dealing with date/time" + (:require [clojure.spec.alpha :as s] + [clojure.test.check.generators :as gen] + [java-time :as jt]) + (:import [java.time + LocalDateTime LocalTime OffsetDateTime ZoneId ZoneOffset + LocalDate Year] + [java.time.format DateTimeFormatter DateTimeParseException] + java.util.Calendar + org.apache.commons.lang3.time.DurationFormatUtils)) + +(set! *warn-on-reflection* true) + +(defprotocol ToOffsetDateTime + (->OffsetDateTime [this] + "Coerces its argument to a `java.time.OffsetDateTime`")) + +(extend-protocol ToOffsetDateTime + OffsetDateTime + (->OffsetDateTime [odt] odt) + + java.util.Date + (->OffsetDateTime [d] + (-> d + .toInstant + (OffsetDateTime/ofInstant (ZoneId/of "UTC"))))) + +(defprotocol ToLocalTime (->LocalTime [this])) +(extend-protocol ToLocalTime + LocalTime + (->LocalTime [lt] lt) + + java.sql.Time + (->LocalTime [t] + (let [^Calendar cal (doto (Calendar/getInstance) + (.setTime t))] + (LocalTime/of + (.get cal Calendar/HOUR_OF_DAY) + (.get cal Calendar/MINUTE) + (.get cal Calendar/SECOND)))) + + java.util.Date + (->LocalTime [d] + (-> d .toInstant (LocalTime/ofInstant (ZoneId/of "UTC"))))) + +(defn local-time? [x] (satisfies? ToLocalTime x)) +(s/def ::local-time + (s/with-gen local-time? + #(gen/let [hour (gen/choose 0 23) + minute (gen/choose 0 59) + second (gen/choose 0 59) + nanos gen/nat] + (LocalTime/of hour minute second nanos)))) + +(defprotocol ToLocalDate (->LocalDate [this])) +(extend-protocol ToLocalDate + LocalDate + (->LocalDate [ld] ld) + + java.util.Date + (->LocalDate [d] + (-> d .toInstant (LocalDate/ofInstant (ZoneId/of "UTC"))))) + +(defn local-date? [x] (satisfies? ToLocalDate x)) +(s/def ::local-date + (s/with-gen local-date? + #(gen/let [year (gen/choose Year/MIN_VALUE Year/MAX_VALUE) + day (gen/choose 1 (if (.isLeap (Year/of year)) + 366 + 365))] + (LocalDate/ofYearDay year day)))) + +(extend-protocol Inst + OffsetDateTime + (inst-ms* [zdt] + (inst-ms* (.toInstant zdt))) + + LocalDateTime + (inst-ms* [^LocalDateTime ldt] + (inst-ms* (.toInstant ldt ZoneOffset/UTC)))) + +(let [formatter DateTimeFormatter/ISO_OFFSET_DATE_TIME] + (defn ^OffsetDateTime parse-iso-8601 + "Parse s as an iso-8601 datetime, returning nil if invalid" + [^String s] + (try + (OffsetDateTime/parse s formatter) + (catch DateTimeParseException _ nil))) + + (defn format-iso-8601 + "Format dt, which can be an OffsetDateTime or java.util.Date, as iso-8601" + [dt] + (some->> dt ->OffsetDateTime (.format formatter)))) + +(let [formatter DateTimeFormatter/ISO_TIME] + (defn parse-iso-8601-time + "Parse s as an iso-8601 timestamp, returning nil if invalid" + [^String s] + (try + (LocalTime/parse s formatter) + (catch DateTimeParseException _ nil))) + + (defn format-iso-8601-time + "Format lt, which can be a LocalTime or java.sql.Time, as an iso-8601 + formatted timestamp without a date." + [lt] + (some->> lt ->LocalTime (.format formatter)))) + +(defmethod print-dup LocalTime [t w] + (binding [*out* w] + (print "#local-time ") + (print (str "\"" (format-iso-8601-time t) "\"")))) + +(defmethod print-method LocalTime [t w] + (print-dup t w)) + +(let [formatter DateTimeFormatter/ISO_LOCAL_DATE] + (defn parse-iso-8601-date + "Parse s as an iso-8601 date, returning nil if invalid" + [^String s] + (try + (LocalDate/parse s formatter) + (catch DateTimeParseException _ nil))) + + (defn format-iso-8601-date + "Format lt, which can be a LocalDate, as an iso-8601 formatted date without + a timestamp." + [lt] + (some->> lt ->LocalDate (.format formatter)))) + +(defmethod print-dup LocalDate [t w] + (binding [*out* w] + (print "#local-date ") + (print (str "\"" (format-iso-8601-date t) "\"")))) + +(defmethod print-method LocalDate [t w] + (print-dup t w)) + + +(defn ^String human-format-duration + "Human-format the given duration" + [^java.time.Duration dur] + (DurationFormatUtils/formatDurationWords (Math/abs (.toMillis dur)) true true)) + +(comment + (human-format-duration (jt/hours 5)) + (human-format-duration (jt/plus (jt/hours 5) (jt/minutes 7))) + ) diff --git a/users/grfn/bbbg/src/bbbg/web.clj b/users/grfn/bbbg/src/bbbg/web.clj index 21e70d1470..4501043e2f 100644 --- a/users/grfn/bbbg/src/bbbg/web.clj +++ b/users/grfn/bbbg/src/bbbg/web.clj @@ -1,6 +1,7 @@ (ns bbbg.web (:require [bbbg.discord.auth :as discord.auth :refer [wrap-discord-auth]] + [bbbg.handlers.attendee-checks :as attendee-checks] [bbbg.handlers.attendees :as attendees] [bbbg.handlers.events :as events] [bbbg.handlers.home :as home] @@ -74,6 +75,7 @@ (content-type "text/javascript"))) (attendees/attendees-routes env) + (attendee-checks/attendee-checks-routes env) (signup-form/signup-form-routes env) (events/events-routes env) (home/home-routes env))) -- cgit 1.4.1