├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── newswall ├── __init__.py ├── admin.py ├── feeds.py ├── locale │ └── de │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── update_newswall.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20151218_1227.py │ └── __init__.py ├── mixin.py ├── models.py ├── providers │ ├── __init__.py │ ├── base.py │ ├── elephantblog.py │ ├── fb_graph_feed.py │ ├── feed.py │ ├── instagram.py │ ├── twitter.py │ └── youtube.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__chg_field_story_title.py │ ├── 0003_auto__chg_field_story_title__chg_field_story_image_url.py │ └── __init__.py ├── tasks.py ├── templates │ └── newswall │ │ ├── newswall_base.html │ │ ├── story_archive.html │ │ └── story_detail.html ├── templatetags │ ├── __init__.py │ └── newswall_tags.py ├── urls.py └── views.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .*.swp 4 | \#*# 5 | .DS_Store 6 | ._* 7 | /MANIFEST 8 | /_build 9 | /build 10 | /dist 11 | django_newswall.egg-info 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, FEINHEIT GmbH and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of FEINHEIT GmbH nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include README.rst 4 | recursive-include newswall/static * 5 | recursive-include newswall/locale * 6 | recursive-include newswall/templates * 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Newswall 3 | ======== 4 | 5 | This is my version of a Tumblelog. Why, you might ask? Because I can. 6 | 7 | 8 | Installation and usage 9 | ====================== 10 | 11 | 1. Add ``newswall`` to ``INSTALLED_APPS`` 12 | 2. Run ``./manage.py migrate newswall`` (or ``syncdb``, if you prefer to work 13 | without South) 14 | 3. Add the following line to your ``urls.py``:: 15 | 16 | url(r'^news/', include('newswall.urls')), 17 | 18 | 4. Add news providers by create a few ``Source`` objects through Django's 19 | admin panel 20 | 21 | Updating newswall 22 | ================= 23 | Method A: Create a cronjob running ``./manage.py update_newswall`` periodically (i.e. 24 | every hour) 25 | 26 | Method B: Use Celery: 27 | 28 | CELERYBEAT_SCHEDULE = { 29 | 'update_newswall': { 30 | 'task': 'update_newswall', 31 | 'schedule': timedelta(seconds=3600), 32 | 'args': (), 33 | }, 34 | } 35 | 36 | Providers 37 | ========= 38 | 39 | ``newswall`` has a few bundled providers, those being: 40 | 41 | 42 | Elephantblog 43 | ------------ 44 | 45 | Adds news entries for every active entry in a elephantblog installation on the 46 | same website. No additional configuration required (or possible). Add the 47 | following JSON configuration to the ``Source`` entry:: 48 | 49 | {"provider": "newswall.providers.elephantblog"} 50 | 51 | 52 | Facebook Graph Feed 53 | ------------------- 54 | 55 | This provider adds news entries for every wall post on a Facebook page. The 56 | wall posts are accessed through the Graph API; you'll need a copy of the Python 57 | Facebook SDK somewhere on your Python path. You'll need an access token with 58 | ``offline_access`` permission for this provider. Required configuration 59 | follows:: 60 | 61 | {"provider": "newswall.providers.fb_graph_feed", 62 | "object": "FEINHEIT", // used to construct the Graph request URL 63 | "from_id": "239846135569", // used to filter stories created by the 64 | // object referenced above, ignores stories 65 | // sent by others 66 | "access_token": "..." 67 | } 68 | 69 | We suggest to use App Access Tokens to query the Facebook Page feed, because they don't expire. 70 | To get an App Access Token, simply open this URL with your browser, after 71 | filling in the required fields (the all caps words):: 72 | 73 | https://graph.facebook.com/oauth/access_token?client_id=YOUR_APP_ID&client_secret=YOUR_APP_SECRET&grant_type=client_credentials 74 | 75 | More infos according the App Access Tokens can be found on the official Facebook documentation: 76 | 77 | 78 | To obtain the "from_id" configuration parameter, you can query the Facebook Open Graph 79 | API Backend with your Browser:: 80 | 81 | https://graph.facebook.com/OBJECT 82 | 83 | f.e.: 84 | 85 | 86 | RSS Feed 87 | -------- 88 | 89 | The RSS feed provider can take any RSS or Atom feed (in fact anything parseable 90 | by ``feedparser`` and turn the stories into news entries:: 91 | 92 | { 93 | "provider": "newswall.providers.feed", 94 | "source": "http://twitter.com/statuses/user_timeline/unsocialrider.rss" 95 | } 96 | 97 | 98 | Twitter API Feed 99 | ---------------- 100 | 101 | Required: tweepy 102 | 103 | Usage: 104 | 105 | Create a twitter app. 106 | You'll find the consumer_key/secret on the detail page. 107 | Because this is a read-only application, you can create 108 | your oauth_token/secret directly on the bottom of the app detail page. 109 | 110 | Required configuration keys:: 111 | 112 | { 113 | "provider": "newswall.providers.twitter", 114 | "user": "feinheit", 115 | "consumer_key": "...", 116 | "consumer_secret": "...", 117 | "oauth_token": "...", 118 | "oauth_secret": "..." 119 | } 120 | 121 | 122 | Youtube Provider 123 | ================ 124 | 125 | Get all video uploads for specific channel 126 | 127 | Create project at Google Developers Console 128 | (https://console.developers.google.com) and request an API key. 129 | 130 | Remember to enable ``YouTube Data API v3`` from APIs & Auth > APIs 131 | 132 | 133 | Required configuration keys:: 134 | 135 | { 136 | "provider": "newswall.providers.youtube", 137 | "channel_id": "...", 138 | "api_key": "..." 139 | } 140 | 141 | 142 | 143 | Instagram Provider 144 | ================== 145 | 146 | Required configuration keys:: 147 | { 148 | "provider": "newswall.providers.instagram", 149 | "username": "...", 150 | } 151 | -------------------------------------------------------------------------------- /newswall/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 1, 0) 2 | __version__ = '.'.join(str(v) for v in VERSION) 3 | -------------------------------------------------------------------------------- /newswall/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db.models import TextField 3 | from django.forms import TextInput 4 | from newswall.models import Source, Story, ExtraData 5 | 6 | 7 | class ExtraDataInline(admin.TabularInline): 8 | model = ExtraData 9 | formfield_overrides = { 10 | TextField: {'widget': TextInput}, 11 | } 12 | 13 | 14 | admin.site.register( 15 | Source, 16 | list_display=('name', 'is_active', 'ordering'), 17 | list_editable=('is_active', 'ordering'), 18 | list_filter=('is_active',), 19 | prepopulated_fields={'slug': ('name',)}, 20 | ) 21 | 22 | admin.site.register( 23 | Story, 24 | date_hierarchy='timestamp', 25 | list_display=('title', 'source', 'is_active', 'timestamp'), 26 | list_editable=('is_active',), 27 | list_filter=('source', 'is_active'), 28 | search_fields=('object_url', 'title', 'author', 'body'), 29 | inlines=(ExtraDataInline,) 30 | ) 31 | -------------------------------------------------------------------------------- /newswall/feeds.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.syndication.views import Feed 3 | 4 | from newswall.models import Story 5 | 6 | 7 | class StoryFeed(Feed): 8 | title = getattr(settings, 'BLOG_TILE', 'Default Title') 9 | link = '/news/' 10 | description = getattr(settings, 'BLOG_DESCRIPTION', 'Default Description') 11 | 12 | def items(self): 13 | return Story.objects.active().order_by('-timestamp')[:20] 14 | 15 | def item_title(self, item): 16 | return item.title 17 | 18 | def item_description(self, item): 19 | return item.body 20 | 21 | def item_pubdate(self, item): 22 | return item.timestamp 23 | -------------------------------------------------------------------------------- /newswall/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /newswall/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2011-09-22 16:04+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | 21 | #: models.py:8 models.py:35 22 | msgid "is active" 23 | msgstr "ist aktiv" 24 | 25 | #: models.py:9 26 | msgid "name" 27 | msgstr "Name" 28 | 29 | #: models.py:10 30 | msgid "slug" 31 | msgstr "Slug" 32 | 33 | #: models.py:11 34 | msgid "ordering" 35 | msgstr "Sortierung" 36 | 37 | #: models.py:13 38 | msgid "configuration data" 39 | msgstr "Konfigurationsdaten" 40 | 41 | #: models.py:17 models.py:39 42 | msgid "source" 43 | msgstr "Quelle" 44 | 45 | #: models.py:18 46 | msgid "sources" 47 | msgstr "Quellen" 48 | 49 | #: models.py:36 50 | msgid "timestamp" 51 | msgstr "Zeitstempel" 52 | 53 | #: models.py:37 54 | msgid "object URL" 55 | msgstr "Objekt-URL" 56 | 57 | #: models.py:42 58 | msgid "title" 59 | msgstr "Titel" 60 | 61 | #: models.py:43 62 | msgid "author" 63 | msgstr "Autor" 64 | 65 | #: models.py:44 66 | msgid "body" 67 | msgstr "Inhalt" 68 | 69 | #: models.py:45 70 | msgid "Content of the story. May contain HTML." 71 | msgstr "Inhalt der Story. Kann HTML enthalten." 72 | 73 | #: models.py:46 74 | msgid "image URL" 75 | msgstr "Bild-URL" 76 | 77 | #: models.py:50 78 | msgid "story" 79 | msgstr "Story" 80 | 81 | #: models.py:51 82 | msgid "stories" 83 | msgstr "Storys" 84 | 85 | #: templates/newswall/story_archive.html:5 86 | #: templates/newswall/story_detail.html:5 87 | msgid "News" 88 | msgstr "News" 89 | -------------------------------------------------------------------------------- /newswall/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/management/__init__.py -------------------------------------------------------------------------------- /newswall/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/management/commands/__init__.py -------------------------------------------------------------------------------- /newswall/management/commands/update_newswall.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | from django.utils import importlib 3 | 4 | try: 5 | import json 6 | except ImportError: 7 | # maintain compatibility with Django < 1.7 8 | from django.utils import simplejson as json 9 | 10 | from newswall.models import Source 11 | 12 | 13 | class Command(NoArgsCommand): 14 | help = 'Updates all active sources' 15 | 16 | def handle_noargs(self, **options): 17 | for source in Source.objects.filter(is_active=True): 18 | config = json.loads(source.data) 19 | provider = importlib.import_module( 20 | config['provider']).Provider(source, config) 21 | provider.update() 22 | -------------------------------------------------------------------------------- /newswall/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Source', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('is_active', models.BooleanField(default=True, verbose_name='is active')), 19 | ('name', models.CharField(max_length=100, verbose_name='name')), 20 | ('slug', models.SlugField(unique=True, verbose_name='slug')), 21 | ('ordering', models.IntegerField(default=0, verbose_name='ordering')), 22 | ('data', models.TextField(verbose_name='configuration data', blank=True)), 23 | ], 24 | options={ 25 | 'ordering': ['ordering', 'name'], 26 | 'verbose_name': 'source', 27 | 'verbose_name_plural': 'sources', 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='Story', 32 | fields=[ 33 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 34 | ('is_active', models.BooleanField(default=True, verbose_name='is active')), 35 | ('timestamp', models.DateTimeField(default=datetime.datetime.now, verbose_name='timestamp')), 36 | ('object_url', models.URLField(unique=True, verbose_name='object URL')), 37 | ('title', models.CharField(max_length=1000, verbose_name='title')), 38 | ('author', models.CharField(max_length=100, verbose_name='author', blank=True)), 39 | ('body', models.TextField(help_text='Content of the story. May contain HTML.', verbose_name='body', blank=True)), 40 | ('image_url', models.CharField(max_length=1000, verbose_name='image URL', blank=True)), 41 | ('source', models.ForeignKey(related_name='stories', verbose_name='source', to='newswall.Source')), 42 | ], 43 | options={ 44 | 'ordering': ['-timestamp'], 45 | 'verbose_name': 'story', 46 | 'verbose_name_plural': 'stories', 47 | }, 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /newswall/migrations/0002_auto_20151218_1227.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('newswall', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ExtraData', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('key', models.CharField(max_length=128)), 19 | ('value', models.TextField(null=True, blank=True)), 20 | ('story', models.ForeignKey(to='newswall.Story')), 21 | ], 22 | ), 23 | migrations.AlterUniqueTogether( 24 | name='extradata', 25 | unique_together=set([('story', 'key')]), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /newswall/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/migrations/__init__.py -------------------------------------------------------------------------------- /newswall/mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from django import http 4 | from django.core.serializers.json import DjangoJSONEncoder 5 | 6 | from .models import Story 7 | 8 | 9 | class NewswallMixin(object): 10 | """ 11 | This mixin autodetects whether the blog is integrated through a FeinCMS 12 | ApplicationContent and automatically switches to inheritance2.0 if that's 13 | the case. Please note that FeinCMS is NOT required, this is purely for the 14 | convenience of FeinCMS users. The functionality for this is contained 15 | inside ``base_template`` and ``render_to_response``. 16 | 17 | Additionally, it adds the view instance to the template context 18 | as ``view``. 19 | """ 20 | 21 | @property 22 | def base_template(self): 23 | if hasattr(self.request, '_feincms_page'): 24 | return self.request._feincms_page.template.path 25 | return 'newswall/newswall_base.html' 26 | 27 | def get_context_data(self, **kwargs): 28 | kwargs.update({'view': self}) 29 | return super(NewswallMixin, self).get_context_data(**kwargs) 30 | 31 | def get_queryset(self): 32 | return Story.objects.active().select_related('source') 33 | 34 | def render_to_response(self, context, **response_kwargs): 35 | if 'app_config' in getattr(self.request, '_feincms_extra_context', {}): 36 | return self.get_template_names(), context 37 | 38 | return super(NewswallMixin, self).render_to_response( 39 | context, **response_kwargs) 40 | 41 | 42 | class JSONResponseMixin(object): 43 | def render_to_response(self, context,**kwargs): 44 | "Returns a JSON response containing 'context' as payload" 45 | return self.get_json_response(self.convert_context_to_json(context), 46 | **kwargs) 47 | 48 | def get_json_response(self, content, **httpresponse_kwargs): 49 | """Construct an `HttpResponse` object.""" 50 | return http.HttpResponse(content, 51 | content_type='application/json', 52 | **httpresponse_kwargs) 53 | 54 | def convert_context_to_json(self, context): 55 | "Convert the context dictionary into a JSON object" 56 | return json.dumps(context, cls=DjangoJSONEncoder) 57 | -------------------------------------------------------------------------------- /newswall/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.db import models 4 | from django.core.exceptions import ObjectDoesNotExist 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | 8 | class SourceManager(models.Manager): 9 | def active(self): 10 | return self.filter(is_active=True) 11 | 12 | 13 | class Source(models.Model): 14 | is_active = models.BooleanField(_('is active'), default=True) 15 | name = models.CharField(_('name'), max_length=100) 16 | slug = models.SlugField(_('slug'), unique=True) 17 | ordering = models.IntegerField(_('ordering'), default=0) 18 | 19 | data = models.TextField(_('configuration data'), blank=True) 20 | 21 | objects = SourceManager() 22 | 23 | class Meta: 24 | ordering = ['ordering', 'name'] 25 | verbose_name = _('source') 26 | verbose_name_plural = _('sources') 27 | 28 | def __unicode__(self): 29 | return self.name 30 | 31 | @models.permalink 32 | def get_absolute_url(self): 33 | return 'newswall_source_detail', (), {'slug': self.slug} 34 | 35 | 36 | class StoryManager(models.Manager): 37 | def active(self): 38 | return self.filter(is_active=True) 39 | 40 | 41 | class Story(models.Model): 42 | # Mandatory data 43 | is_active = models.BooleanField(_('is active'), default=True) 44 | timestamp = models.DateTimeField(_('timestamp'), default=datetime.now) 45 | object_url = models.URLField(_('object URL'), unique=True) 46 | source = models.ForeignKey( 47 | Source, related_name='stories', verbose_name=_('source')) 48 | 49 | # story fields 50 | title = models.CharField(_('title'), max_length=1000) 51 | author = models.CharField(_('author'), max_length=100, blank=True) 52 | body = models.TextField( 53 | _('body'), blank=True, 54 | help_text=_('Content of the story. May contain HTML.')) 55 | image_url = models.CharField(_('image URL'), max_length=1000, blank=True) 56 | 57 | objects = StoryManager() 58 | 59 | class Meta: 60 | ordering = ['-timestamp'] 61 | verbose_name = _('story') 62 | verbose_name_plural = _('stories') 63 | 64 | def __unicode__(self): 65 | return self.title 66 | 67 | def get_absolute_url(self): 68 | return self.object_url 69 | 70 | def update_extra_data(self, key, value): 71 | extra_data, created = ExtraData.objects.get_or_create(story=self, 72 | key=key) 73 | extra_data.value = value 74 | extra_data.save() 75 | 76 | def get_extra_data(self, key): 77 | try: 78 | return ExtraData.objects.get(story=self, key=key) 79 | except ObjectDoesNotExist(): 80 | return None 81 | 82 | 83 | class ExtraData(models.Model): 84 | class Meta: 85 | unique_together = ['story', 'key'] 86 | 87 | story = models.ForeignKey(Story) 88 | key = models.CharField(max_length=128) 89 | value = models.TextField(null=True, blank=True) 90 | -------------------------------------------------------------------------------- /newswall/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/providers/__init__.py -------------------------------------------------------------------------------- /newswall/providers/base.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | from newswall.models import Story 4 | 5 | 6 | class ProviderBase(object): 7 | def __init__(self, source, config): 8 | self.source = source 9 | self.config = config 10 | 11 | def update(self): 12 | raise NotImplementedError 13 | 14 | def create_story(self, object_url, **kwargs): 15 | defaults = {'source': self.source} 16 | defaults.update(kwargs) 17 | 18 | if defaults.get('title'): 19 | if Story.objects.filter( 20 | title=defaults.get('title'), 21 | timestamp__gte=date.today() - timedelta(days=3), 22 | ).exists(): 23 | defaults['is_active'] = False 24 | 25 | return Story.objects.get_or_create( 26 | object_url=object_url, 27 | defaults=defaults, 28 | ) 29 | -------------------------------------------------------------------------------- /newswall/providers/elephantblog.py: -------------------------------------------------------------------------------- 1 | """ 2 | Elephantblog Entry Provider 3 | =========================== 4 | 5 | Required configuration keys:: 6 | 7 | { 8 | "provider": "newswall.providers.elephantblog" 9 | } 10 | """ 11 | 12 | from __future__ import absolute_import 13 | 14 | from django.contrib.sites.models import Site 15 | 16 | from elephantblog.models import Entry 17 | 18 | from newswall.providers.base import ProviderBase 19 | 20 | 21 | class Provider(ProviderBase): 22 | def update(self): 23 | domain = Site.objects.get_current().domain 24 | 25 | for entry in Entry.objects.active(): 26 | url = 'http://%s%s' % (domain, entry.get_absolute_url()) 27 | 28 | try: 29 | body = entry.richtextcontent_set.all()[0].text 30 | except: 31 | body = u'' 32 | 33 | self.create_story( 34 | url, 35 | title=entry.title, 36 | timestamp=entry.published_on, 37 | body=body, 38 | ) 39 | -------------------------------------------------------------------------------- /newswall/providers/fb_graph_feed.py: -------------------------------------------------------------------------------- 1 | """ 2 | Facebook Graph Feed API Provider 3 | ================================ 4 | 5 | This provider needs `offline_access` permission. 6 | 7 | See here how to get an access token with all permissions: 8 | http://liquid9.tv/blog/2011/may/12/obtaining-permanent-facebook-oauth-access-token/ # noqa 9 | 10 | Required configuration keys:: 11 | 12 | { 13 | "provider": "newswall.providers.fb_graph_feed", 14 | "object": "FEINHEIT", 15 | "from_id": "239846135569", 16 | "access_token": "..." 17 | } 18 | """ 19 | 20 | import urllib 21 | 22 | from datetime import datetime 23 | 24 | try: 25 | import json 26 | except ImportError: 27 | # maintain compatibility with Django < 1.7 28 | from django.utils import simplejson as json 29 | 30 | from newswall.providers.base import ProviderBase 31 | 32 | 33 | class Provider(ProviderBase): 34 | def update(self): 35 | args = {'access_token': self.config['access_token']} 36 | query = "https://graph.facebook.com/v2.4/%s/feed?%s&fields=" \ 37 | "full_picture,picture,name,message,story,created_time,from," \ 38 | "likes" % ( 39 | self.config['object'], 40 | urllib.urlencode(args), 41 | ) 42 | file = urllib.urlopen(query) 43 | raw = file.read() 44 | response = json.loads(raw) 45 | 46 | from_id = self.config.get('from_id', None) 47 | 48 | for entry in response['data']: 49 | if from_id and entry['from']['id'] != from_id: 50 | continue 51 | 52 | if 'to' in entry: # messages 53 | continue 54 | 55 | link = 'https://facebook.com/%s' % ( 56 | entry['id'].replace('_', '/posts/'), 57 | ) 58 | try: 59 | like_count = len(entry['likes'].get('data')) 60 | except KeyError: # No likes 61 | like_count = None 62 | story = self.create_story( 63 | link, 64 | title=( 65 | entry.get('name') or entry.get('message') or 66 | entry.get('story', u'') 67 | ), 68 | body=entry.get('message', u''), 69 | image_url=entry.get('full_picture', u''), 70 | timestamp=datetime.strptime( 71 | entry['created_time'], '%Y-%m-%dT%H:%M:%S+0000'), 72 | ) 73 | story[0].update_extra_data(key='FacebookLikeCount', 74 | value=like_count) 75 | -------------------------------------------------------------------------------- /newswall/providers/feed.py: -------------------------------------------------------------------------------- 1 | """ 2 | RSS Feed Provider 3 | ================= 4 | 5 | Required configuration keys:: 6 | 7 | { 8 | "provider": "newswall.providers.feed", 9 | "source": "http://twitter.com/statuses/user_timeline/feinheit.rss" 10 | } 11 | """ 12 | from datetime import datetime 13 | import feedparser 14 | import time 15 | 16 | from newswall.providers.base import ProviderBase 17 | 18 | 19 | class Provider(ProviderBase): 20 | def update(self): 21 | feed = feedparser.parse(self.config['source']) 22 | 23 | for entry in feed['entries']: 24 | if hasattr(entry, 'date_parsed'): 25 | timestamp = datetime.fromtimestamp( 26 | time.mktime(entry.date_parsed)) 27 | elif hasattr(entry, 'published_parsed'): 28 | timestamp = datetime.fromtimestamp( 29 | time.mktime(entry.published_parsed)) 30 | else: 31 | timestamp = datetime.now() 32 | 33 | self.create_story( 34 | entry.link, 35 | title=entry.title, 36 | body=entry.description, 37 | timestamp=timestamp, 38 | ) 39 | -------------------------------------------------------------------------------- /newswall/providers/instagram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Instagram Provider 4 | ================== 5 | 6 | Required configuration keys:: 7 | { 8 | "provider": "newswall.providers.instagram", 9 | "username": "...", 10 | } 11 | 12 | """ 13 | from __future__ import unicode_literals 14 | from lxml import html 15 | 16 | import requests 17 | import datetime 18 | 19 | try: 20 | import json 21 | except ImportError: 22 | # maintain compatibility with Django < 1.7 23 | from django.utils import simplejson as json 24 | 25 | from newswall.providers.base import ProviderBase 26 | 27 | SCRIPT_JSON_PREFIX = 18 28 | SCRIPT_JSON_DATA_INDEX = 21 29 | 30 | 31 | def get_ig_data(username): 32 | url = "https://www.instagram.com/{}/".format(username) 33 | page = requests.get(url) 34 | tree = html.fromstring(page.content) 35 | scripts = tree.xpath('//script') 36 | shared_data = None 37 | 38 | for script in scripts: 39 | if script.text: 40 | if script.text[0:SCRIPT_JSON_PREFIX] == 'window._sharedData': 41 | shared_data = script.text[SCRIPT_JSON_DATA_INDEX:-1] 42 | 43 | if not shared_data: 44 | raise ValueError('Unable to get _sharedData for username "{}"' 45 | .format(username)) 46 | 47 | json_data = json.loads(shared_data) 48 | return json_data 49 | 50 | 51 | class Provider(ProviderBase): 52 | 53 | def update(self): 54 | username = self.config['username'] 55 | data = get_ig_data(self.config['username']) 56 | 57 | user_data = data.get('entry_data').get('ProfilePage')[0].get('user') 58 | user_media = user_data.get('media')['nodes'] 59 | 60 | for obj in user_media: 61 | if bool(obj.get('is_video')): 62 | continue 63 | 64 | obj_id = obj.get('id') 65 | link = "https://www.instagram.com/{}/#{}".format(username, obj_id) 66 | image_url = obj.get('display_src') 67 | timestamp = datetime.datetime.fromtimestamp(obj.get('date')) 68 | caption = obj.get('caption', '') 69 | self.create_story( 70 | link, title=obj_id, body=caption, image_url=image_url, 71 | timestamp=timestamp 72 | ) 73 | -------------------------------------------------------------------------------- /newswall/providers/twitter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Twitter API Feed Provider 3 | ========================= 4 | 5 | --- 6 | 7 | Required: tweepy 8 | 9 | pip install tweepy 10 | 11 | --- 12 | 13 | Usage: 14 | 15 | Create a twitter app. 16 | You'll find the consumer_key/secret on the detail page. 17 | Because this is a read-only application, you can create 18 | your oauth_token/secret directly on the bottom of the app detail page. 19 | 20 | --- 21 | 22 | Required configuration keys:: 23 | 24 | { 25 | "provider": "newswall.providers.twitter", 26 | "user": "feinheit", 27 | "consumer_key": "...", 28 | "consumer_secret": "...", 29 | "oauth_token": "...", 30 | "oauth_secret": "..." 31 | } 32 | 33 | """ 34 | import tweepy 35 | 36 | from newswall.providers.base import ProviderBase 37 | 38 | 39 | class Provider(ProviderBase): 40 | def update(self): 41 | auth = tweepy.OAuthHandler( 42 | self.config['consumer_key'], 43 | self.config['consumer_secret'] 44 | ) 45 | 46 | auth.set_access_token( 47 | self.config['oauth_token'], 48 | self.config['oauth_secret'] 49 | ) 50 | 51 | api = tweepy.API(auth) 52 | entries = api.user_timeline(screen_name=self.config['user']) 53 | 54 | for entry in entries: 55 | link = 'http://twitter.com/%s/status/%s' % ( 56 | self.config['user'], 57 | entry.id, 58 | ) 59 | 60 | self.create_story( 61 | link, 62 | title=entry.text, 63 | timestamp=entry.created_at, 64 | ) 65 | -------------------------------------------------------------------------------- /newswall/providers/youtube.py: -------------------------------------------------------------------------------- 1 | """ 2 | Youtube Provider 3 | ================ 4 | 5 | Get all video uploads for specific channel 6 | 7 | Create project at Google Developers Console: 8 | https://console.developers.google.com/ 9 | 10 | and request an API key. 11 | 12 | Remember to enable "YouTube Data API v3" from APIs & Auth > APIs 13 | 14 | 15 | Required configuration keys:: 16 | { 17 | "provider": "newswall.providers.youtube", 18 | "channel_id": "...", 19 | "api_key": "..." 20 | } 21 | """ 22 | 23 | import urllib 24 | from datetime import datetime 25 | 26 | try: 27 | import json 28 | except ImportError: 29 | # maintain compatibility with Django < 1.7 30 | from django.utils import simplejson as json 31 | 32 | from newswall.providers.base import ProviderBase 33 | 34 | 35 | class Provider(ProviderBase): 36 | def update(self): 37 | playlist_id_query = 'https://www.googleapis.com/youtube/v3/channels?' \ 38 | 'part=contentDetails&id=%s&key=%s' % \ 39 | (self.config['channel_id'], self.config['api_key']) 40 | file_a = urllib.urlopen(playlist_id_query) 41 | playlist_response = json.loads(file_a.read()) 42 | playlist_id = playlist_response['items'][0]['contentDetails']\ 43 | ['relatedPlaylists']['uploads'] 44 | 45 | query = "https://www.googleapis.com/youtube/v3/playlistItems?" \ 46 | "part=snippet&playlistId=%s&key=%s" % \ 47 | (playlist_id, self.config['api_key']) 48 | 49 | file_b = urllib.urlopen(query) 50 | raw = file_b.read() 51 | response = json.loads(raw) 52 | 53 | for entry in response['items']: 54 | snippet = entry['snippet'] 55 | video_id = snippet['resourceId']['videoId'] 56 | video_url = 'https://www.youtube.com/watch?v=%s' % video_id 57 | link = video_url 58 | try: 59 | image_url = snippet['thumbnails']['maxres'].get('url') 60 | except KeyError: 61 | image_url = snippet['thumbnails']['high'].get('url') 62 | self.create_story( 63 | link, 64 | title=snippet.get('title'), 65 | body=snippet['description'], 66 | image_url=image_url, 67 | timestamp=datetime.strptime( 68 | snippet['publishedAt'], '%Y-%m-%dT%H:%M:%S.000Z' 69 | ), 70 | ) 71 | -------------------------------------------------------------------------------- /newswall/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Adding model 'Source' 13 | db.create_table('newswall_source', ( 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)), 16 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), 17 | ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50)), 18 | ('ordering', self.gf('django.db.models.fields.IntegerField')(default=0)), 19 | ('data', self.gf('django.db.models.fields.TextField')(blank=True)), 20 | )) 21 | db.send_create_signal('newswall', ['Source']) 22 | 23 | # Adding model 'Story' 24 | db.create_table('newswall_story', ( 25 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 26 | ('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)), 27 | ('timestamp', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), 28 | ('object_url', self.gf('django.db.models.fields.URLField')(unique=True, max_length=200)), 29 | ('source', self.gf('django.db.models.fields.related.ForeignKey')(related_name='stories', to=orm['newswall.Source'])), 30 | ('title', self.gf('django.db.models.fields.CharField')(max_length=100)), 31 | ('author', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), 32 | ('body', self.gf('django.db.models.fields.TextField')(blank=True)), 33 | ('image_url', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)), 34 | )) 35 | db.send_create_signal('newswall', ['Story']) 36 | 37 | def backwards(self, orm): 38 | 39 | # Deleting model 'Source' 40 | db.delete_table('newswall_source') 41 | 42 | # Deleting model 'Story' 43 | db.delete_table('newswall_story') 44 | 45 | models = { 46 | 'newswall.source': { 47 | 'Meta': {'ordering': "['ordering', 'name']", 'object_name': 'Source'}, 48 | 'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 51 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 52 | 'ordering': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 53 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) 54 | }, 55 | 'newswall.story': { 56 | 'Meta': {'ordering': "['-timestamp']", 'object_name': 'Story'}, 57 | 'author': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), 58 | 'body': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'image_url': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 61 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 62 | 'object_url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}), 63 | 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stories'", 'to': "orm['newswall.Source']"}), 64 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 65 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 66 | } 67 | } 68 | 69 | complete_apps = ['newswall'] 70 | -------------------------------------------------------------------------------- /newswall/south_migrations/0002_auto__chg_field_story_title.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Changing field 'Story.title' 13 | db.alter_column('newswall_story', 'title', self.gf('django.db.models.fields.CharField')(max_length=200)) 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'Story.title' 18 | db.alter_column('newswall_story', 'title', self.gf('django.db.models.fields.CharField')(max_length=100)) 19 | 20 | models = { 21 | 'newswall.source': { 22 | 'Meta': {'ordering': "['ordering', 'name']", 'object_name': 'Source'}, 23 | 'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 26 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 27 | 'ordering': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 28 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) 29 | }, 30 | 'newswall.story': { 31 | 'Meta': {'ordering': "['-timestamp']", 'object_name': 'Story'}, 32 | 'author': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), 33 | 'body': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 34 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'image_url': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 36 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 37 | 'object_url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}), 38 | 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stories'", 'to': "orm['newswall.Source']"}), 39 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 40 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}) 41 | } 42 | } 43 | 44 | complete_apps = ['newswall'] 45 | -------------------------------------------------------------------------------- /newswall/south_migrations/0003_auto__chg_field_story_title__chg_field_story_image_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Changing field 'Story.title' 13 | db.alter_column('newswall_story', 'title', self.gf('django.db.models.fields.CharField')(max_length=1000)) 14 | 15 | # Changing field 'Story.image_url' 16 | db.alter_column('newswall_story', 'image_url', self.gf('django.db.models.fields.CharField')(max_length=1000)) 17 | 18 | def backwards(self, orm): 19 | 20 | # Changing field 'Story.title' 21 | db.alter_column('newswall_story', 'title', self.gf('django.db.models.fields.CharField')(max_length=200)) 22 | 23 | # Changing field 'Story.image_url' 24 | db.alter_column('newswall_story', 'image_url', self.gf('django.db.models.fields.CharField')(max_length=200)) 25 | 26 | models = { 27 | 'newswall.source': { 28 | 'Meta': {'ordering': "['ordering', 'name']", 'object_name': 'Source'}, 29 | 'data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 32 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 33 | 'ordering': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 34 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) 35 | }, 36 | 'newswall.story': { 37 | 'Meta': {'ordering': "['-timestamp']", 'object_name': 'Story'}, 38 | 'author': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), 39 | 'body': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'image_url': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'object_url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}), 44 | 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stories'", 'to': "orm['newswall.Source']"}), 45 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) 47 | } 48 | } 49 | 50 | complete_apps = ['newswall'] 51 | -------------------------------------------------------------------------------- /newswall/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/south_migrations/__init__.py -------------------------------------------------------------------------------- /newswall/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | import logging 4 | from django.utils.datetime_safe import datetime 5 | from django.core.management import call_command 6 | from celery import shared_task 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | @shared_task(name='update_newswall') 12 | def update_newswall(): 13 | logger.info('Newswall update started at {}'.format(datetime.now())) 14 | try: 15 | call_command('update_newswall') 16 | except Exception as e: 17 | logger.exception(e) 18 | -------------------------------------------------------------------------------- /newswall/templates/newswall/newswall_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}{% block title %}{% endblock %}{% endblock %} 4 | 5 | {% block content %} 6 | {% block news_content %} 7 | {% endblock news_content %} 8 | {% endblock content %} -------------------------------------------------------------------------------- /newswall/templates/newswall/story_archive.html: -------------------------------------------------------------------------------- 1 | {% extends view.base_template|default:"newswall/newswall_base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% if year %}{{ year }} - {% endif %} 6 | {% if month %}{{ month|date:"F Y" }} - {% endif %} 7 | {% if day %}{{ day|date:"j. F Y" }} - {% endif %} 8 | {% if source %}{{ source }} - {% endif %} 9 | {% trans "News" %} - {{ block.super }}{% endblock %} 10 | 11 | {% block news_content %} 12 | {% block content_title %} 13 |

