First commit. Taken from open source branch of internal sample-tracking application.
This commit is contained in:
123
src/clj/org/parkerici/sample_tracking/api/email.clj
Normal file
123
src/clj/org/parkerici/sample_tracking/api/email.clj
Normal file
@@ -0,0 +1,123 @@
|
||||
(ns org.parkerici.sample-tracking.api.email
|
||||
(:require [org.parkerici.sample-tracking.configuration :as c]
|
||||
[org.parkerici.sample-tracking.db.study :as study-db]
|
||||
[org.parkerici.sample-tracking.db.site :as site-db]
|
||||
[org.parkerici.sample-tracking.db.cohort :as cohort-db]
|
||||
[org.parkerici.sample-tracking.db.kit-type :as kit-type-db]
|
||||
[org.parkerici.sample-tracking.db.timepoint :as timepoint-db]
|
||||
[org.parkerici.sample-tracking.db.sample-type :as sample-type-db]
|
||||
[org.parkerici.sample-tracking.db.form-type :as form-type-db]
|
||||
[org.parkerici.sample-tracking.db.kit :as kit-db]
|
||||
[org.parkerici.sample-tracking.api.export :as export]
|
||||
[org.parkerici.sample-tracking.utils.path :as path]
|
||||
[org.parkerici.sample-tracking.pages.manifest :as manifest-page]
|
||||
[again.core :as again]
|
||||
[clojure.java.io :as io]
|
||||
[hiccup.core :as hiccup]
|
||||
[clj-htmltopdf.core :as htmltopdf]
|
||||
[org.parkerici.sample-tracking.utils.date-time :as dt])
|
||||
(:import [com.sendgrid SendGrid SendGrid$Email]
|
||||
(java.util UUID)))
|
||||
|
||||
(def csv-attachment-name "samples.csv")
|
||||
(def pdf-attachment-name "manifest.pdf")
|
||||
|
||||
(defn send-message
|
||||
[to subject content csv-file]
|
||||
(let [pdf-path (path/join (c/temp-path) pdf-attachment-name)
|
||||
_ (htmltopdf/->pdf content pdf-path)
|
||||
pdf-file (io/file pdf-path)
|
||||
sg (new SendGrid (c/sendgrid-api-key))
|
||||
email (doto (new SendGrid$Email)
|
||||
(.addTo to)
|
||||
(.setFrom (c/email-sender))
|
||||
(.setSubject subject)
|
||||
(.setHtml content))
|
||||
email-with-csv (if csv-file
|
||||
(.addAttachment email csv-attachment-name csv-file)
|
||||
email)
|
||||
email-with-pdf (if pdf-file
|
||||
(.addAttachment email-with-csv pdf-attachment-name pdf-file)
|
||||
email-with-csv)]
|
||||
(try
|
||||
(again/with-retries
|
||||
[1000 10000]
|
||||
(.send sg email-with-pdf))
|
||||
(catch Exception ex
|
||||
(.printStackTrace ex)
|
||||
(println (.getMessage ex))))
|
||||
(io/delete-file pdf-path)))
|
||||
|
||||
(defn manifest-email-body
|
||||
[kit-map config]
|
||||
(let [{:keys [kit-id site study cohort participant-id air-waybill collection-timestamp timezone
|
||||
completing-first-name completing-last-name completing-email comments timepoints kit-type samples
|
||||
form-type-field-values]} kit-map
|
||||
kit-type-uuid (UUID/fromString kit-type)
|
||||
site-name (:name (site-db/find-site-by-uuid (UUID/fromString site)))
|
||||
study-name (:name (study-db/find-study-by-uuid (UUID/fromString study)))
|
||||
cohort-name (:name (cohort-db/find-cohort-by-uuid (UUID/fromString cohort)))
|
||||
timepoint-names (map #(:name (timepoint-db/find-timepoint-by-uuid (UUID/fromString %))) timepoints)
|
||||
kit-type-name (kit-type-db/get-kit-type-name kit-type-uuid)
|
||||
selected-sample-types (sort-by :id-suffix (sample-type-db/list-sample-types kit-type-uuid))
|
||||
selected-form-type-fields (form-type-db/get-form-type-fields kit-type-uuid)
|
||||
date-display-fn (fn [date] (when date (dt/generate-date-string date timezone)))
|
||||
time-display-fn (fn [time] (when time (dt/generate-time-string time timezone)))
|
||||
completing-user-manifest (:completing-user-manifest config)
|
||||
content (manifest-page/content {:site-name site-name
|
||||
:study-name study-name
|
||||
:cohort-name cohort-name
|
||||
:timepoint-names timepoint-names
|
||||
:kit-name kit-type-name
|
||||
:kit-id kit-id
|
||||
:participant-id participant-id
|
||||
:collection-date collection-timestamp
|
||||
:collection-time collection-timestamp
|
||||
:selected-form-type-fields selected-form-type-fields
|
||||
:form-type-field-values form-type-field-values
|
||||
:selected-sample-types selected-sample-types
|
||||
:sample-values samples
|
||||
:air-waybill air-waybill
|
||||
:completing-first-name completing-first-name
|
||||
:completing-last-name completing-last-name
|
||||
:completing-email completing-email
|
||||
:comments comments
|
||||
:date-display-fn date-display-fn
|
||||
:time-display-fn time-display-fn
|
||||
:add-empty-field-lines completing-user-manifest
|
||||
:add-signature-fields completing-user-manifest})]
|
||||
(hiccup/html [:div.page-body
|
||||
[:h1 "Kit Shipment Manifest"]
|
||||
[:div.kit-manifest
|
||||
content]])))
|
||||
|
||||
(defn send-manifest-email
|
||||
[kit-map kit-uuid]
|
||||
(let [completing-user-body (manifest-email-body kit-map {:completing-user-manifest true})
|
||||
non-completing-user-body (manifest-email-body kit-map {:completing-user-manifest false})
|
||||
subject "Sample Tracking Kit Manifest"
|
||||
completing-email (:completing-email kit-map)
|
||||
vendor-email (kit-db/get-kit-vendor-email kit-uuid)
|
||||
csv-path (path/join (c/temp-path) csv-attachment-name)
|
||||
_ (export/export-samples-to-csv {:uuid kit-uuid :shipped true} csv-path)
|
||||
csv-file (io/file csv-path)]
|
||||
(when (c/send-manifest-emails)
|
||||
(send-message (c/email-manifest-recipient) subject non-completing-user-body csv-file)
|
||||
(send-message completing-email subject completing-user-body csv-file)
|
||||
(when (and (c/send-vendor-emails) (some? vendor-email))
|
||||
(send-message vendor-email subject non-completing-user-body csv-file)))
|
||||
(io/delete-file csv-path)))
|
||||
|
||||
(defn proposed-edit-body
|
||||
[user-email]
|
||||
(hiccup/html
|
||||
[:div.page-body
|
||||
[:p (str user-email " has proposed a kit edit. Please login to the application to approve or deny it.")]]))
|
||||
|
||||
(defn send-proposed-edit-email
|
||||
[update-map user-email]
|
||||
(let [recipient (c/email-manifest-recipient)
|
||||
subject (str "Edit Proposed for Kit " (:kit-id update-map))
|
||||
body (proposed-edit-body user-email)]
|
||||
(when (c/send-manifest-emails)
|
||||
(send-message recipient subject body nil))))
|
||||
201
src/clj/org/parkerici/sample_tracking/api/export.clj
Normal file
201
src/clj/org/parkerici/sample_tracking/api/export.clj
Normal file
@@ -0,0 +1,201 @@
|
||||
(ns org.parkerici.sample-tracking.api.export
|
||||
(:require [clojure.string :as string]
|
||||
[org.parkerici.sample-tracking.configuration :as c]
|
||||
[org.parkerici.sample-tracking.db.form-value :as form-value-db]
|
||||
[org.parkerici.sample-tracking.db.sample :as sample-db]
|
||||
[org.parkerici.sample-tracking.db.sample-attribute :as sample-attribute-db]
|
||||
[org.parkerici.sample-tracking.db.sample-type :as sample-type-db]
|
||||
[org.parkerici.sample-tracking.db.timepoint :as timepoint-db]
|
||||
[org.parkerici.sample-tracking.utils.csv :as csv]
|
||||
[org.parkerici.sample-tracking.utils.date-time :as dt]))
|
||||
|
||||
(defn build-timepoint-map
|
||||
[config-map]
|
||||
(reduce (fn [m value]
|
||||
(let [kit-uuid (:kit-uuid value)
|
||||
cur-values (or (get m kit-uuid) [])
|
||||
updated-values (conj cur-values (:timepoint-name value))]
|
||||
(assoc m kit-uuid updated-values))) {} (timepoint-db/list-kit-timepoints config-map)))
|
||||
|
||||
(defn add-timepoints-to-samples
|
||||
"Add attributes and their values to a map of samples
|
||||
Samples should be a list of maps that all have the key :sample-id
|
||||
Timepoints should be a list of maps that have they keys :sample-id and :timepoint-name
|
||||
Returns the samples list with the timepoint-names that map to each sample added as a comma separated list
|
||||
under the key :timepoints."
|
||||
[timepoint-map samples]
|
||||
(map (fn [sample]
|
||||
(let [kit-uuid (:kit-uuid sample)
|
||||
timepoint-names (string/join ", " (sort (get timepoint-map kit-uuid)))]
|
||||
(assoc sample :timepoints timepoint-names))) samples))
|
||||
|
||||
(defn build-sample-join-map
|
||||
[values-to-join join-key]
|
||||
(reduce (fn [value-map value]
|
||||
(let [join-key-value (get value join-key)
|
||||
existing-values (or (get value-map join-key-value) [])
|
||||
updated-values (conj existing-values value)]
|
||||
(assoc value-map join-key-value updated-values))) {} values-to-join))
|
||||
|
||||
(defn join-to-sample-map
|
||||
"Add the values elements in values to join to the sample maps in samples.
|
||||
Samples should be a list of maps that all have the key :sample-id
|
||||
values-to-join should be a list of maps that have they keys join-key, id-key.
|
||||
Calls the join-fn on sample and a list of the values in values-to-join with the same join-key.
|
||||
Expects the join-fn to returned the sample map joined with corresponding values from values-to-join.
|
||||
Returns the list of samples after joining with the values-to-join and applying join-fn."
|
||||
[join-map join-key join-fn samples]
|
||||
(map (fn [sample]
|
||||
(let [join-key-value (get sample join-key)
|
||||
joining-values (get join-map join-key-value)]
|
||||
(join-fn sample joining-values))) samples))
|
||||
|
||||
(defn parse-and-split-collection-timestamp
|
||||
[samples]
|
||||
(map (fn [sample]
|
||||
(let [timezone (:timezone sample)
|
||||
collection-timestamp (:collection-timestamp sample)]
|
||||
(-> sample
|
||||
(assoc :collection-date (when collection-timestamp (dt/generate-date-string collection-timestamp timezone)))
|
||||
(assoc :collection-time (when collection-timestamp (dt/generate-time-string collection-timestamp timezone)))
|
||||
(dissoc :collection-timestamp))))
|
||||
samples))
|
||||
|
||||
(defn attributes-and-values-join-fn
|
||||
[sample values]
|
||||
(reduce (fn [sample value]
|
||||
(assoc sample (:attribute value) (:value value))) sample values))
|
||||
|
||||
(defn kit-form-values-join-fn
|
||||
[sample values]
|
||||
(if (:collected sample)
|
||||
(reduce (fn [sample value]
|
||||
(if (= (:field-type value) "time")
|
||||
(assoc sample
|
||||
(:field-id value)
|
||||
(dt/generate-time-string (:value value) (:timezone sample)))
|
||||
(assoc sample
|
||||
(:field-id value)
|
||||
(:value value))))
|
||||
sample values)
|
||||
sample))
|
||||
|
||||
(defn kit-type->sample-type-map-reduce-fn
|
||||
"A reduce function that expects a sample-type map as input.
|
||||
Builds an output map of the form
|
||||
{kit-type-uuid: {sample-type-uuid: {:name 'sample-type-name' :id-suffix 'sample-type-id-suffix'}}"
|
||||
[m sample-type]
|
||||
(let [kit-type-uuid (:kit-type-uuid sample-type)
|
||||
cur-values (or (get m kit-type-uuid) {})
|
||||
sample-type-map {:name (:name sample-type)
|
||||
:suffix (:id-suffix sample-type)}
|
||||
updated-values (assoc cur-values
|
||||
(:uuid sample-type)
|
||||
sample-type-map)]
|
||||
(assoc m kit-type-uuid updated-values)))
|
||||
|
||||
(defn kit->sample-map-reduce-fn
|
||||
"A reduce function that expects a sample map as an input.
|
||||
Builds an output map of the form {kit-uuid: {sample-type-uuid: sample}}"
|
||||
[m sample]
|
||||
(let [kit-uuid (:kit-uuid sample)
|
||||
cur-values (or (get m kit-uuid) {})
|
||||
updated-values (assoc cur-values
|
||||
(:sample-type-uuid sample) sample)]
|
||||
(assoc m kit-uuid updated-values)))
|
||||
|
||||
(defn kit-sample-type-uuids
|
||||
"For a given kit, finds the kit-type for that kit and then finds the sample-types for that kit-type.
|
||||
Returns a list of the sample-type-uuids for a given kit."
|
||||
[kit-uuid kit-type-sample-type-map kit-sample-map]
|
||||
(let [samples-map (get kit-sample-map kit-uuid)
|
||||
first-sample (first (vals samples-map))
|
||||
kit-type-uuid (get first-sample :kit-type-uuid)
|
||||
kit-sample-types (get kit-type-sample-type-map kit-type-uuid)]
|
||||
(keys kit-sample-types)))
|
||||
|
||||
(defn build-uncollected-sample
|
||||
"Given a kit and a sample-type for that kit, builds an uncollected sample by taking the first collected sample for
|
||||
that kit and then clearing any sample specific information and adding the correct sample-type-name and sample-id."
|
||||
[kit-uuid sample-type-uuid kit-type-sample-type-map kit-sample-map]
|
||||
(let [samples-map (get kit-sample-map kit-uuid)
|
||||
first-sample (first (vals samples-map))
|
||||
kit-type-uuid (get first-sample :kit-type-uuid)
|
||||
cur-sample-type (get-in kit-type-sample-type-map [kit-type-uuid sample-type-uuid])]
|
||||
(-> first-sample
|
||||
(dissoc :collection-timestamp)
|
||||
(assoc :sample-type-name (:name cur-sample-type))
|
||||
(assoc :sample-id (str (:kit-id first-sample) (:suffix cur-sample-type)))
|
||||
(assoc :collected false)
|
||||
(assoc :shipped false)
|
||||
(assoc :air-waybill ""))))
|
||||
|
||||
(defn add-uncollected-samples
|
||||
"Expects a sample map and a boolean flag to add uncollected. Normally a sample map only contains samples that have
|
||||
been collected. If add-uncollected is true, iterates over the sample-types for the kits represented in samples
|
||||
and then generates samples for the uncollected samples. If add-uncollected is false just returns the passed in
|
||||
samples."
|
||||
[kit-type-sample-type-map add-uncollected]
|
||||
(fn [xf]
|
||||
(fn
|
||||
([] (xf))
|
||||
([processed-samples] (xf processed-samples))
|
||||
([processed-samples new-samples]
|
||||
(if add-uncollected
|
||||
(let [kit-sample-map (reduce kit->sample-map-reduce-fn {} new-samples)
|
||||
full-kit-sample-map (for [cur-kit-uuid (keys kit-sample-map)
|
||||
cur-sample-type-uuid (kit-sample-type-uuids cur-kit-uuid kit-type-sample-type-map
|
||||
kit-sample-map)]
|
||||
(if-let [cur-kit-sample (get-in kit-sample-map [cur-kit-uuid cur-sample-type-uuid])]
|
||||
cur-kit-sample
|
||||
(build-uncollected-sample cur-kit-uuid cur-sample-type-uuid kit-type-sample-type-map
|
||||
kit-sample-map)))]
|
||||
(xf processed-samples full-kit-sample-map))
|
||||
(xf processed-samples new-samples))))))
|
||||
|
||||
(defn remove-unused-columns
|
||||
[samples]
|
||||
(map #(apply dissoc % (c/sample-export-columns-to-drop)) samples))
|
||||
|
||||
; TODO - Could possibly use some refactoring here.
|
||||
; Could be cleaner to get all kit types and sample types, and then iterate through all existing kits and samples.
|
||||
; If a sample is present for a kit, get the sample type and fill in the collected sample's specific information.
|
||||
; Otherwise we would emit a "base" uncollected sample.
|
||||
;
|
||||
; Readability isn't the best to decrease memory usage.
|
||||
(defn get-samples-for-export
|
||||
"Gets all of the samples for export as a list of maps ready to pass to csv/write-csv-file.
|
||||
Config map can have a :uuid key to export only a specific kit, or an :include-uncollected key
|
||||
if we want to include any uncollected samples in the export."
|
||||
([config-map]
|
||||
(let [samples (sample-db/list-samples-for-export config-map)
|
||||
kit-type-sample-type-map (reduce kit-type->sample-type-map-reduce-fn {} (sample-type-db/list-sample-types nil))
|
||||
timepoint-map (build-timepoint-map config-map)
|
||||
attributes-and-values-join-map (build-sample-join-map
|
||||
(sample-attribute-db/list-sample-attributes-and-values-for-export config-map)
|
||||
:sample-id)
|
||||
kit-form-join-map (build-sample-join-map
|
||||
(form-value-db/list-form-values config-map)
|
||||
:kit-uuid)
|
||||
sample-transducer (comp
|
||||
(map (partial join-to-sample-map kit-form-join-map :kit-uuid kit-form-values-join-fn))
|
||||
(map (partial join-to-sample-map attributes-and-values-join-map :sample-id attributes-and-values-join-fn))
|
||||
(map (partial add-timepoints-to-samples timepoint-map))
|
||||
(map parse-and-split-collection-timestamp)
|
||||
(add-uncollected-samples kit-type-sample-type-map (:include-uncollected config-map)))
|
||||
]
|
||||
(->> (transduce sample-transducer concat (vals (group-by :kit-uuid samples)))
|
||||
(sort-by (juxt :kit-uuid :sample-id))
|
||||
(remove-unused-columns)))))
|
||||
|
||||
(defn export-options
|
||||
[]
|
||||
{:column-order (c/sample-export-column-order) :columns-to-rename (c/sample-export-columns-to-rename)})
|
||||
|
||||
(defn export-samples-to-csv
|
||||
[config-map csv-path]
|
||||
(csv/write-csv-file csv-path (get-samples-for-export config-map) (export-options)))
|
||||
|
||||
(defn export-samples-to-streaming-csv
|
||||
[config-map]
|
||||
(csv/csv-output-stream-fn (get-samples-for-export config-map) (export-options)))
|
||||
53
src/clj/org/parkerici/sample_tracking/api/firebase.clj
Normal file
53
src/clj/org/parkerici/sample_tracking/api/firebase.clj
Normal file
@@ -0,0 +1,53 @@
|
||||
(ns org.parkerici.sample-tracking.api.firebase
|
||||
(:require [clojure.string :as str]
|
||||
[org.parkerici.sample-tracking.api.iam :as iam]
|
||||
[taoensso.timbre :as log])
|
||||
(:import [com.google.firebase FirebaseApp FirebaseOptions]
|
||||
[com.google.auth.oauth2 GoogleCredentials]
|
||||
[com.google.firebase.auth FirebaseAuth]))
|
||||
|
||||
(defn get-authorization-jwt
|
||||
[request]
|
||||
(when-let [authorization-header (get-in request [:headers "authorization"])]
|
||||
(let [split-header (str/split authorization-header #" " 2)]
|
||||
(when (= (first split-header) "Bearer")
|
||||
(second split-header)))))
|
||||
|
||||
(defn check-initialize-firebase
|
||||
[]
|
||||
(when (empty? (FirebaseApp/getApps))
|
||||
(let [firebase-options (-> (FirebaseOptions/builder)
|
||||
(.setCredentials (GoogleCredentials/getApplicationDefault))
|
||||
(.build))]
|
||||
(FirebaseApp/initializeApp firebase-options))))
|
||||
|
||||
(defn verify-token
|
||||
"Verifies that the passed in JWT is valid.
|
||||
If it's valid, returns a decoded FirebaseToken"
|
||||
[token]
|
||||
(check-initialize-firebase)
|
||||
(-> (FirebaseAuth/getInstance)
|
||||
(.verifyIdToken token true)))
|
||||
|
||||
(defn process-firebase-jwt-request
|
||||
[session request-jwt]
|
||||
(let [decoded-jwt (verify-token request-jwt)
|
||||
firebase-email (.getEmail decoded-jwt)
|
||||
user (iam/get-user firebase-email)
|
||||
is-a-user (and (some? user) (not (:deactivated user)))
|
||||
email-verified (.isEmailVerified decoded-jwt)
|
||||
roles (set (iam/get-users-roles firebase-email))]
|
||||
(merge session
|
||||
{:identity firebase-email :roles roles :is-a-user is-a-user :email-verified email-verified})))
|
||||
|
||||
(defn add-firebase-auth-to-session
|
||||
[session firebase-jwt]
|
||||
(try
|
||||
(process-firebase-jwt-request session firebase-jwt)
|
||||
(catch Exception e
|
||||
(log/error e)
|
||||
(assoc session :auth-error true))))
|
||||
|
||||
(defn remove-firebase-auth-from-session
|
||||
[session]
|
||||
(dissoc session :identity :roles :is-a-user :email-verified))
|
||||
51
src/clj/org/parkerici/sample_tracking/api/form_type.clj
Normal file
51
src/clj/org/parkerici/sample_tracking/api/form_type.clj
Normal file
@@ -0,0 +1,51 @@
|
||||
(ns org.parkerici.sample-tracking.api.form-type
|
||||
"Form types are custom, configurable forms that are associated with
|
||||
kit types to collect information for that kit type outside of the
|
||||
default fields collected.
|
||||
|
||||
This file is focused on taking in a csv with form type definitions
|
||||
along with the kit item numbers of the kits they are used for,
|
||||
parsing that csv, and then creating the appropriate values in the database."
|
||||
(:require [clojure.edn :as edn]
|
||||
[org.parkerici.sample-tracking.utils.csv :as csv]
|
||||
[org.parkerici.sample-tracking.configuration :as config]
|
||||
[org.parkerici.sample-tracking.db.kit-type :as kit-type-db]
|
||||
[org.parkerici.sample-tracking.db.form-type :as form-type-db]))
|
||||
|
||||
; Used with filter to remove rows in the input CSV that don't have the required values.
|
||||
(defn row-has-required-values
|
||||
[row]
|
||||
(not (or (empty? (:form-type-name row))
|
||||
(empty? (:kit-item-no row)))))
|
||||
|
||||
; Does not create a form-type if form-type-fields is missing from the row.
|
||||
(defn create-form-type
|
||||
[row]
|
||||
(when-not (empty? (:form-type-fields row))
|
||||
(let [raw-form-type-fields (edn/read-string (:form-type-fields row))
|
||||
form-type-fields (map #(vector :form-type-field/uuid (form-type-db/create-form-type-field %)) raw-form-type-fields)
|
||||
form-type (form-type-db/create-form-type (:form-type-name row) form-type-fields)]
|
||||
form-type)))
|
||||
|
||||
; Tries to find form-type by name. Returns it if found, otherwise creates it with row information.
|
||||
(defn find-or-create-form-type
|
||||
[row]
|
||||
(let [form-type-uuid (form-type-db/find-form-type (:form-type-name row))]
|
||||
(if form-type-uuid
|
||||
form-type-uuid
|
||||
(create-form-type row))))
|
||||
|
||||
(defn create-row-in-db
|
||||
[row]
|
||||
(let [form-type (find-or-create-form-type row)
|
||||
kit-item-numbers (csv/split-csv-string (:kit-item-no row))]
|
||||
(doseq [kit-item-number kit-item-numbers]
|
||||
(when (and form-type
|
||||
(not (kit-type-db/kit-type-has-form-type kit-item-number)))
|
||||
(kit-type-db/add-form-type-to-kit-type form-type (Integer/parseInt kit-item-number))))))
|
||||
|
||||
(defn parse-form-type-csv-and-save-to-db
|
||||
[fpath]
|
||||
(let [csv-headers (config/csv-file-headers :form-type)
|
||||
csv-data (csv/read-csv-into-map fpath csv-headers row-has-required-values)]
|
||||
(doseq [row csv-data] (create-row-in-db row))))
|
||||
59
src/clj/org/parkerici/sample_tracking/api/iam.clj
Normal file
59
src/clj/org/parkerici/sample_tracking/api/iam.clj
Normal file
@@ -0,0 +1,59 @@
|
||||
(ns org.parkerici.sample-tracking.api.iam
|
||||
(:require [org.parkerici.sample-tracking.db.user :as user-db]
|
||||
[org.parkerici.sample-tracking.db.role :as role-db]))
|
||||
|
||||
(defn find-or-create-user
|
||||
[email]
|
||||
(let [user-uuid (user-db/find-user-uuid email)]
|
||||
(if user-uuid
|
||||
user-uuid
|
||||
(user-db/create-user email))))
|
||||
|
||||
(defn find-or-create-role
|
||||
[name]
|
||||
(let [role-uuid (role-db/find-role-uuid name)]
|
||||
(if role-uuid
|
||||
role-uuid
|
||||
(role-db/create-role name))))
|
||||
|
||||
(defn add-role-to-user
|
||||
[email role-name]
|
||||
(let [user-uuid (find-or-create-user email)
|
||||
role-uuid (find-or-create-role role-name)]
|
||||
(if (user-db/user-has-role user-uuid role-uuid)
|
||||
(throw (Exception. "User already has role."))
|
||||
(user-db/add-role-to-user user-uuid role-uuid))))
|
||||
|
||||
(defn remove-role-from-user
|
||||
[email role-name]
|
||||
(let [user-uuid (find-or-create-user email)
|
||||
role-uuid (find-or-create-role role-name)]
|
||||
(if (user-db/user-has-role user-uuid role-uuid)
|
||||
(user-db/remove-role-from-user user-uuid role-uuid)
|
||||
(throw (Exception. "User does not have role to remove.")))))
|
||||
|
||||
(defn get-users-roles
|
||||
[email]
|
||||
(doall (map first (user-db/get-users-roles email))))
|
||||
|
||||
(defn get-users-with-role
|
||||
[role-name]
|
||||
(doall (map first (user-db/get-users-with-role role-name))))
|
||||
|
||||
(defn get-user
|
||||
[email]
|
||||
(first (user-db/list-users {:email email})))
|
||||
|
||||
(defn reactivate-user
|
||||
[email]
|
||||
(user-db/set-user-deactivated-status email false)
|
||||
(find-or-create-user email))
|
||||
|
||||
(defn deactivate-user
|
||||
[email]
|
||||
(let [user-uuid (user-db/find-user-uuid email)]
|
||||
(user-db/set-user-deactivated-status email true)
|
||||
(doseq [role (role-db/list-roles)]
|
||||
(when (user-db/user-has-role user-uuid (:uuid role))
|
||||
(user-db/remove-role-from-user user-uuid (:uuid role))))
|
||||
user-uuid))
|
||||
216
src/clj/org/parkerici/sample_tracking/api/kit_shipment.clj
Normal file
216
src/clj/org/parkerici/sample_tracking/api/kit_shipment.clj
Normal file
@@ -0,0 +1,216 @@
|
||||
(ns org.parkerici.sample-tracking.api.kit-shipment
|
||||
(:require [clojure.set :as set]
|
||||
[java-time :as time]
|
||||
[org.parkerici.sample-tracking.db.core :as db]
|
||||
[org.parkerici.sample-tracking.db.timepoint :as timepoint-db]
|
||||
[org.parkerici.sample-tracking.db.kit-type :as kit-type-db]
|
||||
[org.parkerici.sample-tracking.db.kit :as kit-db]
|
||||
[org.parkerici.sample-tracking.db.form-type :as form-type-db]
|
||||
[org.parkerici.sample-tracking.db.form-value :as form-value-db]
|
||||
[org.parkerici.sample-tracking.db.sample :as sample-db]
|
||||
[org.parkerici.sample-tracking.db.shipment :as shipment-db]
|
||||
[org.parkerici.sample-tracking.db.history :as history-db]
|
||||
[org.parkerici.sample-tracking.db.proposed-kit-edit :as proposed-kit-edit-db]
|
||||
[org.parkerici.sample-tracking.api.email :as email]
|
||||
[org.parkerici.sample-tracking.utils.collection :as coll-utils])
|
||||
(:import (java.util Date UUID)))
|
||||
|
||||
(defn create-samples-and-add-to-kit-shipment
|
||||
[kit-uuid shipment-uuid samples]
|
||||
(doseq [sample-type-uuid (keys samples)]
|
||||
(let [sample (get samples sample-type-uuid)
|
||||
sample-collected (boolean (:collected sample))
|
||||
sample-shipped (boolean (:shipped sample))]
|
||||
(when sample-collected
|
||||
(let [sample-uuid (sample-db/create-or-update-sample nil (name sample-type-uuid) (:sample-id sample)
|
||||
sample-collected sample-shipped)]
|
||||
(kit-db/add-sample-to-kit kit-uuid sample-uuid)
|
||||
(when (and shipment-uuid sample-shipped) (sample-db/add-sample-to-shipment shipment-uuid sample-uuid)))))))
|
||||
|
||||
(defn create-update-form-value
|
||||
[form-value-uuid form-value-field-id-key form-fields form-values]
|
||||
(let [form-value-field-id (name form-value-field-id-key)
|
||||
form-field (first (filter #(= form-value-field-id (:field-id %)) form-fields))
|
||||
form-field-type (:type form-field)
|
||||
form-field-uuid (:uuid form-field)
|
||||
raw-form-value (get form-values form-value-field-id-key)
|
||||
parsed-form-value (case form-field-type
|
||||
"time" (if (string? raw-form-value) (time/java-date raw-form-value) raw-form-value)
|
||||
"boolean" (if (string? raw-form-value) (Boolean/valueOf raw-form-value) raw-form-value)
|
||||
"int" (if (string? raw-form-value) (Long/parseLong raw-form-value) raw-form-value)
|
||||
raw-form-value)]
|
||||
(form-value-db/create-or-update-form-value form-value-uuid form-field-uuid form-field-type parsed-form-value)))
|
||||
|
||||
(defn create-form-values-and-add-to-kit
|
||||
[kit-uuid kit-type-uuid form-values]
|
||||
(let [form-fields (form-type-db/get-form-type-fields kit-type-uuid)]
|
||||
(doseq [form-value-field-id-key (keys form-values)]
|
||||
(let [form-value-uuid (create-update-form-value nil form-value-field-id-key form-fields form-values)]
|
||||
(kit-db/add-form-value-to-kit kit-uuid form-value-uuid)))))
|
||||
|
||||
(defn create-kit-shipment
|
||||
[kit-map]
|
||||
(let [{:keys [air-waybill kit-type samples form-type-field-values]} kit-map
|
||||
kit-type-uuid (UUID/fromString kit-type)
|
||||
kit-uuid (kit-db/create-or-update-kit nil kit-map)
|
||||
shipment-uuid (when (some? air-waybill) (shipment-db/create-or-update-shipment nil air-waybill))]
|
||||
(when (some? shipment-uuid) (kit-db/add-shipment-to-kit kit-uuid shipment-uuid))
|
||||
(create-samples-and-add-to-kit-shipment kit-uuid shipment-uuid samples)
|
||||
(create-form-values-and-add-to-kit kit-uuid kit-type-uuid form-type-field-values)
|
||||
kit-uuid))
|
||||
|
||||
; If uuid is passed in then filters on that as a kit uuid. Otherwise returns all kits.
|
||||
; If tx-id is passed in then queries the kit-shipment values as-of the historical tx-id
|
||||
(defn list-kit-shipment
|
||||
[config-map]
|
||||
(let [kits (kit-db/list-kits config-map)
|
||||
timepoints (timepoint-db/list-kit-timepoints config-map)
|
||||
samples (sample-db/list-samples config-map)
|
||||
form-values (form-value-db/list-form-values config-map)
|
||||
shipments (shipment-db/list-shipments config-map)
|
||||
pending-edits (map #(select-keys % [:kit-uuid :uuid :email :time])
|
||||
(proposed-kit-edit-db/list-proposed-edits {:status "pending"}))
|
||||
edit-history (map #(select-keys % [:entity-uuid :agent-email :time])
|
||||
(history-db/list-history nil))]
|
||||
(-> kits
|
||||
(coll-utils/merge-map-colls :uuid timepoints :kit-uuid :timepoints)
|
||||
(coll-utils/merge-map-colls :uuid samples :kit-uuid :samples)
|
||||
(coll-utils/merge-map-colls :uuid form-values :kit-uuid :form-values)
|
||||
(coll-utils/merge-map-colls :uuid shipments :kit-uuid :shipments)
|
||||
(coll-utils/merge-map-colls :uuid pending-edits :kit-uuid :pending-edits)
|
||||
(coll-utils/merge-map-colls :uuid edit-history :entity-uuid :history))))
|
||||
|
||||
(defn delete-samples
|
||||
[current-sample-map sample-type-uuids]
|
||||
(let [uuids (doall (map #(vector :sample-type/uuid (:uuid (get current-sample-map %))) sample-type-uuids))]
|
||||
(db/retract-entities uuids)))
|
||||
|
||||
(defn update-existing-samples
|
||||
[shipment-uuid current-sample-map updated-sample-map sample-type-uuids]
|
||||
(let [shipment-sample-uuids (sample-db/list-shipment-samples shipment-uuid)]
|
||||
(doseq [sample-type-uuid sample-type-uuids]
|
||||
(let [current-sample (get current-sample-map sample-type-uuid)
|
||||
sample-uuid (:uuid current-sample)
|
||||
current-sample-in-shipment (boolean (some #(= sample-uuid %) shipment-sample-uuids))
|
||||
updated-sample (get updated-sample-map sample-type-uuid)
|
||||
updated-sample-collected (boolean (:collected updated-sample))
|
||||
updated-sample-shipped (boolean (:shipped updated-sample))]
|
||||
; Update the existing shipment in the db with the new values
|
||||
(sample-db/create-or-update-sample sample-uuid (name sample-type-uuid) (:sample-id updated-sample)
|
||||
updated-sample-collected updated-sample-shipped)
|
||||
; If the current sample was marked as shipped, but the updated one was not then remove the sample from the associated shipment.
|
||||
(when (and current-sample-in-shipment (not updated-sample-shipped)) (sample-db/remove-sample-from-shipment shipment-uuid sample-uuid))
|
||||
; If the current sample was not marked as shipped, but the updated one is then add the sample to the associated shipment
|
||||
(when (and (not current-sample-in-shipment) updated-sample-shipped) (sample-db/add-sample-to-shipment shipment-uuid sample-uuid))))))
|
||||
|
||||
; Currently assumes one shipment per kit. May not be the case in the future.
|
||||
(defn create-or-update-shipment
|
||||
[kit-uuid air-waybill]
|
||||
(let [current-shipment-uuid (:uuid (first (shipment-db/list-shipments kit-uuid)))
|
||||
new-shipment-uuid (shipment-db/create-or-update-shipment current-shipment-uuid air-waybill)]
|
||||
(when (and (some? new-shipment-uuid) (not= new-shipment-uuid current-shipment-uuid))
|
||||
(kit-db/add-shipment-to-kit kit-uuid new-shipment-uuid))
|
||||
(if current-shipment-uuid
|
||||
current-shipment-uuid
|
||||
new-shipment-uuid)))
|
||||
|
||||
(defn create-update-delete-samples
|
||||
[kit-uuid shipment-uuid current-sample-map updated-sample-map]
|
||||
(let [updated-sample-type-uuids (set (keys updated-sample-map))
|
||||
current-sample-type-uuids (set (keys current-sample-map))
|
||||
new-sample-type-uuids (set/difference updated-sample-type-uuids current-sample-type-uuids)
|
||||
delete-sample-type-uuids (set/difference current-sample-type-uuids updated-sample-type-uuids)
|
||||
update-sample-type-uuids (set/intersection current-sample-type-uuids updated-sample-type-uuids)]
|
||||
(create-samples-and-add-to-kit-shipment kit-uuid shipment-uuid (select-keys updated-sample-map new-sample-type-uuids))
|
||||
(delete-samples current-sample-map delete-sample-type-uuids)
|
||||
(update-existing-samples shipment-uuid current-sample-map updated-sample-map update-sample-type-uuids)))
|
||||
|
||||
(defn update-existing-form-values
|
||||
[kit-uuid kit-type-uuid current-form-values new-form-values]
|
||||
(let [form-fields (form-type-db/get-form-type-fields kit-type-uuid)
|
||||
current-form-field-id-map (reduce (fn [m v] (assoc m (keyword (:field-id v)) (:uuid v))) {} current-form-values)]
|
||||
(doseq [form-value-field-id-key (keys new-form-values)]
|
||||
(let [current-form-value-id (get current-form-field-id-map form-value-field-id-key)
|
||||
updated-form-value-id (create-update-form-value (get current-form-field-id-map form-value-field-id-key) form-value-field-id-key form-fields new-form-values)]
|
||||
(when (nil? current-form-value-id) (kit-db/add-form-value-to-kit kit-uuid updated-form-value-id))))))
|
||||
|
||||
(defn delete-existing-form-values
|
||||
[current-form-values]
|
||||
(let [ids (map #(vector :form-value/uuid (:uuid %)) current-form-values)]
|
||||
(db/retract-entities ids)))
|
||||
|
||||
(defn delete-existing-create-new-form-values
|
||||
[kit-uuid kit-type-uuid current-form-values new-form-values]
|
||||
(delete-existing-form-values current-form-values)
|
||||
(create-form-values-and-add-to-kit kit-uuid kit-type-uuid new-form-values))
|
||||
|
||||
(defn update-form-values
|
||||
[kit-db-id current-kit-type-uuid updated-kit-type-uuid current-form-values new-form-values]
|
||||
(let [current-form-type (kit-type-db/get-kit-type-form-type current-kit-type-uuid)
|
||||
updated-form-type (kit-type-db/get-kit-type-form-type updated-kit-type-uuid)]
|
||||
; If the form-type hasn't changed we can just update the existing form values.
|
||||
; If it has changed then we should delete the existing values and create new ones.
|
||||
(if (= (:uuid current-form-type) (:uuid updated-form-type))
|
||||
(update-existing-form-values kit-db-id updated-kit-type-uuid current-form-values new-form-values)
|
||||
(delete-existing-create-new-form-values kit-db-id updated-kit-type-uuid current-form-values new-form-values))))
|
||||
|
||||
(defn get-kit-values
|
||||
[kit-uuid]
|
||||
(let [config-map {:uuid kit-uuid}
|
||||
kit (first (kit-db/list-kits config-map))
|
||||
sample-map (reduce (fn [m v] (assoc m (keyword (str (:sample-type-uuid v))) v)) {} (sample-db/list-samples config-map))
|
||||
form-values (form-value-db/list-form-values config-map)
|
||||
shipment (first (shipment-db/list-shipments config-map))]
|
||||
{:kit kit
|
||||
:samples sample-map
|
||||
:form-values form-values
|
||||
:shipment shipment}))
|
||||
|
||||
(defn remove-deleted-timepoints
|
||||
[kit-uuid updated-kit-map]
|
||||
(let [current-timepoints (timepoint-db/list-kit-timepoints {:uuid kit-uuid})
|
||||
current-timepoint-uuids (map :uuid current-timepoints)
|
||||
updated-timepoints-uuids (map #(UUID/fromString %) (:timepoints updated-kit-map))
|
||||
deleted-timepoints (set/difference (set current-timepoint-uuids) (set updated-timepoints-uuids))]
|
||||
(doseq [timepoint-uuid deleted-timepoints] (kit-db/remove-timepoint-from-kit kit-uuid timepoint-uuid))))
|
||||
|
||||
(defn update-kit-shipment
|
||||
[kit-uuid kit-map]
|
||||
(let [{:keys [air-waybill kit-type samples form-type-field-values]} kit-map
|
||||
config-map {:uuid kit-uuid}
|
||||
kit-type-uuid (UUID/fromString kit-type)
|
||||
current-kit (first (kit-db/list-kits config-map))
|
||||
current-sample-map (reduce (fn [m v] (assoc m (keyword (str (:sample-type-uuid v))) v)) {} (sample-db/list-samples config-map))
|
||||
current-form-values (form-value-db/list-form-values config-map)
|
||||
shipment-id (create-or-update-shipment kit-uuid air-waybill)]
|
||||
(kit-db/create-or-update-kit kit-uuid kit-map)
|
||||
(remove-deleted-timepoints kit-uuid kit-map)
|
||||
(create-update-delete-samples kit-uuid shipment-id current-sample-map samples)
|
||||
(update-form-values kit-uuid (:kit-type-uuid current-kit) kit-type-uuid current-form-values form-type-field-values)
|
||||
kit-uuid))
|
||||
|
||||
(defn update-kit-shipment-with-history
|
||||
[kit-uuid user kit-map]
|
||||
(let [current-kit-values (get-kit-values kit-uuid)]
|
||||
(update-kit-shipment kit-uuid kit-map)
|
||||
(history-db/create-history user :kit-shipment kit-uuid (str current-kit-values) (str (get-kit-values kit-uuid)))
|
||||
kit-uuid))
|
||||
|
||||
; If a kit hasn't been created, create it as complete otherwise update it to be complete. Send and email when done.
|
||||
(defn submit-kit-shipment
|
||||
[submitted-kit-uuid kit-map]
|
||||
(let [completed-kit-map (merge kit-map {:complete true :submission-timestamp (Date.)})
|
||||
kit-uuid (or submitted-kit-uuid (create-kit-shipment completed-kit-map))]
|
||||
(when-not (nil? submitted-kit-uuid)
|
||||
(update-kit-shipment kit-uuid completed-kit-map))
|
||||
(email/send-manifest-email kit-map kit-uuid)
|
||||
kit-uuid))
|
||||
|
||||
(defn set-kit-shipment-archived
|
||||
[kit-uuid user archived]
|
||||
(let [current-kit-values (get-kit-values kit-uuid)]
|
||||
(kit-db/set-archived kit-uuid archived)
|
||||
(doseq [shipment (shipment-db/list-shipments {:uuid kit-uuid})]
|
||||
(shipment-db/set-archived (:uuid shipment) archived))
|
||||
(history-db/create-history user :kit-shipment kit-uuid (str current-kit-values) (str (get-kit-values kit-uuid)))
|
||||
kit-uuid))
|
||||
179
src/clj/org/parkerici/sample_tracking/api/kit_type.clj
Normal file
179
src/clj/org/parkerici/sample_tracking/api/kit_type.clj
Normal file
@@ -0,0 +1,179 @@
|
||||
(ns org.parkerici.sample-tracking.api.kit-type
|
||||
"This file is focused on taking in a csv with kit and sample type definitions
|
||||
along with the studies, cohorts, and timepoints they belong to,
|
||||
parsing that csv, and then creating the appropriate values in the database."
|
||||
(:require [org.parkerici.sample-tracking.utils.csv :as csv]
|
||||
[org.parkerici.sample-tracking.configuration :as config]
|
||||
[clojure.string :as str]
|
||||
[clojure.set :as set]
|
||||
[org.parkerici.sample-tracking.db.study :as study-db]
|
||||
[org.parkerici.sample-tracking.db.cohort :as cohort-db]
|
||||
[org.parkerici.sample-tracking.db.kit-type :as kit-type-db]
|
||||
[org.parkerici.sample-tracking.db.timepoint :as timepoint-db]
|
||||
[org.parkerici.sample-tracking.db.sample-type :as sample-type-db]
|
||||
[org.parkerici.sample-tracking.db.sample-attribute :as sample-attribute-db]))
|
||||
|
||||
; Used with filter to remove rows in the input CSV that don't have the required values.
|
||||
(defn row-has-required-values
|
||||
[row]
|
||||
(not (or (empty? (:study-name row))
|
||||
(empty? (:cohort-name row))
|
||||
(empty? (:kit-item-no row))
|
||||
(empty? (:kit-name row))
|
||||
(empty? (:sample-id-suffix row))
|
||||
(empty? (:sample-name row))
|
||||
(empty? (:kit-timepoints row))
|
||||
(empty? (:ships-with-kit row)))))
|
||||
|
||||
(defn read-csv
|
||||
[fpath]
|
||||
(filter row-has-required-values (drop 1 (csv/read-csv-file fpath))))
|
||||
|
||||
(defn find-or-create-study
|
||||
[study-name]
|
||||
(or (:uuid (study-db/find-study-by-name study-name)) (study-db/create-study study-name)))
|
||||
|
||||
(defn find-or-create-cohort-and-add-to-study
|
||||
[study-uuid cohort-name]
|
||||
(let [cohort-uuid (or (:uuid (cohort-db/find-cohort-by-name-and-study cohort-name study-uuid) (cohort-db/create-cohort cohort-name study-uuid)))]
|
||||
(when-not (study-db/cohort-associated-with-study study-uuid cohort-uuid)
|
||||
(study-db/add-cohort-to-study study-uuid cohort-uuid))
|
||||
cohort-uuid))
|
||||
|
||||
(defn create-kit-type-and-add-to-cohort
|
||||
[cohort-uuid kit-name kit-item-number vendor-email collection-date-required air-waybill-required]
|
||||
(let [kit-type-uuid (kit-type-db/create-kit-type kit-name kit-item-number vendor-email collection-date-required air-waybill-required)]
|
||||
(cohort-db/add-kit-type-to-cohort cohort-uuid kit-type-uuid)
|
||||
kit-type-uuid))
|
||||
|
||||
(defn add-attribute-to-sample-type
|
||||
[sample-type-uuid attribute value]
|
||||
(let [attribute-uuid (or (sample-attribute-db/find-sample-attribute attribute) (sample-attribute-db/create-sample-attribute attribute))
|
||||
value-uuid (or (sample-attribute-db/find-sample-attribute-value value attribute-uuid) (sample-attribute-db/create-sample-attribute-value value attribute-uuid))]
|
||||
(sample-type-db/add-attribute-value-to-sample-type sample-type-uuid value-uuid)))
|
||||
|
||||
(defn find-or-create-timepoint-and-add-to-kit-type
|
||||
[kit-type-uuid timepoint-name]
|
||||
(let [timepoint-uuid (or (timepoint-db/find-timepoint-uuid-from-name timepoint-name) (timepoint-db/create-timepoint timepoint-name))]
|
||||
(kit-type-db/add-timepoint-to-kit-type timepoint-uuid kit-type-uuid)))
|
||||
|
||||
(defn create-sample-type-and-add-to-kit-type
|
||||
[kit-type-uuid sample-name sample-id-suffix sample-ships-with-kit sample-reminder attributes-and-values]
|
||||
(let [sample-type-uuid (sample-type-db/create-sample-type sample-name sample-id-suffix sample-ships-with-kit sample-reminder)]
|
||||
(doseq [[attribute value] attributes-and-values]
|
||||
(add-attribute-to-sample-type sample-type-uuid attribute value))
|
||||
(kit-type-db/add-sample-type-to-kit-type sample-type-uuid kit-type-uuid)
|
||||
sample-type-uuid))
|
||||
|
||||
(defn parse-boolean
|
||||
[value]
|
||||
(case (str/lower-case value)
|
||||
"yes" true
|
||||
"no" false))
|
||||
|
||||
; Attributes and values are taken from any extra columns in the input CSV.
|
||||
; The first n columns are expected to map to the column names in (config/csv-file-headers :kit-type)
|
||||
; Any remaining columns after the first column are taken as attributes and values.
|
||||
; The column header is used as the attribute and the cell value for the row is used as the value for that attribute.
|
||||
(defn get-attributes-and-values
|
||||
[row]
|
||||
(let [attributes (set/difference (set (keys row)) (set (config/csv-file-headers :kit-type)))]
|
||||
(select-keys row attributes)))
|
||||
|
||||
(defn update-sample-types-map
|
||||
[row kit-type-map]
|
||||
(let [sample-types (or (:sample-types kit-type-map) [])
|
||||
sample-attributes-and-values (get-attributes-and-values row)
|
||||
sample-type {:name (:sample-name row)
|
||||
:id-suffix (:sample-id-suffix row)
|
||||
:ships-with-kit (parse-boolean (:ships-with-kit row))
|
||||
:reminders (:sample-reminders row)
|
||||
:attributes-and-values sample-attributes-and-values}
|
||||
updated-sample-types (conj sample-types sample-type)]
|
||||
updated-sample-types))
|
||||
|
||||
(defn update-kit-type-map
|
||||
[row kit-name cohort-map]
|
||||
(let [kit-type-map (or (get cohort-map kit-name) {})
|
||||
timepoints (or (:timepoints kit-type-map) (csv/split-csv-string (:kit-timepoints row)))
|
||||
item-number (or (:item-number kit-type-map) (Integer/parseInt (:kit-item-no row)))
|
||||
vendor-email (or (:vendor-email kit-type-map) (:vendor-email row))
|
||||
collection-date-required (or (:collection-date-required kit-type-map) (not (= (str/lower-case (:collection-date-optional row)) "true")))
|
||||
air-waybill-required (or (:air-waybill-required kit-type-map) (not (= (str/lower-case (:air-waybill-optional row)) "true")))
|
||||
sample-types (update-sample-types-map row kit-type-map)
|
||||
updated-kit-type-map (-> kit-type-map
|
||||
(assoc :sample-types sample-types)
|
||||
(assoc :timepoints timepoints)
|
||||
(assoc :item-number item-number)
|
||||
(assoc :vendor-email vendor-email)
|
||||
(assoc :collection-date-required collection-date-required)
|
||||
(assoc :air-waybill-required air-waybill-required))]
|
||||
updated-kit-type-map))
|
||||
|
||||
(defn update-cohort-map
|
||||
[row cohort-name study-map]
|
||||
(let [cohort-map (or (get study-map cohort-name) {})
|
||||
kit-name (:kit-name row)
|
||||
kit-type-map (update-kit-type-map row kit-name cohort-map)
|
||||
updated-cohort-map (assoc cohort-map kit-name kit-type-map)]
|
||||
updated-cohort-map))
|
||||
|
||||
(defn update-study-map
|
||||
[row study-name m]
|
||||
(let [study-map (or (get m study-name) {})
|
||||
cohort-name (:cohort-name row)
|
||||
cohort-map (update-cohort-map row cohort-name study-map)
|
||||
updated-study-map (assoc study-map cohort-name cohort-map)]
|
||||
updated-study-map))
|
||||
|
||||
(defn build-type-map
|
||||
[csv-data]
|
||||
(reduce (fn [type-map row]
|
||||
(let [study-name (:study-name row)
|
||||
study-map (update-study-map row study-name type-map)
|
||||
updated-m (assoc type-map study-name study-map)]
|
||||
updated-m)) {} csv-data))
|
||||
|
||||
(defn process-sample-type-map
|
||||
[kit-type-uuid sample-type-map]
|
||||
(create-sample-type-and-add-to-kit-type kit-type-uuid
|
||||
(:name sample-type-map)
|
||||
(:id-suffix sample-type-map)
|
||||
(:ships-with-kit sample-type-map)
|
||||
(:reminders sample-type-map)
|
||||
(:attributes-and-values sample-type-map)))
|
||||
|
||||
(defn process-kit-type-map
|
||||
[cohort-uuid kit-type-name kit-type-map]
|
||||
(when-not (kit-type-db/find-active-kit-type-by-name-and-cohort kit-type-name cohort-uuid)
|
||||
(let [kit-type-uuid (create-kit-type-and-add-to-cohort
|
||||
cohort-uuid kit-type-name (:item-number kit-type-map) (:vendor-email kit-type-map)
|
||||
(:collection-date-required kit-type-map) (:air-waybill-required kit-type-map))]
|
||||
(doseq [timepoint (:timepoints kit-type-map)]
|
||||
(find-or-create-timepoint-and-add-to-kit-type kit-type-uuid timepoint))
|
||||
(doseq [sample-type (:sample-types kit-type-map)]
|
||||
(process-sample-type-map kit-type-uuid sample-type)))))
|
||||
|
||||
(defn process-cohort-map
|
||||
[study-uuid cohort-name cohort-map]
|
||||
(let [cohort-uuid (find-or-create-cohort-and-add-to-study study-uuid cohort-name)]
|
||||
(doseq [kit-type (keys cohort-map)]
|
||||
(process-kit-type-map cohort-uuid kit-type (get cohort-map kit-type)))))
|
||||
|
||||
(defn process-study-map
|
||||
[study-name study-map]
|
||||
(let [study-uuid (find-or-create-study study-name)]
|
||||
(doseq [cohort (keys study-map)]
|
||||
(process-cohort-map study-uuid cohort (get study-map cohort)))))
|
||||
|
||||
(defn process-type-map
|
||||
[type-map]
|
||||
(doseq [study (keys type-map)]
|
||||
(process-study-map study (get type-map study))))
|
||||
|
||||
(defn parse-kit-type-csv-and-save-to-db
|
||||
[fpath]
|
||||
(let [csv-headers (config/csv-file-headers :kit-type)
|
||||
csv-data (csv/read-csv-into-map fpath csv-headers row-has-required-values)
|
||||
type-map (build-type-map csv-data)]
|
||||
(process-type-map type-map)))
|
||||
20
src/clj/org/parkerici/sample_tracking/api/migrate.clj
Normal file
20
src/clj/org/parkerici/sample_tracking/api/migrate.clj
Normal file
@@ -0,0 +1,20 @@
|
||||
(ns org.parkerici.sample-tracking.api.migrate
|
||||
(:require [org.parkerici.sample-tracking.db.migration :as migration]
|
||||
[org.parkerici.sample-tracking.db.migration.air-waybill-required :as air-waybill-migration]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defn migrate-kit-types-without-air-waybill-required
|
||||
[]
|
||||
(doseq [to-migrate (air-waybill-migration/list-kit-types-without-air-waybill-required)]
|
||||
(air-waybill-migration/set-kit-type-air-waybill-required (:uuid to-migrate) true)))
|
||||
|
||||
(defn run-migration
|
||||
[name fn]
|
||||
(when-not (migration/migration-has-been-run name)
|
||||
(log/info "Running migration" name)
|
||||
(fn)
|
||||
(migration/create-migration name)))
|
||||
|
||||
(defn run-pending-migrations
|
||||
[]
|
||||
(run-migration "add-kit-type-air-waybill" migrate-kit-types-without-air-waybill-required))
|
||||
181
src/clj/org/parkerici/sample_tracking/api/propose_kit_edits.clj
Normal file
181
src/clj/org/parkerici/sample_tracking/api/propose_kit_edits.clj
Normal file
@@ -0,0 +1,181 @@
|
||||
(ns org.parkerici.sample-tracking.api.propose-kit-edits
|
||||
(:require [clojure.edn :as edn]
|
||||
[org.parkerici.sample-tracking.api.kit-shipment :as kit-shipment]
|
||||
[org.parkerici.sample-tracking.db.proposed-kit-edit :as proposed-kit-edit-db]
|
||||
[org.parkerici.sample-tracking.db.site :as site-db]
|
||||
[org.parkerici.sample-tracking.db.study :as study-db]
|
||||
[org.parkerici.sample-tracking.db.cohort :as cohort-db]
|
||||
[org.parkerici.sample-tracking.db.form-type :as form-type-db]
|
||||
[org.parkerici.sample-tracking.db.kit-type :as kit-type-db]
|
||||
[org.parkerici.sample-tracking.db.sample-type :as sample-type-db]
|
||||
[org.parkerici.sample-tracking.db.timepoint :as timepoint-db]
|
||||
[org.parkerici.sample-tracking.api.email :as email]
|
||||
[org.parkerici.sample-tracking.utils.collection :as coll-utils])
|
||||
(:import (java.util UUID)))
|
||||
|
||||
(defn propose-kit-edits
|
||||
[kit-map user-email]
|
||||
(let [kit-uuid (UUID/fromString (:uuid kit-map))
|
||||
pending-proposed-edit-uuid (:uuid (first (proposed-kit-edit-db/list-proposed-edits {:kit-uuid kit-uuid :status "pending"})))
|
||||
uuid (proposed-kit-edit-db/create-or-update-proposed-edit pending-proposed-edit-uuid kit-uuid (str kit-map) user-email)]
|
||||
(email/send-proposed-edit-email kit-map user-email)
|
||||
uuid))
|
||||
|
||||
(defn list-proposed-edits
|
||||
[config-map]
|
||||
(let [proposed-edits (proposed-kit-edit-db/list-proposed-edits config-map)
|
||||
timepoints (timepoint-db/list-kit-timepoints config-map)]
|
||||
(coll-utils/merge-map-colls proposed-edits :kit-uuid timepoints :kit-uuid :timepoints)))
|
||||
|
||||
(defn convert-update-map-to-display-map
|
||||
"There are three formats of maps for kits. Form maps for populating edit forms, display maps for generating a view page
|
||||
using manifest.cljc, and create/update maps for creating or updating kits.
|
||||
This function converts an update map to a display map."
|
||||
[update-map]
|
||||
(let [kit-type-uuid (UUID/fromString (:kit-type update-map))
|
||||
site-name (:name (site-db/find-site-by-uuid (UUID/fromString (:site update-map))))
|
||||
study-name (:name (study-db/find-study-by-uuid (UUID/fromString (:study update-map))))
|
||||
cohort-name (:name (cohort-db/find-cohort-by-uuid (UUID/fromString (:cohort update-map))))
|
||||
timepoint-names (map #(:name (timepoint-db/find-timepoint-by-uuid (UUID/fromString %))) (:timepoints update-map))
|
||||
kit-name (kit-type-db/get-kit-type-name kit-type-uuid)
|
||||
collection-timestamp (:collection-timestamp update-map)
|
||||
selected-form-type-fields (form-type-db/get-form-type-fields kit-type-uuid)
|
||||
selected-sample-types (sample-type-db/list-sample-types kit-type-uuid)
|
||||
unchaged-update-map (select-keys update-map [:kit-id :participant-id :form-type-field-values :air-waybill
|
||||
:completing-first-name :completing-last-name :completing-email
|
||||
:comments :complete])
|
||||
display-map {:site-name site-name
|
||||
:study-name study-name
|
||||
:cohort-name cohort-name
|
||||
:timepoint-names timepoint-names
|
||||
:kit-name kit-name
|
||||
:collection-date collection-timestamp
|
||||
:collection-time collection-timestamp
|
||||
:selected-form-type-fields selected-form-type-fields
|
||||
:selected-sample-types selected-sample-types
|
||||
:sample-values (:samples update-map)
|
||||
:archived false
|
||||
}]
|
||||
(merge display-map unchaged-update-map)))
|
||||
|
||||
(defn convert-form-map-to-display-map
|
||||
"There are three formats of maps for kits. Form maps for populating edit forms, display maps for generating a view page
|
||||
using manifest.cljc, and create/update maps for creating or updating kits.
|
||||
This function converts an edit form map to a display map."
|
||||
[original-map]
|
||||
(let [kit-type-uuid (:kit-type-uuid original-map)
|
||||
site-name (:name (site-db/find-site-by-uuid (:site-uuid original-map)))
|
||||
study-name (:name (study-db/find-study-by-uuid (:study-uuid original-map)))
|
||||
cohort-name (:name (cohort-db/find-cohort-by-uuid (:cohort-uuid original-map)))
|
||||
timepoint-names (map :timepoint-name (:timepoints original-map))
|
||||
kit-name (kit-type-db/get-kit-type-name kit-type-uuid)
|
||||
collection-timestamp (:collection-timestamp original-map)
|
||||
selected-form-type-fields (form-type-db/get-form-type-fields kit-type-uuid)
|
||||
form-type-field-values (reduce (fn [m v]
|
||||
(assoc m (keyword (:field-id v)) (:value v))) {} (:form-values original-map))
|
||||
selected-sample-types (sample-type-db/list-sample-types kit-type-uuid)
|
||||
samples (reduce (fn [m s] (assoc m (:sample-type-uuid s) s)) {} (:samples original-map))
|
||||
air-waybill (:air-waybill (first (:shipments original-map)))
|
||||
unchaged-map-entries (select-keys original-map [:kit-id :participant-id :completing-first-name
|
||||
:completing-last-name :completing-email :comments :complete
|
||||
:archived])
|
||||
display-map {:site-name site-name
|
||||
:study-name study-name
|
||||
:cohort-name cohort-name
|
||||
:timepoint-names timepoint-names
|
||||
:kit-name kit-name
|
||||
:collection-date collection-timestamp
|
||||
:collection-time collection-timestamp
|
||||
:selected-form-type-fields selected-form-type-fields
|
||||
:form-type-field-values form-type-field-values
|
||||
:selected-sample-types selected-sample-types
|
||||
:sample-values samples
|
||||
:air-waybill air-waybill
|
||||
:archived false
|
||||
}]
|
||||
(merge display-map unchaged-map-entries)))
|
||||
|
||||
(defn get-proposed-kit-edit-for-display
|
||||
[uuid]
|
||||
(let [proposed-edit (first (proposed-kit-edit-db/list-proposed-edits {:uuid uuid}))
|
||||
tx-id (proposed-kit-edit-db/get-proposed-edit-tx-id uuid)
|
||||
unedited-map (first (kit-shipment/list-kit-shipment {:uuid (:kit-uuid proposed-edit) :tx-id tx-id}))
|
||||
unedited-display-map (convert-form-map-to-display-map unedited-map)
|
||||
update-map (edn/read-string (:update-map proposed-edit))
|
||||
updated-display-map (convert-update-map-to-display-map update-map)]
|
||||
{:original-map unedited-display-map :update-map updated-display-map :status (:status proposed-edit)}))
|
||||
|
||||
(defn get-proposed-kit-edit
|
||||
[uuid]
|
||||
(first (proposed-kit-edit-db/list-proposed-edits {:uuid uuid})))
|
||||
|
||||
(defn approve-proposed-kit-edit
|
||||
[uuid reviewing-user]
|
||||
(let [proposed-edit (first (proposed-kit-edit-db/list-proposed-edits {:uuid uuid}))
|
||||
update-map (edn/read-string (:update-map proposed-edit))]
|
||||
(kit-shipment/update-kit-shipment-with-history (:kit-uuid proposed-edit) reviewing-user update-map)
|
||||
(proposed-kit-edit-db/approve-proposed-edit uuid reviewing-user)))
|
||||
|
||||
(defn deny-proposed-kit-edit
|
||||
[uuid reviewing-user]
|
||||
(proposed-kit-edit-db/deny-proposed-edit uuid reviewing-user))
|
||||
|
||||
(defn kit-has-pending-edits
|
||||
[uuid]
|
||||
(> (count (proposed-kit-edit-db/list-proposed-edits {:uuid uuid :status "pending"})) 0))
|
||||
|
||||
(defn convert-update-map-form-values
|
||||
[kit-type form-type-field-values]
|
||||
(let [form-type-fields (form-type-db/get-form-type-fields (UUID/fromString kit-type))
|
||||
updated-form-values form-type-field-values]
|
||||
(map (fn [field-key]
|
||||
(let [field (first (filter #(= (name field-key) (:field-id %)) form-type-fields))
|
||||
value (get updated-form-values field-key)
|
||||
field-type (:type field)
|
||||
field-id (:field-id field)]
|
||||
(-> {}
|
||||
(assoc :field-id field-id)
|
||||
(assoc :value value)
|
||||
(assoc :field-type field-type))))
|
||||
(keys updated-form-values))))
|
||||
|
||||
(defn convert-update-map-to-form-map
|
||||
" There are three formats of maps for kits. Form maps for populating edit forms, display maps for generating a view page
|
||||
using manifest.cljc, and create/update maps for creating or updating kits.
|
||||
This function converts an update map to an edit form map."
|
||||
[proposed-edit]
|
||||
(let [proposed-edit-uuid (:uuid proposed-edit)
|
||||
proposed-edit-email (:email proposed-edit)
|
||||
proposed-edit-time (:time proposed-edit)
|
||||
update-map (edn/read-string (:update-map proposed-edit))
|
||||
{:keys [samples timepoints form-type-field-values air-waybill site kit-type cohort study]} update-map
|
||||
form-samples (map (fn [sample-type-uuid]
|
||||
(let [sample (get samples sample-type-uuid)]
|
||||
(assoc sample :sample-type-uuid sample-type-uuid)))
|
||||
(keys samples))
|
||||
form-timepoints (map #(assoc {} :uuid %) timepoints)
|
||||
shipments [{:air-waybill air-waybill}]
|
||||
form-values (convert-update-map-form-values kit-type form-type-field-values)
|
||||
unchaged-map-entries (select-keys update-map [:timezone :completing-last-name :collection-timestamp
|
||||
:completing-email :comments :completing-first-name
|
||||
:participant-id :kit-id :uuid])
|
||||
form-map {:pending-edits [{:uuid proposed-edit-uuid :email proposed-edit-email :time proposed-edit-time}]
|
||||
:samples form-samples
|
||||
:timepoints form-timepoints
|
||||
:shipments shipments
|
||||
:site-uuid site
|
||||
:kit-type-uuid kit-type
|
||||
:cohort-uuid cohort
|
||||
:form-values form-values
|
||||
:study-uuid study}]
|
||||
(merge unchaged-map-entries form-map)))
|
||||
|
||||
(defn get-kit-or-proposed-edit
|
||||
"Gets a kit map if there is no pending proposed edit, otherwise gets a map of the pending proposed edit. When using
|
||||
this function you should get the kit from kit-shipment first so that the email filtering from the config-map is used."
|
||||
[config-map]
|
||||
(let [kit (first (kit-shipment/list-kit-shipment config-map))]
|
||||
(if (= (count (:pending-edits kit)) 0)
|
||||
kit
|
||||
(let [pending-proposed-edit (first (proposed-kit-edit-db/list-proposed-edits {:kit-uuid (:uuid kit) :status "pending"}))]
|
||||
(convert-update-map-to-form-map pending-proposed-edit)))))
|
||||
31
src/clj/org/parkerici/sample_tracking/api/site.clj
Normal file
31
src/clj/org/parkerici/sample_tracking/api/site.clj
Normal file
@@ -0,0 +1,31 @@
|
||||
(ns org.parkerici.sample-tracking.api.site
|
||||
"This file deals with reading in a csv with sites along with the studies they are running,
|
||||
parsing that csv, and then creating the appropriate associations of sites with studies in the database."
|
||||
(:require [org.parkerici.sample-tracking.utils.csv :as csv]
|
||||
[org.parkerici.sample-tracking.configuration :as config]
|
||||
[org.parkerici.sample-tracking.db.study :as study-db]
|
||||
[org.parkerici.sample-tracking.db.site :as site-db]))
|
||||
|
||||
; Used with filter to remove rows in the input CSV that don't have the required values.
|
||||
(defn row-has-required-values
|
||||
[row]
|
||||
(not (or (empty? (:site row))
|
||||
(empty? (:study-names row)))))
|
||||
|
||||
(defn find-or-create-site
|
||||
[name]
|
||||
(or (:id (site-db/find-site-by-name name)) (site-db/create-site name)))
|
||||
|
||||
(defn create-site
|
||||
[row]
|
||||
(let [site-uuid (find-or-create-site (:site row))
|
||||
study-names (csv/split-csv-string (:study-names row))]
|
||||
(doseq [study-name study-names]
|
||||
(when-not (study-db/site-is-associated-with-study site-uuid study-name)
|
||||
(study-db/add-site-to-study site-uuid study-name)))))
|
||||
|
||||
(defn parse-site-csv-and-save-to-db
|
||||
[fpath]
|
||||
(let [csv-headers (config/csv-file-headers :site)
|
||||
csv-data (csv/read-csv-into-map fpath csv-headers row-has-required-values)]
|
||||
(doseq [row csv-data] (create-site row))))
|
||||
28
src/clj/org/parkerici/sample_tracking/api/study.clj
Normal file
28
src/clj/org/parkerici/sample_tracking/api/study.clj
Normal file
@@ -0,0 +1,28 @@
|
||||
(ns org.parkerici.sample-tracking.api.study
|
||||
"This file deals with reading in a csv with studies along with validation information for participant and kit ids,
|
||||
parsing that csv, and then creating the appropriate associations of studies with validation information."
|
||||
(:require [org.parkerici.sample-tracking.utils.csv :as csv]
|
||||
[org.parkerici.sample-tracking.configuration :as config]
|
||||
[org.parkerici.sample-tracking.db.study :as study-db]
|
||||
[org.parkerici.sample-tracking.utils.str :as str]))
|
||||
|
||||
; Used with filter to remove rows in the input CSV that don't have the required values.
|
||||
(defn row-has-required-values
|
||||
[row]
|
||||
(str/not-blank? (:study row)))
|
||||
|
||||
(defn add-validation-to-study
|
||||
[row]
|
||||
(let [{:keys [study participant-id-prefix participant-id-regex participant-id-validation-message kit-id-prefix
|
||||
kit-id-regex kit-id-validation-message]} row]
|
||||
(when (str/not-blank? participant-id-regex)
|
||||
(study-db/add-participant-id-validation-to-study study participant-id-prefix participant-id-regex participant-id-validation-message))
|
||||
(when (str/not-blank? kit-id-regex)
|
||||
(study-db/add-kit-id-validation-to-study study kit-id-prefix kit-id-regex kit-id-validation-message))))
|
||||
|
||||
(defn parse-study-csv-and-save-to-db
|
||||
[fpath]
|
||||
(let [csv-headers (config/csv-file-headers :study)
|
||||
csv-data (csv/read-csv-into-map fpath csv-headers row-has-required-values)]
|
||||
(doseq [row csv-data]
|
||||
(add-validation-to-study row))))
|
||||
Reference in New Issue
Block a user