From 2398b28ba8162a88f2301249a7c439f8883e1e16 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 6 Nov 2012 19:10:16 +0100 Subject: [PATCH] [#3009] Initial implementation of activity streams on-site notification API --- ckan/logic/action/get.py | 50 +++++++++++- ckan/model/__init__.py | 3 + ckan/model/dashboard.py | 47 +++++++++++ ckan/tests/functional/api/test_dashboard.py | 87 +++++++++++++++++++++ 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 ckan/model/dashboard.py create mode 100644 ckan/tests/functional/api/test_dashboard.py diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index c60877734ee..0adc1bb4623 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1,6 +1,7 @@ import uuid import logging import json +import datetime from pylons import config from pylons.i18n import _ @@ -2114,14 +2115,14 @@ def dashboard_activity_list(context, data_dict): # authorized to read. if 'user' not in context: raise logic.NotAuthorized( - _("You must be logged in to see your dashboard activity stream.")) + _("You must be logged in to access your dashboard.")) model = context['model'] userobj = model.User.get(context['user']) if not userobj: raise logic.NotAuthorized( - _("You must be logged in to see your dashboard activity stream.")) + _("You must be logged in to access your dashboard.")) user_id = userobj.id activity_query = model.Session.query(model.Activity) @@ -2141,6 +2142,7 @@ def dashboard_activity_list(context, data_dict): return model_dictize.activity_list_dictize(activity_objects, context) + def dashboard_activity_list_html(context, data_dict): '''Return the authorized user's dashboard activity stream as HTML. @@ -2154,6 +2156,50 @@ def dashboard_activity_list_html(context, data_dict): return activity_streams.activity_list_to_html(context, activity_stream) +def dashboard_new_activities_count(context, data_dict): + '''Return the number of new activities in the user's activity stream. + + Return the number of new activities in the authorized user's dashboard + activity stream. + + :rtype: int + + ''' + # We don't bother to do our own auth check in this function, because we + # assume dashboard_activity_list will do it. + activities = dashboard_activity_list(context, data_dict) + + model = context['model'] + user = model.User.get(context['user']) # The authorized user. + last_viewed = model.Dashboard.get_activity_stream_last_viewed(user.id) + + strptime = datetime.datetime.strptime + fmt = '%Y-%m-%dT%H:%M:%S.%f' + new_activities = [activity for activity in activities if + strptime(activity['timestamp'], fmt) > last_viewed] + return len(new_activities) + + +def dashboard_mark_activities_as_read(context, data_dict): + '''Mark all the authorized user's new dashboard activities as old. + + This will reset dashboard_new_activities_count to 0. + + ''' + if 'user' not in context: + raise logic.NotAuthorized( + _("You must be logged in to access your dashboard.")) + + model = context['model'] + + userobj = model.User.get(context['user']) + if not userobj: + raise logic.NotAuthorized( + _("You must be logged in to access your dashboard.")) + user_id = userobj.id + model.Dashboard.update_activity_stream_last_viewed(user_id) + + def _unpick_search(sort, allowed_fields=None, total=None): ''' This is a helper function that takes a sort string eg 'name asc, last_modified desc' and returns a list of diff --git a/ckan/model/__init__.py b/ckan/model/__init__.py index 98c647e74f4..6502b6b9a66 100644 --- a/ckan/model/__init__.py +++ b/ckan/model/__init__.py @@ -147,6 +147,9 @@ DomainObjectOperation, DomainObject, ) +from dashboard import ( + Dashboard, +) import ckan.migration diff --git a/ckan/model/dashboard.py b/ckan/model/dashboard.py new file mode 100644 index 00000000000..438fc865eca --- /dev/null +++ b/ckan/model/dashboard.py @@ -0,0 +1,47 @@ +import datetime +import sqlalchemy +import meta + +dashboard_table = sqlalchemy.Table('dashboard', meta.metadata, + sqlalchemy.Column('user_id', sqlalchemy.types.UnicodeText, + sqlalchemy.ForeignKey('user.id', onupdate='CASCADE', + ondelete='CASCADE'), + primary_key=True, nullable=False), + sqlalchemy.Column('activity_stream_last_viewed', sqlalchemy.types.DateTime, + nullable=False) +) + + +class Dashboard(object): + '''Saved data used for the user's dashboard.''' + + def __init__(self, user_id): + self.user_id = user_id + self.activity_stream_last_viewed = datetime.datetime.now() + + @classmethod + def get_activity_stream_last_viewed(cls, user_id): + query = meta.Session.query(Dashboard) + query = query.filter(Dashboard.user_id == user_id) + try: + row = query.one() + return row.activity_stream_last_viewed + except sqlalchemy.orm.exc.NoResultFound: + # No dashboard row has been created for this user so they have no + # activity_stream_last_viewed date. Return the oldest date we can + # (i.e. all activities are new to this user). + return datetime.datetime.min + + @classmethod + def update_activity_stream_last_viewed(cls, user_id): + query = meta.Session.query(Dashboard) + query = query.filter(Dashboard.user_id == user_id) + try: + row = query.one() + row.activity_stream_last_viewed = datetime.datetime.now() + except sqlalchemy.orm.exc.NoResultFound: + row = Dashboard(user_id) + meta.Session.add(row) + meta.Session.commit() + +meta.mapper(Dashboard, dashboard_table) diff --git a/ckan/tests/functional/api/test_dashboard.py b/ckan/tests/functional/api/test_dashboard.py new file mode 100644 index 00000000000..9cd330dd905 --- /dev/null +++ b/ckan/tests/functional/api/test_dashboard.py @@ -0,0 +1,87 @@ +import ckan +from ckan.lib.helpers import json +import paste +import pylons.test + + +class TestDashboard(object): + '''Tests for the logic action functions related to the user's dashboard.''' + + @classmethod + def setup_class(cls): + ckan.tests.CreateTestData.create() + cls.app = paste.fixture.TestApp(pylons.test.pylonsapp) + joeadmin = ckan.model.User.get('joeadmin') + cls.joeadmin = { + 'id': joeadmin.id, + 'apikey': joeadmin.apikey + } + + @classmethod + def teardown_class(cls): + ckan.model.repo.rebuild_db() + + def new_activities_count(self, user): + '''Return the given user's new activities count from the CKAN API.''' + + params = json.dumps({}) + response = self.app.post('/api/action/dashboard_new_activities_count', + params=params, + extra_environ={'Authorization': str(user['apikey'])}) + assert response.json['success'] is True + new_activities_count = response.json['result'] + return new_activities_count + + def mark_as_read(self, user): + params = json.dumps({}) + response = self.app.post( + '/api/action/dashboard_mark_activities_as_read', + params=params, + extra_environ={'Authorization': str(user['apikey'])}) + assert response.json['success'] is True + + def test_01_num_new_activities_new_user(self): + '''Test retreiving the number of new activities for a new user.''' + + # Create a new user. + params = json.dumps({ + 'name': 'mr_new_user', + 'email': 'mr@newuser.com', + 'password': 'iammrnew', + }) + response = self.app.post('/api/action/user_create', params=params, + extra_environ={'Authorization': str(self.joeadmin['apikey'])}) + assert response.json['success'] is True + new_user = response.json['result'] + + # We expect to find only one new activity for a newly registered user + # (A "{USER} signed up" activity). + assert self.new_activities_count(new_user) == 1 + + self.mark_as_read(new_user) + assert self.new_activities_count(new_user) == 0 + + # Create a dataset. + params = json.dumps({ + 'name': 'my_new_package', + }) + response = self.app.post('/api/action/package_create', params=params, + extra_environ={'Authorization': str(new_user['apikey'])}) + assert response.json['success'] is True + + # Now there should be a new 'user created dataset' activity. + assert self.new_activities_count(new_user) == 1 + + # Update the dataset. + params = json.dumps({ + 'name': 'my_new_package', + 'title': 'updated description', + }) + response = self.app.post('/api/action/package_update', params=params, + extra_environ={'Authorization': str(new_user['apikey'])}) + assert response.json['success'] is True + + assert self.new_activities_count(new_user) == 2 + + self.mark_as_read(new_user) + assert self.new_activities_count(new_user) == 0