Merge branch 'master' of git://github.com/paulosman/batucada

This commit is contained in:
theinterned 2010-12-29 17:45:31 -05:00
Родитель 39d61f42be c42af2cb65
Коммит c971afdc09
20 изменённых файлов: 389 добавлений и 90 удалений

Просмотреть файл

@ -0,0 +1,5 @@
from activity.models import Activity, RemoteObject
def send(actor, verb, object, target=None):
pass

42
apps/activity/feeds.py Normal file
Просмотреть файл

@ -0,0 +1,42 @@
from django.contrib.syndication.views import Feed
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.utils.feedgenerator import Atom1Feed
from activity.models import Activity
from users.models import UserProfile
class UserActivityFeed(Feed):
"""Atom feed of user activities."""
feed_type = Atom1Feed
def author_name(self, user):
return user.name
def title(self, user):
return user.name
def subtitle(self, user):
return _('Activity feed for %s' % (user.name,))
def link(self, user):
return reverse('users_profile_view',
kwargs={'username': user.username})
def get_object(self, request, username):
return get_object_or_404(UserProfile, username=username)
def items(self, user):
return Activity.objects.filter(actor=user)[:25]
def item_title(self, item):
return item.verb
def item_description(self, item):
return u"%s activity performed by %s" % (item.verb, item.actor.name)
def item_link(self, item):
return u'http://blah.com'

24
apps/activity/models.py Normal file
Просмотреть файл

@ -0,0 +1,24 @@
from django.db import models
from drumbeat.models import ModelBase
class RemoteObject(models.Model):
"""Represents an object originating from another system."""
object_type = models.URLField(verify_exists=False)
link = models.ForeignKey('links.Link')
title = models.CharField(max_length=255)
uri = models.URLField(null=True)
created_on = models.DateTimeField(auto_now_add=True)
class Activity(ModelBase):
"""Represents a single activity entry."""
actor = models.ForeignKey('users.UserProfile')
verb = models.URLField(verify_exists=False)
status = models.ForeignKey('statuses.Status', null=True)
project = models.ForeignKey('projects.Project', null=True)
target_user = models.ForeignKey('users.UserProfile', null=True,
related_name='target_user')
remote_object = models.ForeignKey(RemoteObject, null=True)
parent = models.ForeignKey('self', null=True)
created_on = models.DateTimeField(auto_now_add=True)

63
apps/activity/schema.py Normal file
Просмотреть файл

@ -0,0 +1,63 @@
from django.utils.translation import ugettext_lazy as _lazy
# a list of verbs defined in the activity schema
verbs = {
'favorite': 'http://activitystrea.ms/schema/1.0/favorite',
'follow': 'http://activitystrea.ms/schema/1.0/follow',
'like': 'http://activitystrea.ms/schema/1.0/like',
'make-friend': 'http://activitystrea.ms/schema/1.0/make-friend',
'join': 'http://activitystrea.ms/schema/1.0/join',
'play': 'http://activitystrea.ms/schema/1.0/play',
'post': 'http://activitystrea.ms/schema/1.0/post',
'save': 'http://activitystrea.ms/schema/1.0/save',
'share': 'http://activitystrea.ms/schema/1.0/share',
'tag': 'http://activitystrea.ms/schema/1.0/tag',
'update': 'http://activitystrea.ms/schema/1.0/update',
'rsvp-yes': 'http://activitystrea.ms/schema/1.0/rsvp-yes',
'rsvp-no': 'http://activitystrea.ms/schema/1.0/rsvp-no',
'rsvp-maybe': 'http://activitystrea.ms/schema/1.0/rsvp-maybe',
}
verbs_by_uri = {}
for key, value in verbs.iteritems():
verbs_by_uri[value] = key
past_tense = {
'favorite': _lazy('favorited'),
'follow': _lazy('started following'),
'like': _lazy('liked'),
'make-friend': _lazy('is now friends with'),
'join': _lazy('joined'),
'play': _lazy('played'),
'post': _lazy('posted'),
'save': _lazy('saved'),
'share': _lazy('shared'),
'tag': _lazy('tagged'),
'update': _lazy('updated'),
'rsvp-yes': _lazy('is attending'),
'rsvp-no': _lazy('is not attending'),
'rsvp-maybe': _lazy('might be attending'),
}
# a list of base object types defined in the activity schema
object_types = {
'article': 'http://activitystrea.ms/schema/1.0/article',
'audio': 'http://activitystrea.ms/schema/1.0/audio',
'bookmark': 'http://activitystrea.ms/schema/1.0/bookmark',
'comment': 'http://activitystrea.ms/schema/1.0/comment',
'file': 'http://activitystrea.ms/schema/1.0/file',
'folder': 'http://activitystrea.ms/schema/1.0/folder',
'group': 'http://activitystrea.ms/schema/1.0/group',
'note': 'http://activitystrea.ms/schema/1.0/note',
'person': 'http://activitystrea.ms/schema/1.0/person',
'photo': 'http://activitystrea.ms/schema/1.0/photo',
'photo-album': 'http://activitystrea.ms/schema/1.0/photo-album',
'place': 'http://activitystrea.ms/schema/1.0/place',
'playlist': 'http://activitystrea.ms/schema/1.0/playlist',
'product': 'http://activitystrea.ms/schema/1.0/product',
'review': 'http://activitystrea.ms/schema/1.0/review',
'service': 'http://activitystrea.ms/schema/1.0/service',
'status': 'http://activitystrea.ms/schema/1.0/status',
'video': 'http://activitystrea.ms/schema/1.0/video',
'event': 'http://activitystrea.ms/schema/1.0/event',
}

