Repository: netbox-community/netbox

Issue:
Create an RSS feed dashboard widget
### NetBox version

v3.5

### Feature type

New functionality

### Proposed functionality

Create a new dashboard widget for NetBox v3.5 (see #9416) which can embed an RSS feed on a user's dashboard.

### Use case

This can be configured to display the most recent posts from https://netbox.dev, a vendor blog, vulnerability/EOS notifications, etc.

### Database changes

Likely no SQL changes, although we'll want to cache the feed content in Redis.

### External dependencies

_No response_


Discussion:

Resolving patch:
diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py
--- 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 @@
     '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 ConfigForm(forms.Form):
 
     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 @@ def form_data(self):
 
 @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 @@ def render(self, request):
         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
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -686,7 +686,7 @@ def get(self, request):
         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,


Test patch: