Skip to content

Commit

Permalink
Initial implementation of Atom feeds / Activity Streams; simplify Bad…
Browse files Browse the repository at this point in the history
…ge Awarded notification; settings fixes; added vendor and libs directories
  • Loading branch information
lmorchard committed Aug 10, 2010
1 parent 0e9dd99 commit 88e727f
Show file tree
Hide file tree
Showing 21 changed files with 1,019 additions and 78 deletions.
187 changes: 187 additions & 0 deletions apps/badges/feeds.py
@@ -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 added apps/badges/media/badges/img/feed-icon-14x14.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/badges/templates/notification/badge_awarded/full.txt
@@ -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 %}
Expand Up @@ -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 changes: 3 additions & 3 deletions apps/badges/templatetags/badge_tags.py
Expand Up @@ -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()

Expand Down
148 changes: 148 additions & 0 deletions apps/badges/tests/test_atom_feeds.py
@@ -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)

23 changes: 0 additions & 23 deletions apps/badges/tests/test_models.py

This file was deleted.

0 comments on commit 88e727f

Please sign in to comment.