├── newswall
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── update_newswall.py
├── migrations
│ ├── __init__.py
│ ├── 0002_auto_20151218_1227.py
│ └── 0001_initial.py
├── providers
│ ├── __init__.py
│ ├── base.py
│ ├── elephantblog.py
│ ├── feed.py
│ ├── twitter.py
│ ├── instagram.py
│ ├── youtube.py
│ └── fb_graph_feed.py
├── templatetags
│ ├── __init__.py
│ └── newswall_tags.py
├── south_migrations
│ ├── __init__.py
│ ├── 0002_auto__chg_field_story_title.py
│ ├── 0003_auto__chg_field_story_title__chg_field_story_image_url.py
│ └── 0001_initial.py
├── __init__.py
├── locale
│ └── de
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── templates
│ └── newswall
│ │ ├── newswall_base.html
│ │ ├── story_detail.html
│ │ └── story_archive.html
├── tasks.py
├── feeds.py
├── admin.py
├── urls.py
├── mixin.py
├── models.py
└── views.py
├── setup.cfg
├── .gitignore
├── MANIFEST.in
├── LICENSE
├── setup.py
└── README.rst
/newswall/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/newswall/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/newswall/providers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/newswall/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/newswall/south_migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/newswall/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude=migrations
3 |
--------------------------------------------------------------------------------
/newswall/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = (0, 1, 0)
2 | __version__ = '.'.join(str(v) for v in VERSION)
3 |
--------------------------------------------------------------------------------
/newswall/locale/de/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthiask/django-newswall/HEAD/newswall/locale/de/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------