Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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

…ge Awarded notification; settings fixes; added vendor and libs directories
  • Loading branch information...
commit 88e727f2f687262a2395b8e9a1ce2da23d322f72 1 parent 0e9dd99
@lmorchard authored
View
187 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]
+
View
BIN  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.
View
2  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 %}
View
2  apps/badges/templates/notification/badge_awarded/notice.html
@@ -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 %}
View
6 apps/badges/templatetags/badge_tags.py
@@ -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()
View
148 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)
+
View
23 apps/badges/tests/test_models.py
@@ -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()
View
6 apps/badges/urls.py
@@ -1,4 +1,5 @@
from django.conf.urls.defaults import *
+from badges.feeds import RecentlyClaimedAwardsFeed, AwardsClaimedForProfileFeed
urlpatterns = patterns("badger.apps.badges.views",
url(r"^$", "index", name="badge_index"),
@@ -11,4 +12,9 @@
url(r"^badge/(.*)/awards/(.*)/(.*)/showhide$", "award_show_hide_single", name="badge_award_show_hide_single"),
url(r"^badge/(.*)/edit$", "edit", name="badge_edit"),
url(r"^badge/(.*)$", "badge_details", name="badge_details"),
+
+ url(r'feeds/atom/recentawards/', RecentlyClaimedAwardsFeed(),
+ name="badge_feed_recentawards"),
+ url(r'feeds/atom/profiles/(.*)/awards/', AwardsClaimedForProfileFeed(),
+ name="badge_feed_profileawards"),
)
View
7 local_settings.py-dist
@@ -4,7 +4,14 @@ from os.path import abspath, dirname, join
DEBUG = True
DEV = True
+SERVE_MEDIA = DEBUG
+EMAIL_DEBUG = DEBUG
CACHE_BACKEND = 'file://%s/cache' % ( abspath(dirname(__file__)) )
MAILER_EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+INSTALLED_APPS += [
+ "django_nose",
+]
+SOUTH_TESTS_MIGRATE = False
+TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
View
2  requirements/base.txt
@@ -70,4 +70,4 @@ django-timezones==0.2.dev1
idios==0.1.dev6
#Pinax
--e git+http://github.com/lmorchard/pinax.git#egg=pinax
+-e git://github.com/lmorchard/pinax.git#egg=Pinax
View
3  requirements/dev.txt
@@ -6,10 +6,11 @@
coverage
nose
-e git+git://github.com/cmheisel/nose-xcover.git#egg=nosexcover
--e git+http://github.com/jbalogh/django-nose.git#egg=django-nose
+-e git://github.com/jbalogh/django-nose.git#egg=django_nose
-e git+http://github.com/jbalogh/check.git#egg=check
lxml
pyquery
freshen
mock
+
View
2  requirements/pinax-dev.txt
@@ -9,7 +9,7 @@ python-memcached
coverage
nose
-e git+git://github.com/cmheisel/nose-xcover.git#egg=nosexcover
--e git+http://github.com/jbalogh/django-nose.git#egg=django-nose
+-e git://github.com/jbalogh/django-nose.git#egg=django_nose
-e git+http://github.com/jbalogh/check.git#egg=check
lxml
View
16 settings.py
@@ -1,17 +1,24 @@
# -*- coding: utf-8 -*-
# Django settings for social pinax project.
+import os
+import sys
+from os.path import abspath, dirname, join
+from site import addsitedir
import os.path
import posixpath
import pinax
+sys.path.insert(0, abspath(join(dirname(__file__), "libs")))
+sys.path.insert(0, abspath(join(dirname(__file__), "vendor")))
+
PINAX_ROOT = os.path.abspath(os.path.dirname(pinax.__file__))
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
# tells Pinax to use the default theme
PINAX_THEME = "default"
-DEBUG = True
+DEBUG = False
TEMPLATE_DEBUG = DEBUG
# tells Pinax to serve media through the staticfiles app.
@@ -282,13 +289,6 @@ def write(*args, **kwargs):
"INTERCEPT_REDIRECTS": False,
}
-if DEBUG:
- INSTALLED_APPS += [
- "django_nose",
- ]
- SOUTH_TESTS_MIGRATE = False
- TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
-
# local_settings.py can be used to override environment-specific settings
# like database and email that differ between development and production.
try:
View
6 templates/homepage.html
@@ -8,6 +8,10 @@
{% load humanize i18n %}
{% load timezone_filters %}
+{% block extra_head %}
+ <link rel="alternate" type="application/atom+xml" title="{% trans "Recently Claimed Badges" %}" href="{% url badge_feed_recentawards %}" />
+{% endblock %}
+
{% block head_title %}{% trans "Welcome" %}{% endblock %}
{% block body_class %}home{% endblock %}
@@ -36,7 +40,7 @@
{% recent_badge_awards as recent_awards %}
<div>
- <h2>Recent awards</h2>
+ <h2>Recent awards <a href="{% url badge_feed_recentawards %}"><img src="{{ STATIC_URL }}badges/img/feed-icon-14x14.png" width="14" height="14" /></a></h2>
<ul class="recent_awards">
{% for award in recent_awards %}
{% cache 500 homepage_recent_award award.id %}
View
6 templates/profiles/profile.html
@@ -7,6 +7,10 @@
{% load uni_form_tags %}
{% load tagging_tags %}
+{% block extra_head %}
+ <link rel="alternate" type="application/atom+xml" title="{% trans "Recently Claimed Badges" %}" href="{% url badge_feed_profileawards other_user %}" />
+{% endblock %}
+
{% block head_title %}{% user_display other_user as other_user_display %}{% blocktrans %}Profile for {{ other_user_display }}{% endblocktrans %}{% endblock %}
{% block body %}
@@ -43,7 +47,7 @@
{% endif %}
<div class="badge_awards clearfix">
- <h2>Badges claimed</h2>
+ <h2>Badges claimed <a href="{% url badge_feed_profileawards other_user %}"><img src="{{ STATIC_URL }}badges/img/feed-icon-14x14.png" width="14" height="14" /></a></h2>
{% if can_show_hidden %}
<div>
{% if show_hidden %}
View
1  templates/site_base.html
@@ -19,7 +19,6 @@
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/jquery.autocomplete.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}css/main.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}css/smoothness/jquery-ui-1.8.2.custom.css" />
-
{% block extra_head %}{% endblock %}
{% endblock %}
View
36 tests/smoke_test.py
@@ -1,36 +0,0 @@
-## initially a quick smoke test to see if certain URLs throw exceptions or not
-## would have caught a high percentage of recent trunk breakages
-
-## run with ./manage.py runscript tests.smoke_test
-
-def run():
- from django.test.client import Client
- c = Client()
-
- pages = [
- '/',
- '/about/',
- '/profiles/',
- '/blog/',
- '/invitations/',
- '/notices/',
- '/messages/',
- '/announcements/',
- '/tweets/',
- '/tribes/',
- '/robots.txt',
- '/photos/',
- '/bookmarks/',
- ]
-
- for page in pages:
- print page,
- try:
- x = c.get(page)
- if x.status_code in [301, 302]:
- print x.status_code, "=>", x["Location"]
- else:
- print x.status_code
-
- except Exception, e:
- print e
View
2  vendor/activitystreams/README
@@ -0,0 +1,2 @@
+see also: http://github.com/apparentlymart/activity-streams-python
+
View
127 vendor/activitystreams/__init__.py
@@ -0,0 +1,127 @@
+
+
+class Activity(object):
+ actor = None
+ object = None
+ target = None
+ verb = None
+ time = None
+ generator = None
+ icon_url = None
+ service_provider = None
+ links = None
+
+ def __init__(self, actor=None, object=None, target=None, verb=None, time=None, generator=None, icon_url=None, service_provider=None, links=None):
+ self.actor = actor
+ self.object = object
+ self.target = target
+ self.verb = verb
+ self.time = time
+ self.service_provider = service_provider
+ self.generator = generator
+ self.icon_url = icon_url
+
+ if links is not None:
+ self.links = links
+ else:
+ self.links = []
+
+class Object(object):
+ id = None
+ name = None
+ url = None
+ object_type = None
+ summary = None
+ image = None
+ in_reply_to_object = None
+ attached_objects = None
+ reply_objects = None
+ reaction_activities = None
+ action_links = None
+ upstream_duplicate_ids = None
+ downstream_duplicate_ids = None
+ links = None
+
+ def __init__(self, id=None, name=None, url=None, object_type=None, summary=None, image=None, in_reply_to_object=None, attached_objects=None, reply_objects=None, reaction_activities=None, action_links=None, upstream_duplicate_ids=None, downstream_duplicate_ids=None, links=None):
+ self.id = id
+ self.name = name
+ self.url = url
+ self.object_type = object_type
+ self.summary = summary
+ self.image = image
+ self.in_reply_to_object = in_reply_to_object
+
+ if attached_objects is not None:
+ self.attached_objects = attached_objects
+ else:
+ self.attached_objects = []
+
+ if reply_objects is not None:
+ self.reply_objects = reply_objects
+ else:
+ self.reply_objects = []
+
+ if reaction_activities is not None:
+ self.reaction_activities = reaction_activities
+ else:
+ self.reaction_activities = []
+
+ if action_links is not None:
+ self.action_links = action_links
+ else:
+ self.action_links = []
+
+ if upstream_duplicate_ids is not None:
+ self.upstream_duplicate_ids = upstream_duplicate_ids
+ else:
+ self.upstream_duplicate_ids = []
+
+ if downstream_duplicate_ids is not None:
+ self.downstream_duplicate_ids = downstream_duplicate_ids
+ else:
+ self.downstream_duplicate_ids = []
+
+ if links is not None:
+ self.links = links
+ else:
+ self.links = []
+
+
+class MediaLink(object):
+ url = None
+ media_type = None
+ width = None
+ height = None
+ duration = None
+
+ def __init__(self, url=None, media_type=None, width=None, height=None, duration=None):
+ self.url = url
+ self.media_type = media_type
+ self.width = width
+ self.height = height
+ self.duration = duration
+
+
+class ActionLink(object):
+ url = None
+ caption = None
+
+ def __init__(self, url=None, caption=None):
+ self.url = url
+ self.caption = caption
+
+
+class Link(object):
+ url = None
+ media_type = None
+ rel = None
+
+ def __init__(self, url=None, media_type=None, rel=None):
+ self.url = url
+ self.media_type = media_type
+ self.rel = rel
+
+
+
+
+
View
272 vendor/activitystreams/atom.py
@@ -0,0 +1,272 @@
+
+
+from activitystreams import Activity, Object, MediaLink, ActionLink, Link
+
+
+import re
+import datetime
+import time
+
+
+class AtomActivity(Activity):
+ pass
+
+
+# This is a weird enum-like thing.
+class ObjectParseMode(object):
+ def __init__(self, reprstring):
+ self.reprstring = reprstring
+
+ def __repr__(self):
+ return self.reprstring
+ObjectParseMode.ATOM_ENTRY = ObjectParseMode("ObjectParseMode.ATOM_ENTRY")
+ObjectParseMode.ATOM_AUTHOR = ObjectParseMode("ObjectParseMode.ATOM_AUTHOR")
+ObjectParseMode.ACTIVITY_OBJECT = ObjectParseMode("ObjectParseMode.ACTIVITY_OBJECT")
+
+
+ATOM_PREFIX = "{http://www.w3.org/2005/Atom}"
+ACTIVITY_PREFIX = "{http://activitystrea.ms/spec/1.0/}"
+MEDIA_PREFIX = "{http://purl.org/syndication/atommedia}"
+
+ATOM_FEED = ATOM_PREFIX + "feed"
+ATOM_ENTRY = ATOM_PREFIX + "entry"
+ATOM_ID = ATOM_PREFIX + "id"
+ATOM_AUTHOR = ATOM_PREFIX + "author"
+ATOM_SOURCE = ATOM_PREFIX + "source"
+ATOM_TITLE = ATOM_PREFIX + "title"
+ATOM_SUMMARY = ATOM_PREFIX + "summary"
+ATOM_CONTENT = ATOM_PREFIX + "content"
+ATOM_LINK = ATOM_PREFIX + "link"
+ATOM_PUBLISHED = ATOM_PREFIX + "published"
+ATOM_NAME = ATOM_PREFIX + "name"
+ATOM_URI = ATOM_PREFIX + "uri"
+ATOM_GENERATOR = ATOM_PREFIX + "generator"
+ATOM_ICON = ATOM_PREFIX + "icon"
+ACTIVITY_SUBJECT = ACTIVITY_PREFIX + "subject"
+ACTIVITY_OBJECT = ACTIVITY_PREFIX + "object"
+ACTIVITY_OBJECT_TYPE = ACTIVITY_PREFIX + "object-type"
+ACTIVITY_VERB = ACTIVITY_PREFIX + "verb"
+ACTIVITY_TARGET = ACTIVITY_PREFIX + "target"
+ACTIVITY_ACTOR = ACTIVITY_PREFIX + "actor"
+POST_VERB = "http://activitystrea.ms/schema/1.0/post"
+MEDIA_WIDTH = MEDIA_PREFIX + "width"
+MEDIA_HEIGHT = MEDIA_PREFIX + "height"
+MEDIA_DURATION = MEDIA_PREFIX + "duration"
+MEDIA_DESCRIPTION = MEDIA_PREFIX + "description"
+
+
+def make_activities_from_feed(et):
+ feed_elem = et.getroot()
+ entry_elems = feed_elem.findall(ATOM_ENTRY)
+
+ activities = []
+
+ for entry_elem in entry_elems:
+ activities.extend(make_activities_from_entry(entry_elem, feed_elem))
+
+ return activities
+
+
+def make_activities_from_entry(entry_elem, feed_elem):
+ object_elems = entry_elem.findall(ACTIVITY_OBJECT)
+
+ activity_is_implied = False
+
+ if len(object_elems) == 0:
+ # Implied activity, so the entry itself represents the object.
+ activity_is_implied = True
+ object_elems = [ entry_elem ]
+
+ author_elem = entry_elem.find(ATOM_AUTHOR)
+ if author_elem is None:
+ source_elem = entry_elem.find(ATOM_SOURCE)
+ if source_elem is not None:
+ author_elem = source_elem.find(ATOM_AUTHOR)
+ if author_elem is None:
+ author_elem = feed_elem.find(ATOM_AUTHOR)
+
+ target_elem = entry_elem.find(ACTIVITY_TARGET)
+
+ published_elem = entry_elem.find(ATOM_PUBLISHED)
+ published_datetime = None
+ if published_elem is not None:
+ published_w3cdtf = published_elem.text
+ published_datetime = _parse_date_w3cdtf(published_w3cdtf)
+
+ verb_elem = entry_elem.find(ACTIVITY_VERB)
+ verb = None
+ if verb_elem is not None:
+ verb = verb_elem.text
+ else:
+ verb = POST_VERB
+
+ generator_elem = entry_elem.find(ATOM_GENERATOR)
+
+ icon_url = None
+ icon_elem = entry_elem.find(ATOM_ICON)
+ if icon_elem is not None:
+ icon_url = icon_elem.text
+
+ target = None
+ if target_elem:
+ target = make_object_from_elem(target_elem, feed_elem, ObjectParseMode.ACTIVITY_OBJECT)
+
+ actor = None
+ if author_elem:
+ actor = make_object_from_elem(author_elem, feed_elem, ObjectParseMode.ATOM_AUTHOR)
+
+ activities = []
+ for object_elem in object_elems:
+ if activity_is_implied:
+ object = make_object_from_elem(object_elem, feed_elem, ObjectParseMode.ATOM_ENTRY)
+ else:
+ object = make_object_from_elem(object_elem, feed_elem, ObjectParseMode.ACTIVITY_OBJECT)
+
+ activity = Activity(object=object, actor=actor, target=target, verb=verb, time=published_datetime, icon_url=icon_url)
+ activities.append(activity)
+
+ return activities
+
+
+def make_object_from_elem(object_elem, feed_elem, mode):
+
+ id = None
+ id_elem = object_elem.find(ATOM_ID)
+ if id_elem is not None:
+ id = id_elem.text
+
+ summary = None
+ summary_elem = object_elem.find(ATOM_SUMMARY)
+ if summary_elem is not None:
+ summary = summary_elem.text
+
+ name_tag_name = ATOM_TITLE
+ # The ATOM_AUTHOR parsing mode looks in atom:name instead of atom:title
+ if mode == ObjectParseMode.ATOM_AUTHOR:
+ name_tag_name = ATOM_NAME
+ name = None
+ name_elem = object_elem.find(name_tag_name)
+ if name_elem is not None:
+ name = name_elem.text
+
+ url = None
+ image = None
+ for link_elem in object_elem.findall(ATOM_LINK):
+ type = link_elem.get("type")
+ rel = link_elem.get("rel")
+ if rel is None or rel == "alternate":
+ if type is None or type == "text/html":
+ url = link_elem.get("href")
+ if rel == "preview":
+ if type is None or type == "image/jpeg" or type == "image/gif" or type == "image/png":
+ # FIXME: Should pull out the width/height/duration attributes from AtomMedia too.
+ image = MediaLink(url=link_elem.get("href"))
+
+ # In the atom:author parse mode we fall back on atom:uri if there's no link rel="alternate"
+ if url is None and mode == ObjectParseMode.ATOM_AUTHOR:
+ uri_elem = object_elem.find(ATOM_URI)
+ if uri_elem is not None:
+ url = uri_elem.text
+
+ object_type_elem = object_elem.find(ACTIVITY_OBJECT_TYPE)
+ object_type = None
+ if object_type_elem is not None:
+ object_type = object_type_elem.text
+
+ return Object(id=id, name=name, url=url, object_type=object_type, image=image, summary=summary)
+
+
+# This is pilfered from Universal Feed Parser.
+def _parse_date_w3cdtf(dateString):
+ def __extract_date(m):
+ year = int(m.group('year'))
+ if year < 100:
+ year = 100 * int(time.gmtime()[0] / 100) + int(year)
+ if year < 1000:
+ return 0, 0, 0
+ julian = m.group('julian')
+ if julian:
+ julian = int(julian)
+ month = julian / 30 + 1
+ day = julian % 30 + 1
+ jday = None
+ while jday != julian:
+ t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
+ jday = time.gmtime(t)[-2]
+ diff = abs(jday - julian)
+ if jday > julian:
+ if diff < day:
+ day = day - diff
+ else:
+ month = month - 1
+ day = 31
+ elif jday < julian:
+ if day + diff < 28:
+ day = day + diff
+ else:
+ month = month + 1
+ return year, month, day
+ month = m.group('month')
+ day = 1
+ if month is None:
+ month = 1
+ else:
+ month = int(month)
+ day = m.group('day')
+ if day:
+ day = int(day)
+ else:
+ day = 1
+ return year, month, day
+
+ def __extract_time(m):
+ if not m:
+ return 0, 0, 0
+ hours = m.group('hours')
+ if not hours:
+ return 0, 0, 0
+ hours = int(hours)
+ minutes = int(m.group('minutes'))
+ seconds = m.group('seconds')
+ if seconds:
+ seconds = int(float(seconds))
+ else:
+ seconds = 0
+ return hours, minutes, seconds
+
+ def __extract_tzd(m):
+ '''Return the Time Zone Designator as an offset in seconds from UTC.'''
+ if not m:
+ return 0
+ tzd = m.group('tzd')
+ if not tzd:
+ return 0
+ if tzd == 'Z':
+ return 0
+ hours = int(m.group('tzdhours'))
+ minutes = m.group('tzdminutes')
+ if minutes:
+ minutes = int(minutes)
+ else:
+ minutes = 0
+ offset = (hours*60 + minutes) * 60
+ if tzd[0] == '+':
+ return -offset
+ return offset
+
+ __date_re = ('(?P<year>\d\d\d\d)'
+ '(?:(?P<dsep>-|)'
+ '(?:(?P<julian>\d\d\d)'
+ '|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
+ __tzd_re = '(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
+ __tzd_rx = re.compile(__tzd_re)
+ __time_re = ('(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
+ '(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
+ + __tzd_re)
+ __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
+ __datetime_rx = re.compile(__datetime_re)
+ m = __datetime_rx.match(dateString)
+ if (m is None) or (m.group() != dateString): return
+ gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0)
+ if gmt[0] == 0: return
+ return datetime.datetime.utcfromtimestamp(time.mktime(gmt) + __extract_tzd(m) - time.timezone)
View
243 vendor/activitystreams/json.py
@@ -0,0 +1,243 @@
+
+
+from activitystreams import Activity, Object, MediaLink, ActionLink, Link
+
+
+import re
+import datetime
+import time
+
+
+def make_activities_from_stream_dict(stream_dict):
+ activities = []
+
+ for activity_dict in stream_dict["items"]:
+ activities.append(make_activity_from_activity_dict(activity_dict))
+
+ return activities
+
+
+def make_activity_from_activity_dict(activity_dict, implied_object=None):
+ object_dict = None
+ actor_dict = None
+ target_dict = None
+ generator_dict = None
+ service_provider_dict = None
+ verb = None
+ published_datetime = None
+
+ object = None
+ if implied_object is not None:
+ object = implied_object
+ else:
+ if "object" in activity_dict:
+ object_dict = activity_dict["object"]
+ object = make_object_from_object_dict(object_dict)
+
+ if "actor" in activity_dict:
+ actor_dict = activity_dict["actor"]
+
+ if "target" in activity_dict:
+ target_dict = activity_dict["target"]
+
+ if "generator" in activity_dict:
+ generator_dict = activity_dict["generator"]
+
+ if "provider" in activity_dict:
+ service_provider_dict = activity_dict["provider"]
+
+ if "postedTime" in activity_dict:
+ published_w3cdtf = activity_dict["postedTime"]
+ published_datetime = _parse_date_w3cdtf(published_w3cdtf)
+
+ if "verb" in activity_dict:
+ verb = activity_dict["verb"]
+
+ return Activity(
+ object=object,
+ actor=make_object_from_object_dict(actor_dict),
+ target=make_object_from_object_dict(target_dict),
+ generator=make_object_from_object_dict(generator_dict),
+ service_provider=make_object_from_object_dict(service_provider_dict),
+ time=published_datetime,
+ verb=verb
+ )
+
+
+def make_object_from_object_dict(object_dict, implied_in_reply_to_object=None):
+
+ if object_dict is None:
+ return None
+
+ id = None
+ name = None
+ summary = None
+ image = None
+ url = None
+ object_type = None
+ in_reply_to_object = None
+ reply_objects = []
+ reaction_activities = []
+ attached_objects = []
+ upstream_duplicate_ids = []
+ downstream_duplicate_ids = []
+ links = []
+
+ if "id" in object_dict:
+ id = object_dict["id"]
+
+ if "displayName" in object_dict:
+ name = object_dict["displayName"]
+
+ if "summary" in object_dict:
+ summary = object_dict["summary"]
+
+ if "permalinkUrl" in object_dict:
+ url = object_dict["permalinkUrl"]
+
+ if "objectType" in object_dict:
+ object_type = object_dict["objectType"]
+
+ if "image" in object_dict:
+ image = make_media_link_from_media_link_dict(object_dict["image"])
+
+ # FIXME: implement the rest of the components
+
+ return Object(
+ id=id,
+ name=name,
+ summary=summary,
+ image=image,
+ url=url,
+ object_type=object_type,
+ in_reply_to_object=in_reply_to_object,
+ reply_objects=reply_objects,
+ reaction_activities=reaction_activities,
+ attached_objects=attached_objects,
+ upstream_duplicate_ids=upstream_duplicate_ids,
+ downstream_duplicate_ids=downstream_duplicate_ids,
+ links=links,
+ )
+
+
+def make_media_link_from_media_link_dict(media_link_dict):
+ url = None
+ width = None
+ height = None
+ duration = None
+
+ if "url" in media_link_dict:
+ url = media_link_dict["url"]
+
+ if "width" in media_link_dict:
+ width = media_link_dict["width"]
+
+ if "height" in media_link_dict:
+ height = media_link_dict["height"]
+
+ if "duration" in media_link_dict:
+ duration = media_link_dict["duration"]
+
+ return MediaLink(
+ url=url,
+ width=width,
+ height=height,
+ duration=duration,
+ )
+
+
+
+# This is pilfered from Universal Feed Parser.
+def _parse_date_w3cdtf(dateString):
+ def __extract_date(m):
+ year = int(m.group('year'))
+ if year < 100:
+ year = 100 * int(time.gmtime()[0] / 100) + int(year)
+ if year < 1000:
+ return 0, 0, 0
+ julian = m.group('julian')
+ if julian:
+ julian = int(julian)
+ month = julian / 30 + 1
+ day = julian % 30 + 1
+ jday = None
+ while jday != julian:
+ t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
+ jday = time.gmtime(t)[-2]
+ diff = abs(jday - julian)
+ if jday > julian:
+ if diff < day:
+ day = day - diff
+ else:
+ month = month - 1
+ day = 31
+ elif jday < julian:
+ if day + diff < 28:
+ day = day + diff
+ else:
+ month = month + 1
+ return year, month, day
+ month = m.group('month')
+ day = 1
+ if month is None:
+ month = 1
+ else:
+ month = int(month)
+ day = m.group('day')
+ if day:
+ day = int(day)
+ else:
+ day = 1
+ return year, month, day
+
+ def __extract_time(m):
+ if not m:
+ return 0, 0, 0
+ hours = m.group('hours')
+ if not hours:
+ return 0, 0, 0
+ hours = int(hours)
+ minutes = int(m.group('minutes'))
+ seconds = m.group('seconds')
+ if seconds:
+ seconds = int(float(seconds))
+ else:
+ seconds = 0
+ return hours, minutes, seconds
+
+ def __extract_tzd(m):
+ '''Return the Time Zone Designator as an offset in seconds from UTC.'''
+ if not m:
+ return 0
+ tzd = m.group('tzd')
+ if not tzd:
+ return 0
+ if tzd == 'Z':
+ return 0
+ hours = int(m.group('tzdhours'))
+ minutes = m.group('tzdminutes')
+ if minutes:
+ minutes = int(minutes)
+ else:
+ minutes = 0
+ offset = (hours*60 + minutes) * 60
+ if tzd[0] == '+':
+ return -offset
+ return offset
+
+ __date_re = ('(?P<year>\d\d\d\d)'
+ '(?:(?P<dsep>-|)'
+ '(?:(?P<julian>\d\d\d)'
+ '|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
+ __tzd_re = '(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
+ __tzd_rx = re.compile(__tzd_re)
+ __time_re = ('(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
+ '(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
+ + __tzd_re)
+ __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
+ __datetime_rx = re.compile(__datetime_re)
+ m = __datetime_rx.match(dateString)
+ if (m is None) or (m.group() != dateString): return
+ gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0)
+ if gmt[0] == 0: return
+ return datetime.datetime.utcfromtimestamp(time.mktime(gmt) + __extract_tzd(m) - time.timezone)
Please sign in to comment.
Something went wrong with that request. Please try again.