Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of Atom feeds / Activity Streams; simplify Bad…
…ge Awarded notification; settings fixes; added vendor and libs directories
- Loading branch information
Showing
21 changed files
with
1,019 additions
and
78 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,187 @@ | ||
""" | ||
Feeds for badges | ||
""" | ||
#from django.contrib.syndication.feeds import Feed | ||
from django.contrib.syndication.views import Feed, FeedDoesNotExist | ||
from django.utils.feedgenerator import Atom1Feed, get_tag_uri | ||
from django.shortcuts import get_object_or_404 | ||
|
||
from django.utils.translation import ugettext as _ | ||
|
||
from django.contrib.auth.models import User | ||
from django.contrib.sites.models import Site | ||
from django.conf import settings | ||
|
||
from badger.apps.badges.models import Badge, BadgeNomination | ||
from badger.apps.badges.models import BadgeAward, BadgeAwardee | ||
|
||
from avatar.templatetags.avatar_tags import avatar_url | ||
from badges.templatetags.badge_tags import badge_url | ||
|
||
|
||
class ActivityStreamFeedGenerator(Atom1Feed): | ||
"""Tweaks to Atom feed to include Activity Stream data""" | ||
|
||
def root_attributes(self): | ||
attrs = super(ActivityStreamFeedGenerator, self).root_attributes() | ||
attrs['xmlns:activity'] = 'http://activitystrea.ms/spec/1.0/' | ||
attrs['xmlns:media'] = 'http://purl.org/syndication/atommedia' | ||
return attrs | ||
|
||
def add_item_elements(self, handler, item): | ||
"""Inject Activity Stream elements into an item""" | ||
|
||
handler.addQuickElement('published', item['pubdate'].isoformat()) | ||
item['pubdate'] = None | ||
|
||
# Author information. | ||
if item['author_name'] is not None: | ||
handler.startElement(u"author", {}) | ||
handler.addQuickElement(u"activity:object-type", | ||
'http://activitystrea.ms/schema/1.0/person') | ||
handler.addQuickElement(u"name", item['author_name']) | ||
if item['author_email'] is not None: | ||
handler.addQuickElement(u"email", item['author_email']) | ||
if item['author_link'] is not None: | ||
handler.addQuickElement(u"uri", item['author_link']) | ||
handler.addQuickElement(u"id", | ||
get_tag_uri(item['author_link'], item['pubdate'])) | ||
handler.addQuickElement(u"link", u"", { | ||
'type': 'text/html', 'rel':'alternate', | ||
'href': item['author_link'] | ||
}) | ||
handler.addQuickElement(u"link", u"", { | ||
'type': 'image/jpeg', 'rel':'photo', | ||
'media:width': '64', 'media:height': '64', | ||
'href': 'http://%s%s' % ( | ||
Site.objects.get_current().domain, | ||
avatar_url(item['obj'].claimed_by, 64) | ||
), | ||
}) | ||
avatar_href = avatar_url(item['obj'].claimed_by, 64) | ||
if avatar_href.startswith('/'): | ||
avatar_href = 'http://%s%s' % ( | ||
Site.objects.get_current().domain, avatar_href | ||
) | ||
handler.addQuickElement(u"link", u"", { | ||
'type': 'image/jpeg', 'rel':'preview', | ||
'media:width': '64', 'media:height': '64', | ||
'href': avatar_href, | ||
}) | ||
handler.endElement(u"author") | ||
item['author_name'] = None | ||
|
||
handler.addQuickElement('activity:verb', item['activity']['verb']) | ||
|
||
a_object = item['activity']['object'] | ||
handler.startElement(u"activity:object", {}) | ||
handler.addQuickElement(u"activity:object-type", a_object['object-type']) | ||
handler.addQuickElement(u"title", a_object['name']) | ||
handler.addQuickElement(u"id", get_tag_uri(a_object['link'], | ||
item['pubdate'])) | ||
handler.addQuickElement(u"link", '', { | ||
'href': a_object['link'], 'rel':'alternate', 'type':'text/html' | ||
}) | ||
handler.addQuickElement(u"link", u"", { | ||
'type': 'image/jpeg', 'rel':'preview', | ||
'media:width': a_object['preview']['width'], | ||
'media:height': a_object['preview']['height'], | ||
'href': a_object['preview']['href'], | ||
}) | ||
handler.endElement(u"activity:object") | ||
|
||
super(ActivityStreamFeedGenerator, self).add_item_elements(handler, item) | ||
|
||
|
||
class AwardActivityStreamFeed(Feed): | ||
"""Tweaks to standard feed to include Activity Stream info | ||
for lists of badge awards""" | ||
feed_type = ActivityStreamFeedGenerator | ||
|
||
def item_author_name(self, item): | ||
return '%s' % item.claimed_by | ||
|
||
def item_author_link(self, item): | ||
current_site = Site.objects.get(id=settings.SITE_ID) | ||
return 'http://%s%s' % (Site.objects.get_current().domain, | ||
item.claimed_by.get_absolute_url()) | ||
|
||
def item_pubdate(self, item): | ||
return item.updated_at | ||
|
||
def item_title(self, item): | ||
return '%s claimed the badge "%s"' % (item.claimed_by, item.badge) | ||
|
||
def item_description(self, item): | ||
# TODO: Stick this in a template? | ||
avatar_img = avatar_url(item.claimed_by, 64) | ||
if avatar_img.startswith('/'): | ||
avatar_img = 'http://%s%s' % ( | ||
Site.objects.get_current().domain, avatar_img) | ||
badge_img = badge_url(item.badge, 64) | ||
if badge_img.startswith('/'): | ||
badge_img = 'http://%s%s' % ( | ||
Site.objects.get_current().domain, badge_img) | ||
return """ | ||
<a href="%(claimed_by_url)s"><img src="%(avatar_img)s" width="64" height="64" /> %(claimed_by)s</a> | ||
<a href="%(award_url)s">claimed</a> the badge | ||
<a href="%(badge_url)s">"%(badge_title)s" <img src="%(badge_img)s" width="64" height="64" /></a> | ||
""" % { | ||
'avatar_img': avatar_img, | ||
'badge_img': badge_img, | ||
'claimed_by': item.claimed_by, | ||
'claimed_by_url': item.claimed_by.get_absolute_url(), | ||
'badge_title': item.badge.title, | ||
'badge_url': item.badge.get_absolute_url(), | ||
'award_url': item.get_absolute_url(), | ||
} | ||
|
||
def item_extra_kwargs(self, obj): | ||
return { | ||
'obj': obj, | ||
'activity': { | ||
'verb': 'http://badger.decafbad.com/activity/1.0/verbs/claim', | ||
'object': { | ||
'object-type': | ||
'http://badger.decafbad.com/activity/1.0/objects/badge', | ||
'name': obj.badge.title, | ||
'link': 'http://%s%s' % ( | ||
Site.objects.get_current().domain, | ||
obj.badge.get_absolute_url() | ||
), | ||
'preview': { | ||
'width': '64', 'height': '64', | ||
'href': 'http://%s%s' % ( | ||
Site.objects.get_current().domain, | ||
badge_url(obj.badge, 64) | ||
), | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
|
||
class RecentlyClaimedAwardsFeed(AwardActivityStreamFeed): | ||
"""Feed of recently claimed badge awards""" | ||
|
||
title = _('Recently claimed badges') | ||
subtitle = _('Badges recently claimed by people') | ||
link = '/' | ||
|
||
def items(self): | ||
return BadgeAward.objects.filter(claimed=True).exclude(hidden=True)\ | ||
.order_by('-updated_at')[:15] | ||
|
||
class AwardsClaimedForProfileFeed(AwardActivityStreamFeed): | ||
|
||
title = _('Recently claimed badges') | ||
link = '/' | ||
|
||
def get_object(self, request, username): | ||
return get_object_or_404(User, username=username) | ||
|
||
def items(self, user): | ||
self.title = "%s's recently claimed badges" % user.username | ||
return BadgeAward.objects.filter(claimed_by=user, claimed=True)\ | ||
.exclude(hidden=True).order_by('-updated_at')[:15] | ||
|
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 |
---|---|---|
@@ -1,3 +1,3 @@ | ||
{% load i18n %}{% load account_tags %}{% load badge_tags %}{% awardee_display award.awardee as awardee_display %}{% user_display award.nomination.approved_by as approved_by_display %}{% user_display award.nomination.nominator as nominator_display %}{% blocktrans with award.get_absolute_url as award_url and award.badge.title as badge_title %}{{ approved_by_display }} has approved {{ nominator_display }}'s nomination of {{ awardee_display }} for the badge {{ badge_title }} | ||
{% load i18n %}{% load account_tags %}{% load badge_tags %}{% awardee_display award.awardee as awardee_display %}{% user_display award.nomination.approved_by as approved_by_display %}{% user_display award.nomination.nominator as nominator_display %}{% blocktrans with award.get_absolute_url as award_url and award.badge.title as badge_title %}{{ awardee_display }} has been awarded the badge {{ badge_title }} | ||
|
||
http://{{ current_site }}{{ award_url }}{% endblocktrans %} |
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,148 @@ | ||
""" """ | ||
import logging | ||
import re | ||
import urlparse | ||
import StringIO | ||
import time | ||
|
||
from lxml import etree | ||
from pyquery import PyQuery | ||
|
||
from xml.etree import ElementTree | ||
from activitystreams.atom import make_activities_from_feed | ||
from activitystreams.json import make_activities_from_stream_dict | ||
from django.utils import simplejson as json | ||
|
||
from django.http import HttpRequest | ||
from django.test import TestCase | ||
from django.test.client import Client | ||
|
||
from django.contrib.auth.models import User | ||
|
||
from pinax.apps.profiles.models import Profile | ||
from pinax.apps.account.models import Account | ||
|
||
from badger.apps.badges.models import Badge, BadgeNomination, BadgeAward | ||
|
||
from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_ | ||
from nose.plugins.attrib import attr | ||
|
||
from django.contrib.auth.models import User | ||
from pinax.apps.profiles.models import Profile | ||
from pinax.apps.account.models import Account | ||
from badger.apps.badges.models import Badge, BadgeNomination | ||
from badger.apps.badges.models import BadgeAward, BadgeAwardee | ||
from mailer.models import Message, MessageLog | ||
from notification.models import NoticeType, Notice | ||
|
||
class TestAtomFeeds(TestCase): | ||
|
||
def setUp(self): | ||
self.log = logging.getLogger('nose.badger') | ||
self.browser = Client() | ||
|
||
for user in User.objects.all(): | ||
user.delete() | ||
|
||
self.users = {} | ||
for name in ( 'user1', 'user2', 'user3'): | ||
self.users[name] = self.get_user(name) | ||
|
||
def tearDown(self): | ||
pass | ||
|
||
def test_recent_awards(self): | ||
"""Ensure the recent awards feed parses as an Activity Stream""" | ||
badge_awards = ( | ||
( 'badge1', 'user1', 'user2', 'user3' ), | ||
( 'badge2', 'user1', 'user3', 'user2' ), | ||
( 'badge3', 'user3', 'user1', 'user2' ), | ||
( 'badge4', 'user1', 'user1', 'user1' ), | ||
) | ||
badges, awards = self.build_awards(badge_awards) | ||
self.verify_activity_stream(badge_awards, | ||
'/badges/feeds/atom/recentawards/') | ||
|
||
def test_profile_awards(self): | ||
"""Ensure the award feed for a single profile parses as an Activity Stream""" | ||
badge_awards = ( | ||
( 'badge1', 'user1', 'user2', 'user3' ), | ||
( 'badge2', 'user2', 'user1', 'user3' ), | ||
( 'badge3', 'user1', 'user2', 'user3' ), | ||
( 'badge4', 'user2', 'user1', 'user3' ), | ||
) | ||
badges, awards = self.build_awards(badge_awards) | ||
self.verify_activity_stream(badge_awards, | ||
'/badges/feeds/atom/profiles/user3/awards/') | ||
|
||
####################################################################### | ||
|
||
def get_user(self, username, password=None, email=None): | ||
"""Get a user for the given username, creating it if necessary.""" | ||
if password is None: password = '%s_password' % username | ||
if email is None: email = '%s@example.com' % username | ||
try: | ||
user = User.objects.get(username=username) | ||
except User.DoesNotExist: | ||
user = User.objects.create_user(username, email, password) | ||
ok_(user is not None, "user should exist") | ||
return user | ||
|
||
def build_awards(self, badge_awards): | ||
badges, awards = {}, {} | ||
|
||
for details in badge_awards: | ||
badge_name, creator_name, nominator_name, nominee_name = details | ||
|
||
creator = self.users[creator_name] | ||
nominator = self.users[nominator_name] | ||
nominee_user = self.users[nominee_name] | ||
nominee, c = BadgeAwardee.objects.get_or_create(user=nominee_user) | ||
|
||
badge = Badge(title=badge_name, creator=creator, | ||
description='%s description' % badge_name) | ||
badge.save() | ||
badges[badge_name] = badge | ||
|
||
nomination = badge.nominate(nominator, nominee, | ||
'%s nomination reason' % badge_name) | ||
award = nomination.approve(creator, | ||
'%s approval reason' % badge_name) | ||
award.claim(nominee_user) | ||
|
||
awards[details] = award | ||
|
||
time.sleep(1) | ||
|
||
return badges, awards | ||
|
||
def verify_activity_stream(self, badge_awards, path): | ||
resp = self.browser.get(path) | ||
|
||
et = ElementTree.parse(StringIO.StringIO(resp.content)) | ||
activities = make_activities_from_feed(et) | ||
|
||
# Ensure feed activity count matches award count | ||
eq_(len(badge_awards), len(activities)) | ||
|
||
for details in badge_awards: | ||
badge_name, creator_name, nominator_name, nominee_name = details | ||
act = activities.pop() | ||
|
||
# Check the actor for this activity | ||
eq_(nominee_name, act.actor.name) | ||
eq_('http://activitystrea.ms/schema/1.0/person', | ||
act.actor.object_type) | ||
eq_('http://example.com/profiles/profile/%s/' % nominee_name, | ||
act.actor.url) | ||
|
||
# Check the verb for this activity | ||
eq_('http://badger.decafbad.com/activity/1.0/verbs/claim', act.verb) | ||
|
||
# Check the object for this activity | ||
eq_(badge_name, act.object.name) | ||
eq_('http://badger.decafbad.com/activity/1.0/objects/badge', | ||
act.object.object_type) | ||
eq_('http://example.com/badges/badge/%s' % (badge_name), | ||
act.object.url) | ||
|
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.