{% trans 'News' %} 14 | {% if year %}{% trans 'for' %} {{ year }}{% endif %} 15 | {% if month %}{% trans 'for' %} {{ month|date:"F Y" }}{% endif %} 16 | {% if day %}{% trans 'for' %} {{ day|date:"j. F Y" }}{% endif %} 17 | {% if category %}{% trans 'for' %} {{ category }}{% endif %} 18 |

19 | {% endblock %} 20 | 21 | {% block object_list %} 22 | {% for story in object_list %} 23 |
24 | {% if story.image_url %} 25 | 26 | {% endif %} 27 |

{{ story.title }}

28 | 29 | {{ story.source }} | 30 | {{ story.timestamp|date:"j F Y" }} 31 | 32 |

{{ story.body|striptags|safe }}

33 |
34 | {% endfor %} 35 | {% endblock %} 36 | 37 | {% block pagination %} 38 | 54 | {% endblock %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /newswall/templates/newswall/story_detail.html: -------------------------------------------------------------------------------- 1 | {% extends view.base_template|default:"newswall/newswall_base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "News" %} - {{ block.super }}{% endblock %} 6 | 7 | {% block news_content %} 8 |

{{ object }}

9 | 10 | {{ object.source }} | 11 | {{ object.timestamp|date:"j F Y" }} 12 | 13 | 14 |

{{ story.body|striptags|truncatewords:20|safe|urlize }}

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /newswall/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-newswall/f968eb9233ed30386399b29586b5743cde79744c/newswall/templatetags/__init__.py -------------------------------------------------------------------------------- /newswall/templatetags/newswall_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from newswall.models import Source, Story 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.assignment_tag 10 | def newswall_sources(): 11 | return Source.objects.active() 12 | 13 | 14 | @register.assignment_tag 15 | def newswall_archive_months(): 16 | return Story.objects.active().dates('timestamp', 'month', 'DESC') 17 | -------------------------------------------------------------------------------- /newswall/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, patterns 2 | 3 | from newswall.feeds import StoryFeed 4 | from newswall import views 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | url(r'^feed/$', StoryFeed()), 10 | url(r'^get/$', 11 | views.FeedDataView.as_view(), 12 | name='newswall_feed_data'), 13 | url(r'^$', 14 | views.ArchiveIndexView.as_view(), 15 | name='newswall_entry_archive'), 16 | url(r'^(?P\d{4})/$', 17 | views.YearArchiveView.as_view(), 18 | name='newswall_entry_archive_year'), 19 | url(r'^(?P\d{4})/(?P\d{2})/$', 20 | views.MonthArchiveView.as_view(), 21 | name='newswall_entry_archive_month'), 22 | url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/$', 23 | views.DayArchiveView.as_view(), 24 | name='newswall_entry_archive_day'), 25 | url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/$', 26 | views.DateDetailView.as_view(), 27 | name='newswall_entry_detail'), 28 | url(r'^source/(?P[-\w]+)/$', 29 | views.SourceArchiveIndexView.as_view(), 30 | name='newswall_source_detail'), 31 | ) 32 | -------------------------------------------------------------------------------- /newswall/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseForbidden 2 | from django.shortcuts import get_object_or_404 3 | from django.views.generic import dates, View 4 | from django.forms.models import model_to_dict 5 | 6 | from .models import Source, Story 7 | from .mixin import NewswallMixin, JSONResponseMixin 8 | 9 | try: 10 | from towel import paginator 11 | except ImportError: 12 | from django.core import paginator 13 | 14 | 15 | __all__ = ( 16 | 'ArchiveIndexView', 'YearArchiveView', 'MonthArchiveView', 17 | 'DayArchiveView', 'DateDetailView', 'SourceArchiveIndexView') 18 | 19 | 20 | class ArchiveIndexView(NewswallMixin, dates.ArchiveIndexView): 21 | paginator_class = paginator.Paginator 22 | paginate_by = 20 23 | date_field = 'timestamp' 24 | template_name_suffix = '_archive' 25 | allow_empty = True 26 | 27 | 28 | class YearArchiveView(NewswallMixin, dates.YearArchiveView): 29 | paginator_class = paginator.Paginator 30 | paginate_by = 20 31 | date_field = 'timestamp' 32 | make_object_list = True 33 | template_name_suffix = '_archive' 34 | 35 | 36 | class MonthArchiveView(NewswallMixin, dates.MonthArchiveView): 37 | paginator_class = paginator.Paginator 38 | paginate_by = 20 39 | month_format = '%m' 40 | date_field = 'timestamp' 41 | template_name_suffix = '_archive' 42 | 43 | 44 | class DayArchiveView(NewswallMixin, dates.DayArchiveView): 45 | paginator_class = paginator.Paginator 46 | paginate_by = 20 47 | month_format = '%m' 48 | date_field = 'timestamp' 49 | template_name_suffix = '_archive' 50 | 51 | 52 | class DateDetailView(NewswallMixin, dates.DateDetailView): 53 | paginator_class = paginator.Paginator 54 | paginate_by = 20 55 | month_format = '%m' 56 | date_field = 'timestamp' 57 | 58 | def get_queryset(self): 59 | return Story.objects.active() 60 | 61 | 62 | class SourceArchiveIndexView(ArchiveIndexView): 63 | template_name_suffix = '_archive' 64 | 65 | def get_queryset(self): 66 | self.source = get_object_or_404(Source, slug=self.kwargs['slug']) 67 | 68 | queryset = super(SourceArchiveIndexView, self).get_queryset() 69 | return queryset.filter(source=self.source) 70 | 71 | def get_context_data(self, **kwargs): 72 | return super(SourceArchiveIndexView, self).get_context_data( 73 | source=self.source, 74 | **kwargs) 75 | 76 | 77 | class FeedDataView(JSONResponseMixin, View): 78 | def dispatch(self, request, *args, **kwargs): 79 | if not request.is_ajax(): 80 | return HttpResponseForbidden() 81 | return super(FeedDataView, self).dispatch(request, *args, **kwargs) 82 | 83 | def get_context_data(self, **kwargs): 84 | data = Story.objects.active() 85 | clean_data = [] 86 | for item in data: 87 | clean_item = model_to_dict(item) 88 | clean_data.append(clean_item) 89 | 90 | context = { 91 | 'stories': clean_data, 92 | } 93 | return context 94 | 95 | def get(self, request, *args, **kwargs): 96 | return self.render_to_response(self.get_context_data()) 97 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=migrations 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | 7 | def read(filename): 8 | return open(os.path.join(os.path.dirname(__file__), filename)).read() 9 | 10 | 11 | setup( 12 | name='django-newswall', 13 | version=__import__('newswall').__version__, 14 | description='My version of a Tumblelog, because I can.', 15 | long_description=read('README.rst'), 16 | author='Matthias Kestenholz', 17 | author_email='mk@406.ch', 18 | url='https://github.com/matthiask/django-newswall', 19 | license='BSD License', 20 | platforms=['OS Independent'], 21 | packages=find_packages( 22 | exclude=[], 23 | ), 24 | package_data={ 25 | '': ['*.html', '*.txt'], 26 | 'newswall': [ 27 | 'locale/*/*/*.*', 28 | # 'static/newswall.*', 29 | # 'static/newswall.*', 30 | 'templates/*.*', 31 | 'templates/*/*.*', 32 | 'templates/*/*/*.*', 33 | 'templates/*/*/*/*.*', 34 | ], 35 | }, 36 | install_requires=[ 37 | 'Django>=1.4.2', 38 | ], 39 | classifiers=[ 40 | # 'Development Status :: 5 - Production/Stable', 41 | 'Environment :: Web Environment', 42 | 'Framework :: Django', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: BSD License', 45 | 'Operating System :: OS Independent', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 2', 48 | 'Programming Language :: Python :: 2.6', 49 | 'Programming Language :: Python :: 2.7', 50 | # 'Programming Language :: Python :: 3', 51 | # 'Programming Language :: Python :: 3.3', 52 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 53 | 'Topic :: Software Development', 54 | ], 55 | zip_safe=False, 56 | ) 57 | --------------------------------------------------------------------------------