Commits

Guilherme Gondim committed 5ce0b26

First commit

  • Participants

Comments (0)

Files changed (18)

+*.pyc
+*.bak
+*.swp
+*~
+._*
+.\#*
+*\#
+dist/
+build/
+*.egg-info/
+Copyright (c) 2012 Guilherme Gondim
+All rights reserved.
+
+Copyright (c) 2010 Bruno Renié
+All rights reserved.
+
+Copyright (c) 2005-2010 Django Software Foundation and individual contributors.
+All rights reserved.
+
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice, 
+       this list of conditions and the following disclaimer.
+    
+    2. Redistributions in binary form must reproduce the above copyright 
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of Django nor the names of its contributors may be used
+       to endorse or promote products derived from this software without
+       specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include LICENSE
+include README.rst
+recursive-include docs *
+recursive-include agregador/locale *
+recursive-include agregador/static *
+recursive-include agregador/templates *
+================
+Django Agregador
+================
+
+**Django Agregador** is a RSS/Atom feeds aggregator application for `Django`_
+projects.
+
+It is a fork of Bruno Renié's `django-aggregator`.
+
+Project page
+    https://bitbucket.org/semente/django-agregador/
+Translations
+    https://www.transifex.net/projects/p/django-agregador/
+
+.. _`Django`: http://www.djangoproject.com
+
+
+Installing & Setup
+==================
+
+*TODO*

File agregador/__init__.py

+VERSION = (0, 1)
+
+
+def get_version():
+    """Returns the version as a human-format string.
+    """
+    return '.'.join([str(i) for i in VERSION])
+
+
+__author__ = 'See the file AUTHORS'
+__license__ = 'BSD License'
+__url__ = 'https://bitbucket.org/semente/django-agregador'
+__version__ = get_version()

File agregador/admin.py

+from django.contrib import admin
+
+from agregador.models import Feed, Entry
+
+
+class FeedAdmin(admin.ModelAdmin):
+    list_display = ('title', 'public_url', 'is_defunct')
+    list_filter = ('is_defunct',)
+    search_fields = ('title', 'public_url')
+    list_per_page = 500
+
+
+class EntryAdmin(admin.ModelAdmin):
+    list_display = ('title', 'feed', 'date')
+    list_filter = ('feed',)
+    search_fields = ('feed__title', 'feed__public_url', 'title')
+    date_hierarchy = 'date'
+
+admin.site.register(Feed, FeedAdmin)
+admin.site.register(Entry, EntryAdmin)

File agregador/feeds.py

+from django.contrib.syndication.views import Feed as BaseFeed
+
+from agregador.models import Entry
+
+
+class Feed(BaseFeed):
+
+    def items(self):
+        return Entry.objects.select_related()[:10]
+
+    def item_title(self, item):
+        return '%s: %s' % (item.feed.title, item.title)
+
+    def item_description(self, item):
+        return item.summary
+
+    def item_link(self, item):
+        return item.link
+
+    def item_guid(self, item):
+        return item.guid
+
+    def item_pubdate(self, item):
+        return item.date

File agregador/management/__init__.py

Empty file added.

File agregador/management/commands/__init__.py

Empty file added.

File agregador/management/commands/mark_defunct_feeds.py

+"""
+Mark people with 404'ing feeds as defunct.
+"""
+import urllib2
+
+from django.core.management.base import NoArgsCommand
+
+from agregador.models import Feed
+
+
+class Command(NoArgsCommand):
+
+    def handle_noargs(self, **options):
+        for feed in Feed.objects.all():
+            try:
+                response = urllib2.urlopen(feed.feed_url)
+            except urllib2.HTTPError, e:
+                if e.code == 404 or e.code == 500:
+                    print "%s on %s; marking defunct" % (e.code, feed)
+                    feed.is_defunct = True
+                    feed.save()
+                else:
+                    raise

File agregador/management/commands/update_feeds.py

+import os
+import socket
+
+from django.core.management.base import NoArgsCommand
+
+from agregador.models import Feed
+
+LOCKFILE = "/tmp/update_feeds.lock"
+
+
+class Command(NoArgsCommand):
+
+    def handle_noargs(self, **options):
+        socket.setdefaulttimeout(15)
+        try:
+            lockfile = os.open(LOCKFILE, os.O_CREAT | os.O_EXCL)
+        except OSError:
+            return
+        try:
+            self.update_feeds()
+
+        finally:
+            os.close(lockfile)
+            os.unlink(LOCKFILE)
+
+    def update_feeds(self):
+        for feed in Feed.objects.filter(is_defunct=False):
+            feed.update()

File agregador/migrations/0001_initial.py

