Permalink
Browse files

Initial implementation of Atom feeds / Activity Streams; simplify Bad…

…ge Awarded notification; settings fixes; added vendor and libs directories
  • Loading branch information...
lmorchard committed Aug 10, 2010
1 parent 0e9dd99 commit 88e727f2f687262a2395b8e9a1ce2da23d322f72
View
@@ -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]
+
Binary file not shown.
@@ -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 %}
@@ -4,4 +4,4 @@
{% user_display award.nomination.nominator as nominator_display %}
{% url profile_detail username=award.nomination.approved_by.username as approved_by_url %}
{% url profile_detail username=award.nomination.nominator.username as nominator_url %}
-{% blocktrans with award.awardee.get_absolute_url as awardee_url and award.awardee.display as awardee_display and award.get_absolute_url as award_url and award.get_absolute_url as badge_url and award.badge.title as badge_title and award.reason_why as reason_why %}<a href="{{ approved_by_url }}">{{ approved_by_display }}</a> has approved <a href="{{ nominator_url }}">{{ nominator_display }}</a>'s nomination of <a href="{{ awardee_url }}">{{ awardee_display }}</a> for the badge <a href="{{ badge_url }}">{{ badge_title }}</a>{% endblocktrans %}
+{% blocktrans with award.awardee.get_absolute_url as awardee_url and award.awardee.display as awardee_display and award.get_absolute_url as award_url and award.badge.get_absolute_url as badge_url and award.badge.title as badge_title and award.reason_why as reason_why %}<a href="{{ awardee_url }}">{{ awardee_display }}</a> has <a href="{{ award_url }}">been awarded</a> the badge <a href="{{ badge_url }}">{{ badge_title }}</a>.{% endblocktrans %}
@@ -6,10 +6,10 @@
from django.utils.hashcompat import md5_constructor
from django.contrib.auth.models import User
-from badger.apps.badges.models import Badge, BadgeNomination
-from badger.apps.badges.models import BadgeAward, BadgeAwardee
+from badges.models import Badge, BadgeNomination
+from badges.models import BadgeAward, BadgeAwardee
-from badger.apps.badges import BADGE_DEFAULT_URL
+from badges import BADGE_DEFAULT_URL
register = template.Library()
@@ -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)
+
@@ -1,23 +0,0 @@
-""" """
-from django.http import HttpRequest
-from django.test import TestCase
-
-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_
-
-class TestBadges(TestCase):
-
- def setUp(self):
- self.users = {}
- for user_name in ( 'user1', 'user2', 'user3' ):
- self.users[user_name] = User.objects.create(username=user_name)
-
- def tearDown(self):
- for name, user in self.users.items():
- user.delete()
Oops, something went wrong.

0 comments on commit 88e727f

Please sign in to comment.