Skip to content
Snippets Groups Projects
Commit c9bd2807 authored by Allen Gilliland's avatar Allen Gilliland
Browse files

activity feed entries for segments.

parent 727c522a
No related branches found
No related tags found
No related merge requests found
......@@ -4,10 +4,11 @@
[metabase.db :as db]
[metabase.config :as config]
[metabase.events :as events]
(metabase.models [activity :refer [Activity]]
(metabase.models [activity :refer [Activity] :as activity]
[dashboard :refer [Dashboard]]
[database :refer [Database]]
[session :refer [Session first-session-for-user]])))
[session :refer [Session first-session-for-user]]
[table :as table])))
(def activity-feed-topics
......@@ -22,6 +23,9 @@
:install
:pulse-create
:pulse-delete
:segment-create
:segment-update
:segment-delete
:user-login})
(def ^:private activity-feed-channel
......@@ -32,34 +36,16 @@
;;; ## ---------------------------------------- EVENT PROCESSING ----------------------------------------
(defn- record-activity
"Simple base function for recording activity using defaults.
Allows caller to specify a custom serialization function to apply to `object` to generate the activity `:details`."
([topic object details-fn database-table-fn]
(let [{:keys [table-id database-id]} (when (fn? database-table-fn)
(database-table-fn object))]
(db/ins Activity
:topic topic
:user_id (events/object->user-id object)
:model (events/topic->model topic)
:model_id (events/object->model-id topic object)
:database_id database-id
:table_id table-id
:custom_id (:custom_id object)
:details (if (fn? details-fn)
(details-fn object)
object))))
([topic object details-fn]
(record-activity topic object details-fn nil))
([topic object]
(record-activity topic object nil)))
(defn- process-card-activity [topic object]
(let [details-fn #(select-keys % [:name :description :public_perms])
database-table-fn (fn [obj]
{:database-id (get-in obj [:dataset_query :database])
:table-id (get-in obj [:dataset_query :query :source_table])})]
(record-activity topic object details-fn database-table-fn)))
(let [details-fn #(select-keys % [:name :description :public_perms])
database-id (get-in object [:dataset_query :database])
table-id (get-in object [:dataset_query :query :source_table])]
(activity/record-activity
:topic topic
:object object
:details-fn details-fn
:database-id database-id
:table-id table-id)))
(defn- process-dashboard-activity [topic object]
(let [create-delete-details #(select-keys % [:description :name :public_perms])
......@@ -74,10 +60,22 @@
(assoc :id id)
(assoc :card_id card_id))))))]
(case topic
:dashboard-create (record-activity topic object create-delete-details)
:dashboard-delete (record-activity topic object create-delete-details)
:dashboard-add-cards (record-activity topic object add-remove-card-details)
:dashboard-remove-cards (record-activity topic object add-remove-card-details))))
:dashboard-create (activity/record-activity
:topic topic
:object object
:details-fn create-delete-details)
:dashboard-delete (activity/record-activity
:topic topic
:object object
:details-fn create-delete-details)
:dashboard-add-cards (activity/record-activity
:topic topic
:object object
:details-fn add-remove-card-details)
:dashboard-remove-cards (activity/record-activity
:topic topic
:object object
:details-fn add-remove-card-details))))
;; disabled for now as it's overly verbose in the feed
;(defn- process-database-activity [topic object]
......@@ -99,17 +97,30 @@
(defn- process-pulse-activity [topic object]
(let [details-fn #(select-keys % [:name :public_perms])]
(record-activity topic object details-fn)))
(activity/record-activity
:topic topic
:object object
:details-fn details-fn)))
(defn- process-segment-activity [topic object]
(let [details-fn #(select-keys % [:name :description :revision_message])
table-id (:table_id object)
database-id (table/table-id->database-id table-id)]
(activity/record-activity
:topic topic
:object object
:details-fn details-fn
:database-id database-id
:table-id table-id)))
(defn- process-user-activity [topic object]
;; we only care about login activity when its the users first session (a.k.a. new user!)
(when (and (= :user-login topic)
(= (:session_id object) (first-session-for-user (:user_id object))))
(db/ins Activity
(activity/record-activity
:topic :user-joined
:user_id (:user_id object)
:model (events/topic->model topic)
:model_id (:user_id object))))
:user-id (:user_id object)
:model-id (:user_id object))))
(defn process-activity-event
"Handle processing for a single event notification received on the activity-feed-channel"
......@@ -123,6 +134,7 @@
"install" (when-not (db/sel :one :fields [Activity :id])
(db/ins Activity :topic "install" :model "install"))
"pulse" (process-pulse-activity topic object)
"segment" (process-segment-activity topic object)
"user" (process-user-activity topic object)))
(catch Throwable e
(log/warn (format "Failed to process activity event. %s" (:topic activity-event)) e))))
......
(ns metabase.models.activity
(:require [korma.core :refer :all, :exclude [defentity update]]
[metabase.db :refer [exists?]]
[metabase.db :as db]
[metabase.events :as events]
(metabase.models [card :refer [Card]]
[dashboard :refer [Dashboard]]
[database :refer [Database]]
[interface :refer :all]
[pulse :refer [Pulse]]
[segment :refer [Segment]]
[table :refer [Table]]
[user :refer [User]])
[metabase.util :as u]))
......@@ -21,25 +22,64 @@
(defentity Activity
[(table :activity)
(types :details :json, :topic :keyword)]
(pre-insert [_ {:keys [details] :as activity}]
(let [defaults {:timestamp (u/new-sql-timestamp)
:details {}}]
(merge defaults activity)))
(post-select [_ {:keys [user_id database_id table_id model model_id] :as activity}]
(-> (map->ActivityFeedItemInstance activity)
(assoc :user (delay (User user_id)))
(assoc :database (delay (-> (Database database_id)
(select-keys [:id :name :description]))))
(assoc :table (delay (-> (Table table_id)
(select-keys [:id :name :display_name :description]))))
(assoc :model_exists (delay (case model
"card" (exists? Card :id model_id)
"dashboard" (exists? Dashboard :id model_id)
"pulse" (exists? Pulse :id model_id)
nil))))))
[(table :activity)
(types :details :json, :topic :keyword)]
(pre-insert [_ {:keys [details] :as activity}]
(let [defaults {:timestamp (u/new-sql-timestamp)
:details {}}]
(merge defaults activity)))
(post-select [_ {:keys [user_id database_id table_id model model_id] :as activity}]
(-> (map->ActivityFeedItemInstance activity)
(assoc
:database (delay (-> (Database database_id)
(select-keys [:id :name :description])))
:model_exists (delay (case model
"card" (db/exists? Card :id model_id)
"dashboard" (db/exists? Dashboard :id model_id)
"pulse" (db/exists? Pulse :id model_id)
"segment" (db/exists? Segment :id model_id :is_active true)
nil))
:table (delay (-> (Table table_id)
(select-keys [:id :name :display_name :description])))
:user (delay (User user_id))))))
(extend-ICanReadWrite ActivityEntity :read :public-perms, :write :public-perms)
;; ## Persistence Functions
(defn record-activity
"Inserts a new `Activity` entry.
Takes the following kwargs:
:topic Required. The activity topic.
:object Optional. The activity object being saved.
:database-id Optional. ID of the `Database` related to the activity.
:table-id Optional. ID of the `Table` related to the activity.
:details-fn Optional. Gets called with `object` as the arg and the result is saved as the `:details` of the Activity.
:user-id Optional. ID of the `User` responsible for the activity. defaults to (events/object->user-id object)
:model Optional. name of the model representing the activity. defaults to (events/topic->model topic)
:model-id Optional. ID of the model representing the activity. defaults to (events/object->model-id topic object)
ex: (record-activity
:topic :segment-update
:object segment
:database-id 1
:table-id 13
:details-fn #(dissoc % :some-key))"
[& {:keys [topic object details-fn database-id table-id user-id model model-id]}]
{:pre [(keyword? topic)]}
(let [object (or object {})]
(db/ins Activity
:topic topic
:user_id (or user-id (events/object->user-id object))
:model (or model (events/topic->model topic))
:model_id (or model-id (events/object->model-id topic object))
:database_id database-id
:table_id table-id
:custom_id (:custom_id object)
:details (if (fn? details-fn)
(details-fn object)
object))))
......@@ -56,3 +56,12 @@
(db/cascade-delete Field :table_id id)))
(extend-ICanReadWrite TableEntity :read :always, :write :superuser)
;; ## Persistence Functions
(defn table-id->database-id
"Retrieve the `Database` ID for the given table-id."
[table-id]
{:pre [(integer? table-id)]}
(db/sel :one :field [Table :db_id] :id table-id))
......@@ -9,15 +9,14 @@
[dashboard-card :refer [DashboardCard]]
[database :refer [Database]]
[pulse :refer [Pulse]]
[segment :refer [Segment]]
[session :refer [Session]]
[table :refer [Table]]
[user :refer [User]])
[metabase.test.data :refer :all]
[metabase.test.util :refer [expect-eval-actual-first with-temp random-name]]
[metabase.test-setup :refer :all]))
;; TODO - we can simplify the cleanup work we do by using the :in-context :expectations-options
;; the only downside is that it then runs the annotated function on ALL tests :/
(defn- create-test-objects
"Simple helper function which creates a series of test objects for use in the tests"
......@@ -52,12 +51,30 @@
pulse (db/ins Pulse
:creator_id (:id user)
:name rand-name
:public_perms 2)]
:public_perms 2)
database (db/ins Database
:name "Activity Database"
:engine :yeehaw
:details {}
:is_sample false)
table (db/ins Table
:name "Activity Table"
:db_id (:id database)
:active true)
segment (db/ins Segment
:creator_id (:id user)
:table_id (:id table)
:name "Activity Segment"
:description "Something worth reading"
:definition {:a "b"})]
{:card card
:dashboard dashboard
:dashcard dashcard
:database database
:pulse pulse
:segment segment
:session {:id rand-name}
:table table
:user user}))
......@@ -277,6 +294,64 @@
(-> (db/sel :one Activity :topic "pulse-delete")
(select-keys [:topic :user_id :model :model_id :database_id :table_id :details]))))
;; `:segment-create`
(expect-let [{:keys [database table segment user]} (create-test-objects)]
{:topic :segment-create
:user_id (:id user)
:model "segment"
:model_id (:id segment)
:database_id (:id database)
:table_id (:id table)
:details {:name (:name segment)
:description (:description segment)}}
(do
(k/delete Activity)
(process-activity-event {:topic :segment-create
:item segment})
(-> (db/sel :one Activity :topic "segment-create")
(select-keys [:topic :user_id :model :model_id :database_id :table_id :details]))))
;; `:segment-update`
(expect-let [{:keys [database table segment user]} (create-test-objects)]
{:topic :segment-update
:user_id (:id user)
:model "segment"
:model_id (:id segment)
:database_id (:id database)
:table_id (:id table)
:details {:name (:name segment)
:description (:description segment)
:revision_message "update this mofo"}}
(do
(k/delete Activity)
(process-activity-event {:topic :segment-update
:item (-> segment
(assoc :actor_id (:id user)
:revision_message "update this mofo")
;; doing this specifically to ensure :actor_id is utilized
(dissoc :creator_id))})
(-> (db/sel :one Activity :topic "segment-update")
(select-keys [:topic :user_id :model :model_id :database_id :table_id :details]))))
;; `:segment-delete`
(expect-let [{:keys [database table segment user]} (create-test-objects)]
{:topic :segment-delete
:user_id (:id user)
:model "segment"
:model_id (:id segment)
:database_id (:id database)
:table_id (:id table)
:details {:name (:name segment)
:description (:description segment)
:revision_message "deleted"}}
(do
(k/delete Activity)
(process-activity-event {:topic :segment-delete
:item (assoc segment :actor_id (:id user)
:revision_message "deleted")})
(-> (db/sel :one Activity :topic "segment-delete")
(select-keys [:topic :user_id :model :model_id :database_id :table_id :details]))))
;; `:user-login` event
(expect-let [{{user-id :id} :user {session-id :id :as session} :session} (create-test-objects)]
{:topic :user-joined
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment