diff options
Diffstat (limited to 'users/aspen/bbbg/src/bbbg/handlers')
-rw-r--r-- | users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj | 68 | ||||
-rw-r--r-- | users/aspen/bbbg/src/bbbg/handlers/attendees.clj | 162 | ||||
-rw-r--r-- | users/aspen/bbbg/src/bbbg/handlers/core.clj | 91 | ||||
-rw-r--r-- | users/aspen/bbbg/src/bbbg/handlers/events.clj | 259 | ||||
-rw-r--r-- | users/aspen/bbbg/src/bbbg/handlers/home.clj | 52 | ||||
-rw-r--r-- | users/aspen/bbbg/src/bbbg/handlers/signup_form.clj | 93 |
6 files changed, 725 insertions, 0 deletions
diff --git a/users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj b/users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj new file mode 100644 index 000000000000..d7307c40673b --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj @@ -0,0 +1,68 @@ +(ns bbbg.handlers.attendee-checks + (:require + [bbbg.attendee :as attendee] + [bbbg.attendee-check :as attendee-check] + [bbbg.db :as db] + [bbbg.db.attendee-check :as db.attendee-check] + [bbbg.handlers.core :refer [page-response wrap-auth-required]] + [bbbg.user :as user] + [bbbg.util.display :refer [format-date]] + [compojure.coercions :refer [as-uuid]] + [compojure.core :refer [context GET POST]] + [ring.util.response :refer [not-found redirect]] + [bbbg.views.flash :as flash])) + +(defn- edit-attendee-checks-page [{:keys [existing-check] + attendee-id ::attendee/id}] + [:div.page + (when existing-check + [:p + "Already checked on " + (-> existing-check ::attendee-check/checked-at format-date) + " by " + (::user/username existing-check)]) + [:form.attendee-checks-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 "/" {{:keys [last-dose-at]} :params + {user-id ::user/id} :session} + (db.attendee-check/create! + db + {::attendee/id attendee-id + ::user/id user-id + ::attendee-check/last-dose-at last-dose-at}) + (-> (redirect "/attendees") + (flash/add-flash + #:flash{:type :success + :message "Successfully updated vaccination status"})))))) diff --git a/users/aspen/bbbg/src/bbbg/handlers/attendees.clj b/users/aspen/bbbg/src/bbbg/handlers/attendees.clj new file mode 100644 index 000000000000..ce84b88e97c1 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/attendees.clj @@ -0,0 +1,162 @@ +(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 not-found redirect response]]) + (:import + java.util.UUID)) + +(defn- attendees-page [{:keys [attendees q edit-notes]}] + [:div.page + [:form.search-form {:method :get :action "/attendees"} + [:input.search-input + {:type "search" + :name "q" + :value q + :title "Search Attendees"}] + [:input {:type "submit" + :value "Search Attendees"}]] + [:table.attendees + [:thead + [:tr + [:th "Meetup Name"] + [:th "Discord Name"] + [:th "Events RSVPd"] + [:th "Events Attended"] + [:th "No-Shows"] + [:th "Last Vaccination Check"] + [:th "Notes"]]] + [:tbody + (for [attendee (sort-by + (comp #{edit-notes} ::attendee/id) + (comp - compare) + attendees) + :let [id (::attendee/id attendee)]] + [:tr + [:td.attendee-name (::attendee/meetup-name attendee)] + [:td + [:label.mobile-label "Discord Name: "] + (or (not-empty (::attendee/discord-name attendee)) + "—")] + [:td + [:label.mobile-label "Events RSVPd: "] + (:events-rsvpd attendee)] + [:td + [:label.mobile-label "Events Attended: "] + (:events-attended attendee)] + [:td + [:label.mobile-label "No-shows: "] + (:no-shows attendee)] + [:td + [:label.mobile-label "Last Vaccination Check: "] + (if-let [last-check (:last-check attendee)] + (str "✔️ "(-> last-check + ::attendee-check/checked-at + format-date) + ", by " + (get-in last-check [:user ::user/username])) + (list + [:span {:title "Not Checked"} + "❌"] + " " + [:a {:href (str "/attendees/" id "/checks/edit")} + "Edit"] ))] + (if (= edit-notes id) + [:td + [:form.organizer-notes {:method :post + :action (str "/attendees/" id "/notes")} + [:div.form-group + [:input {:type :text :name "notes" + :value (::attendee/organizer-notes attendee) + :autofocus true}]] + [:div.form-group + [:input {:type "Submit" :value "Save Notes"}]]]] + [:td + [:p + (::attendee/organizer-notes attendee)] + [:p + [:a {:href (str "/attendees?edit-notes=" id)} + "Edit Notes"]]])])]]]) + +(defn attendees-routes [{:keys [db]}] + (routes + (wrap-auth-required + (routes + (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 + :edit-notes edit-notes})))) + + (POST "/attendees/:id/notes" [id :<< as-uuid notes] + (if (seq (db/update! db + :attendee + {::attendee/organizer-notes notes} + [:= :id id])) + (-> (redirect "/attendees") + (flash/add-flash + #:flash{:type :success + :message "Notes updated successfully"})) + (not-found "Attendee not found"))))) + + (GET "/attendees.json" [q event_id attended] + (let [results + (db/list + db + (cond-> + (if q + (db.attendee/search q) + {:select [:attendee.*] :from [:attendee]}) + event_id (db.attendee/for-event event_id) + (some? attended) + (merge-where + (case attended + "true" :attended + "false" [:or [:= :attended nil] [:not :attended]]))))] + (-> {:results results} + json/generate-string + response + (content-type "application/json")))) + + (POST "/event_attendees" [event_id attendee_id] + (if (and (db/exists? db {:select [:id] :from [:event] :where [:= :id event_id]}) + (db/exists? db {:select [:id] :from [:attendee] :where [:= :id attendee_id]})) + (do + (db.event/attended! db {::event/id event_id + ::attendee/id attendee_id}) + (-> (redirect (str "/signup-forms/" event_id)) + (flash/add-flash + #:flash{:type :success + :message "Thank you for signing in! Enjoy the event."}))) + (response "Something went wrong"))))) + +(comment + (def db (:db bbbg.core/system)) + (db/list db :attendee) + (db/list db + (-> + (db.attendee/search "gr") + (db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf"))) + (honeysql.format/format + (-> + (db.attendee/search "gr") + (db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf"))) + ) diff --git a/users/aspen/bbbg/src/bbbg/handlers/core.clj b/users/aspen/bbbg/src/bbbg/handlers/core.clj new file mode 100644 index 000000000000..caa679ee873f --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/core.clj @@ -0,0 +1,91 @@ +(ns bbbg.handlers.core + (:require + [bbbg.user :as user] + [bbbg.views.flash :as flash] + [hiccup.core :refer [html]] + [ring.util.response :refer [content-type response]] + [clojure.string :as str])) + +(def ^:dynamic *authenticated?* false) + +(defn authenticated? [request] + (some? (get-in request [:session ::user/id]))) + +(defn wrap-auth-required [handler] + (fn [req] + (when (authenticated? req) + (handler req)))) + +(defn wrap-dynamic-auth [handler] + (fn [req] + (binding [*authenticated?* (authenticated? req)] + (handler req)))) + +(def ^:dynamic *current-uri*) + +(defn wrap-current-uri [handler] + (fn [req] + (binding [*current-uri* (:uri req)] + (handler req)))) + +(defn nav-item [href label] + (let [active? + (when *current-uri* + (str/starts-with? + *current-uri* + href))] + [:li {:class (when active? "active")} + [:a {:href href} + label]])) + +(defn global-nav [] + [:nav.global-nav + [:ul + (nav-item "/events" "Events") + (when *authenticated?* + (nav-item "/attendees" "Attendees")) + [:li.spacer] + [:li + (if *authenticated?* + [:form.link-form + {:method :post + :action "/auth/sign-out"} + [:input {:type "submit" + :value "Sign Out"}]] + [:a {:href "/auth/discord"} + "Sign In"])]]]) + +(defn render-page [opts & body] + (let [[{:keys [title]} body] + (if (map? opts) + [opts body] + [{} (concat [opts] body)])] + (html + [:html {:lang "en"} + [:head + [:meta {:charset "UTF-8"}] + [:meta {:name "viewport" + :content "width=device-width,initial-scale=1"}] + [:title (if title + (str title " - BBBG") + "BBBG")] + [:link {:rel "stylesheet" + :type "text/css" + :href "/main.css"}]] + [:body + [:div.content + (global-nav) + #_(flash/render-flash flash/test-flash) + (flash/render-flash) + body] + [:script {:src "/main.js"}]]]))) + +(defn page-response [& render-page-args] + (-> (apply render-page render-page-args) + response + (content-type "text/html"))) + +(comment + (render-page + [:h1 "hi"]) + ) diff --git a/users/aspen/bbbg/src/bbbg/handlers/events.clj b/users/aspen/bbbg/src/bbbg/handlers/events.clj new file mode 100644 index 000000000000..6f6d6f3585ae --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/events.clj @@ -0,0 +1,259 @@ +(ns bbbg.handlers.events + (:require + [bbbg.db :as db] + [bbbg.db.attendee :as db.attendee] + [bbbg.db.event :as db.event] + [bbbg.event :as event] + [bbbg.handlers.core :refer [*authenticated?* page-response]] + [bbbg.meetup.import :refer [import-attendees!]] + [bbbg.util.display :refer [format-date pluralize]] + [bbbg.util.time :as t] + [bbbg.views.flash :as flash] + [compojure.coercions :refer [as-uuid]] + [compojure.core :refer [context GET POST]] + [java-time :refer [local-date]] + [ring.util.response :refer [not-found redirect]] + [bbbg.attendee :as attendee] + [bbbg.event-attendee :as event-attendee] + [bbbg.db.attendee-check :as db.attendee-check] + [bbbg.attendee-check :as attendee-check] + [bbbg.user :as user]) + (:import + java.time.format.FormatStyle)) + +(defn- num-attendees [event] + (str + (:num-attendees event) + (if (= (t/->LocalDate (::event/date event)) + (local-date)) + " Signed In" + (str " Attendee" (when-not (= 1 (:num-attendees event)) "s"))))) + +(def index-type->label + {:upcoming "Upcoming" + :past "Past"}) +(def other-index-type + {:upcoming :past + :past :upcoming}) + +(defn events-index + [{:keys [events num-events type]}] + [:div.page + [:div.page-header + [:h1 + (pluralize + num-events + (str (index-type->label type) " Event"))] + [:a {:href (str "/events" + (when (= :upcoming type) + "/past"))} + "View " + (index-type->label (other-index-type type)) + " Events"]] + (when *authenticated?* + [:a.button {:href "/events/new"} + "Create New Event"]) + [:ul.events-list + (for [event (sort-by + ::event/date + (comp - compare) + events)] + [:li + [:p + [:a {:href (str "/events/" (::event/id event))} + (format-date (::event/date event) + FormatStyle/FULL)]] + [:p + (pluralize (:num-rsvps event) "RSVP") + ", " + (num-attendees event)]])]]) + +(defn- import-attendee-list-form-group [] + [:div.form-group + [:label "Import Attendee List" + [:br] + [:input {:type :file + :name :attendees}]]]) + +(defn import-attendees-form [event] + [:form {:method :post + :action (str "/events/" (::event/id event) "/attendees") + :enctype "multipart/form-data"} + (import-attendee-list-form-group) + [:div.form-group + [:input {:type :submit + :value "Import"}]]]) + +(defn event-page [{:keys [event attendees]}] + [:div.page + [:div.page-header + [:h1 (format-date (::event/date event) + FormatStyle/FULL)] + [:div.spacer] + [:a.button {:href (str "/signup-forms/" (::event/id event) )} + "Go to Signup Form"] + [:form#delete-event + {:method :post + :action (str "/events/" (::event/id event) "/delete") + :data-confirm "Are you sure you want to delete this event?"} + [:input.error {:type "submit" + :value "Delete Event"}]]] + [:div.stats + [:p (pluralize (:num-rsvps event) "RSVP")] + [:p (num-attendees event)]] + [:div + (import-attendees-form event)] + [:div + [:table.attendees + [:thead + [:th "Meetup Name"] + [:th "Discord Name"] + [:th "RSVP"] + [:th "Signed In"] + [:th "Last Vaccination Check"]] + [:tbody + (for [attendee (sort-by (juxt (comp not ::event-attendee/rsvpd-attending?) + (comp not ::event-attendee/attended?) + (comp some? :last-check) + ::attendee/meetup-name) + attendees)] + [:tr + [:td.attendee-name (::attendee/meetup-name attendee)] + [:td + [:label.mobile-label "Discord Name: "] + (or (not-empty (::attendee/discord-name attendee)) + "—")] + [:td + [:label.mobile-label "RSVP: "] + (if (::event-attendee/rsvpd-attending? attendee) + [:span {:title "Yes"} "✔️"] + [:span {:title "No"} "❌"])] + [:td + [:label.mobile-label "Signed In: "] + (if (::event-attendee/attended? attendee) + [:span {:title "Yes"} "✔️"] + [:span {:title "No"} "❌"])] + [:td + [:label.mobile-label "Last Vaccination Check: "] + (if-let [last-check (:last-check attendee)] + (str "✔️ "(-> last-check + ::attendee-check/checked-at + format-date) + ", by " + (get-in last-check [:user ::user/username])) + (list + [:span {:title "Not Checked"} + "❌"] + " " + [:a {:href (str "/attendees/" + (::attendee/id attendee) + "/checks/edit")} + "Edit"]))]])]]]]) + +(defn import-attendees-page [{:keys [event]}] + [:div.page + [:h1 "Import Attendees for " (format-date (::event/date event))] + (import-attendees-form event)]) + +(defn event-form + ([] (event-form {})) + ([event] + [:div.page + [:div.page-header + [:h1 "Create New Event"]] + [:form {:method "POST" + :action "/events" + :enctype "multipart/form-data"} + [:div.form-group + [:label "Date" + [:input {:type "date" + :id "date" + :name "date" + :value (str (::event/date event))}]]] + (import-attendee-list-form-group) + [:div.form-group + [:input {:type "submit" + :value "Create Event"}]]]])) + +(defn- events-list-handler [db query type] + (let [events (db/list db (db.event/with-stats query)) + num-events (db/count db query)] + (page-response + (events-index {:events events + :num-events num-events + :type type})))) + +(defn events-routes [{:keys [db]}] + (context "/events" [] + (GET "/" [] + (events-list-handler db (db.event/upcoming) :upcoming)) + + (GET "/past" [] + (events-list-handler db (db.event/past) :past)) + + (GET "/new" [date] + (page-response + {:title "New Event"} + (event-form {::event/date date}))) + + (POST "/" [date attendees] + (let [event (db.event/create! db {::event/date date}) + message + (if attendees + (let [num-attendees + (import-attendees! db + (::event/id event) + (:tempfile attendees))] + (format "Event created with %d attendees" + num-attendees)) + "Event created")] + (-> (str "/signup-forms/" (::event/id event)) + redirect + (flash/add-flash {:flash/type :success + :flash/message message})))) + + (context "/:id" [id :<< as-uuid] + (GET "/" [] + (if-let [event (db/fetch db + (-> {:select [:event.*] + :from [:event] + :where [:= :event.id id]} + (db.event/with-stats)))] + (let [attendees (db.attendee-check/attendees-with-last-checks + db + (db/list db (db.attendee/for-event id)))] + (page-response + (event-page {:event event + :attendees attendees}))) + (not-found "Event Not Found"))) + + (POST "/delete" [] + (db/delete! db :event_attendee [:= :event-id id]) + (db/delete! db :event [:= :id id]) + (-> (redirect "/events") + (flash/add-flash + #:flash {:type :success + :message "Successfully deleted event"}))) + + (GET "/attendees/import" [] + (if-let [event (db/get db :event id)] + (page-response + (import-attendees-page {:event event})) + (not-found "Event Not Found"))) + + (POST "/attendees" [attendees] + (let [num-imported (import-attendees! db id (:tempfile attendees))] + (-> (redirect (str "/events/" id)) + (flash/add-flash + #:flash{:type :success + :message (format "Successfully imported %d attendees" + num-imported)}))))))) + +(comment + (def db (:db bbbg.core/system)) + + (-> (db/list db :event) + first + ::event/date + format-date) + ) diff --git a/users/aspen/bbbg/src/bbbg/handlers/home.clj b/users/aspen/bbbg/src/bbbg/handlers/home.clj new file mode 100644 index 000000000000..17d48755365c --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/home.clj @@ -0,0 +1,52 @@ +(ns bbbg.handlers.home + (:require + [bbbg.db.user :as db.user] + [bbbg.discord.auth :as discord.auth] + [bbbg.handlers.core :refer [page-response authenticated?]] + [bbbg.user :as user] + [bbbg.views.flash :as flash] + [compojure.core :refer [GET POST routes]] + [ring.util.response :refer [redirect]] + [bbbg.discord :as discord])) + +(defn- home-page [] + [:div.home-page + [:a.signup-form-link {:href "/signup-forms"} + "Event Signup Form"]]) + +(defn auth-failure [] + [:div.auth-failure + [:p + "Sorry, only users with the Organizers role in discord can sign in"] + [:p + [:a {:href "/"} "Go Back"]]]) + +(defn home-routes [{:keys [db] :as env}] + (routes + (GET "/" [] (page-response (home-page))) + + (POST "/auth/sign-out" request + (if (authenticated? request) + (-> (redirect "/") + (update :session dissoc ::user/id) + (flash/add-flash + {:flash/message "Successfully Signed Out" + :flash/type :success})) + (redirect "/"))) + + (GET "/auth/success" request + (let [token (get-in request [:oauth2/access-tokens :discord])] + (if (discord.auth/check-discord-auth env token) + (let [discord-user (discord/me token) + user (db.user/find-or-create! + db + #::user{:username (:username discord-user) + :discord-user-id (:id discord-user)})] + (-> (redirect "/") + (assoc-in [:session ::user/id] (::user/id user)) + (flash/add-flash + {:flash/message "Successfully Signed In" + :flash/type :success}))) + (-> + (page-response (auth-failure)) + (assoc :status 401))))))) diff --git a/users/aspen/bbbg/src/bbbg/handlers/signup_form.clj b/users/aspen/bbbg/src/bbbg/handlers/signup_form.clj new file mode 100644 index 000000000000..ed1d7644f539 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/signup_form.clj @@ -0,0 +1,93 @@ +(ns bbbg.handlers.signup-form + (:require + [bbbg.attendee :as attendee] + [bbbg.db :as db] + [bbbg.db.attendee :as db.attendee] + [bbbg.db.event :as db.event] + [bbbg.event :as event] + [bbbg.handlers.core + :refer [*authenticated?* authenticated? page-response]] + [cheshire.core :as json] + [compojure.core :refer [context GET]] + [honeysql.helpers :refer [merge-where]] + [java-time :refer [local-date]] + [ring.util.response :refer [redirect]])) + +(defn no-events-page [{:keys [authenticated?]}] + [:div.page + [:p + "There are no events for today"] + (when authenticated? + [:p + [:a.button {:href (str "/events/new?date=" (str (local-date)))} + "Create New Event"]])]) + +(defn signup-page [{:keys [event attendees]}] + [:div.signup-page + [:form#signup-form + {:method "POST" + :action "/event_attendees" + :disabled "disabled"} + [:input#name-autocomplete + {:type "search" + :title "Name" + :name "name" + :spellcheck "false" + :autocorrect "off" + :autocomplete "off" + :autocapitalize "off" + :maxlength "2048"}] + [:input#attendee-id {:type "hidden" :name "attendee_id"}] + [:input#event-id {:type "hidden" :name "event_id" :value (::event/id event)}] + [:input#submit-button.hidden + {:type "submit" + :value "Sign In" + :disabled "disabled"}]] + [:ul#attendees-list + (if (seq attendees) + (for [attendee attendees] + [:li {:data-attendee (json/generate-string attendee) + :role "button"} + (::attendee/meetup-name attendee)]) + [:li.no-attendees + [:p + "Nobody has RSVPed to this event yet, or no attendee list has been + imported"] + (when *authenticated?* + [:p + [:a.button + {:href (str "/events/" + (::event/id event) + "/attendees/import")} + "Import Attendee List"]])])]]) + +(defn event-not-found [] + [:div.event-not-found + [:p "Event not found"] + [:p [:a {:href (str "/events/new")} "Create a new event"]]]) + +;;; + +(defn signup-form-routes [{:keys [db]}] + (context "/signup-forms" [] + (GET "/" request + (if-let [event (db/fetch db (db.event/today))] + (redirect (str "/signup-forms/" (::event/id event))) + (page-response (no-events-page + {:authenticated? (authenticated? request)})))) + + (GET "/:event-id" [event-id] + (if-let [event (db/get db :event event-id)] + (let [attendees (db/list db + (-> + (db.attendee/for-event event-id) + (merge-where + [:and + [:or + [:= :attended nil] + [:not :attended]] + :rsvpd_attending])))] + (page-response + (signup-page {:event event + :attendees attendees}))) + (event-not-found))))) |