Просмотреть файл

Просмотреть файл

@ -0,0 +1,80 @@
from django import template
from activity import schema
register = template.Library()
@register.filter
def truncate(value, arg):
"""
Truncates a string after a given number of chars
Argument: Number of chars to truncate after
"""
try:
length = int(arg)
except ValueError: # invalid literal for int()
return value # Fail silently.
if not isinstance(value, basestring):
value = str(value)
if (len(value) > length):
return value[:length] + "..."
else:
return value
@register.filter
def should_hyperlink(activity):
if activity.verb == schema.verbs['follow'] and activity.target_user:
return True
if not activity.remote_object:
return False
if not activity.remote_object.uri:
return False
if activity.remote_object.object_type != schema.object_types['article']:
return False
return True
@register.filter
def get_link(activity):
if activity.remote_object and activity.remote_object.uri:
return activity.remote_object.uri
if activity.target_user:
return activity.target_user.get_absolute_url()
@register.filter
def get_link_name(activity):
if activity.remote_object:
return activity.remote_object.title
if activity.target_user:
return activity.target_user.name
@register.filter
def activity_representation(activity):
if activity.status:
return activity.status
if activity.remote_object:
if activity.remote_object.title:
return activity.remote_object.title
return None
@register.filter
def should_show_verb(activity):
if activity.status:
return False
if activity.remote_object:
return False
return True
@register.filter
def friendly_verb(activity):
try:
verb = schema.verbs_by_uri[activity.verb]
return schema.past_tense[verb].capitalize()
except KeyError:
return activity.verb

13
apps/activity/urls.py Normal file
Просмотреть файл

@ -0,0 +1,13 @@
from django.conf.urls.defaults import patterns, url
from activity.feeds import UserActivityFeed
urlpatterns = patterns('',
url(r'^activity/(?P<activity_id>[\d]+)/', 'activity.views.index',
name='activity_index'),
url(r'^activity/delete/$', 'activity.views.delete',
name='activity_delete'),
url(r'^(?P<username>[\w-]+)/feed', UserActivityFeed(),
name='activity_user_feed'),
)

9
apps/activity/views.py Normal file
Просмотреть файл

@ -0,0 +1,9 @@
from django.http import HttpResponse
def index(request, activity_id):
return HttpResponse('activity_index')
def delete(request):
return HttpResponse('activity_delete')

Просмотреть файл

