Starting to work on links in projects. Basically a project can add links and a scheduled task will pull in the RSS or Atom feed and add entries to the activity stream of that project. Work in progress

This commit is contained in:
Paul Osman 2010-11-18 17:48:08 -05:00
Родитель 6d3b1e7e15
Коммит 3470f5d307
12 изменённых файлов: 261 добавлений и 28 удалений

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

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

@ -0,0 +1,8 @@
from django.db import models
from projects.models import Link
class Entry(models.Model):
link = models.ForeignKey(Link, related_name='feed_entries')
processed_on = models.DateTimeField(auto_now_add=True)

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

@ -0,0 +1,19 @@
import logging
import feedparser
from celery.task.schedules import crontab
from celery.decorators import periodic_task
from projects.models import Link
log = logging.getLogger(__name__)
@periodic_task(run_every=crontab())
def load_project_feeds():
links = Link.objects.all()
for link in links:
feed = feedparser.parse(link.url)
log.debug(len(feed.entries))
log.info("Running test task")

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

@ -4,25 +4,35 @@ from django import forms
from django.utils.translation import ugettext as _
from messages.models import Message
from projects.models import Project
from projects.models import Project, Link
class ProtectedProjectForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ProtectedProjectForm, self).__init__(*args,**kwargs)
super(ProtectedProjectForm, self).__init__(*args, **kwargs)
protected = getattr(self.Meta, 'protected')
project = kwargs.get('instance', None)
if not project or not project.featured:
for field in protected:
self.fields.pop(field)
class ProjectForm(ProtectedProjectForm):
class Meta:
model = Project
exclude = ('created_by', 'slug', 'featured')
protected = ('template', 'css')
class ProjectLinkForm(forms.ModelForm):
class Meta:
model = Link
widgets = {
'project': forms.HiddenInput(),
}
class ProjectContactUsersForm(forms.Form):
"""
A modified version of ``messages.forms.ComposeForm`` that enables
@ -31,12 +41,12 @@ class ProjectContactUsersForm(forms.Form):
"""
project = forms.IntegerField(
required=True,
widget=forms.HiddenInput()
widget=forms.HiddenInput(),
)
subject = forms.CharField(label=_(u'Subject'))
body = forms.CharField(
label=_(u'Body'),
widget=forms.Textarea(attrs={'rows': '12', 'cols': '55'})
widget=forms.Textarea(attrs={'rows': '12', 'cols': '55'}),
)
def save(self, sender, parent_msg=None):
@ -44,17 +54,18 @@ class ProjectContactUsersForm(forms.Form):
try:
project = Project.objects.get(id=int(project))
except Project.DoesNotExist:
raise forms.ValidationError(_(u'Hmm, that does not look like a valid project'))
raise forms.ValidationError(
_(u'Hmm, that does not look like a valid project'))
recipients = project.followers()
subject = self.cleaned_data['subject']
body = self.cleaned_data['body']
message_list = []
for r in recipients:
msg = Message(
sender = sender,
recipient = r,
subject = subject,
body = body,
sender=sender,
recipient=r,
subject=subject,
body=body,
)
if parent_msg is not None:
msg.parent_msg = parent_msg

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

@ -0,0 +1,86 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Link.feed_url'
db.add_column('projects_link', 'feed_url', self.gf('django.db.models.fields.URLField')(default='', max_length=200), keep_default=False)
# Adding unique constraint on 'Link', fields ['project', 'url']
db.create_unique('projects_link', ['project_id', 'url'])
def backwards(self, orm):
# Removing unique constraint on 'Link', fields ['project', 'url']
db.delete_unique('projects_link', ['project_id', 'url'])
# Deleting field 'Link.feed_url'
db.delete_column('projects_link', 'feed_url')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'projects.link': {
'Meta': {'unique_together': "(('project', 'url'),)", 'object_name': 'Link'},
'feed_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']"}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '250'}),
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
},
'projects.project': {
'Meta': {'object_name': 'Project'},
'call_to_action': ('django.db.models.fields.TextField', [], {}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}),
'css': ('django.db.models.fields.TextField', [], {}),
'description': ('django.db.models.fields.TextField', [], {}),
'featured': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
'template': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['projects']

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