+# -*- coding: 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 model 'Feed'
+        db.create_table('agregador_feed', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=500)),
+            ('feed_url', self.gf('django.db.models.fields.URLField')(unique=True, max_length=500)),
+            ('public_url', self.gf('django.db.models.fields.URLField')(max_length=500)),
+            ('is_defunct', self.gf('django.db.models.fields.BooleanField')(default=False)),
+        ))
+        db.send_create_signal('agregador', ['Feed'])
+
+        # Adding model 'Entry'
+        db.create_table('agregador_entry', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('feed', self.gf('django.db.models.fields.related.ForeignKey')(related_name='entries', to=orm['agregador.Feed'])),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=500)),
+            ('link', self.gf('django.db.models.fields.URLField')(max_length=500)),
+            ('summary', self.gf('django.db.models.fields.TextField')(blank=True)),
+            ('date', self.gf('django.db.models.fields.DateTimeField')()),
+            ('guid', self.gf('django.db.models.fields.CharField')(unique=True, max_length=500, db_index=True)),
+        ))
+        db.send_create_signal('agregador', ['Entry'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'Feed'
+        db.delete_table('agregador_feed')
+
+        # Deleting model 'Entry'
+        db.delete_table('agregador_entry')
+
+
+    models = {
+        'agregador.entry': {
+            'Meta': {'ordering': "('-date',)", 'object_name': 'Entry'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'feed': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entries'", 'to': "orm['agregador.Feed']"}),
+            'guid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '500', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'link': ('django.db.models.fields.URLField', [], {'max_length': '500'}),
+            'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '500'})
+        },
+        'agregador.feed': {
+            'Meta': {'ordering': "('title',)", 'object_name': 'Feed'},
+            'feed_url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '500'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_defunct': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'public_url': ('django.db.models.fields.URLField', [], {'max_length': '500'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '500'})
+        }
+    }
+
+    complete_apps = ['agregador']

File agregador/migrations/0002_auto__add_field_feed_date_created__add_field_feed_date_modified__add_f.py

+# -*- coding: 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 'Feed.date_created'
+        db.add_column('agregador_feed', 'date_created',
+                      self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2013, 1, 13, 0, 0), blank=True),
+                      keep_default=False)
+
+        # Adding field 'Feed.date_modified'
+        db.add_column('agregador_feed', 'date_modified',
+                      self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2013, 1, 13, 0, 0), blank=True),
+                      keep_default=False)
+
+        # Adding field 'Entry.date_created'
+        db.add_column('agregador_entry', 'date_created',
+                      self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2013, 1, 13, 0, 0), blank=True),
+                      keep_default=False)
+
+        # Adding field 'Entry.date_modified'
+        db.add_column('agregador_entry', 'date_modified',
+                      self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2013, 1, 13, 0, 0), blank=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Feed.date_created'
+        db.delete_column('agregador_feed', 'date_created')
+
+        # Deleting field 'Feed.date_modified'
+        db.delete_column('agregador_feed', 'date_modified')
+
+        # Deleting field 'Entry.date_created'
+        db.delete_column('agregador_entry', 'date_created')
+
+        # Deleting field 'Entry.date_modified'
+        db.delete_column('agregador_entry', 'date_modified')
+
+
+    models = {
+        'agregador.entry': {
+            'Meta': {'ordering': "('-date',)", 'object_name': 'Entry'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'feed': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entries'", 'to': "orm['agregador.Feed']"}),
+            'guid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '500', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'link': ('django.db.models.fields.URLField', [], {'max_length': '500'}),
+            'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '500'})
+        },
+        'agregador.feed': {
+            'Meta': {'ordering': "('title',)", 'object_name': 'Feed'},
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'feed_url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '500'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_defunct': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'public_url': ('django.db.models.fields.URLField', [], {'max_length': '500'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '500'})
+        }
+    }
+
+    complete_apps = ['agregador']

File agregador/migrations/__init__.py

Empty file added.

File agregador/models.py

+import datetime
+import feedparser
+
+from django.db import models
+from django.utils.encoding import smart_unicode as smart_text
+from django.utils.timezone import now, localtime
+from django.utils.translation import ugettext_lazy as _
+
+ua = 'django-agregador/dev +http://bitbucket.org/semente/django-agregador' # TODO
+feedparser.USER_AGENT = ua
+
+
+class Feed(models.Model):
+    title = models.CharField(_('Title'), max_length=500)
+    feed_url = models.URLField(_('Feed URL'), unique=True, max_length=500)
+    public_url = models.URLField(_('Public URL'), max_length=500)
+    is_defunct = models.BooleanField(_('Is defunct?'))
+    date_created = models.DateTimeField(_('Date created'), auto_now_add=True)
+    date_modified = models.DateTimeField(_('Date modified'), auto_now=True)
+
+    class Meta:
+        ordering = ('title',)
+
+    def __unicode__(self):
+        return u'%s' % self.title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('feed-detail', None, {
+            'pk' : str(self.pk),
+        })
+
+    def update(self):
+        parsed = feedparser.parse(self.feed_url)
+        for entry in parsed.entries:
+            title = smart_text(entry.title, encoding=parsed.encoding)
+            guid = smart_text(entry.get('id', entry.link), encoding=parsed.encoding)
+            link = smart_text(entry.link, encoding=parsed.encoding)
+
+            if not guid:
+                guid = link
+
+            if 'content' in entry:
+                content = entry.content[0].value
+            elif 'description' in entry:
+                content = entry.description
+            elif 'summary' in entry:
+                content = entry.summary
+            else:
+                content = u''
+
+            content = smart_text(content, encoding=parsed.encoding)
+
+            if 'updated_parsed' in entry and entry.updated_parsed is not None:
+                date = datetime.datetime(*entry.updated_parsed[:6])
+            else:
+                date = now()
+
+            try:
+                self.entries.get(guid=guid)
+            except Entry.DoesNotExist:
+                self.entries.create(title=title, link=link, summary=content,
+                                    guid=guid, date=date)
+
+
+class Entry(models.Model):
+    feed = models.ForeignKey(
+        Feed,
+        verbose_name=_('Feed'),
+        related_name='entries'
+    )
+    title = models.CharField(_('Title'), max_length=500)
+    link = models.URLField(_('Link'), max_length=500)
+    summary = models.TextField(_('Summary'), blank=True)
+    date = models.DateTimeField(_('Date'))
+    guid = models.CharField(
+        _('GUID'),
+        max_length=500,
+        unique=True,
+        db_index=True,
+    )
+    date_created = models.DateTimeField(_('Date created'), auto_now_add=True)
+    date_modified = models.DateTimeField(_('Date modified'), auto_now=True)
+
+    class Meta:
+        ordering = ('-date',)
+        verbose_name_plural = _('Entries')
+
+    def __unicode__(self):
+        return u'%s' % self.title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('entry-detail', None, {
+            'feed_pk' : str(self.feed.pk),
+            'pk' : str(self.pk),
+        })

File agregador/templatetags/__init__.py

Empty file added.

File agregador/templatetags/aggregator_tags.py

+from django import template
+
+from agregador.models import Feed, Entry
+
+register = template.Library()
+
+
+class FeedListNode(template.Node):
+
+    def __init__(self, var_name):
+        self.var_name = var_name
+
+    def render(self, context):
+        context[self.var_name] = Feed.objects.filter(is_defunct=False)
+        return ''
+
+
+@register.tag
+def get_feed_list(parser, token):
+    """{% get_feed_list as feed_list %}"""
+    bits = token.split_contents()
+    if len(bits) != 3:
+        raise template.TemplateSyntaxError, \
+                "'%s' tag takes two arguments" % bits[0]
+
+    if bits[1] != "as":
+        raise template.TemplateSyntaxError, \
+                "First argument to '%s' tag must be 'as'" % bits[0]
+
+    return FeedListNode(bits[2])
+
+
+class EntryNode(template.Node):
+
+    def __init__(self, var_name, count):
+        self.var_name = var_name
+        self.count = int(count)
+
+    def render(self, context):
+        context[self.var_name] = Entry.objects.select_related()[:self.count]
+        return ''
+
+
+@register.tag
+def get_entries(parser, token):
+    """{% get_entries 30 as entries %}"""
+    bits = token.split_contents()
+    if len(bits) != 4:
+        raise template.TemplateSyntaxError, \
+                "'%s' tag takes three arguments" % bits[0]
+
+    if bits[2] != 'as':
+        raise template.TemplateSyntaxError, \
+                "Second argument to '%s' tag must be 'as'" % bits[0]
+
+    return EntryNode(bits[3], bits[1])
+#/usr/bin/env python
+
+import codecs
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+
+if 'publish' in sys.argv:
+    os.system('python setup.py sdist upload')
+    sys.exit()
+
+read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
+
+
+# Dynamically calculate the version based on agregador.VERSION.
+version = __import__('agregador').get_version()
+
+setup(
+    name='django-agregador',
+    version=version,
+    description='RSS/Atom feeds aggregator application for Django projects.',
+    long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')),
+    keywords = 'django rss atom feed aggregator app',
+    author='Guilherme Gondim',
+    author_email='semente+django-agregador@taurinus.org',
+    maintainer='Guilherme Gondim',
+    maintainer_email='semente+django-agregador@taurinus.org',
+    license='BSD License',
+    url='https://bitbucket.org/semente/django-agregador/',
+    download_url='https://bitbucket.org/semente/django-agregador/downloads/',
+    packages=find_packages(),
+    zip_safe=False,
+    include_package_data=True,
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+    install_requires=['feedparser==5.1.3'],
+)