diff options
author | Griffin Smith <grfn@gws.fyi> | 2021-12-20T01·17-0500 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2021-12-20T01·23+0000 |
commit | 4643585e01e5dc68d6a815be23017ffe396a107a (patch) | |
tree | 5e166226e5e2a1ee553f79fe3bb0f3d572bea87e /users | |
parent | 4ad94b9cf82177975b04f5de23ddaba35b2f6683 (diff) |
feat(grfn/bbbg): Add attendee checks r/3314
Change-Id: I7f96597ab3f0552cdecd0abac1ef50a68d3e0b7b Reviewed-on: https://cl.tvl.fyi/c/depot/+/4508 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi> Autosubmit: grfn <grfn@gws.fyi>
Diffstat (limited to 'users')
-rw-r--r-- | users/grfn/bbbg/deps.edn | 4 | ||||
-rw-r--r-- | users/grfn/bbbg/deps.nix | 26 | ||||
-rw-r--r-- | users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql | 1 | ||||
-rw-r--r-- | users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql | 7 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/attendee_check.clj | 4 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/db.clj | 4 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/db/attendee.clj | 3 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/db/attendee_check.clj | 46 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj | 55 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/handlers/attendees.clj | 22 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/util/display.clj | 17 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/util/time.clj | 149 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/web.clj | 2 |
13 files changed, 333 insertions, 7 deletions
diff --git a/users/grfn/bbbg/deps.edn b/users/grfn/bbbg/deps.edn index c1a05c7a7c90..433d0c64f01b 100644 --- a/users/grfn/bbbg/deps.edn +++ b/users/grfn/bbbg/deps.edn @@ -40,11 +40,13 @@ yogthos/config {:mvn/version "1.1.8"} clojure.java-time/clojure.java-time {:mvn/version "0.3.3"} cheshire/cheshire {:mvn/version "5.10.1"} + org.apache.commons/commons-lang3 {:mvn/version "3.11"} ;; Spec org.clojure/spec.alpha {:mvn/version "0.3.214"} org.clojure/core.specs.alpha {:mvn/version "0.2.62"} - expound/expound {:mvn/version "0.8.10"}} + expound/expound {:mvn/version "0.8.10"} + org.clojure/test.check {:mvn/version "1.1.0"}} :paths ["src" diff --git a/users/grfn/bbbg/deps.nix b/users/grfn/bbbg/deps.nix index 9110ccb9ae05..8bf2ad5ab839 100644 --- a/users/grfn/bbbg/deps.nix +++ b/users/grfn/bbbg/deps.nix @@ -120,6 +120,19 @@ let repos = [ } rec { + name = "commons-lang3/org.apache.commons"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-lang3"; + groupId = "org.apache.commons"; + sha512 = "c1f6b5cb9ac47cfb612423a71b347568f3697cf88018b5808678be5234c50b22888db23cb833b7d8d458d39707ab9e4d839107d1d3306de2e4e422010c95180f"; + version = "3.11"; + + }; + paths = [ src ]; + } + + rec { name = "tools.logging/org.clojure"; src = fetchMavenArtifact { inherit repos; @@ -1199,6 +1212,19 @@ let repos = [ } rec { + name = "test.check/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "test.check"; + groupId = "org.clojure"; + sha512 = "68caa189e7292da5dfde92d795ce35ff1980108a579dc11ca618bf0e480101b8ded16fc8ab816b30f364afbccbb8f02fc9194271e7f5323b8042d468164ecb64"; + version = "1.1.0"; + + }; + paths = [ src ]; + } + + rec { name = "ring-servlet/ring"; src = fetchMavenArtifact { inherit repos; diff --git a/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql new file mode 100644 index 000000000000..936abf6c7d19 --- /dev/null +++ b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql @@ -0,0 +1 @@ +DROP TABLE "attendee_check"; diff --git a/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql new file mode 100644 index 000000000000..5e82dcb1711c --- /dev/null +++ b/users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE attendee_check ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "attendee_id" UUID NOT NULL REFERENCES attendee ("id"), + "user_id" UUID NOT NULL REFERENCES "public"."user" ("id"), + "last_dose_at" DATE, + "checked_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); 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 000000000000..f34c41198e66 --- /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 c5820f982bb9..f10cae3b8f04 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 c8b1df613f94..06f495c57e3d 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 000000000000..66090b16c603 --- /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 000000000000..f04b94ba3037 --- /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 3a5b49900476..366f4b0973e3 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 000000000000..79bd9808874d --- /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 000000000000..46506fe32b51 --- /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 21e70d1470ad..4501043e2f5f 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))) |