@ -1,32 +1,35 @@
import urllib
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.template.defaultfilters import slugify
from BeautifulSoup import BeautifulSoup
from relationships.models import followers
class Project(models.Model):
"""Placeholder model for projects."""
object_type = 'http://drumbeat.org/activity/schema/1.0/project'
generalized_object_type = 'http://activitystrea.ms/schema/1.0/group'
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField()
call_to_action = models.TextField()
created_by = models.ForeignKey(User, related_name='projects')
featured = models.BooleanField()
template = models.TextField()
css = models.TextField()
def __unicode__(self):
return self.name
@models.permalink
def get_absolute_url(self):
return ('projects_show', (), {
'slug': self.slug
'slug': self.slug,
})
def save(self):
@ -44,18 +47,55 @@ class Project(models.Model):
Project.followers = followers
class Link(models.Model):
title = models.CharField(max_length=250)
url = models.URLField()
project = models.ForeignKey(Project)
feed_url = models.URLField(editable=False, default='')
class Meta:
unique_together = ('project', 'url',)
def get_syndication_url(self):
"""
Parse the contents of this link and return the first Atom
or RSS feed URI we find.
@TODO - Account for cases where multiple rel="alternate"
link elements are found in the document.
"""
contents = urllib.urlopen(self.url).read()
soup = BeautifulSoup(contents)
links = soup.head.findAll('link')
# BeautifulSoup instances are not actually dictionaries, so
# we can't use the more proper 'key in dict' syntax and
# must instead use the deprecated 'has_key()' method.
alternate = [link for link in links
if link.has_key('rel') and link['rel'] == 'alternate']
atom = [link['href'] for link in alternate
if (link.has_key('href') and link.has_key('type')
and link['type'] == 'application/atom+xml')]
# we prefer atom to rss
if atom:
return atom[0]
rss = [link['href'] for link in links
if (link.has_key('href') and link.has_key('type')
and link['type'] == 'application/rss+xml')]
if rss:
return rss[0]
return None
def project_creation_handler(sender, **kwargs):
project = kwargs.get('instance', None)
created = kwargs.get('created', False)
if not created or not isinstance(project, Project):
return
try:
import activity
activity.send(project.created_by, 'create', project)

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

