Merge pull request #20 from loleg/refresh-token
Refresh token and sync button
This commit is contained in:
commit
5fbdd3da06
3
Makefile
3
Makefile
|
@ -66,7 +66,8 @@ logs:
|
||||||
docker-compose logs -f --tail=500
|
docker-compose logs -f --tail=500
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
docker-compose exec web ./manage.py dumpdata --natural-foreign --indent=4 -e contenttypes -e auth.Permission -e sessions -e wagtailcore.pagerevision -e wagtailcore.groupcollectionpermission > ~/publichealth.home.json
|
docker-compose start postgres
|
||||||
|
docker-compose exec web ./manage.py dumpdata --natural-foreign -e auth.permission -e contenttypes -e wagtailcore.GroupCollectionPermission -e wagtailimages.rendition -e sessions -e feedler.feedlysettings > ~/publichealth.home.json
|
||||||
zip ~/publichealth.home.json.`date +"%d%m%Y-%H%M"`.zip ~/publichealth.home.json
|
zip ~/publichealth.home.json.`date +"%d%m%Y-%H%M"`.zip ~/publichealth.home.json
|
||||||
rm ~/publichealth.home.json
|
rm ~/publichealth.home.json
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.conf.urls import url
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper
|
||||||
|
from wagtail.contrib.modeladmin.options import ModelAdmin
|
||||||
|
from wagtail.contrib.modeladmin.views import IndexView
|
||||||
|
from wagtail.wagtailadmin import messages
|
||||||
|
|
||||||
|
from feedler.models import Entry
|
||||||
|
from feedler.refresh import refresh_streams
|
||||||
|
from feedler.models.admin import FeedlySettings
|
||||||
|
|
||||||
|
class RefreshButtonHelper(ButtonHelper):
|
||||||
|
"""
|
||||||
|
This helper constructs a refresh button
|
||||||
|
"""
|
||||||
|
button_classnames = ['icon', 'icon-download']
|
||||||
|
def refresh_button(self, classnames_add=None, classnames_exclude=None):
|
||||||
|
if classnames_add is None: classnames_add = []
|
||||||
|
if classnames_exclude is None: classnames_exclude = []
|
||||||
|
classnames = self.button_classnames + classnames_add
|
||||||
|
cn = self.finalise_classname(classnames, classnames_exclude)
|
||||||
|
text = _('Sync {}'.format(self.verbose_name_plural.title()))
|
||||||
|
return {
|
||||||
|
'url': self.url_helper.get_action_url('refresh', query_params=self.request.GET),
|
||||||
|
'label': text, 'classname': cn, 'title': text,
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshAdminURLHelper(AdminURLHelper):
|
||||||
|
"""
|
||||||
|
This helper constructs the different urls, to overwrite the default behaviour
|
||||||
|
and append the filters to the action.
|
||||||
|
"""
|
||||||
|
non_object_specific_actions = ('create', 'choose_parent', 'index', 'refresh')
|
||||||
|
def get_action_url(self, action, *args, **kwargs):
|
||||||
|
query_params = kwargs.pop('query_params', None)
|
||||||
|
url_name = self.get_action_url_name(action)
|
||||||
|
if action in self.non_object_specific_actions:
|
||||||
|
url = reverse(url_name)
|
||||||
|
else:
|
||||||
|
url = reverse(url_name, args=args, kwargs=kwargs)
|
||||||
|
if query_params:
|
||||||
|
url += '?{params}'.format(params=query_params.urlencode())
|
||||||
|
return url
|
||||||
|
def get_action_url_pattern(self, action):
|
||||||
|
if action in self.non_object_specific_actions:
|
||||||
|
return self._get_action_url_pattern(action)
|
||||||
|
return self._get_object_specific_action_url_pattern(action)
|
||||||
|
|
||||||
|
class RefreshView(IndexView):
|
||||||
|
"""
|
||||||
|
A Class Based View which will handle the button click
|
||||||
|
"""
|
||||||
|
# def export_csv(self):
|
||||||
|
# data = self.queryset.all()
|
||||||
|
# response = ...
|
||||||
|
# return response
|
||||||
|
@method_decorator(login_required)
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
super().dispatch(request, *args, **kwargs)
|
||||||
|
if not refresh_streams(FeedlySettings.for_site(request.site)):
|
||||||
|
messages.error(
|
||||||
|
request, _('Sorry, could not refresh streams. Please contact your administrator.'))
|
||||||
|
return redirect('/admin/feedler/entry/')
|
||||||
|
|
||||||
|
|
||||||
|
class EntryModelAdminMixin(object):
|
||||||
|
"""
|
||||||
|
A mixin to add to your model admin which hooks the different helpers, the view
|
||||||
|
and register the new urls.
|
||||||
|
"""
|
||||||
|
button_helper_class = RefreshButtonHelper
|
||||||
|
url_helper_class = RefreshAdminURLHelper
|
||||||
|
view_class = RefreshView
|
||||||
|
|
||||||
|
def get_admin_urls_for_registration(self):
|
||||||
|
urls = super().get_admin_urls_for_registration()
|
||||||
|
urls += (
|
||||||
|
url(
|
||||||
|
self.url_helper.get_action_url_pattern('refresh'),
|
||||||
|
self.refresh_view,
|
||||||
|
name=self.url_helper.get_action_url_name('refresh')
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return urls
|
||||||
|
|
||||||
|
def refresh_view(self, request):
|
||||||
|
kwargs = {'model_admin': self}
|
||||||
|
view_class = self.view_class
|
||||||
|
return view_class.as_view(**kwargs)(request)
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2017-10-16 08:59
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('feedler', '0006_auto_20171012_1458'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='feedlysettings',
|
||||||
|
old_name='feedly_stream',
|
||||||
|
new_name='streams',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='feedlysettings',
|
||||||
|
name='feedly_auth',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='feedlysettings',
|
||||||
|
name='feedly_pages',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='feedlysettings',
|
||||||
|
name='refresh',
|
||||||
|
field=models.CharField(blank=True, help_text='Refresh Token for automatic update (pro account)', max_length=1024),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='feedlysettings',
|
||||||
|
name='token',
|
||||||
|
field=models.CharField(blank=True, help_text='Access Token from feedly.com/v3/auth/dev', max_length=1024),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stream',
|
||||||
|
name='ident',
|
||||||
|
field=models.CharField(help_text='Example: enterprise/myuser/tag/abcd...', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import requests, json, codecs
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -11,58 +9,21 @@ from django.core.mail import send_mail
|
||||||
|
|
||||||
from wagtail.contrib.settings.models import BaseSetting, register_setting
|
from wagtail.contrib.settings.models import BaseSetting, register_setting
|
||||||
|
|
||||||
from .models import Entry, Stream
|
from .models import Stream
|
||||||
import feedler.feedparser as feedparser
|
|
||||||
|
|
||||||
import logging
|
from feedler.refresh import refresh_streams
|
||||||
logger = logging.getLogger('feedler')
|
|
||||||
|
|
||||||
# Feedly integration module
|
|
||||||
|
|
||||||
@register_setting
|
@register_setting
|
||||||
class FeedlySettings(BaseSetting):
|
class FeedlySettings(BaseSetting):
|
||||||
feedly_auth = models.TextField(
|
streams = models.ManyToManyField(Stream,
|
||||||
help_text='Your developer authorization key', blank=True)
|
|
||||||
feedly_pages = models.IntegerField(
|
|
||||||
choices=(
|
|
||||||
(1, '2'),
|
|
||||||
(2, '5'),
|
|
||||||
(3, '10'),
|
|
||||||
(4, '50'),
|
|
||||||
), blank=True, null=True, editable=False,
|
|
||||||
help_text='How many pages to fetch?'
|
|
||||||
)
|
|
||||||
feedly_stream = models.ManyToManyField(Stream,
|
|
||||||
help_text='Which streams to update')
|
help_text='Which streams to update')
|
||||||
|
token = models.CharField(max_length=1024, blank=True,
|
||||||
|
help_text='Access Token from feedly.com/v3/auth/dev')
|
||||||
|
refresh = models.CharField(max_length=1024, blank=True,
|
||||||
|
help_text='Refresh Token for automatic update (pro account)')
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Feedly'
|
verbose_name = 'Feedly'
|
||||||
|
|
||||||
API_BASEURL = 'https://cloud.feedly.com/v3/streams/contents?streamId='
|
# @receiver(pre_save, sender=FeedlySettings)
|
||||||
|
# def handle_save_settings(sender, instance, *args, **kwargs):
|
||||||
@receiver(pre_save, sender=FeedlySettings)
|
# if instance.token: refresh_streams(instance)
|
||||||
def handle_save_settings(sender, instance, *args, **kwargs):
|
|
||||||
if instance.feedly_auth:
|
|
||||||
streams = instance.feedly_stream.all()
|
|
||||||
for stream in streams:
|
|
||||||
# Start a request to download the feed
|
|
||||||
logger.info("Processing stream %s" % stream.title)
|
|
||||||
url = API_BASEURL + stream.ident
|
|
||||||
headers = {
|
|
||||||
'Authorization': 'OAuth ' + instance.feedly_auth
|
|
||||||
}
|
|
||||||
contents = requests.get(url, headers=headers).json()
|
|
||||||
if 'errorMessage' in contents:
|
|
||||||
raise PermissionError(contents['errorMessage'])
|
|
||||||
for raw_entry in contents['items']:
|
|
||||||
eid = raw_entry['id']
|
|
||||||
# Create or update data
|
|
||||||
try:
|
|
||||||
entry = Entry.objects.get(entry_id=eid)
|
|
||||||
logger.info("Updating entry '%s'" % eid)
|
|
||||||
except Entry.DoesNotExist:
|
|
||||||
logger.info("Adding entry '%s'" % eid)
|
|
||||||
entry = Entry()
|
|
||||||
# Parse the Feedly object
|
|
||||||
entry = feedparser.parse(entry, raw_entry, stream)
|
|
||||||
# Persist resulting object
|
|
||||||
entry.save()
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ from wagtail.wagtailcore.fields import RichTextField
|
||||||
|
|
||||||
class Stream(models.Model):
|
class Stream(models.Model):
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
ident = models.CharField(max_length=255)
|
ident = models.CharField(max_length=255,
|
||||||
|
help_text='Example: enterprise/myuser/tag/abcd...')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import requests, json, codecs
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('feedler')
|
||||||
|
|
||||||
|
from .models import Entry
|
||||||
|
from feedler import feedparser
|
||||||
|
|
||||||
|
API_BASEURL = 'https://cloud.feedly.com/v3/'
|
||||||
|
API_STREAMS = API_BASEURL + 'streams/contents?streamId='
|
||||||
|
API_TOKENS = API_BASEURL + 'auth/token'
|
||||||
|
|
||||||
|
def refresh_streams(settings):
|
||||||
|
# Iterate through all saved streams
|
||||||
|
logger.debug("Refreshing all streams")
|
||||||
|
for stream in settings.streams.all():
|
||||||
|
if not refresh_stream(stream, settings):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def refresh_token(settings):
|
||||||
|
# Request a new token
|
||||||
|
url = API_TOKENS
|
||||||
|
if not settings.refresh:
|
||||||
|
logger.warn("No Refresh token available")
|
||||||
|
return False
|
||||||
|
logger.info("Refreshing Feedly access token")
|
||||||
|
payload = {
|
||||||
|
'refresh_token': settings.refresh,
|
||||||
|
'client_id': 'feedlydev',
|
||||||
|
'client_secret': 'feedlydev',
|
||||||
|
'grant_type': 'refresh_token'
|
||||||
|
}
|
||||||
|
contents = requests.post(url, data=payload).json()
|
||||||
|
if not 'access_token' in contents or not contents['access_token']:
|
||||||
|
logger.warn("Access token could not be refreshed.")
|
||||||
|
logger.debug(contents)
|
||||||
|
return False
|
||||||
|
settings.token = contents['access_token']
|
||||||
|
settings.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def refresh_stream(stream, settings, retry=False):
|
||||||
|
# Start a request to download the feed for a particular stream
|
||||||
|
logger.info("Processing stream %s" % stream.title)
|
||||||
|
url = API_STREAMS + stream.ident
|
||||||
|
headers = { 'Authorization': 'OAuth ' + settings.token }
|
||||||
|
contents = requests.get(url, headers=headers).json()
|
||||||
|
if 'errorMessage' in contents:
|
||||||
|
# Usually this is a token expired
|
||||||
|
if 'token expired' in contents['errorMessage'] or 'unauthorized' in contents['errorMessage']:
|
||||||
|
logger.debug(contents['errorMessage'])
|
||||||
|
if not refresh_token(settings): return False
|
||||||
|
# Make another attempt
|
||||||
|
if retry or not refresh_stream(stream, settings, True):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.error(contents['errorMessage'])
|
||||||
|
return False
|
||||||
|
for raw_entry in contents['items']:
|
||||||
|
eid = raw_entry['id']
|
||||||
|
# Create or update data
|
||||||
|
try:
|
||||||
|
entry = Entry.objects.get(entry_id=eid)
|
||||||
|
logger.info("Skipping entry '%s'" % eid)
|
||||||
|
except Entry.DoesNotExist:
|
||||||
|
logger.info("Adding entry '%s'" % eid)
|
||||||
|
entry = Entry()
|
||||||
|
# Parse the Feedly object
|
||||||
|
entry = feedparser.parse(entry, raw_entry, stream)
|
||||||
|
# Persist resulting object
|
||||||
|
entry.save()
|
||||||
|
return True
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "modeladmin/index.html" %}
|
||||||
|
|
||||||
|
{% block header_extra %}
|
||||||
|
{% if user_can_create %}
|
||||||
|
<div class="right">
|
||||||
|
<div style="position: relative; top: -1px;">
|
||||||
|
{% include 'modeladmin/includes/button.html' with button=view.button_helper.refresh_button %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{{ block.super }}{% comment %}Show original buttons{% endcomment %}
|
||||||
|
{% endblock %}
|
|
@ -3,9 +3,10 @@
|
||||||
from wagtail.contrib.modeladmin.options import (
|
from wagtail.contrib.modeladmin.options import (
|
||||||
ModelAdmin, modeladmin_register)
|
ModelAdmin, modeladmin_register)
|
||||||
|
|
||||||
|
from .admin import EntryModelAdminMixin
|
||||||
from .models import Entry, Stream
|
from .models import Entry, Stream
|
||||||
|
|
||||||
class EntryModelAdmin(ModelAdmin):
|
class EntryModelAdmin(EntryModelAdminMixin, ModelAdmin):
|
||||||
model = Entry
|
model = Entry
|
||||||
menu_icon = 'date'
|
menu_icon = 'date'
|
||||||
menu_order = 200
|
menu_order = 200
|
||||||
|
@ -20,7 +21,7 @@ modeladmin_register(EntryModelAdmin)
|
||||||
class StreamModelAdmin(ModelAdmin):
|
class StreamModelAdmin(ModelAdmin):
|
||||||
model = Stream
|
model = Stream
|
||||||
menu_icon = 'date'
|
menu_icon = 'date'
|
||||||
menu_order = 1000
|
menu_order = 900
|
||||||
add_to_settings_menu = True
|
add_to_settings_menu = True
|
||||||
exclude_from_explorer = True
|
exclude_from_explorer = True
|
||||||
list_display = ('title', 'ident')
|
list_display = ('title', 'ident')
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2017-10-13 21:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0020_auto_20170920_1204'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='dataletssettings',
|
||||||
|
options={'verbose_name': 'Datalets'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dataletssettings',
|
||||||
|
name='feedback_comment',
|
||||||
|
field=models.TextField(blank=True, help_text='Any questions or general feedback', verbose_name='Comments..'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dataletssettings',
|
||||||
|
name='feedback_question',
|
||||||
|
field=models.TextField(blank=True, help_text='Who should we reply to for questions?', verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dataletssettings',
|
||||||
|
name='feedback_status',
|
||||||
|
field=models.IntegerField(blank=True, choices=[(1, '★'), (2, '★★'), (3, '★★★'), (4, '★★★★'), (5, '★★★★★')], help_text='How are you enjoying Wagtail?', null=True, verbose_name='Rating'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,23 +9,24 @@ from wagtail.contrib.settings.models import BaseSetting, register_setting
|
||||||
|
|
||||||
# A simple feedback module built into the site admin
|
# A simple feedback module built into the site admin
|
||||||
|
|
||||||
@register_setting
|
@register_setting(icon='help')
|
||||||
class DataletsSettings(BaseSetting):
|
class DataletsSettings(BaseSetting):
|
||||||
feedback_question = models.TextField(
|
feedback_question = models.TextField(verbose_name='Name',
|
||||||
help_text='Send us a question', blank=True)
|
help_text='Who should we reply to for questions?', blank=True)
|
||||||
feedback_status = models.IntegerField(
|
feedback_status = models.IntegerField(verbose_name='Rating',
|
||||||
choices=(
|
choices=(
|
||||||
(1, ':-('),
|
(1, u'★'),
|
||||||
(2, ':-|'),
|
(2, u'★'*2),
|
||||||
(3, ':-)'),
|
(3, u'★'*3),
|
||||||
(4, ':-D'),
|
(4, u'★'*4),
|
||||||
|
(5, u'★'*5),
|
||||||
), blank=True, null=True,
|
), blank=True, null=True,
|
||||||
help_text='How are you enjoying Wagtail?'
|
help_text='How are you enjoying Wagtail?'
|
||||||
)
|
)
|
||||||
feedback_comment = models.TextField(
|
feedback_comment = models.TextField(verbose_name='Comments..',
|
||||||
help_text='Any general feedback', blank=True)
|
help_text='Any questions or general feedback', blank=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Get support'
|
verbose_name = 'Datalets'
|
||||||
|
|
||||||
@receiver(pre_save, sender=DataletsSettings)
|
@receiver(pre_save, sender=DataletsSettings)
|
||||||
def handle_save_settings(sender, instance, *args, **kwargs):
|
def handle_save_settings(sender, instance, *args, **kwargs):
|
||||||
|
@ -41,3 +42,4 @@ def handle_save_settings(sender, instance, *args, **kwargs):
|
||||||
instance.feedback_status = None
|
instance.feedback_status = None
|
||||||
instance.feedback_question = ""
|
instance.feedback_question = ""
|
||||||
instance.feedback_comment = ""
|
instance.feedback_comment = ""
|
||||||
|
instance.save()
|
||||||
|
|
|
@ -21,6 +21,10 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
INSTALLED_APPS = INSTALLED_APPS + [
|
||||||
|
'wagtail.contrib.wagtailstyleguide',
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .local import *
|
from .local import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -156,6 +156,12 @@ LOGGING = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
|
'feedler': {
|
||||||
|
'handlers': [],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
'publichealth': {
|
'publichealth': {
|
||||||
'handlers': [],
|
'handlers': [],
|
||||||
'level': 'INFO',
|
'level': 'INFO',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pytest==3.0.3
|
pytest>=3.0.3
|
||||||
pytest-splinter==1.7.6
|
pytest-splinter>=1.8.5
|
||||||
tox==2.3.1
|
tox>=2.9.1
|
||||||
|
django-debug-toolbar>=1.8
|
||||||
|
|
Loading…
Reference in New Issue