First commit. Taken from open source branch of internal sample-tracking application.

This commit is contained in:
Robert Schiemann
2023-03-08 10:11:42 -07:00
parent fa491f63de
commit fe0946c53a
220 changed files with 15808 additions and 0 deletions

View 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))))

View 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)))

View 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))

View 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))))

View 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))

View 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))

View 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)))

View 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))

View 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)))))

View 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))))

View 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))))