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

adding in the new view tracking code so that we can have an accurate Recents...

adding in the new view tracking code so that we can have an accurate Recents section on the homepage (implements #938)
parent aa1a6547
Branches
Tags
No related merge requests found
......@@ -8,6 +8,12 @@ ActivityServices.factory('Activity', ['$resource', '$cookies', function($resourc
list: {
method: 'GET',
isArray: true
},
recent_views: {
url: '/api/activity/recent_views',
method: 'GET',
isArray: true
}
});
}]);
......@@ -34,7 +34,7 @@ function createThunkAction(actionType, actionThunkCreator) {
}
// resource wrappers
const ActivityApi = new AngularResourceProxy("Activity", ["list"]);
const ActivityApi = new AngularResourceProxy("Activity", ["list", "recent_views"]);
const CardApi = new AngularResourceProxy("Card", ["list"]);
const MetadataApi = new AngularResourceProxy("Metabase", ["db_list", "db_metadata"]);
......@@ -58,6 +58,7 @@ export const FETCH_ACTIVITY = 'FETCH_ACTIVITY';
export const FETCH_CARDS = 'FETCH_CARDS';
export const FETCH_DATABASES = 'FETCH_DATABASES';
export const FETCH_DATABASE_METADATA = 'FETCH_DATABASE_METADATA';
export const FETCH_RECENT_VIEWS = 'FETCH_RECENT_VIEWS';
// action creators
......@@ -93,6 +94,16 @@ export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
};
});
export const fetchRecentViews = createThunkAction(FETCH_RECENT_VIEWS, function() {
return async function(dispatch, getState) {
let recentViews = await ActivityApi.recent_views();
for (var v of recentViews) {
v.timestamp = moment(v.timestamp);
}
return recentViews;
};
});
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterModelId) {
return async function(dispatch, getState) {
let cards = await CardApi.list({'filterMode' : filterMode, 'model_id' : filterModelId });
......
......@@ -3,8 +3,9 @@
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.react";
import Urls from "metabase/lib/urls";
import { fetchRecents } from "../actions";
import { fetchRecentViews } from "../actions";
export default class RecentViews extends Component {
......@@ -17,23 +18,28 @@ export default class RecentViews extends Component {
async componentDidMount() {
try {
await this.props.dispatch(fetchRecents());
await this.props.dispatch(fetchRecentViews());
} catch (error) {
this.setState({ error });
}
}
render() {
let { recentViews } = this.props;
return (
<div className="p2">
<div className="text-brand clearfix pt2 pb2">
<Icon className="float-left" name={'history'} width={24} height={24}></Icon>
<div>Recents</div>
<Icon className="float-left" name={'clock'} width={18} height={18}></Icon>
<span className="pl1">Recents</span>
</div>
<div className="bordered rounded bg-white">
<ul>
<li>recent stuff</li>
<ul className="px3 py1">
{recentViews.map(item =>
<li key={item.id} className="py1">
<a className="link text-dark" href={Urls.modelToUrl(item.model, item.model_id)}>{item.model_object.name}</a>
</li>
)}
</ul>
</div>
</div>
......
......@@ -8,7 +8,8 @@ import {
FETCH_ACTIVITY,
FETCH_CARDS,
FETCH_DATABASES,
FETCH_DATABASE_METADATA
FETCH_DATABASE_METADATA,
FETCH_RECENT_VIEWS
} from './actions';
......@@ -29,6 +30,10 @@ export const activityIdList = handleActions({
[FETCH_ACTIVITY]: { next: (state, { payload }) => payload.result }
}, null);
export const recentViews = handleActions({
[FETCH_RECENT_VIEWS]: { next: (state, { payload }) => payload }
}, []);
export const cards = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => ({ ...payload.entities.card }) }
......
......@@ -9,6 +9,8 @@ const cardsFilterSelector = state => state.cardsFilter;
const activitySelector = state => state.activity;
const activityIdListSelector = state => state.activityIdList;
const recentViewsSelector = state => state.recentViews;
const cardsSelector = state => state.cards;
const cardIdListSelector = state => state.cardIdList;
......@@ -28,6 +30,6 @@ const cardListSelector = createSelector(
// our master selector which combines all of our partial selectors above
export const homepageSelectors = createSelector(
[selectedTabSelector, cardsFilterSelector, activityListSelector, cardListSelector, databasesSelector, databaseMetadataSelector],
(selectedTab, cardsFilter, activity, cards, databases, databaseMetadata) => ({selectedTab, cardsFilter, activity, cards, databases, databaseMetadata})
[selectedTabSelector, cardsFilterSelector, activityListSelector, recentViewsSelector, cardListSelector, databasesSelector, databaseMetadataSelector],
(selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata) => ({selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata})
);
\ No newline at end of file
databaseChangeLog:
- changeSet:
id: 14
author: agilliland
changes:
- createTable:
tableName: view_log
columns:
- column:
name: id
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: user_id
type: int
constraints:
nullable: true
references: core_user(id)
foreignKeyName: fk_view_log_ref_user_id
deferrable: false
initiallyDeferred: false
- column:
name: model
type: varchar(16)
constraints:
nullable: false
- column:
name: model_id
type: int
constraints:
nullable: false
- column:
name: timestamp
type: DATETIME
constraints:
nullable: false
- createIndex:
tableName: view_log
indexName: idx_view_log_user_id
columns:
column:
name: user_id
- createIndex:
tableName: view_log
indexName: idx_view_log_timestamp
columns:
column:
name: model_id
- modifySql:
dbms: postgresql
replace:
replace: WITHOUT
with: WITH
......@@ -11,6 +11,7 @@
{"include": {"file": "migrations/010_add_revision_table.yaml"}},
{"include": {"file": "migrations/011_cleanup_dashboard_perms.yaml"}},
{"include": {"file": "migrations/012_add_card_query_fields.yaml"}},
{"include": {"file": "migrations/013_add_activity_table.yaml"}}
{"include": {"file": "migrations/013_add_activity_table.yaml"}},
{"include": {"file": "migrations/014_add_view_log_table.yaml"}}
]
}
......@@ -2,16 +2,41 @@
(:require [compojure.core :refer [GET POST]]
[korma.core :as k]
[metabase.api.common :refer :all]
[metabase.db :refer [exists? sel]]
[metabase.db :as db]
(metabase.models [activity :refer [Activity]]
[card :refer [Card]]
[dashboard :refer [Dashboard]]
[hydrate :refer [hydrate]])))
[hydrate :refer [hydrate]]
[user :refer [User]]
[view-log :refer [ViewLog]])))
(defendpoint GET "/"
"Get recent activity."
[]
(-> (sel :many Activity (k/order :timestamp :DESC))
(-> (db/sel :many Activity (k/order :timestamp :DESC))
(hydrate :user :table :database)))
(defendpoint GET "/recent_views"
"Get the list of 15 things the current user has been viewing most recently."
[]
;; use a custom Korma query because we are doing some groupings and aggregations
;; expected output of the query is a single row per unique model viewed by the current user
;; including a `:max_ts` which has the most recent view timestamp of the item and `:cnt` which has total views
;; and we order the results by most recently viewed then hydrate the basic details of the model
(-> (->> (k/select ViewLog
(k/fields :user_id :model :model_id)
(k/aggregate (count :*) :cnt)
(k/aggregate (max :timestamp) :max_ts)
(k/where (= :user_id *current-user-id*))
(k/group :user_id :model :model_id)
(k/order :max_ts :desc)
(k/limit 15))
(map #(assoc % :model_object (delay (case (:model %)
"card" (-> (Card (:model_id %))
(select-keys [:id :name :description]))
"dashboard" (-> (Dashboard (:model_id %))
(select-keys [:id :name :description]))
nil)))))
(hydrate :model_object)))
(define-routes)
(ns metabase.events.view-counts
(ns metabase.events.view-log
(:require [clojure.core.async :as async]
[clojure.tools.logging :as log]
[metabase.db :as db]
[metabase.events :as events]
(metabase.models [activity :refer [Activity]]
[dashboard :refer [Dashboard]]
[session :refer [Session]])))
[metabase.models.view-log :refer [ViewLog]]))
(def view-counts-topics
......@@ -20,23 +18,53 @@
;;; ## ---------------------------------------- EVENT PROCESSING ----------------------------------------
;(defn- tally-in-time-period
; ""
; [period-days now tracking-start cnt]
; {:pre [(integer? now)
; (integer? tracking-start)
; (integer? period-days)
; (integer? cnt)]}
; (let [milliseconds-since (- now tracking-start)]
; (if (> (* period-days 24 60 60 1000) milliseconds-since)
; {:timestamp tracking-start
; :count (inc cnt)}
; {:timestamp now
; :count 1})))
;
;(defn- record-view
; "Simple base function for recording a view of a given `model` and `model-id` by a certain `user`."
; [model model-id user-id]
; (println "weeeeee" model model-id user-id)
; (let [{:keys [id] :as before-tally} (or (-> (db/sel :one ViewCounts :user_id user-id :model model :model_id model-id)
; (dissoc :user :model_object))
; {:user_id user-id
; :model model
; :model_id model-id})
; now (System/currentTimeMillis)
; tally-period (fn [period-days]
; (let [ts-keyword (keyword (str period-days "_day_ts"))
; cnt-keyword (keyword (str period-days "_day_cnt"))]
; (-> (tally-in-time-period period-days now (or (ts-keyword before-tally) 0) (or (cnt-keyword before-tally) 0))
; (set/rename-keys {:timestamp ts-keyword, :count cnt-keyword}))))
; after-tally (-> before-tally
; (assoc :all_time_cnt (inc (or (:all_time_cnt before-tally) 0)))
; (merge (tally-period 1))
; (merge (tally-period 7))
; (merge (tally-period 30)))]
; (clojure.pprint/pprint after-tally)
; (if id
; (m/mapply db/upd ViewCounts id after-tally)
; (m/mapply db/ins ViewCounts after-tally))))
(defn- record-view
"Simple base function for recording a view of a given `model` and `model-id` by a certain `user`."
[model model-id user-id]
(println "weeeeee" model model-id user-id)
;(db/ins Activity
; :topic topic
; :user_id (object->user-id object)
; :model (topic->model topic)
; :model_id (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))
)
;; TODO - we probably want a little code that prunes old entries so that this doesn't get too big
(db/ins ViewLog
:user_id user-id
:model model
:model_id model-id))
(defn- topic->model
"Determine a valid `model` identifier for the given `topic`."
......
(ns metabase.models.view-counts
(:require [clojure.core.async :as async]
[clojure.tools.logging :as log]
[metabase.events :as events]))
(ns metabase.models.view-log
(:require [korma.core :refer :all, :exclude [defentity update]]
[metabase.api.common :refer [*current-user-id*]]
[metabase.db :refer :all]
[metabase.events :as events]
(metabase.models [card :refer [Card]]
[dashboard :refer [Dashboard]]
[interface :refer :all]
[user :refer [User]])
[metabase.util :as u]))
(defrecord ViewLogItemInstance []
clojure.lang.IFn
(invoke [this k]
(get this k)))
(extend-ICanReadWrite ViewLogItemInstance :read :public-perms, :write :public-perms)
(defentity ViewLog
[(table :view_log)]
(pre-insert [_ log-entry]
(let [defaults {:timestamp (u/new-sql-timestamp)}]
(merge defaults log-entry))))
(extend-ICanReadWrite ViewLogEntity :read :public-perms, :write :public-perms)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment