From f308176e37b5a62b719a37f166d49ed10923d7c9 Mon Sep 17 00:00:00 2001 From: khashashin Date: Thu, 14 Sep 2017 16:03:14 +0200 Subject: [PATCH 01/17] added Entry template to create a additional button --- feedler/models/models.py | 120 ++++++++++++++++++++++++++++ modeladmin/feedler/entry/index.html | 7 ++ 2 files changed, 127 insertions(+) create mode 100644 modeladmin/feedler/entry/index.html diff --git a/feedler/models/models.py b/feedler/models/models.py index 3b77c6c..2ed0395 100644 --- a/feedler/models/models.py +++ b/feedler/models/models.py @@ -1,6 +1,14 @@ # -*- 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.core.urlresolvers import reverse +from django.utils.functional import cached_property +from django.utils.translation import ugettext as _ +from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper +from wagtail.contrib.modeladmin.options import ModelAdmin +from wagtail.contrib.modeladmin.views import IndexView from django.utils import translation from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger @@ -46,6 +54,118 @@ class Entry(models.Model): class Meta: verbose_name_plural = 'Entries' +# Button +class ExportButtonHelper(ButtonHelper): + """ + This helper constructs all the necessary attributes to create a button. + + There is a lot of boilerplate just for the classnames to be right :( + """ + + export_button_classnames = ['icon', 'icon-download'] + + def export_button(self, classnames_add=None, classnames_exclude=None): + if classnames_add is None: + classnames_add = [] + if classnames_exclude is None: + classnames_exclude = [] + + classnames = self.export_button_classnames + classnames_add + cn = self.finalise_classname(classnames, classnames_exclude) + text = _('Export {}'.format(self.verbose_name_plural.title())) + + return { + 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), + 'label': text, + 'classname': cn, + 'title': text, + } + + +class ExportAdminURLHelper(AdminURLHelper): + """ + This helper constructs the different urls. + + This is mostly just to overwrite the default behaviour + which consider any action other than 'create', 'choose_parent' and 'index' + as `object specific` and will try to add the object PK to the url + which is not what we want for the `export` option. + + In addition, it appends the filters to the action. + """ + + non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') + + 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 ExportView(IndexView): + """ + A Class Based View which will generate + """ + + 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) + return self.export_csv() + + +class ExportModelAdminMixin(object): + """ + A mixin to add to your model admin which hooks the different helpers, the view + and register the new urls. + """ + + button_helper_class = ExportButtonHelper + url_helper_class = ExportAdminURLHelper + + export_view_class = ExportView + + def get_admin_urls_for_registration(self): + urls = super().get_admin_urls_for_registration() + urls += ( + url( + self.url_helper.get_action_url_pattern('export'), + self.export_view, + name=self.url_helper.get_action_url_name('export') + ), + ) + + return urls + + def export_view(self, request): + kwargs = {'model_admin': self} + view_class = self.export_view_class + return view_class.as_view(**kwargs)(request) + + +class MenuModelAdmin(ExportModelAdminMixin, ModelAdmin): + model = Entry + class FeedPage(Page): intro = RichTextField(default='', blank=True) stream = models.ForeignKey(Stream, on_delete=models.PROTECT, diff --git a/modeladmin/feedler/entry/index.html b/modeladmin/feedler/entry/index.html new file mode 100644 index 0000000..3289d81 --- /dev/null +++ b/modeladmin/feedler/entry/index.html @@ -0,0 +1,7 @@ +{% extends "modeladmin/index.html %} + +{% block header_extra %} + {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} + My New Button + {{ block.super }}{% comment %}Display the original buttons {% endcomment %} +{% endblock %} From fd2ccf68eb488365a97d6fc49d0e1c8d6f4a5b49 Mon Sep 17 00:00:00 2001 From: khashashin Date: Thu, 14 Sep 2017 16:16:33 +0200 Subject: [PATCH 02/17] template update --- modeladmin/feedler/entry/index.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modeladmin/feedler/entry/index.html b/modeladmin/feedler/entry/index.html index 3289d81..09a3d40 100644 --- a/modeladmin/feedler/entry/index.html +++ b/modeladmin/feedler/entry/index.html @@ -1,7 +1,11 @@ {% extends "modeladmin/index.html %} {% block header_extra %} - {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} - My New Button - {{ block.super }}{% comment %}Display the original buttons {% endcomment %} + {% if user_can_create %} +
+
+ {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} +
+
+ {% endif %} {% endblock %} From ec02c97c606b1155cd606e1243c762ad21100b3c Mon Sep 17 00:00:00 2001 From: khashashin Date: Mon, 18 Sep 2017 13:31:30 +0200 Subject: [PATCH 03/17] have to fix some bud --- feedler/models/admin.py | 133 ++++++++++++++++++ feedler/models/models.py | 112 --------------- .../modeladmin/feedler/entry/index.html | 7 + feedler/wagtail_hooks.py | 7 + modeladmin/feedler/entry/index.html | 11 -- .../home/templates/home/photo_gallery.html | 25 ++++ 6 files changed, 172 insertions(+), 123 deletions(-) create mode 100644 feedler/templates/modeladmin/feedler/entry/index.html delete mode 100644 modeladmin/feedler/entry/index.html create mode 100644 publichealth/home/templates/home/photo_gallery.html diff --git a/feedler/models/admin.py b/feedler/models/admin.py index c6b6587..5d1cd1d 100644 --- a/feedler/models/admin.py +++ b/feedler/models/admin.py @@ -1,4 +1,11 @@ # -*- coding: utf-8 -*- +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse +from django.utils.decorators import method_decorator +from django.utils.functional import cached_property +from django.utils.translation import ugettext as _ +from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper +from wagtail.contrib.modeladmin.views import IndexView import requests, json, codecs @@ -65,3 +72,129 @@ def handle_save_settings(sender, instance, *args, **kwargs): entry = feedparser.parse(entry, raw_entry, stream) # Persist resulting object entry.save() + +class ExportButtonHelper(ButtonHelper): + """ + This helper constructs all the necessary attributes to create a button. + + There is a lot of boilerplate just for the classnames to be right :( + """ + + export_button_classnames = ['icon', 'icon-download'] + + def export_button(self, classnames_add=None, classnames_exclude=None): + if classnames_add is None: + classnames_add = [] + if classnames_exclude is None: + classnames_exclude = [] + + classnames = self.export_button_classnames + classnames_add + cn = self.finalise_classname(classnames, classnames_exclude) + text = _('Export {}'.format(self.verbose_name_plural.title())) + + return { + 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), + 'label': text, + 'classname': cn, + 'title': text, + } + + +class ExportAdminURLHelper(AdminURLHelper): + """ + This helper constructs the different urls. + + This is mostly just to overwrite the default behaviour + which consider any action other than 'create', 'choose_parent' and 'index' + as `object specific` and will try to add the object PK to the url + which is not what we want for the `export` option. + + In addition, it appends the filters to the action. + """ + + non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') + + 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 ExportView(IndexView): + """ + A Class Based View which will generate + """ + + def export_csv(self): + data = self.queryset.all() + data_headings = [field.verbose_name for field + in EventRegistration._meta.get_fields()] + + # return a CSV instead + response = HttpResponse(content_type='text/csv; charset=utf-8') + response['Content-Disposition'] = 'attachment;filename=' + \ + 'registrations.csv' + + # Prevents UnicodeEncodeError for labels with non-ansi symbols + data_headings = [smart_str(label) for label in data_headings] + + writer = csv.writer(response) + writer.writerow(data_headings) + for reg in data: + data_row = [] + data_row.extend([ + reg.title, reg.example_field2, reg.example_field3 + ]) + writer.writerow(data_row) + + return response + + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + super().dispatch(request, *args, **kwargs) + return self.export_csv() + + +class ExportModelAdminMixin(object): + """ + A mixin to add to your model admin which hooks the different helpers, the view + and register the new urls. + """ + + button_helper_class = ExportButtonHelper + url_helper_class = ExportAdminURLHelper + + export_view_class = ExportView + + def get_admin_urls_for_registration(self): + urls = super().get_admin_urls_for_registration() + urls += ( + url( + self.url_helper.get_action_url_pattern('export'), + self.export_view, + name=self.url_helper.get_action_url_name('export') + ), + ) + + return urls + + def export_view(self, request): + kwargs = {'model_admin': self} + view_class = self.export_view_class + return view_class.as_view(**kwargs)(request) diff --git a/feedler/models/models.py b/feedler/models/models.py index 2ed0395..0aeb4cc 100644 --- a/feedler/models/models.py +++ b/feedler/models/models.py @@ -54,118 +54,6 @@ class Entry(models.Model): class Meta: verbose_name_plural = 'Entries' -# Button -class ExportButtonHelper(ButtonHelper): - """ - This helper constructs all the necessary attributes to create a button. - - There is a lot of boilerplate just for the classnames to be right :( - """ - - export_button_classnames = ['icon', 'icon-download'] - - def export_button(self, classnames_add=None, classnames_exclude=None): - if classnames_add is None: - classnames_add = [] - if classnames_exclude is None: - classnames_exclude = [] - - classnames = self.export_button_classnames + classnames_add - cn = self.finalise_classname(classnames, classnames_exclude) - text = _('Export {}'.format(self.verbose_name_plural.title())) - - return { - 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), - 'label': text, - 'classname': cn, - 'title': text, - } - - -class ExportAdminURLHelper(AdminURLHelper): - """ - This helper constructs the different urls. - - This is mostly just to overwrite the default behaviour - which consider any action other than 'create', 'choose_parent' and 'index' - as `object specific` and will try to add the object PK to the url - which is not what we want for the `export` option. - - In addition, it appends the filters to the action. - """ - - non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') - - 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 ExportView(IndexView): - """ - A Class Based View which will generate - """ - - 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) - return self.export_csv() - - -class ExportModelAdminMixin(object): - """ - A mixin to add to your model admin which hooks the different helpers, the view - and register the new urls. - """ - - button_helper_class = ExportButtonHelper - url_helper_class = ExportAdminURLHelper - - export_view_class = ExportView - - def get_admin_urls_for_registration(self): - urls = super().get_admin_urls_for_registration() - urls += ( - url( - self.url_helper.get_action_url_pattern('export'), - self.export_view, - name=self.url_helper.get_action_url_name('export') - ), - ) - - return urls - - def export_view(self, request): - kwargs = {'model_admin': self} - view_class = self.export_view_class - return view_class.as_view(**kwargs)(request) - - -class MenuModelAdmin(ExportModelAdminMixin, ModelAdmin): - model = Entry - class FeedPage(Page): intro = RichTextField(default='', blank=True) stream = models.ForeignKey(Stream, on_delete=models.PROTECT, diff --git a/feedler/templates/modeladmin/feedler/entry/index.html b/feedler/templates/modeladmin/feedler/entry/index.html new file mode 100644 index 0000000..eed6636 --- /dev/null +++ b/feedler/templates/modeladmin/feedler/entry/index.html @@ -0,0 +1,7 @@ +{% extends "modeladmin/index.html %} + +{% block header_extra %} + My New Button + {{ block.super }}{% comment %}Display the original buttons {% endcomment %} + {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} +{% endblock %} diff --git a/feedler/wagtail_hooks.py b/feedler/wagtail_hooks.py index ed8abd5..afd2b14 100644 --- a/feedler/wagtail_hooks.py +++ b/feedler/wagtail_hooks.py @@ -4,6 +4,7 @@ from wagtail.contrib.modeladmin.options import ( ModelAdmin, modeladmin_register) from .models import Entry, Stream +from .models.admin import ExportModelAdminMixin class EntryModelAdmin(ModelAdmin): model = Entry @@ -26,3 +27,9 @@ class StreamModelAdmin(ModelAdmin): list_display = ('title', 'ident') modeladmin_register(StreamModelAdmin) + +class EntryModelAdmin(ExportModelAdminMixin, ModelAdmin): + model = Entry + index_template_name = 'templates/modeladmin/feedler/entry/index.html' + +modeladmin_register(EntryModelAdmin) diff --git a/modeladmin/feedler/entry/index.html b/modeladmin/feedler/entry/index.html deleted file mode 100644 index 09a3d40..0000000 --- a/modeladmin/feedler/entry/index.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "modeladmin/index.html %} - -{% block header_extra %} - {% if user_can_create %} -
-
- {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} -
-
- {% endif %} -{% endblock %} diff --git a/publichealth/home/templates/home/photo_gallery.html b/publichealth/home/templates/home/photo_gallery.html new file mode 100644 index 0000000..9b1f1a9 --- /dev/null +++ b/publichealth/home/templates/home/photo_gallery.html @@ -0,0 +1,25 @@ +{% load compress static wagtailcore_tags wagtailimages_tags %} + + + + + From 8912ceabe65f695b16ab295d25d2bc7a30f1d0e5 Mon Sep 17 00:00:00 2001 From: khashashin Date: Mon, 18 Sep 2017 15:23:19 +0200 Subject: [PATCH 04/17] This reverts commit ec02c97c606b1155cd606e1243c762ad21100b3c. --- feedler/models/admin.py | 133 ------------------ feedler/models/models.py | 112 +++++++++++++++ .../modeladmin/feedler/entry/index.html | 7 - feedler/wagtail_hooks.py | 7 - modeladmin/feedler/entry/index.html | 11 ++ .../home/templates/home/photo_gallery.html | 25 ---- 6 files changed, 123 insertions(+), 172 deletions(-) delete mode 100644 feedler/templates/modeladmin/feedler/entry/index.html create mode 100644 modeladmin/feedler/entry/index.html delete mode 100644 publichealth/home/templates/home/photo_gallery.html diff --git a/feedler/models/admin.py b/feedler/models/admin.py index 5d1cd1d..c6b6587 100644 --- a/feedler/models/admin.py +++ b/feedler/models/admin.py @@ -1,11 +1,4 @@ # -*- coding: utf-8 -*- -from django.contrib.auth.decorators import login_required -from django.core.urlresolvers import reverse -from django.utils.decorators import method_decorator -from django.utils.functional import cached_property -from django.utils.translation import ugettext as _ -from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper -from wagtail.contrib.modeladmin.views import IndexView import requests, json, codecs @@ -72,129 +65,3 @@ def handle_save_settings(sender, instance, *args, **kwargs): entry = feedparser.parse(entry, raw_entry, stream) # Persist resulting object entry.save() - -class ExportButtonHelper(ButtonHelper): - """ - This helper constructs all the necessary attributes to create a button. - - There is a lot of boilerplate just for the classnames to be right :( - """ - - export_button_classnames = ['icon', 'icon-download'] - - def export_button(self, classnames_add=None, classnames_exclude=None): - if classnames_add is None: - classnames_add = [] - if classnames_exclude is None: - classnames_exclude = [] - - classnames = self.export_button_classnames + classnames_add - cn = self.finalise_classname(classnames, classnames_exclude) - text = _('Export {}'.format(self.verbose_name_plural.title())) - - return { - 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), - 'label': text, - 'classname': cn, - 'title': text, - } - - -class ExportAdminURLHelper(AdminURLHelper): - """ - This helper constructs the different urls. - - This is mostly just to overwrite the default behaviour - which consider any action other than 'create', 'choose_parent' and 'index' - as `object specific` and will try to add the object PK to the url - which is not what we want for the `export` option. - - In addition, it appends the filters to the action. - """ - - non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') - - 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 ExportView(IndexView): - """ - A Class Based View which will generate - """ - - def export_csv(self): - data = self.queryset.all() - data_headings = [field.verbose_name for field - in EventRegistration._meta.get_fields()] - - # return a CSV instead - response = HttpResponse(content_type='text/csv; charset=utf-8') - response['Content-Disposition'] = 'attachment;filename=' + \ - 'registrations.csv' - - # Prevents UnicodeEncodeError for labels with non-ansi symbols - data_headings = [smart_str(label) for label in data_headings] - - writer = csv.writer(response) - writer.writerow(data_headings) - for reg in data: - data_row = [] - data_row.extend([ - reg.title, reg.example_field2, reg.example_field3 - ]) - writer.writerow(data_row) - - return response - - - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - super().dispatch(request, *args, **kwargs) - return self.export_csv() - - -class ExportModelAdminMixin(object): - """ - A mixin to add to your model admin which hooks the different helpers, the view - and register the new urls. - """ - - button_helper_class = ExportButtonHelper - url_helper_class = ExportAdminURLHelper - - export_view_class = ExportView - - def get_admin_urls_for_registration(self): - urls = super().get_admin_urls_for_registration() - urls += ( - url( - self.url_helper.get_action_url_pattern('export'), - self.export_view, - name=self.url_helper.get_action_url_name('export') - ), - ) - - return urls - - def export_view(self, request): - kwargs = {'model_admin': self} - view_class = self.export_view_class - return view_class.as_view(**kwargs)(request) diff --git a/feedler/models/models.py b/feedler/models/models.py index 0aeb4cc..2ed0395 100644 --- a/feedler/models/models.py +++ b/feedler/models/models.py @@ -54,6 +54,118 @@ class Entry(models.Model): class Meta: verbose_name_plural = 'Entries' +# Button +class ExportButtonHelper(ButtonHelper): + """ + This helper constructs all the necessary attributes to create a button. + + There is a lot of boilerplate just for the classnames to be right :( + """ + + export_button_classnames = ['icon', 'icon-download'] + + def export_button(self, classnames_add=None, classnames_exclude=None): + if classnames_add is None: + classnames_add = [] + if classnames_exclude is None: + classnames_exclude = [] + + classnames = self.export_button_classnames + classnames_add + cn = self.finalise_classname(classnames, classnames_exclude) + text = _('Export {}'.format(self.verbose_name_plural.title())) + + return { + 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), + 'label': text, + 'classname': cn, + 'title': text, + } + + +class ExportAdminURLHelper(AdminURLHelper): + """ + This helper constructs the different urls. + + This is mostly just to overwrite the default behaviour + which consider any action other than 'create', 'choose_parent' and 'index' + as `object specific` and will try to add the object PK to the url + which is not what we want for the `export` option. + + In addition, it appends the filters to the action. + """ + + non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') + + 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 ExportView(IndexView): + """ + A Class Based View which will generate + """ + + 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) + return self.export_csv() + + +class ExportModelAdminMixin(object): + """ + A mixin to add to your model admin which hooks the different helpers, the view + and register the new urls. + """ + + button_helper_class = ExportButtonHelper + url_helper_class = ExportAdminURLHelper + + export_view_class = ExportView + + def get_admin_urls_for_registration(self): + urls = super().get_admin_urls_for_registration() + urls += ( + url( + self.url_helper.get_action_url_pattern('export'), + self.export_view, + name=self.url_helper.get_action_url_name('export') + ), + ) + + return urls + + def export_view(self, request): + kwargs = {'model_admin': self} + view_class = self.export_view_class + return view_class.as_view(**kwargs)(request) + + +class MenuModelAdmin(ExportModelAdminMixin, ModelAdmin): + model = Entry + class FeedPage(Page): intro = RichTextField(default='', blank=True) stream = models.ForeignKey(Stream, on_delete=models.PROTECT, diff --git a/feedler/templates/modeladmin/feedler/entry/index.html b/feedler/templates/modeladmin/feedler/entry/index.html deleted file mode 100644 index eed6636..0000000 --- a/feedler/templates/modeladmin/feedler/entry/index.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "modeladmin/index.html %} - -{% block header_extra %} - My New Button - {{ block.super }}{% comment %}Display the original buttons {% endcomment %} - {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} -{% endblock %} diff --git a/feedler/wagtail_hooks.py b/feedler/wagtail_hooks.py index afd2b14..ed8abd5 100644 --- a/feedler/wagtail_hooks.py +++ b/feedler/wagtail_hooks.py @@ -4,7 +4,6 @@ from wagtail.contrib.modeladmin.options import ( ModelAdmin, modeladmin_register) from .models import Entry, Stream -from .models.admin import ExportModelAdminMixin class EntryModelAdmin(ModelAdmin): model = Entry @@ -27,9 +26,3 @@ class StreamModelAdmin(ModelAdmin): list_display = ('title', 'ident') modeladmin_register(StreamModelAdmin) - -class EntryModelAdmin(ExportModelAdminMixin, ModelAdmin): - model = Entry - index_template_name = 'templates/modeladmin/feedler/entry/index.html' - -modeladmin_register(EntryModelAdmin) diff --git a/modeladmin/feedler/entry/index.html b/modeladmin/feedler/entry/index.html new file mode 100644 index 0000000..09a3d40 --- /dev/null +++ b/modeladmin/feedler/entry/index.html @@ -0,0 +1,11 @@ +{% extends "modeladmin/index.html %} + +{% block header_extra %} + {% if user_can_create %} +
+
+ {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} +
+
+ {% endif %} +{% endblock %} diff --git a/publichealth/home/templates/home/photo_gallery.html b/publichealth/home/templates/home/photo_gallery.html deleted file mode 100644 index 9b1f1a9..0000000 --- a/publichealth/home/templates/home/photo_gallery.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load compress static wagtailcore_tags wagtailimages_tags %} - - - - - From 97b4dac813afd7a6bd7e3483ef8a3657146039e8 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 00:08:15 +0200 Subject: [PATCH 05/17] Token expired exception --- feedler/models/admin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/feedler/models/admin.py b/feedler/models/admin.py index b259777..cabc088 100644 --- a/feedler/models/admin.py +++ b/feedler/models/admin.py @@ -10,6 +10,7 @@ from django.dispatch import receiver from django.core.mail import send_mail from wagtail.contrib.settings.models import BaseSetting, register_setting +from wagtail.wagtailadmin import messages from .models import Entry, Stream import feedler.feedparser as feedparser @@ -52,7 +53,13 @@ def handle_save_settings(sender, instance, *args, **kwargs): } contents = requests.get(url, headers=headers).json() if 'errorMessage' in contents: - raise PermissionError(contents['errorMessage']) + # Usually this is a token expired + if 'token expired' in contents['errorMessage']: + # TODO: request new token + pass + logger.error(contents['errorMessage']) + messages.error(sender, "Failed to fetch items: %s" % contents['errorMessage']) + return for raw_entry in contents['items']: eid = raw_entry['id'] # Create or update data From 4f2562cbc30922a8460898ded611ff03b7e2f47d Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 15:40:24 +0200 Subject: [PATCH 06/17] Token refresh --- feedler/migrations/0007_auto_20171013_1515.py | 33 +++++++++ feedler/models/admin.py | 60 ++------------- feedler/refresh.py | 73 +++++++++++++++++++ publichealth/settings/production.py | 6 ++ 4 files changed, 118 insertions(+), 54 deletions(-) create mode 100644 feedler/migrations/0007_auto_20171013_1515.py create mode 100644 feedler/refresh.py diff --git a/feedler/migrations/0007_auto_20171013_1515.py b/feedler/migrations/0007_auto_20171013_1515.py new file mode 100644 index 0000000..458fffd --- /dev/null +++ b/feedler/migrations/0007_auto_20171013_1515.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-13 13:15 +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='token', + field=models.CharField(blank=True, help_text='Access Token from developer.feedly.com', max_length=255), + ), + ] diff --git a/feedler/models/admin.py b/feedler/models/admin.py index cabc088..43c5972 100644 --- a/feedler/models/admin.py +++ b/feedler/models/admin.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import requests, json, codecs - from django.contrib import admin from django.db import models @@ -10,66 +8,20 @@ from django.dispatch import receiver from django.core.mail import send_mail from wagtail.contrib.settings.models import BaseSetting, register_setting -from wagtail.wagtailadmin import messages -from .models import Entry, Stream -import feedler.feedparser as feedparser +from .models import Stream -import logging -logger = logging.getLogger('feedler') - -# Feedly integration module +from feedler.refresh import refresh_streams @register_setting class FeedlySettings(BaseSetting): - feedly_auth = models.TextField( - 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, + streams = models.ManyToManyField(Stream, help_text='Which streams to update') + token = models.CharField(max_length=255, blank=True, + help_text='Access Token from developer.feedly.com') class Meta: 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): - 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: - # Usually this is a token expired - if 'token expired' in contents['errorMessage']: - # TODO: request new token - pass - logger.error(contents['errorMessage']) - messages.error(sender, "Failed to fetch items: %s" % contents['errorMessage']) - return - 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() + if instance.token: refresh_streams(instance) diff --git a/feedler/refresh.py b/feedler/refresh.py new file mode 100644 index 0000000..e361358 --- /dev/null +++ b/feedler/refresh.py @@ -0,0 +1,73 @@ +# -*- 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.warn("Refreshing all streams") + for stream in settings.streams.all(): + if not refresh_stream(stream, settings): + return False + return True + +def get_headers(settings): + return { + 'Authorization': 'OAuth ' + settings.token + } + +def refresh_token(settings): + # Request a new token + url = API_TOKENS + logger.warn("Refreshing Feedly access token") + payload = { + 'refresh_token': settings.token, + 'client_id': 'feedlydev', + 'client_secret': 'feedlydev', + 'grant_type': 'refresh_token' + } + contents = requests.get(url, data=payload, headers=get_headers(settings)).json() + if not 'access_token' in contents or not contents['access_token']: + logger.error("Access token could not be refreshed.") + 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.warn("Processing stream %s" % stream.title) + url = API_STREAMS + stream.ident + contents = requests.get(url, headers=get_headers(settings)).json() + if 'errorMessage' in contents: + # Usually this is a token expired + if 'token expired' in contents['errorMessage'] or 'unauthorized' in 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("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() diff --git a/publichealth/settings/production.py b/publichealth/settings/production.py index eb23ad7..7b753de 100644 --- a/publichealth/settings/production.py +++ b/publichealth/settings/production.py @@ -156,6 +156,12 @@ LOGGING = { } }, 'loggers': { + 'feedler': { + 'handlers': [], + 'level': 'INFO', + 'propagate': False, + 'formatter': 'verbose', + }, 'publichealth': { 'handlers': [], 'level': 'INFO', From 1d7545bbe239cea2826bb6f67d0cb07f516f9967 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 23:22:34 +0200 Subject: [PATCH 07/17] Datalets settings --- .../migrations/0021_auto_20171013_2321.py | 34 +++++++++++++++++++ publichealth/home/models/admin.py | 21 ++++++------ 2 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 publichealth/home/migrations/0021_auto_20171013_2321.py diff --git a/publichealth/home/migrations/0021_auto_20171013_2321.py b/publichealth/home/migrations/0021_auto_20171013_2321.py new file mode 100644 index 0000000..37f3aaa --- /dev/null +++ b/publichealth/home/migrations/0021_auto_20171013_2321.py @@ -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'), + ), + ] diff --git a/publichealth/home/models/admin.py b/publichealth/home/models/admin.py index 8cf4eec..8d58946 100644 --- a/publichealth/home/models/admin.py +++ b/publichealth/home/models/admin.py @@ -11,21 +11,22 @@ from wagtail.contrib.settings.models import BaseSetting, register_setting @register_setting class DataletsSettings(BaseSetting): - feedback_question = models.TextField( - help_text='Send us a question', blank=True) - feedback_status = models.IntegerField( + feedback_question = models.TextField(verbose_name='Name', + help_text='Who should we reply to for questions?', blank=True) + feedback_status = models.IntegerField(verbose_name='Rating', choices=( - (1, ':-('), - (2, ':-|'), - (3, ':-)'), - (4, ':-D'), + (1, u'★'), + (2, u'★'*2), + (3, u'★'*3), + (4, u'★'*4), + (5, u'★'*5), ), blank=True, null=True, help_text='How are you enjoying Wagtail?' ) - feedback_comment = models.TextField( - help_text='Any general feedback', blank=True) + feedback_comment = models.TextField(verbose_name='Comments..', + help_text='Any questions or general feedback', blank=True) class Meta: - verbose_name = 'Get support' + verbose_name = 'Datalets' @receiver(pre_save, sender=DataletsSettings) def handle_save_settings(sender, instance, *args, **kwargs): From 362c7fca4d1a2d915b1e1d4255288627c3d7f197 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 23:45:55 +0200 Subject: [PATCH 08/17] Auto refresh token --- feedler/migrations/0008_auto_20171013_2342.py | 30 +++++++++++++++++++ feedler/models/admin.py | 6 ++-- feedler/models/models.py | 3 +- feedler/refresh.py | 26 ++++++++-------- 4 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 feedler/migrations/0008_auto_20171013_2342.py diff --git a/feedler/migrations/0008_auto_20171013_2342.py b/feedler/migrations/0008_auto_20171013_2342.py new file mode 100644 index 0000000..f958257 --- /dev/null +++ b/feedler/migrations/0008_auto_20171013_2342.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-13 21:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedler', '0007_auto_20171013_1515'), + ] + + operations = [ + 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.AlterField( + 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), + ), + ] diff --git a/feedler/models/admin.py b/feedler/models/admin.py index 43c5972..8cf5429 100644 --- a/feedler/models/admin.py +++ b/feedler/models/admin.py @@ -17,8 +17,10 @@ from feedler.refresh import refresh_streams class FeedlySettings(BaseSetting): streams = models.ManyToManyField(Stream, help_text='Which streams to update') - token = models.CharField(max_length=255, blank=True, - help_text='Access Token from developer.feedly.com') + 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: verbose_name = 'Feedly' diff --git a/feedler/models/models.py b/feedler/models/models.py index a1f9a82..ae79926 100644 --- a/feedler/models/models.py +++ b/feedler/models/models.py @@ -10,7 +10,8 @@ from wagtail.wagtailcore.fields import RichTextField class Stream(models.Model): 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): return self.title diff --git a/feedler/refresh.py b/feedler/refresh.py index e361358..abd0181 100644 --- a/feedler/refresh.py +++ b/feedler/refresh.py @@ -4,6 +4,7 @@ import requests, json, codecs import logging logger = logging.getLogger('feedler') +logger.setLevel(logging.DEBUG) from .models import Entry from feedler import feedparser @@ -14,30 +15,29 @@ API_TOKENS = API_BASEURL + 'auth/token' def refresh_streams(settings): # Iterate through all saved streams - logger.warn("Refreshing all streams") + logger.debug("Refreshing all streams") for stream in settings.streams.all(): if not refresh_stream(stream, settings): return False return True -def get_headers(settings): - return { - 'Authorization': 'OAuth ' + settings.token - } - def refresh_token(settings): # Request a new token url = API_TOKENS - logger.warn("Refreshing Feedly access token") + if not settings.refresh: + logger.warn("No Refresh token available") + return False + logger.info("Refreshing Feedly access token") payload = { - 'refresh_token': settings.token, + 'refresh_token': settings.refresh, 'client_id': 'feedlydev', 'client_secret': 'feedlydev', 'grant_type': 'refresh_token' } - contents = requests.get(url, data=payload, headers=get_headers(settings)).json() + contents = requests.post(url, data=payload).json() if not 'access_token' in contents or not contents['access_token']: - logger.error("Access token could not be refreshed.") + logger.warn("Access token could not be refreshed.") + logger.debug(contents) return False settings.token = contents['access_token'] settings.save() @@ -45,12 +45,14 @@ def refresh_token(settings): def refresh_stream(stream, settings, retry=False): # Start a request to download the feed for a particular stream - logger.warn("Processing stream %s" % stream.title) + logger.info("Processing stream %s" % stream.title) url = API_STREAMS + stream.ident - contents = requests.get(url, headers=get_headers(settings)).json() + 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): From fb6c901299be72bd5a3c873eda6e73b4a7e92cc5 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 23:46:00 +0200 Subject: [PATCH 09/17] Update test requirements --- requirements-test.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index be7e033..310cf26 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -pytest==3.0.3 -pytest-splinter==1.7.6 -tox==2.3.1 +pytest>=3.0.3 +pytest-splinter>=1.8.5 +tox>=2.9.1 +django-debug-toolbar>=1.8 From e9a7bd26e70015f3b563aa4290fb3472da0c6d48 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 23:46:13 +0200 Subject: [PATCH 10/17] Skip existing entries --- feedler/refresh.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/feedler/refresh.py b/feedler/refresh.py index abd0181..da1a5a5 100644 --- a/feedler/refresh.py +++ b/feedler/refresh.py @@ -65,11 +65,11 @@ def refresh_stream(stream, settings, retry=False): # Create or update data try: entry = Entry.objects.get(entry_id=eid) - logger.info("Updating entry '%s'" % 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() + # Parse the Feedly object + entry = feedparser.parse(entry, raw_entry, stream) + # Persist resulting object + entry.save() From 7dcd330f9da90acb6f6e8701681f9483d8061511 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Fri, 13 Oct 2017 23:47:24 +0200 Subject: [PATCH 11/17] Logging level --- feedler/refresh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/feedler/refresh.py b/feedler/refresh.py index da1a5a5..ccf65a6 100644 --- a/feedler/refresh.py +++ b/feedler/refresh.py @@ -4,7 +4,6 @@ import requests, json, codecs import logging logger = logging.getLogger('feedler') -logger.setLevel(logging.DEBUG) from .models import Entry from feedler import feedparser From a11d8a27965a92c6ffe49b58df44548b5093c1b5 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Sat, 14 Oct 2017 23:31:21 +0200 Subject: [PATCH 12/17] Showing extra button --- feedler/admin.py | 126 ++++++++++++++++++ feedler/models/models.py | 120 ----------------- .../modeladmin}/feedler/entry/index.html | 3 +- feedler/wagtail_hooks.py | 3 +- 4 files changed, 130 insertions(+), 122 deletions(-) create mode 100644 feedler/admin.py rename {modeladmin => feedler/templates/modeladmin}/feedler/entry/index.html (71%) diff --git a/feedler/admin.py b/feedler/admin.py new file mode 100644 index 0000000..ed805ab --- /dev/null +++ b/feedler/admin.py @@ -0,0 +1,126 @@ +# -*- 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 wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper +from wagtail.contrib.modeladmin.options import ModelAdmin +from wagtail.contrib.modeladmin.views import IndexView + +from feedler.models import Entry + +class ExportButtonHelper(ButtonHelper): + """ + This helper constructs all the necessary attributes to create a button. + + There is a lot of boilerplate just for the classnames to be right :( + """ + + export_button_classnames = ['icon', 'icon-download'] + + def export_button(self, classnames_add=None, classnames_exclude=None): + if classnames_add is None: + classnames_add = [] + if classnames_exclude is None: + classnames_exclude = [] + + classnames = self.export_button_classnames + classnames_add + cn = self.finalise_classname(classnames, classnames_exclude) + text = _('Export {}'.format(self.verbose_name_plural.title())) + + return { + 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), + 'label': text, + 'classname': cn, + 'title': text, + } + + +class ExportAdminURLHelper(AdminURLHelper): + """ + This helper constructs the different urls. + + This is mostly just to overwrite the default behaviour + which consider any action other than 'create', 'choose_parent' and 'index' + as `object specific` and will try to add the object PK to the url + which is not what we want for the `export` option. + + In addition, it appends the filters to the action. + """ + + non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') + + 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 ExportView(IndexView): + """ + A Class Based View which will generate + """ + + 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) + return self.export_csv() + + +class ExportModelAdminMixin(object): + """ + A mixin to add to your model admin which hooks the different helpers, the view + and register the new urls. + """ + + button_helper_class = ExportButtonHelper + url_helper_class = ExportAdminURLHelper + + export_view_class = ExportView + + def get_admin_urls_for_registration(self): + urls = super().get_admin_urls_for_registration() + urls += ( + url( + self.url_helper.get_action_url_pattern('export'), + self.export_view, + name=self.url_helper.get_action_url_name('export') + ), + ) + + return urls + + def export_view(self, request): + kwargs = {'model_admin': self} + view_class = self.export_view_class + return view_class.as_view(**kwargs)(request) + + +class MenuModelAdmin(ExportModelAdminMixin, ModelAdmin): + model = Entry diff --git a/feedler/models/models.py b/feedler/models/models.py index 77e1190..ae79926 100644 --- a/feedler/models/models.py +++ b/feedler/models/models.py @@ -1,14 +1,6 @@ # -*- 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.core.urlresolvers import reverse -from django.utils.functional import cached_property -from django.utils.translation import ugettext as _ -from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper -from wagtail.contrib.modeladmin.options import ModelAdmin -from wagtail.contrib.modeladmin.views import IndexView from django.utils import translation from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger @@ -60,118 +52,6 @@ class Entry(models.Model): class Meta: verbose_name_plural = 'Entries' -# Button -class ExportButtonHelper(ButtonHelper): - """ - This helper constructs all the necessary attributes to create a button. - - There is a lot of boilerplate just for the classnames to be right :( - """ - - export_button_classnames = ['icon', 'icon-download'] - - def export_button(self, classnames_add=None, classnames_exclude=None): - if classnames_add is None: - classnames_add = [] - if classnames_exclude is None: - classnames_exclude = [] - - classnames = self.export_button_classnames + classnames_add - cn = self.finalise_classname(classnames, classnames_exclude) - text = _('Export {}'.format(self.verbose_name_plural.title())) - - return { - 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), - 'label': text, - 'classname': cn, - 'title': text, - } - - -class ExportAdminURLHelper(AdminURLHelper): - """ - This helper constructs the different urls. - - This is mostly just to overwrite the default behaviour - which consider any action other than 'create', 'choose_parent' and 'index' - as `object specific` and will try to add the object PK to the url - which is not what we want for the `export` option. - - In addition, it appends the filters to the action. - """ - - non_object_specific_actions = ('create', 'choose_parent', 'index', 'export') - - 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 ExportView(IndexView): - """ - A Class Based View which will generate - """ - - 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) - return self.export_csv() - - -class ExportModelAdminMixin(object): - """ - A mixin to add to your model admin which hooks the different helpers, the view - and register the new urls. - """ - - button_helper_class = ExportButtonHelper - url_helper_class = ExportAdminURLHelper - - export_view_class = ExportView - - def get_admin_urls_for_registration(self): - urls = super().get_admin_urls_for_registration() - urls += ( - url( - self.url_helper.get_action_url_pattern('export'), - self.export_view, - name=self.url_helper.get_action_url_name('export') - ), - ) - - return urls - - def export_view(self, request): - kwargs = {'model_admin': self} - view_class = self.export_view_class - return view_class.as_view(**kwargs)(request) - - -class MenuModelAdmin(ExportModelAdminMixin, ModelAdmin): - model = Entry - class FeedPage(Page): intro = RichTextField(default='', blank=True) stream = models.ForeignKey(Stream, on_delete=models.PROTECT, diff --git a/modeladmin/feedler/entry/index.html b/feedler/templates/modeladmin/feedler/entry/index.html similarity index 71% rename from modeladmin/feedler/entry/index.html rename to feedler/templates/modeladmin/feedler/entry/index.html index 09a3d40..f86f8eb 100644 --- a/modeladmin/feedler/entry/index.html +++ b/feedler/templates/modeladmin/feedler/entry/index.html @@ -1,4 +1,4 @@ -{% extends "modeladmin/index.html %} +{% extends "modeladmin/index.html" %} {% block header_extra %} {% if user_can_create %} @@ -8,4 +8,5 @@ {% endif %} + {{ block.super }}{% comment %}Show original buttons{% endcomment %} {% endblock %} diff --git a/feedler/wagtail_hooks.py b/feedler/wagtail_hooks.py index 971c2c7..79052ed 100644 --- a/feedler/wagtail_hooks.py +++ b/feedler/wagtail_hooks.py @@ -3,9 +3,10 @@ from wagtail.contrib.modeladmin.options import ( ModelAdmin, modeladmin_register) +from .admin import ExportModelAdminMixin from .models import Entry, Stream -class EntryModelAdmin(ModelAdmin): +class EntryModelAdmin(ExportModelAdminMixin, ModelAdmin): model = Entry menu_icon = 'date' menu_order = 200 From 65944deaf59254ca25d75c575ba44af88c6b4dc3 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Sat, 14 Oct 2017 23:57:39 +0200 Subject: [PATCH 13/17] Sync entries button --- feedler/admin.py | 102 +++++++----------- feedler/refresh.py | 1 + .../modeladmin/feedler/entry/index.html | 6 +- feedler/wagtail_hooks.py | 4 +- 4 files changed, 43 insertions(+), 70 deletions(-) diff --git a/feedler/admin.py b/feedler/admin.py index ed805ab..391117d 100644 --- a/feedler/admin.py +++ b/feedler/admin.py @@ -7,120 +7,92 @@ 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 ExportButtonHelper(ButtonHelper): +class RefreshButtonHelper(ButtonHelper): """ - This helper constructs all the necessary attributes to create a button. - - There is a lot of boilerplate just for the classnames to be right :( + This helper constructs a refresh button """ - - export_button_classnames = ['icon', 'icon-download'] - - def export_button(self, classnames_add=None, classnames_exclude=None): - if classnames_add is None: - classnames_add = [] - if classnames_exclude is None: - classnames_exclude = [] - - classnames = self.export_button_classnames + classnames_add + 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 = _('Export {}'.format(self.verbose_name_plural.title())) - + text = _('Sync {}'.format(self.verbose_name_plural.title())) return { - 'url': self.url_helper.get_action_url('export', query_params=self.request.GET), - 'label': text, - 'classname': cn, - 'title': text, + 'url': self.url_helper.get_action_url('refresh', query_params=self.request.GET), + 'label': text, 'classname': cn, 'title': text, } - -class ExportAdminURLHelper(AdminURLHelper): +class RefreshAdminURLHelper(AdminURLHelper): """ - This helper constructs the different urls. - - This is mostly just to overwrite the default behaviour - which consider any action other than 'create', 'choose_parent' and 'index' - as `object specific` and will try to add the object PK to the url - which is not what we want for the `export` option. - - In addition, it appends the filters to the action. + 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', 'export') - + 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 ExportView(IndexView): +class RefreshView(IndexView): """ - A Class Based View which will generate + A Class Based View which will handle the button click """ - - def export_csv(self): - data = self.queryset.all() - response = ... - return response - - + # 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) - return self.export_csv() + 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 ExportModelAdminMixin(object): +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 = ExportButtonHelper - url_helper_class = ExportAdminURLHelper - - export_view_class = ExportView + 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('export'), - self.export_view, - name=self.url_helper.get_action_url_name('export') + self.url_helper.get_action_url_pattern('refresh'), + self.refresh_view, + name=self.url_helper.get_action_url_name('refresh') ), ) - return urls - def export_view(self, request): + def refresh_view(self, request): kwargs = {'model_admin': self} - view_class = self.export_view_class + view_class = self.view_class return view_class.as_view(**kwargs)(request) - - -class MenuModelAdmin(ExportModelAdminMixin, ModelAdmin): - model = Entry diff --git a/feedler/refresh.py b/feedler/refresh.py index ccf65a6..f5cc3df 100644 --- a/feedler/refresh.py +++ b/feedler/refresh.py @@ -72,3 +72,4 @@ def refresh_stream(stream, settings, retry=False): entry = feedparser.parse(entry, raw_entry, stream) # Persist resulting object entry.save() + return True diff --git a/feedler/templates/modeladmin/feedler/entry/index.html b/feedler/templates/modeladmin/feedler/entry/index.html index f86f8eb..234e82f 100644 --- a/feedler/templates/modeladmin/feedler/entry/index.html +++ b/feedler/templates/modeladmin/feedler/entry/index.html @@ -3,9 +3,9 @@ {% block header_extra %} {% if user_can_create %}
-
- {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} -
+
+ {% include 'modeladmin/includes/button.html' with button=view.button_helper.refresh_button %} +
{% endif %} {{ block.super }}{% comment %}Show original buttons{% endcomment %} diff --git a/feedler/wagtail_hooks.py b/feedler/wagtail_hooks.py index 79052ed..b40200f 100644 --- a/feedler/wagtail_hooks.py +++ b/feedler/wagtail_hooks.py @@ -3,10 +3,10 @@ from wagtail.contrib.modeladmin.options import ( ModelAdmin, modeladmin_register) -from .admin import ExportModelAdminMixin +from .admin import EntryModelAdminMixin from .models import Entry, Stream -class EntryModelAdmin(ExportModelAdminMixin, ModelAdmin): +class EntryModelAdmin(EntryModelAdminMixin, ModelAdmin): model = Entry menu_icon = 'date' menu_order = 200 From f9c1e08dd1536afbcf2024de52ac18bc05e56f00 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Sat, 14 Oct 2017 23:58:50 +0200 Subject: [PATCH 14/17] No sync on save settings --- feedler/models/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feedler/models/admin.py b/feedler/models/admin.py index 8cf5429..275ba31 100644 --- a/feedler/models/admin.py +++ b/feedler/models/admin.py @@ -24,6 +24,6 @@ class FeedlySettings(BaseSetting): class Meta: verbose_name = 'Feedly' -@receiver(pre_save, sender=FeedlySettings) -def handle_save_settings(sender, instance, *args, **kwargs): - if instance.token: refresh_streams(instance) +# @receiver(pre_save, sender=FeedlySettings) +# def handle_save_settings(sender, instance, *args, **kwargs): +# if instance.token: refresh_streams(instance) From 52360ae1052c6b47be1cfd6f72fbc3dea19e892b Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Mon, 16 Oct 2017 10:59:59 +0200 Subject: [PATCH 15/17] Cleaned up migrations, tweak settings --- feedler/migrations/0007_auto_20171013_1515.py | 33 ------------------- ...013_2342.py => 0007_auto_20171016_1059.py} | 19 +++++++++-- feedler/wagtail_hooks.py | 2 +- publichealth/home/models/admin.py | 3 +- publichealth/settings/dev.py | 4 +++ 5 files changed, 23 insertions(+), 38 deletions(-) delete mode 100644 feedler/migrations/0007_auto_20171013_1515.py rename feedler/migrations/{0008_auto_20171013_2342.py => 0007_auto_20171016_1059.py} (62%) diff --git a/feedler/migrations/0007_auto_20171013_1515.py b/feedler/migrations/0007_auto_20171013_1515.py deleted file mode 100644 index 458fffd..0000000 --- a/feedler/migrations/0007_auto_20171013_1515.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-13 13:15 -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='token', - field=models.CharField(blank=True, help_text='Access Token from developer.feedly.com', max_length=255), - ), - ] diff --git a/feedler/migrations/0008_auto_20171013_2342.py b/feedler/migrations/0007_auto_20171016_1059.py similarity index 62% rename from feedler/migrations/0008_auto_20171013_2342.py rename to feedler/migrations/0007_auto_20171016_1059.py index f958257..fa988ac 100644 --- a/feedler/migrations/0008_auto_20171013_2342.py +++ b/feedler/migrations/0007_auto_20171016_1059.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-10-13 21:42 +# Generated by Django 1.11.6 on 2017-10-16 08:59 from __future__ import unicode_literals from django.db import migrations, models @@ -8,16 +8,29 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('feedler', '0007_auto_20171013_1515'), + ('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.AlterField( + 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), diff --git a/feedler/wagtail_hooks.py b/feedler/wagtail_hooks.py index b40200f..0129417 100644 --- a/feedler/wagtail_hooks.py +++ b/feedler/wagtail_hooks.py @@ -21,7 +21,7 @@ modeladmin_register(EntryModelAdmin) class StreamModelAdmin(ModelAdmin): model = Stream menu_icon = 'date' - menu_order = 1000 + menu_order = 900 add_to_settings_menu = True exclude_from_explorer = True list_display = ('title', 'ident') diff --git a/publichealth/home/models/admin.py b/publichealth/home/models/admin.py index 8d58946..e250a41 100644 --- a/publichealth/home/models/admin.py +++ b/publichealth/home/models/admin.py @@ -9,7 +9,7 @@ from wagtail.contrib.settings.models import BaseSetting, register_setting # A simple feedback module built into the site admin -@register_setting +@register_setting(icon='help') class DataletsSettings(BaseSetting): feedback_question = models.TextField(verbose_name='Name', help_text='Who should we reply to for questions?', blank=True) @@ -42,3 +42,4 @@ def handle_save_settings(sender, instance, *args, **kwargs): instance.feedback_status = None instance.feedback_question = "" instance.feedback_comment = "" + instance.save() diff --git a/publichealth/settings/dev.py b/publichealth/settings/dev.py index a22cce2..e855f5d 100644 --- a/publichealth/settings/dev.py +++ b/publichealth/settings/dev.py @@ -21,6 +21,10 @@ DATABASES = { } } +INSTALLED_APPS = INSTALLED_APPS + [ + 'wagtail.contrib.wagtailstyleguide', +] + try: from .local import * except ImportError: From c45a30a868f914905cde9bb7664e26989b043c46 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Mon, 16 Oct 2017 11:18:50 +0200 Subject: [PATCH 16/17] Makefile update --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3914c67..2938b3f 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,8 @@ logs: docker-compose logs -f --tail=500 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 contenttypes -e sessions -e feedler -e auth.Permission -e wagtailcore.groupcollectionpermission > ~/publichealth.home.json zip ~/publichealth.home.json.`date +"%d%m%Y-%H%M"`.zip ~/publichealth.home.json rm ~/publichealth.home.json From 8fe9b6812a92af2b1082c2d44b2511392154f3f3 Mon Sep 17 00:00:00 2001 From: Oleg Lavrovsky Date: Mon, 16 Oct 2017 11:43:24 +0200 Subject: [PATCH 17/17] Makefile backup --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2938b3f..35c358a 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ logs: backup: docker-compose start postgres - docker-compose exec web ./manage.py dumpdata --natural-foreign -e contenttypes -e sessions -e feedler -e auth.Permission -e wagtailcore.groupcollectionpermission > ~/publichealth.home.json + 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 rm ~/publichealth.home.json