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