about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGriffin Smith <grfn@gws.fyi>2021-12-20T01·17-0500
committerclbot <clbot@tvl.fyi>2021-12-20T01·23+0000
commit4643585e01e5dc68d6a815be23017ffe396a107a (patch)
tree5e166226e5e2a1ee553f79fe3bb0f3d572bea87e
parent4ad94b9cf82177975b04f5de23ddaba35b2f6683 (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>
-rw-r--r--users/grfn/bbbg/deps.edn4
-rw-r--r--users/grfn/bbbg/deps.nix26
-rw-r--r--users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql1
-rw-r--r--users/grfn/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql7
-rw-r--r--users/grfn/bbbg/src/bbbg/attendee_check.clj4
-rw-r--r--users/grfn/bbbg/src/bbbg/db.clj4
-rw-r--r--users/grfn/bbbg/src/bbbg/db/attendee.clj3
-rw-r--r--users/grfn/bbbg/src/bbbg/db/attendee_check.clj46
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/attendee_checks.clj55
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/attendees.clj22
-rw-r--r--users/grfn/bbbg/src/bbbg/util/display.clj17
-rw-r--r--users/grfn/bbbg/src/bbbg/util/time.clj149
-rw-r--r--users/grfn/bbbg/src/bbbg/web.clj2
13 files changed, 333 insertions, 7 deletions
diff --git a/users/grfn/bbbg/deps.edn b/users/grfn/bbbg/deps.edn
index c1a05c7a7c..433d0c64f0 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 9110ccb9ae..8bf2ad5ab8 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 0000000000..936abf6c7d
--- /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 0000000000..5e82dcb171
--- /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 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)))