diff --git a/base_requirements.txt b/base_requirements.txt index 7292c676b..ac1d1115e 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -66,6 +66,10 @@ djangorestframework # https://github.com/axnsan12/drf-yasg drf-yasg[validation] +# RSS feed parser +# https://github.com/kurtmckee/feedparser +feedparser + # Django wrapper for Graphene (GraphQL support) # https://github.com/graphql-python/graphene-django graphene_django diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 85bee2e19..a73c3d759 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -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 diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 5656050a7..39dff15f6 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -686,7 +686,7 @@ class DashboardWidgetAddView(LoginRequiredMixin, View): widget_form = DashboardWidgetAddForm(initial=initial) widget_name = get_field_value(widget_form, 'widget_class') widget_class = get_widget_class(widget_name) - config_form = widget_class.ConfigForm(prefix='config') + config_form = widget_class.ConfigForm(initial=widget_class.default_config, prefix='config') return render(request, self.template_name, { 'widget_class': widget_class, diff --git a/netbox/templates/extras/dashboard/widget.html b/netbox/templates/extras/dashboard/widget.html index 4ed84f067..1559363d3 100644 --- a/netbox/templates/extras/dashboard/widget.html +++ b/netbox/templates/extras/dashboard/widget.html @@ -30,7 +30,7 @@ {{ widget.title }} {% endif %} -