@ -27,10 +27,10 @@ def dashboard(request):
project_ids = [p.pk for p in projects_following]
user_ids = [u.pk for u in users_following]
activities = Activity.objects.select_related(
'actor', 'target', 'actor__user').filter(
Q(actor__user__exact=request.user) |
Q(actor__user__in=user_ids) | Q(target_id__in=project_ids) |
Q(object_id__in=project_ids),
'actor', 'status', 'project', 'remote_object',
'remote_object__link').filter(
Q(actor__exact=profile) |
Q(actor__in=user_ids) | Q(project__in=project_ids),
).order_by('-created_on')[0:25]
user_projects = Project.objects.filter(created_by=profile)
return render_to_response('dashboard/dashboard.html', {

Просмотреть файл

@ -1,4 +1,3 @@
import sys
import urllib2
from django.conf import settings
@ -7,8 +6,7 @@ from celery.task import Task
from django_push.subscriber.models import Subscription
from links import utils
import activity
from activity.models import RemoteObject, Activity
class SubscribeToFeed(Task):
@ -77,22 +75,49 @@ class HandleNotification(Task):
When a notification of a new or updated entry is received, parse
the entry and create an activity representation of it.
"""
def get_activity_namespace_prefix(self, feed):
"""Discover the prefix used for the activity namespace."""
namespaces = feed.namespaces
activity_prefix = [prefix for prefix, ns in namespaces.iteritems()
if ns == 'http://activitystrea.ms/spec/1.0/']
if activity_prefix:
return activity_prefix[0]
return None
def get_namespaced_attr(self, entry, prefix, attr):
"""Feedparser prepends namespace prefixes to attribute names."""
qname = '_'.join((prefix, attr))
return getattr(entry, qname, None)
def create_activity_entry(self, entry, sender, activity_prefix=None):
"""Create activity feed entries for the provided feed entry."""
verb, object_type = None, None
if activity_prefix:
verb = self.get_namespaced_attr(
entry, activity_prefix, 'verb')
object_type = self.get_namespaced_attr(
entry, activity_prefix, 'object-type')
if not verb:
verb = 'http://activitystrea.ms/schema/1.0/post'
if not object_type:
object_type = 'http://activitystrea.ms/schema/1.0/article'
title = getattr(entry, 'title', None)
uri = getattr(entry, 'link', None)
if not (title and uri):
return
for link in sender.link_set.all():
remote_obj = RemoteObject(
link=link, title=title, uri=uri, object_type=object_type)
remote_obj.save()
activity = Activity(
actor=link.user, verb=verb, remote_object=remote_obj)
activity.save()
def run(self, notification, sender, **kwargs):
"""Parse feed and create activity entries."""
log = self.get_logger(**kwargs)
prefix = self.get_activity_namespace_prefix(notification)
for entry in notification.entries:
log.debug("Received notification of entry: %s, %s" % (
entry.title, entry.link))
if isinstance(entry.content, list):
content = entry.content[0]
if 'value' in content:
content = content['value']
else:
content = entry.content
for link in sender.link_set.all():
activity.send(link.user.user, 'post', {
'type': 'note',
'title': entry.title,
'content': content,
})
self.create_activity_entry(entry, sender, activity_prefix=prefix)

Просмотреть файл

@ -88,3 +88,31 @@ class TestLinkParsing(TestCase):
self.assertEqual(
'http://pubsubhubbub.appspot.com/',
hub_url)
def test_normalize_url(self):
url = '/feed.rss'
base_url = 'http://example.com'
self.assertEqual('http://example.com/feed.rss',
utils.normalize_url(url, base_url))
def test_normalize_url_two_slashes(self):
url = '/feed.rss'
base_url = 'http://example.com/'
self.assertEqual('http://example.com/feed.rss',
utils.normalize_url(url, base_url))
def test_normalize_url_trailing_slash_base(self):
url = 'feed.rss'
base_url = 'http://example.com/'
self.assertEqual('http://example.com/feed.rss',
utils.normalize_url(url, base_url))
def test_normalize_url_no_slashes(self):
url = 'feed.rss'
base_url = 'http://example.com'
self.assertEqual('http://example.com/feed.rss',
utils.normalize_url(url, base_url))
def test_normalize_url_good_url(self):
url = 'http://example.com/atom'
self.assertEqual(url, utils.normalize_url(url, 'http://example.com'))

Просмотреть файл

@ -17,8 +17,15 @@ def normalize_url(url, base_url):
parts = urlparse.urlparse(url)
if parts.scheme and parts.netloc:
return url # looks fine
if not base_url:
return url
base_parts = urlparse.urlparse(base_url)
return base_parts.scheme + '://' + base_parts.netloc + '/' + url
server = '://'.join((base_parts.scheme, base_parts.netloc))
if server[-1] != '/' and url[0] != '/':
server = server + '/'
if server[-1] == '/' and url[0] == '/':
server = server[:-1]
return server + url
class FeedHandler(sax.ContentHandler):
@ -85,7 +92,7 @@ def parse_feed_url(content, url=None):
return None
def parse_hub_url(content, base_url):
def parse_hub_url(content, base_url=None):
"""Parse the provided xml and find a hub link."""
handler = FeedHandler()
parser = sax.make_parser()

Просмотреть файл

@ -87,8 +87,11 @@ def project_creation_handler(sender, **kwargs):
target_project=project).save()
try:
import activity
activity.send(project.created_by.user, 'post', project)
from activity.models import Activity
act = Activity(actor=project.created_by,
verb='http://activitystrea.ms/schema/1.0/post',
project=project)
act.save()
except ImportError:
return

Просмотреть файл

@ -2,7 +2,6 @@ import logging
from django import http
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
@ -24,10 +23,8 @@ def show(request, slug):
if request.user.is_authenticated():
profile = request.user.get_profile()
is_following = profile.is_following(project)
project_type = ContentType.objects.get_for_model(project)
activities = Activity.objects.filter(
target_id=project.id,
target_content_type=project_type).order_by('-created_on')[0:10]
project=project).order_by('-created_on')[0:10]
nstatuses = Status.objects.filter(project=project).count()
context = {
'project': project,

Просмотреть файл

@ -8,6 +8,7 @@ from django.db.models.signals import post_save
from django.utils.translation import ugettext as _
from drumbeat.models import ModelBase
from activity.models import Activity
log = logging.getLogger(__name__)
@ -43,7 +44,6 @@ class Relationship(ModelBase):
'to': repr(self.target_user or self.target_project),
}
admin.site.register(Relationship)
###########
@ -55,14 +55,11 @@ def follow_handler(sender, **kwargs):
rel = kwargs.get('instance', None)
if not isinstance(rel, Relationship):
return
try:
import activity
if rel.target_user:
activity.send(rel.source.user, 'follow', rel.target_user.user)
else:
activity.send(rel.source.user, 'follow', rel.target_project)
except ImportError:
pass
activity = Activity(actor=rel.source,
verb='http://activitystrea.ms/schema/1.0/follow')
if rel.target_user:
activity.target_user = rel.target_user
else:
activity.project = rel.target_project
activity.save()
post_save.connect(follow_handler, sender=Relationship)

Просмотреть файл

@ -5,6 +5,7 @@ from django.db import models
from django.db.models.signals import post_save
from django.utils.timesince import timesince
from activity.models import Activity
from drumbeat.models import ModelBase
@ -36,11 +37,12 @@ def status_creation_handler(sender, **kwargs):
status = kwargs.get('instance', None)
if not isinstance(status, Status):
return
try:
import activity
activity.send(
status.author.user, 'post', status, target=status.project)
except ImportError:
return
activity = Activity(
actor=status.author,
verb='http://activitystrea.ms/schema/1.0/post',
status=status,
)
if status.project:
activity.project = status.project
activity.save()
post_save.connect(status_creation_handler, sender=Status)

Просмотреть файл

@ -17,6 +17,7 @@ from users.decorators import anonymous_only
from links.models import Link
from projects.models import Project
from drumbeat import messages
from activity.models import Activity
log = logging.getLogger(__name__)
@ -173,6 +174,9 @@ def profile_view(request, username):
projects = profile.following(model=Project)
followers = profile.followers()
links = Link.objects.select_related('subscription').filter(user=profile)
activities = Activity.objects.select_related(
'actor', 'status', 'project').filter(
actor=profile).order_by('-created_on')[0:25]
return render_to_response('users/profile.html', {
'profile': profile,
'following': following,
@ -181,6 +185,7 @@ def profile_view(request, username):
'skills': profile.tags.filter(category='skill'),
'interests': profile.tags.filter(category='interest'),
'links': links,
'activities': activities,
}, context_instance=RequestContext(request))

Просмотреть файл

@ -1,21 +1,29 @@
Django==1.2.3
pil
django-messages
south
jogging
feedparser
BeautifulSoup
IPython
django-cache-machine
django-taggit
djcelery
# Development
ipython==0.10.1
nose==0.11.1
-e git://github.com/jbalogh/django-nose#egg=django-nose
-e git://github.com/clouserw/tower.git#egg=tower
-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar
# Production
django==1.2.3
python-memcached==1.45
celery==2.0.3
django-celery==2.0.2
django-taggit==0.9.1
beautifulsoup==3.2.0
feedparser==4.1
jogging==0.2.2
south==0.7.3
django-messages==0.4.4
-e git://github.com/jbalogh/django-cache-machine.git#egg=django-cache-machine
-e git://github.com/jsocol/commonware.git#egg=commonware
-e git://github.com/paulosman/python-xrd#egg=xrd
-e git://github.com/paulosman/django-wellknown.git#egg=wellknown
-e git://github.com/paulosman/django-activity.git#egg=activity
-e git://github.com/jsocol/commonware.git#egg=commonware
-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar
-e git://github.com/mozilla/django-recaptcha.git#egg=django-recaptcha
-e git://github.com/brutasse/django-push.git#egg=django_push
# Compiled
MySQL-python==1.2.3c1
PIL==1.1.7

Просмотреть файл

@ -4,48 +4,39 @@
<li class="post-container">
<a href="#" class="reply-to">Reply</a>
<form method="post" action="{% locale_url activity_delete %}">
{% csrf_token %}
<input type="hidden" name="id" value="{{ activity.id }}">
<a href="#" class="delete activity-delete">Delete</a>
</form>
{% if activity.actor.is_remote %}
<img class="member-picture" width="54" height="54" src="{{ MEDIA_URL }}images/member-missing.png">
<div class="post-contents">
<div class="post-details">
{{ activity.actor.get_full_name }}
<a href="{{ activity.get_absolute_url }}">{{ activity.created_on|timesince }} {{ _('ago') }}</a>
</div>
{% else %}
<a href="{{ activity.actor.user.get_profile.get_absolute_url }}">
<img class="member-picture" width="54" height="54" src="{{ MEDIA_URL }}{{ activity.actor.user.get_profile.image_or_default }}">
<a href="{{ activity.actor.get_absolute_url }}">
<img class="member-picture" width="54" height="54" src="{{ MEDIA_URL }}{{ activity.actor.image_or_default }}">
</a>
<div class="post-contents">
<div class="post-details">
<a class="member-name" href="{{ activity.actor.user.get_profile.get_absolute_url }}">{{ activity.actor.user.get_profile.name }}</a>
<a href="{{ activity.get_absolute_url }}">{{ activity.created_on|timesince }} {{ _(' ago') }}</a>
<a class="member-name" href="{{ activity.actor.get_absolute_url }}">{{ activity.actor.name }}</a>
{{ activity.created_on|timesince }} {{ _(' ago') }}
{% if activity.remote_object %}
{{ _('via ') }}<a href="{{ activity.remote_object.link.url }}">{{ activity.remote_object.link.name }}</a>
{% endif %}
</div>
{% endif %}
<div class="post-body">
{% if activity.verb == 'http://activitystrea.ms/schema/1.0/post' %}
{{ activity.object|urlize }}
{% if activity|should_show_verb %}
{{ activity|friendly_verb }}
{% endif %}
{% if activity|should_hyperlink %}
{% if activity.target_user %}
<a href="{{ activity|get_link }}">{{ activity|get_link_name }}</a>
{% else %}
{{ activity|get_link_name }} <a href="{{ activity|get_link }}">{{ activity|get_link|truncate:50 }}</a>
{% endif %}
{% else %}
{{ activity.verb|friendly }} <a href="{{ activity.obj.get_absolute_url }}">{{ activity.object }}</a>
{{ activity|activity_representation }}
{% endif %}
</div> <!-- /.post-body -->
{% if activity.target %}
{% if activity.project %}
<ul class="post-tags">
<li><a href="{{ activity.target.get_absolute_url }}">{{ activity.target }}</a></li>
<li><a href="{{ activity.project.get_absolute_url }}">{{ activity.project }}</a></li>
</ul>
{% endif %}

Просмотреть файл

@ -8,7 +8,7 @@ urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
(r'', include('dashboard.urls')),
(r'', include('wellknown.urls')),
(r'^activity/', include('activity.urls')),
(r'', include('activity.urls')),
(r'^statuses/', include('statuses.urls')),
(r'^projects/', include('projects.urls')),
(r'^events/', include('events.urls')),