From 8899d47cb6e96742b8accc9d2e829e7aa6184e7f Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Mon, 23 Oct 2023 10:14:17 -0600 Subject: [PATCH] Add basic sugarwod import for lifts (#81) * add basic sugarwod import for lifts * remove middleware * remove reference to admin * add snatch grip pp and snatch pull to mvmt list --------- Co-authored-by: Zac Jones --- deps.edn | 16 +-- src/com/spicy/app.clj | 28 ++++++ src/com/spicy/numbers.clj | 5 +- src/com/spicy/sugarwod/core.clj | 56 +++++++++++ src/com/spicy/sugarwod/csv.clj | 45 +++++++++ src/com/spicy/sugarwod/transform.clj | 144 +++++++++++++++++++++++++++ 6 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 src/com/spicy/sugarwod/core.clj create mode 100644 src/com/spicy/sugarwod/csv.clj create mode 100644 src/com/spicy/sugarwod/transform.clj diff --git a/deps.edn b/deps.edn index 9dc1e0d..307e51b 100644 --- a/deps.edn +++ b/deps.edn @@ -1,7 +1,11 @@ {:paths ["src" "resources" "target/resources"] - :deps {com.biffweb/biff {:git/url "https://github.com/jacobobryant/biff", :sha "3ff1256", :tag "v0.7.4"} - camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} - org.clojure/clojure {:mvn/version "1.11.1"} - org.slf4j/slf4j-simple {:mvn/version "2.0.0-alpha5"} - clojure.java-time/clojure.java-time {:mvn/version "1.3.0"} - djblue/portal {:mvn/version "0.40.0"}}} + :deps {com.biffweb/biff {:git/url "https://github.com/jacobobryant/biff", :sha "3ff1256", :tag "v0.7.4"} + camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} + net.clojars.wkok/openai-clojure {:mvn/version "0.11.0"} + cheshire/cheshire {:mvn/version "5.12.0"} + metosin/malli {:mvn/version "0.13.0"} + org.clojure/clojure {:mvn/version "1.11.1"} + org.clojure/data.csv {:mvn/version "1.0.1"} + org.slf4j/slf4j-simple {:mvn/version "2.0.0-alpha5"} + clojure.java-time/clojure.java-time {:mvn/version "1.3.0"} + djblue/portal {:mvn/version "0.40.0"}}} diff --git a/src/com/spicy/app.clj b/src/com/spicy/app.clj index a3425a3..25be004 100644 --- a/src/com/spicy/app.clj +++ b/src/com/spicy/app.clj @@ -1,11 +1,14 @@ (ns com.spicy.app (:require + [clojure.java.io :as io] [com.biffweb :as biff :refer [q]] [com.spicy.middleware :as mid] [com.spicy.movements.core :as movements] [com.spicy.results.core :as results] [com.spicy.results.ui :as r] [com.spicy.settings :as settings] + [com.spicy.sugarwod.core :as sugar.core] + [com.spicy.sugarwod.csv :as csv] [com.spicy.ui :as ui] [com.spicy.workouts.core :as workouts] [com.spicy.workouts.ui :as w])) @@ -80,10 +83,35 @@ [:a.link {:href "https://biffweb.com"} "Biff"] "."])) +(defn import-page + [ctx] + (ui/page + ctx + (ui/panel [:h1.text-2xl.font-bold.mb-8 + "Import your data"] + [:h2.text-xl.font-bold.mb-4 + "SugarWOD"] + (biff/form {:method :post :enctype "multipart/form-data"} + [:input {:type :file :name :file :id :file}] + [:button.btn {:type "submit"} "Submit"])))) + + +(defn process-import + [{:keys [params session] :as ctx}] + (let [user (:uid session) + data (csv/parse-sugar-csv (io/reader (-> params :file :tempfile))) + tx-data (sugar.core/sugar-movements->tx-data (assoc ctx :user user) data)] + (biff/submit-tx ctx tx-data)) + {:status 303 + :headers {"location" "/app/import"}}) + + (def plugin {:static {"/about/" about-page} :routes ["/app" {:middleware [mid/wrap-signed-in]} ["" {:get app}] + ["/import" {:get import-page + :post process-import}] workouts/routes results/routes movements/routes] diff --git a/src/com/spicy/numbers.clj b/src/com/spicy/numbers.clj index fbfc760..8b4fa68 100644 --- a/src/com/spicy/numbers.clj +++ b/src/com/spicy/numbers.clj @@ -3,7 +3,9 @@ (defn parse-int [s] - (Integer/parseInt (re-find #"\A-?\d+" s))) + (if (int? s) + s + (Integer/parseInt (re-find #"\A-?\d+" s)))) (defn safe-parse-int @@ -12,4 +14,3 @@ (parse-int s) (catch Exception e (prn "Error while parsing int: " e)))) - diff --git a/src/com/spicy/sugarwod/core.clj b/src/com/spicy/sugarwod/core.clj new file mode 100644 index 0000000..79855db --- /dev/null +++ b/src/com/spicy/sugarwod/core.clj @@ -0,0 +1,56 @@ +(ns com.spicy.sugarwod.core + (:require + [com.biffweb :as biff] + [com.spicy.sugarwod.transform :as t])) + + +(defn sugar-lift? + [{:keys [barbell-lift] :as _sugar-record}] + (not-empty barbell-lift)) + + +(defn sugar-movements->tx-data + [{:keys [user] :as ctx} sugarwod-csv-data] + (let [spicy-lifts (biff/q (:biff/db ctx) + '{:find (pull m [*]) + :where [[m :movement/name] + [m :movement/type :strength]]}) + sugar-lifts (filter sugar-lift? sugarwod-csv-data)] + (filterv seq (flatten + (map + (partial t/transform-sugar-strength->tx {:user user :movements spicy-lifts}) + sugar-lifts))))) + + +(comment + (require '[com.spicy.portal :as p]) + (require '[com.spicy.repl :as r]) + (require '[com.spicy.sugarwod.csv :as s]) + (require '[clojure.java.io :as io]) + + (p/open-portal) + + (s/decode-record {:description "Hang Power Clean for load: #1: 8 reps #2: 8 reps #3: 8 reps #4: 8 reps #5: 8 reps #6: 8 reps" + :date "08/12/2017" + :score-type "Load" + :set-details "[{\"success\":true,\"load\":115},{\"success\":true,\"load\":115},{\"success\":true,\"load\":115},{\"success\":true,\"load\":125},{\"success\":true,\"load\":125},{\"success\":true,\"load\":125}]" + :barbell-lift "Hang Power Clean" + :best-result-raw "125" + :title "Hang Power Clean 6x8" + :rx-or-scaled "RX" + :notes "Barbell Cycling, trying to bounce the bar out of the power position." :best-result-display "125" + :pr ""}) + + (def e (io/reader (io/resource "sugarwod_workouts.csv"))) + + (def data + (s/parse-sugar-csv e)) + + (tap> data) + + + (let [ctx (r/get-context)] + (tap> + (sugar-movements->tx-data (assoc ctx :user r/user-b) data))) + + (biff/add-libs)) diff --git a/src/com/spicy/sugarwod/csv.clj b/src/com/spicy/sugarwod/csv.clj new file mode 100644 index 0000000..8fb0133 --- /dev/null +++ b/src/com/spicy/sugarwod/csv.clj @@ -0,0 +1,45 @@ +(ns com.spicy.sugarwod.csv + (:require + [camel-snake-kebab.core :as csk] + [cheshire.core :as json] + [clojure.data.csv :as csv])) + + +(def BarbellLiftEnums + [:enum "" "Shoulder Press" "Bench Press" "Split Jerk" "Push Press" "Sotts Press" "Clean & Jerk" "Power Clean" "Hang Power Snatch" "Pendlay Row" "Good Morning" "Clean Pull" "Deadlift" "Romanian Deadlift" "Squat Clean Thruster" "Back Pause Squat" "Hang Squat Clean" "Back Squat" "Thruster" "Sumo Deadlift" "Squat Snatch" "Box Squat" "Power Snatch" "Front Squat" "Muscle Snatch" "Power Clean & Jerk" "Snatch" "Hang Squat Snatch" "Muscle Clean" "Overhead Squat" "Hang Clean" "Hang Power Clean" "Squat Clean" "Push Jerk" "Front Pause Squat" "Clean"]) + + +(def SugarRecord + [:map + [[:barbell-lift BarbellLiftEnums] + [:best-result-display :int] + [:best-result-raw :int] + [:date inst?] + [:description :string] + [:notes :string] + [:pr [:enum "PR"]] + [:rx-or-scaled [:enum "RX" "SCALED"]] + [:score-type [:enum "Load" "Rounds + Reps" "Other / Text" "Reps" "Time"]] + [:set-details :string] + [:title :string]]]) + + +(defn csv-data->maps + [csv-data] + (map zipmap + (->> (first csv-data) ; First row is the header + (map csk/->kebab-case-keyword) ; Drop if you want string keys instead + repeat) + (rest csv-data))) + + +(defn decode-record + [r] + (assoc r :set-details (json/decode (:set-details r) keyword))) + + +(defn parse-sugar-csv + [csv] + (->> (csv/read-csv csv) + csv-data->maps + (map decode-record))) diff --git a/src/com/spicy/sugarwod/transform.clj b/src/com/spicy/sugarwod/transform.clj new file mode 100644 index 0000000..40fd2da --- /dev/null +++ b/src/com/spicy/sugarwod/transform.clj @@ -0,0 +1,144 @@ +(ns com.spicy.sugarwod.transform + (:require + [clojure.string :as string] + [com.spicy.calendar :as c] + [com.spicy.numbers :as n] + [java-time.api :as jt])) + + +(defn transformer + "Takes a translation map data to transform" + [transform-map raw-data] + (reduce-kv (fn [m k v] + (let [resolved + (cond + (vector? v) (if (fn? (last v)) + ((last v) (get-in raw-data (butlast v))) + (get-in raw-data v)) + (fn? v) (v raw-data) + (map? v) (transformer v raw-data) + :else v)] + (assoc m k resolved))) + {} + transform-map)) + + +(def sugar-lift->spicy-lift + {"Shoulder Press" "strict press" + "Bench Press" "bench press" + "Push Jerk" "push jerk" + "Split Jerk" "split jerk" + "Push Press" "push press" + "Sotts Press" "sotts press" + "Clean & Jerk" "clean and jerk" + "Power Clean" "power clean" + "Power Clean & Jerk" "power clean and jerk" + "Clean" "clean" + "Muscle Clean" "muscle clean" + "Hang Clean" "hang clean" + "Hang Power Clean" "hang power clean" "Clean Pull" "clean pull" + "Hang Squat Clean" "hang squat clean" + "Squat Clean" "squat clean" + "Squat Clean Thruster" "squat clean thruster (cluster)" + "Thruster" "thruster" + "Snatch" "snatch" + "Hang Power Snatch" "hang power snatch" + "Snatch Grip Push Press" "snatch grip push press" + "Snatch Pull" "snatch pull" + "Squat Snatch" "squat snatch" + "Power Snatch" "power snatch" + "Muscle Snatch" "muscle snatch" + "Hang Squat Snatch" "hang squat snatch" + "Pendlay Row" "pendlay row" + "Deadlift" "deadlift" + "Romanian Deadlift" "romainian deadlift" + "Sumo Deadlift" "sumo deadlift" + "Good Morning" "good morning" + "Back Squat" "back squat" + "Back Pause Squat" "back pause squat" + "Overhead Squat" "overhead squat" + "Box Squat" "box squat" + "Front Squat" "front squat" + "Front Pause Squat" "front pause squat" + }) + + +(defn sugar-lift->xt-id + [movements lift-str] + (:xt/id (first (filter #(= (get sugar-lift->spicy-lift lift-str) (:movement/name %)) movements)))) + + +(defmulti title->reps + (fn [title] + (cond + (seq (re-find #"^(\D+)\s+(\d+x\d+)+$" title)) :constant + (seq (re-find #"^(\D+)\s+((?:\d+-)+\d+)$" title)) :variable))) + + +(defmethod title->reps :constant + [title] + (when-let [[_ _ reps-str] (re-find #"^(\D+)\s+(\d+x\d+)+$" title)] + (let [[sets reps] (string/split reps-str #"x")] + (repeat (n/parse-int sets) (n/parse-int reps))))) + + +(defmethod title->reps :variable + [title] + (when-let [[_ _ reps-str] (re-find #"^(\D+)\s+((?:\d+-)+\d+)$" title)] + (let [reps (string/split reps-str #"-")] + (map n/parse-int reps)))) + + +(defn ->instant + [date-str] + (->> date-str + (jt/local-date "MM/dd/yyyy") + c/->instant + java.util.Date/from)) + + +(def sugar-set->spicy-set + {:db/doc-type :strength-set + :result-set/status [:success (fn [success?] (if success? :pass :fail))] + :result-set/weight [:load n/parse-int]}) + + +(defn map-set-details + [{:keys [type-id] :as _opts}] + (fn [{:keys [set-details title] :as _sugar-record}] + (let [reps (title->reps title) + map-fn (fn [idx set] + (-> (transformer sugar-set->spicy-set set) + (assoc :result-set/number (inc idx) + :result-set/parent type-id + :db/op :create + :result-set/reps (nth reps idx))))] + (into [] (map-indexed map-fn set-details))))) + + +(defn sugar-strength->spicy-strength + [{:keys [type-id user movements] :as opts}] + {:strength-result {:result/movement [:barbell-lift (partial sugar-lift->xt-id movements)] + :result/notes [:notes] + :result/set-count [:set-details count] + :db/doc-type :strength-result + :db/op :create + :xt/id type-id} + :result {:result/date [:date ->instant] + :db/doc-type :result + :db/op :create + :result/type type-id + :result/user user} + :sets (map-set-details opts)}) + + +(defn transform-sugar-strength->tx + [{:keys [_user _movements] :as ctx} sugar-record] + (try + (let [{:keys [strength-result result sets]} + (transformer + (sugar-strength->spicy-strength (assoc ctx :type-id (random-uuid))) + sugar-record)] + (concat [result strength-result] sets)) + (catch Exception _e + nil)))