A namespace to handle getting the last edited information about items that satisfy the revision system. The revision
system is a 'reversion' system, built to easily revert to previous states and can compute on demand a changelog. The
revision system works through events and so when editing something, you should construct the last-edit-info
yourself (using This constructs | (ns metabase.revisions.models.revision.last-edit (:require [clj-time.core :as time] [clojure.set :as set] [medley.core :as m] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [steffan-westcott.clj-otel.api.trace.span :as span] [toucan2.core :as t2])) |
(def ^:private model->db-model {:card "Card" :dashboard "Dashboard"}) | |
Schema of the these are all maybes as sometimes revisions don't exist, or users might be missing the names, etc | (def ^:private LastEditInfo [:map [:timestamp [:maybe :any]] [:id [:maybe ms/PositiveInt]] [:first_name [:maybe :string]] [:last_name [:maybe :string]] [:email [:maybe :string]]]) |
Spec for an item annotated with last-edit-info. Items are cards or dashboards. Optional because we may not always have revision history for all cards/dashboards. | (def MaybeAnnotated [:map [:last-edit-info {:optional true} LastEditInfo]]) |
(mu/defn with-last-edit-info :- [:maybe [:sequential MaybeAnnotated]] "Add the last edited information to a card. Will add a key `:last-edit-info`. Model should be one of `:dashboard` or `:card`. Gets the last edited information from the revisions table. If you need this information from a put route, use `@api/*current-user*` and a current timestamp since revisions are events and asynchronous." [items model :- [:enum :dashboard :card]] (let [ids (into #{} (map :id) items)] (span/with-span! {:name "with-last-edit-info" :attributes {:item/ids ids}} (when (seq ids) (let [id->updated-info (reduce (fn [m card-updated-info] (assoc m (:model_id card-updated-info) (select-keys card-updated-info [:id :email :first_name :last_name :timestamp]))) {} (t2/reducible-query {:select [:r.model_id :u.id :u.email :u.first_name :u.last_name :r.timestamp] :from [[:revision :r]] :left-join [[:core_user :u] [:= :u.id :r.user_id]] :where [:and [:= :r.most_recent true] [:= :r.model (model->db-model model)] [:in :r.model_id ids]]}))] (map (fn [item] (m/assoc-some item :last-edit-info (-> item :id id->updated-info))) items)))))) | |
(mu/defn edit-information-for-user :- LastEditInfo "Construct the `:last-edit-info` map given a user. Useful for editing routes. Most edit info information comes from the revisions table. But this table is populated from events asynchronously so when editing and wanting last-edit-info, you must construct it from `@api/*current-user*` and the current timestamp rather than checking the revisions table as those revisions may not be present yet." [user] (merge {:timestamp (time/now)} (select-keys user [:id :first_name :last_name :email]))) | |
Schema for the map of bulk last-item-info. A map of two keys, | (def ^:private CollectionLastEditInfo [:map [:card {:optional true} [:map-of :int LastEditInfo]] [:dashboard {:optional true} [:map-of :int LastEditInfo]]]) |
(mu/defn fetch-last-edited-info :- [:maybe CollectionLastEditInfo] "Fetch edited info from the revisions table. Revision information is timestamp, user id, email, first and last name. Takes card-ids and dashboard-ids and returns a map structured like {:card {card_id {:id :email :first_name :last_name :timestamp}} :dashboard {dashboard_id {:id :email :first_name :last_name :timestamp}}}" [{:keys [card-ids dashboard-ids]}] (when (seq (concat card-ids dashboard-ids)) (let [latest-changes (t2/query {:select [:u.id :u.email :u.first_name :u.last_name :r.model :r.model_id :r.timestamp] :from [[:revision :r]] :left-join [[:core_user :u] [:= :u.id :r.user_id]] :where [:and [:= :r.most_recent true] (into [:or] (keep (fn [[model-name ids]] (when (seq ids) [:and [:= :model model-name] [:in :model_id ids]]))) [["Card" card-ids] ["Dashboard" dashboard-ids]])]})] (->> latest-changes (group-by :model) (m/map-vals (fn [model-changes] (into {} (map (juxt :model_id #(dissoc % :model :model_id))) model-changes))) ;; keys are "Card" and "Dashboard" (model in revision table) back to keywords (m/map-keys (set/map-invert model->db-model)))))) | |