Skip to content

Commit

Permalink
[#3009] Initial implementation of activity streams on-site notificati…
Browse files Browse the repository at this point in the history
…on API
  • Loading branch information
Sean Hammond committed Nov 6, 2012
1 parent 60d61d8 commit 2398b28
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 2 deletions.
50 changes: 48 additions & 2 deletions 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 _
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ckan/model/__init__.py
Expand Up @@ -147,6 +147,9 @@
DomainObjectOperation,
DomainObject,
)
from dashboard import (
Dashboard,
)

import ckan.migration

Expand Down
47 changes: 47 additions & 0 deletions 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)
87 changes: 87 additions & 0 deletions 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

0 comments on commit 2398b28

Please sign in to comment.