diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | __init__.py | 0 | ||||
-rw-r--r-- | aggregator/__init__.py | 0 | ||||
-rw-r--r-- | aggregator/fixtures/initial_data.json | 43 | ||||
-rw-r--r-- | aggregator/management/__init__.py | 0 | ||||
-rw-r--r-- | aggregator/management/commands/__init__.py | 0 | ||||
-rw-r--r-- | aggregator/management/commands/load_feeds.py | 37 | ||||
-rw-r--r-- | aggregator/migrations/0001_initial.py | 72 | ||||
-rw-r--r-- | aggregator/migrations/__init__.py | 0 | ||||
-rw-r--r-- | aggregator/models.py | 25 | ||||
-rw-r--r-- | aggregator/views.py | 20 | ||||
-rwxr-xr-x | manage.py | 14 | ||||
-rw-r--r-- | settings.py | 149 | ||||
-rw-r--r-- | templates/aggregator/posts.html | 45 | ||||
-rw-r--r-- | urls.py | 11 |
15 files changed, 417 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/__init__.py diff --git a/aggregator/__init__.py b/aggregator/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/aggregator/__init__.py diff --git a/aggregator/fixtures/initial_data.json b/aggregator/fixtures/initial_data.json new file mode 100644 index 0000000..76c6471 --- /dev/null +++ b/aggregator/fixtures/initial_data.json @@ -0,0 +1,43 @@ +[ + { + "model": "aggregator.Feed", + "pk": 1, + "fields": { + "base_url": "http://www.advogato.org/", + "feed_url": "person/ryuslash/rss.xml", + "uses_title": false, + "favicon_ext": "ico" + } + }, + { + "model": "aggregator.Feed", + "pk": 2, + "fields": { + "base_url": "http://diasp.org/", + "feed_url": "public/ryuslash.atom", + "uses_title": false, + "favicon_ext": "png", + "br2nl": true + } + }, + { + "model": "aggregator.Feed", + "pk": 3, + "fields": { + "base_url": "http://identi.ca/", + "feed_url": "api/statuses/user_timeline/107950.rss", + "uses_title": false, + "favicon_ext": "ico" + } + }, + { + "model": "aggregator.Feed", + "pk": 4, + "fields": { + "base_url": "https://github.com/", + "feed_url": "ryuslash.atom", + "uses_title": true, + "favicon_ext": "png" + } + } +] diff --git a/aggregator/management/__init__.py b/aggregator/management/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/aggregator/management/__init__.py diff --git a/aggregator/management/commands/__init__.py b/aggregator/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/aggregator/management/commands/__init__.py diff --git a/aggregator/management/commands/load_feeds.py b/aggregator/management/commands/load_feeds.py new file mode 100644 index 0000000..c143052 --- /dev/null +++ b/aggregator/management/commands/load_feeds.py @@ -0,0 +1,37 @@ +import feedparser +import datetime + +from django.core.management.base import BaseCommand + +from aggregator.models import Feed, Post + +class Command(BaseCommand): + help = "hi" + + def handle(self, *args, **kwargs): + feeds = Feed.objects.all() + + for feed in feeds: + parsed = feedparser.parse(feed.get_feed_url()) + feed.title = parsed.feed.title + + for entry in parsed.entries: + if not Post.objects.filter(post_id=entry.id).exists(): + updated = datetime.datetime( + entry.updated_parsed.tm_year, + entry.updated_parsed.tm_mon, + entry.updated_parsed.tm_mday, + entry.updated_parsed.tm_hour, + entry.updated_parsed.tm_min, + entry.updated_parsed.tm_sec) + + post = Post(post_id=entry.id, + title=entry.title, + body=entry.summary, + remote_url=entry.link, + updated=updated, + feed=feed) + post.save() + feed.updated = datetime.datetime.now() + + feed.save() diff --git a/aggregator/migrations/0001_initial.py b/aggregator/migrations/0001_initial.py new file mode 100644 index 0000000..73ca455 --- /dev/null +++ b/aggregator/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# 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 model 'Feed' + db.create_table('aggregator_feed', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('base_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('feed_url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('favicon_ext', self.gf('django.db.models.fields.CharField')(max_length=4)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=500, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('uses_title', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('br2nl', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('aggregator', ['Feed']) + + # Adding model 'Post' + db.create_table('aggregator_post', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('post_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=500)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=500)), + ('body', self.gf('django.db.models.fields.TextField')()), + ('remote_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('updated', self.gf('django.db.models.fields.DateTimeField')()), + ('added', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('feed', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['aggregator.Feed'])), + )) + db.send_create_signal('aggregator', ['Post']) + + + def backwards(self, orm): + + # Deleting model 'Feed' + db.delete_table('aggregator_feed') + + # Deleting model 'Post' + db.delete_table('aggregator_post') + + + models = { + 'aggregator.feed': { + 'Meta': {'object_name': 'Feed'}, + 'base_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'br2nl': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'favicon_ext': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'feed_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '500', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'uses_title': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'aggregator.post': { + 'Meta': {'object_name': 'Post'}, + 'added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {}), + 'feed': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['aggregator.Feed']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '500'}), + 'remote_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}) + } + } + + complete_apps = ['aggregator'] diff --git a/aggregator/migrations/__init__.py b/aggregator/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/aggregator/migrations/__init__.py diff --git a/aggregator/models.py b/aggregator/models.py new file mode 100644 index 0000000..302f774 --- /dev/null +++ b/aggregator/models.py @@ -0,0 +1,25 @@ +from django.db import models + +class Feed(models.Model): + base_url = models.URLField(max_length=255) + feed_url = models.CharField(max_length=255) + favicon_ext = models.CharField(max_length=4) + title = models.CharField(max_length=500, blank=True) + updated = models.DateTimeField(null=True, blank=True) + uses_title = models.BooleanField(default=False) + br2nl = models.BooleanField(default=False) + + def get_feed_url(self): + return self.base_url + self.feed_url + + def get_favicon_url(self): + return self.base_url + 'favicon.' + self.favicon_ext + +class Post(models.Model): + post_id = models.CharField(max_length=500, unique=True) + title = models.CharField(max_length=500) + body = models.TextField() + remote_url = models.URLField(max_length=255) + updated = models.DateTimeField() + added = models.DateTimeField(auto_now_add=True) + feed = models.ForeignKey(Feed) diff --git a/aggregator/views.py b/aggregator/views.py new file mode 100644 index 0000000..c946c31 --- /dev/null +++ b/aggregator/views.py @@ -0,0 +1,20 @@ +from django.core.paginator import Paginator, InvalidPage, EmptyPage +from django.http import Http404 +from django.shortcuts import render_to_response + +from .models import Post + +def posts(request, page=1): + queryset = Post.objects.order_by('-updated') + paginator = Paginator(queryset, 20) + + if page == None: + page = 1 + + try: + object_list = paginator.page(page) + except (EmptyPage, InvalidPage): + raise Http404 + + return render_to_response('aggregator/posts.html', + { 'list': object_list }) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..1473748 --- /dev/null +++ b/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python2 +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..2a0cf36 --- /dev/null +++ b/settings.py @@ -0,0 +1,149 @@ +# Django settings for ryuslash_org project. +import os +import sys + +DEPLOY_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, DEPLOY_PATH) + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'test.sqlite', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Europe/Brussels' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'f3ga_iu$h^-+$xg)r2rnbiocn4qx+c(c3!9_k=!a9#)g9o5*#=' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'ryuslash_org.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + '%s/templates' % DEPLOY_PATH, +) + +INSTALLED_APPS = ('django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + 'django.contrib.admindocs', + 'aggregator', + 'south') + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/templates/aggregator/posts.html b/templates/aggregator/posts.html new file mode 100644 index 0000000..32f1ed7 --- /dev/null +++ b/templates/aggregator/posts.html @@ -0,0 +1,45 @@ +<html> + <head> + <title>ryuslash.org</title> + </head> + <body> + + {% for post in list.object_list %} + <p> + <div> + <a href="{{ post.remote_url }}"> + <img src="{{ post.feed.get_favicon_url }}" /> + {{ post.updated }} + </a> + </div> + {% if post.feed.uses_title %} + <div>{{ post.title }}</div> + {% endif %} + + {% autoescape off %} + {% if post.feed.br2nl %} + {{ post.body|linebreaks }} + {% else %} + {{ post.body }} + {% endif %} + {% endautoescape %} + </p> + {% endfor %} + + <div class="pagination"> + <span class="step-links"> + {% if list.has_previous %} + <a href="/{{ list.previous_page_number }}/">previous</a> + {% endif %} + + <span class="current"> + Page {{ list.number }} of {{ list.paginator.num_pages }}. + </span> + + {% if list.has_next %} + <a href="/{{ list.next_page_number }}/">next</a> + {% endif %} + </span> + </div> + </body> +</html> @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + url(r'^((?P<page>\d+)/)?$', 'aggregator.views.posts'), + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + url(r'^admin/', include(admin.site.urls)), +) |