Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#1635] Initial implementation of email notifications for activity st…
…reams ckan/model/user.py Added @classmethod all() ckan/model/dashboard.py Added last_activity_stream_email_notification and @classmethods get_last_activity_stream_email_notification, update_last_activity_stream_email_notification logic/action/get.py: Added dashboard_email_notification_last_sent and get_email_notifications actions. These may not really need to be action functions as they may only be needed internally. ckan/lib/email_notifications.py This is the email notifier job meant to be run by paster (but there's no paster command for it yet) ckan/tests/lib/test_email_notifications.py The beginning of some tests
- Loading branch information
Sean Hammond
committed
Nov 16, 2012
1 parent
373432f
commit a678e0e
Showing
5 changed files
with
282 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
''' | ||
Code for generating email notifications for users (e.g. email notifications for | ||
new activities in your dashboard activity stream) and emailing them to the | ||
users. | ||
''' | ||
import datetime | ||
|
||
import ckan.model as model | ||
import ckan.logic as logic | ||
import ckan.lib.mailer | ||
|
||
|
||
def _notifications_for_activities(activities): | ||
'''Return one or more email notifications covering the given activities. | ||
This function handles grouping multiple activities into a single digest | ||
email. | ||
:param activities: the activities to consider | ||
:type activities: list of activity dicts like those returned by | ||
ckan.logic.action.get.dashboard_activity_list() | ||
:returns: a list of email notifications | ||
:rtype: list of dicts each with keys 'subject' and 'body' | ||
''' | ||
if not activities: | ||
return [] | ||
|
||
# We just group all activities into a single "new activity" email that | ||
# doesn't say anything about _what_ new activities they are. | ||
# TODO: Here we could generate some smarter content for the emails e.g. | ||
# say something about the contents of the activities, or single out | ||
# certain types of activity to be sent in their own individual emails, | ||
# etc. | ||
notifications = [{ | ||
'subject': "You have new activity", | ||
'body': "You have new activity" | ||
}] | ||
|
||
return notifications | ||
|
||
|
||
def _notifications_from_dashboard_activity_list(user_id, since): | ||
'''Return any email notifications from user_id's dashboard activity list | ||
since `since`. | ||
''' | ||
# Get the user's dashboard activity stream. | ||
context = {'model': model, 'session': model.Session, 'user': user_id} | ||
activity_list = logic.get_action('dashboard_activity_list')(context, {}) | ||
|
||
# Filter out the user's own activities., so they don't get an email every | ||
# time they themselves do something (we are not Trac). | ||
activity_list = [activity for activity in activity_list | ||
if activity['user_id'] != user_id] | ||
|
||
# Filter out the old activities. | ||
strptime = datetime.datetime.strptime | ||
fmt = '%Y-%m-%dT%H:%M:%S.%f' | ||
activity_list = [activity for activity in activity_list | ||
if strptime(activity['timestamp'], fmt) > since] | ||
|
||
return _notifications_for_activities(activity_list) | ||
|
||
|
||
# A list of functions that provide email notifications for users from different | ||
# sources. Add to this list if you want to implement a new source of email | ||
# notifications. | ||
_notifications_functions = [ | ||
_notifications_from_dashboard_activity_list, | ||
] | ||
|
||
|
||
def get_notifications(user_id, since): | ||
'''Return any email notifications for `user_id` since `since`. | ||
For example email notifications about activity streams will be returned for | ||
any activities the occurred since `since`. | ||
:param user_id: id of the user to return notifications for | ||
:type user_id: string | ||
:param since: datetime after which to return notifications from | ||
:rtype since: datetime.datetime | ||
:returns: a list of email notifications | ||
:rtype: list of dicts with keys 'subject' and 'body' | ||
''' | ||
notifications = [] | ||
for function in _notifications_functions: | ||
notifications.extend(function(user_id, since)) | ||
return notifications | ||
|
||
|
||
def send_notification(user, email_dict): | ||
'''Email `email_dict` to `user`.''' | ||
|
||
if not user.get('email'): | ||
# FIXME: Raise an exception. | ||
return | ||
|
||
try: | ||
ckan.lib.mailer.mail_recipient(user['display_name'], user['email'], | ||
email_dict['subject'], email_dict['body']) | ||
except ckan.lib.mailer.MailerException: | ||
raise | ||
|
||
|
||
def get_and_send_notifications_for_user(user): | ||
# FIXME: Get the time that the last email notification was sent to the | ||
# user and the time that they last viewed their dashboard, set `since` to | ||
# whichever is more recent. | ||
since = datetime.datetime.min | ||
notifications = get_notifications(user['id'], since) | ||
# TODO: Handle failures from send_email_notification. | ||
for notification in notifications: | ||
send_notification(user, notification) | ||
|
||
|
||
def get_and_send_notifications_for_all_users(): | ||
context = {'model': model, 'session': model.Session, 'ignore_auth': True, | ||
'keep_sensitive_data': True} | ||
users = logic.get_action('user_list')(context, {}) | ||
for user in users: | ||
get_and_send_notifications_for_user(user) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import json | ||
|
||
import ckan.model as model | ||
import ckan.tests.mock_mail_server as mock_mail_server | ||
import ckan.lib.email_notifications as email_notifications | ||
import ckan.tests | ||
import ckan.tests.pylons_controller | ||
|
||
import paste | ||
import pylons.test | ||
|
||
|
||
class TestEmailNotifications(mock_mail_server.SmtpServerHarness, | ||
ckan.tests.pylons_controller.PylonsTestCase): | ||
|
||
@classmethod | ||
def setup_class(cls): | ||
mock_mail_server.SmtpServerHarness.setup_class() | ||
ckan.tests.pylons_controller.PylonsTestCase.setup_class() | ||
ckan.tests.CreateTestData.create() | ||
cls.app = paste.fixture.TestApp(pylons.test.pylonsapp) | ||
joeadmin = model.User.get('joeadmin') | ||
cls.joeadmin = {'id': joeadmin.id, | ||
'apikey': joeadmin.apikey, | ||
} | ||
|
||
@classmethod | ||
def teardown_class(self): | ||
mock_mail_server.SmtpServerHarness.teardown_class() | ||
ckan.tests.pylons_controller.PylonsTestCase.teardown_class() | ||
model.repo.rebuild_db() | ||
|
||
def test_01_no_email_notifications_after_registration(self): | ||
'''Test that a newly registered user who is not following anything | ||
doesn't get any email notifications.''' | ||
|
||
# Clear any emails already sent due to CreateTestData.create(). | ||
email_notifications.get_and_send_notifications_for_all_users() | ||
self.clear_smtp_messages() | ||
|
||
# Register a new user. | ||
params = {'name': 'sara', | ||
'email': 'sara@sararollins.com', | ||
'password': 'sara', | ||
'fullname': 'Sara Rollins', | ||
} | ||
extra_environ = {'Authorization': str(self.joeadmin['apikey'])} | ||
response = self.app.post('/api/action/user_create', | ||
params=json.dumps(params), extra_environ=extra_environ).json | ||
assert response['success'] is True | ||
|
||
# Save the user for later tests to use. | ||
TestEmailNotifications.user = response['result'] | ||
|
||
# No notification emails should be sent to anyone at this point. | ||
email_notifications.get_and_send_notifications_for_all_users() | ||
assert len(self.get_smtp_messages()) == 0 | ||
|
||
def test_02_fuck_yeah_email_notifications(self): | ||
|
||
# You have to follow something or you don't get any emails. | ||
params = {'id': 'warandpeace'} | ||
extra_environ = {'Authorization': str(self.user['apikey'])} | ||
response = self.app.post('/api/action/follow_dataset', | ||
params=json.dumps(params), extra_environ=extra_environ).json | ||
assert response['success'] is True | ||
|
||
# Make someone else update the dataset we're following to create an | ||
# email notification. | ||
params = {'name': 'warandpeace', 'notes': 'updated'} | ||
extra_environ = {'Authorization': str(self.joeadmin['apikey'])} | ||
response = self.app.post('/api/action/package_update', | ||
params=json.dumps(params), extra_environ=extra_environ).json | ||
assert response['success'] is True | ||
|
||
# One notification email should be sent to anyone at this point. | ||
email_notifications.get_and_send_notifications_for_all_users() | ||
assert len(self.get_smtp_messages()) == 1 |