@ -1,7 +1,7 @@
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('',
url(r'^gallery/$', 'projects.views.gallery',
url(r'^list/$', 'projects.views.list',
name='projects_gallery'),
url(r'^create/$', 'projects.views.create',
name='projects_create'),
@ -14,4 +14,6 @@ urlpatterns = patterns('',
name='projects_contact_followers'),
url(r'^(?P<slug>[\w-]+)/style.css$', 'projects.views.featured_css',
name='projects_featured_css'),
url(r'^(?P<slug>[\w-]+)/link/create/$', 'projects.views.link_create',
name='projects_link_create'),
)

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

@ -1,14 +1,21 @@
import urllib
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponseForbidden
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext, Context, Template
from django.utils.translation import ugettext as _
from BeautifulSoup import BeautifulSoup
from projects.models import Project
from projects.forms import ProjectForm, ProjectContactUsersForm
from projects.forms import ProjectLinkForm
def show(request, slug):
project = get_object_or_404(Project, slug=slug)
@ -30,6 +37,7 @@ def show(request, slug):
return render_to_response('projects/featured.html', context,
context_instance=RequestContext(request))
@login_required
def edit(request, slug):
project = get_object_or_404(Project, slug=slug)
@ -44,17 +52,19 @@ def edit(request, slug):
reverse('projects_show', kwargs=dict(slug=project.slug)))
else:
form = ProjectForm(instance=project)
return render_to_response('projects/edit.html', {
'form': form,
'project': project,
}, context_instance=RequestContext(request))
def gallery(request):
def list(request):
return render_to_response('projects/gallery.html', {
'projects': Project.objects.all()
'projects': Project.objects.all(),
}, context_instance=RequestContext(request))
@login_required
def create(request):
if request.method == 'POST':
@ -64,19 +74,20 @@ def create(request):
project.created_by = request.user
project.save()
return HttpResponseRedirect(reverse('projects_show', kwargs={
'slug': project.slug
'slug': project.slug,
}))
else:
form = ProjectForm()
return render_to_response('projects/create.html', {
'form': form
'form': form,
}, context_instance=RequestContext(request))
@login_required
def contact_followers(request, slug):
project = get_object_or_404(Project, slug=slug)
if project.created_by != request.user:
return HttpResponseForbidden
return HttpResponseForbidden()
if request.method == 'POST':
form = ProjectContactUsersForm(request.POST)
if form.is_valid():
@ -84,7 +95,7 @@ def contact_followers(request, slug):
messages.add_message(request, messages.INFO,
_("Message successfully sent."))
return HttpResponseRedirect(reverse('projects_show', kwargs={
'slug': project.slug
'slug': project.slug,
}))
else:
form = ProjectContactUsersForm()
@ -94,6 +105,31 @@ def contact_followers(request, slug):
'project': project,
}, context_instance=RequestContext(request))
def featured_css(request, slug):
project = get_object_or_404(Project, slug=slug)
return HttpResponse(project.css, mimetype='text/css')
def link_create(request, slug):
project = get_object_or_404(Project, slug=slug)
if project.created_by != request.user:
return HttpResponseForbidden()
form = ProjectLinkForm(initial=dict(project=project.pk))
if request.method == 'POST':
form = ProjectLinkForm(data=request.POST)
if form.is_valid():
messages.add_message(request, messages.INFO,
_("Your link has been created"))
link = form.save()
feed_url = link.get_syndication_url()
if feed_url:
link.feed_url = feed_url
link.save()
return HttpResponseRedirect(reverse('projects_show', kwargs={
'slug': project.slug,
}))
return render_to_response('projects/create_link.html', {
'form': form,
'project': project,
}, context_instance=RequestContext(request))

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

@ -5,6 +5,10 @@ pil
django-messages
south
jogging
feedparser
Celery==2.1.3
django-celery
BeautifulSoup
-e git://github.com/jbalogh/django-nose#egg=django-nose
-e git://github.com/clouserw/tower.git#egg=tower

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

@ -3,6 +3,9 @@
import os
import logging
import djcelery
djcelery.setup_loader()
# Make filepaths relative to settings.
ROOT = os.path.dirname(os.path.abspath(__file__))
@ -103,6 +106,9 @@ INSTALLED_APPS = (
'django.contrib.admin',
'django_nose',
'django_openid_auth',
'south',
'jogging',
'djcelery',
'wellknown',
'users',
'profiles',
@ -113,8 +119,7 @@ INSTALLED_APPS = (
'projects',
'statuses',
'messages',
'south',
'jogging',
'feeds',
)
if DEBUG:

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

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load l10n_tags %}
{% block title %}Add Link{% endblock %}
{% block body %}
<h3>{{ _('Add Link') }}</h3>
<form action="{% locale_url projects_link_create slug=project.slug %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create" />
</form>
{% endblock %}

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

@ -10,9 +10,20 @@
{% include "projects/_project_admin.html" %}
{% include "projects/_followers.html" %}
{% include "projects/_message_followers.html" %}
<h4>{{ _('Links') }}</h4>
<ul>
{% for link in project.link_set.all %}
<li><a href="{{ link.url }}">{{ link.title }}</a></li>
{% endfor %}
</ul>
{% if user == project.created_by %}
<p>
<a href="{% locale_url projects_edit slug=project.slug %}">Edit</a>
</p>
{% endif %}
{% if user == project.created_by %}
<p>
<a href="{% locale_url projects_link_create slug=project.slug %}">Add Link</a>
</p>
{% endif %}
{% endblock %}