|
|
@@ -1,7 +1,11 @@
|
|
|
import uuid
|
|
|
+from functools import cached_property
|
|
|
+from hashlib import sha256
|
|
|
|
|
|
+import feedparser
|
|
|
from django import forms
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
+from django.core.cache import cache
|
|
|
from django.template.loader import render_to_string
|
|
|
from django.utils.translation import gettext as _
|
|
|
|
|
|
@@ -15,6 +19,7 @@ __all__ = (
|
|
|
'NoteWidget',
|
|
|
'ObjectCountsWidget',
|
|
|
'ObjectListWidget',
|
|
|
+ 'RSSFeedWidget',
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -27,6 +32,7 @@ def get_content_type_labels():
|
|
|
|
|
|
class DashboardWidget:
|
|
|
default_title = None
|
|
|
+ default_config = {}
|
|
|
description = None
|
|
|
width = 4
|
|
|
height = 3
|
|
|
@@ -36,7 +42,7 @@ class DashboardWidget:
|
|
|
|
|
|
def __init__(self, id=None, title=None, color=None, config=None, width=None, height=None, x=None, y=None):
|
|
|
self.id = id or str(uuid.uuid4())
|
|
|
- self.config = config or {}
|
|
|
+ self.config = config or self.default_config
|
|
|
self.title = title or self.default_title
|
|
|
self.color = color
|
|
|
if width:
|
|
|
@@ -72,6 +78,7 @@ class DashboardWidget:
|
|
|
|
|
|
@register_widget
|
|
|
class NoteWidget(DashboardWidget):
|
|
|
+ default_title = _('Note')
|
|
|
description = _('Display some arbitrary custom content. Markdown is supported.')
|
|
|
|
|
|
class ConfigForm(BootstrapMixin, forms.Form):
|
|
|
@@ -128,3 +135,59 @@ class ObjectListWidget(DashboardWidget):
|
|
|
return render_to_string(self.template_name, {
|
|
|
'viewname': viewname,
|
|
|
})
|
|
|
+
|
|
|
+
|
|
|
+@register_widget
|
|
|
+class RSSFeedWidget(DashboardWidget):
|
|
|
+ default_title = _('RSS Feed')
|
|
|
+ default_config = {
|
|
|
+ 'max_entries': 10,
|
|
|
+ 'cache_timeout': 3600, # seconds
|
|
|
+ }
|
|
|
+ description = _('Embed an RSS feed from an external website.')
|
|
|
+ template_name = 'extras/dashboard/widgets/rssfeed.html'
|
|
|
+ width = 6
|
|
|
+ height = 4
|
|
|
+
|
|
|
+ class ConfigForm(BootstrapMixin, forms.Form):
|
|
|
+ feed_url = forms.URLField(
|
|
|
+ label=_('Feed URL')
|
|
|
+ )
|
|
|
+ max_entries = forms.IntegerField(
|
|
|
+ min_value=1,
|
|
|
+ max_value=1000,
|
|
|
+ help_text=_('The maximum number of objects to display')
|
|
|
+ )
|
|
|
+ cache_timeout = forms.IntegerField(
|
|
|
+ min_value=600, # 10 minutes
|
|
|
+ max_value=86400, # 24 hours
|
|
|
+ help_text=_('How long to stored the cached content (in seconds)')
|
|
|
+ )
|
|
|
+
|
|
|
+ def render(self, request):
|
|
|
+ url = self.config['feed_url']
|
|
|
+ feed = self.get_feed()
|
|
|
+
|
|
|
+ return render_to_string(self.template_name, {
|
|
|
+ 'url': url,
|
|
|
+ 'feed': feed,
|
|
|
+ })
|
|
|
+
|
|
|
+ @cached_property
|
|
|
+ def cache_key(self):
|
|
|
+ url = self.config['feed_url']
|
|
|
+ url_checksum = sha256(url.encode('utf-8')).hexdigest()
|
|
|
+ return f'dashboard_rss_{url_checksum}'
|
|
|
+
|
|
|
+ def get_feed(self):
|
|
|
+ # Fetch RSS content from cache
|
|
|
+ if feed_content := cache.get(self.cache_key):
|
|
|
+ feed = feedparser.FeedParserDict(feed_content)
|
|
|
+ else:
|
|
|
+ feed = feedparser.parse(self.config['feed_url'])
|
|
|
+ # Cap number of entries
|
|
|
+ max_entries = self.config.get('max_entries')
|
|
|
+ feed['entries'] = feed['entries'][:max_entries]
|
|
|
+ cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
|
|
|
+
|
|
|
+ return feed
|