Просмотр исходного кода

Closes #11826: RSS feed widget (#11976)

* Add feedparser as a dependency

* Introduce RSSFeedWidget

* Clean up widget templates
Jeremy Stretch 2 лет назад
Родитель
Сommit
8bd0a2ef9d

+ 4 - 0
base_requirements.txt

@@ -66,6 +66,10 @@ djangorestframework
 # https://github.com/axnsan12/drf-yasg
 # https://github.com/axnsan12/drf-yasg
 drf-yasg[validation]
 drf-yasg[validation]
 
 
+# RSS feed parser
+# https://github.com/kurtmckee/feedparser
+feedparser
+
 # Django wrapper for Graphene (GraphQL support)
 # Django wrapper for Graphene (GraphQL support)
 # https://github.com/graphql-python/graphene-django
 # https://github.com/graphql-python/graphene-django
 graphene_django
 graphene_django

+ 64 - 1
netbox/extras/dashboard/widgets.py

@@ -1,7 +1,11 @@
 import uuid
 import uuid
+from functools import cached_property
+from hashlib import sha256
 
 
+import feedparser
 from django import forms
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.core.cache import cache
 from django.template.loader import render_to_string
 from django.template.loader import render_to_string
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
@@ -15,6 +19,7 @@ __all__ = (
     'NoteWidget',
     'NoteWidget',
     'ObjectCountsWidget',
     'ObjectCountsWidget',
     'ObjectListWidget',
     'ObjectListWidget',
+    'RSSFeedWidget',
 )
 )
 
 
 
 
@@ -27,6 +32,7 @@ def get_content_type_labels():
 
 
 class DashboardWidget:
 class DashboardWidget:
     default_title = None
     default_title = None
+    default_config = {}
     description = None
     description = None
     width = 4
     width = 4
     height = 3
     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):
     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.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.title = title or self.default_title
         self.color = color
         self.color = color
         if width:
         if width:
@@ -72,6 +78,7 @@ class DashboardWidget:
 
 
 @register_widget
 @register_widget
 class NoteWidget(DashboardWidget):
 class NoteWidget(DashboardWidget):
+    default_title = _('Note')
     description = _('Display some arbitrary custom content. Markdown is supported.')
     description = _('Display some arbitrary custom content. Markdown is supported.')
 
 
     class ConfigForm(BootstrapMixin, forms.Form):
     class ConfigForm(BootstrapMixin, forms.Form):
@@ -128,3 +135,59 @@ class ObjectListWidget(DashboardWidget):
         return render_to_string(self.template_name, {
         return render_to_string(self.template_name, {
             'viewname': viewname,
             '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

+ 1 - 1
netbox/extras/views.py

@@ -686,7 +686,7 @@ class DashboardWidgetAddView(LoginRequiredMixin, View):
         widget_form = DashboardWidgetAddForm(initial=initial)
         widget_form = DashboardWidgetAddForm(initial=initial)
         widget_name = get_field_value(widget_form, 'widget_class')
         widget_name = get_field_value(widget_form, 'widget_class')
         widget_class = get_widget_class(widget_name)
         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, {
         return render(request, self.template_name, {
             'widget_class': widget_class,
             'widget_class': widget_class,

+ 1 - 1
netbox/templates/extras/dashboard/widget.html

@@ -30,7 +30,7 @@
         <strong>{{ widget.title }}</strong>
         <strong>{{ widget.title }}</strong>
       {% endif %}
       {% endif %}
     </div>
     </div>
-    <div class="card-body p-2">
+    <div class="card-body p-2 overflow-auto">
       {% render_widget widget %}
       {% render_widget widget %}
     </div>
     </div>
   </div>
   </div>

+ 1 - 0
netbox/templates/extras/dashboard/widget_add.html

@@ -4,6 +4,7 @@
   {% csrf_token %}
   {% csrf_token %}
   <div class="modal-header">
   <div class="modal-header">
     <h5 class="modal-title">Add a Widget</h5>
     <h5 class="modal-title">Add a Widget</h5>
+    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
   </div>
   </div>
   <div class="modal-body">
   <div class="modal-body">
     {% block form %}
     {% block form %}

+ 13 - 0
netbox/templates/extras/dashboard/widgets/rssfeed.html

@@ -0,0 +1,13 @@
+<div class="list-group list-group-flush">
+  {% for entry in feed.entries %}
+    <div class="list-group-item px-1">
+      <h6><a href="{{ entry.link }}">{{ entry.title }}</a></h6>
+      <div>
+        {{ entry.summary|safe }}
+      </div>
+    </div>
+  {% empty %}
+    <div class="list-group-item text-muted">No content found</div>
+  {% endfor %}
+</div>
+

+ 1 - 0
requirements.txt

@@ -15,6 +15,7 @@ django-taggit==3.1.0
 django-timezone-field==5.0
 django-timezone-field==5.0
 djangorestframework==3.14.0
 djangorestframework==3.14.0
 drf-yasg[validation]==1.21.5
 drf-yasg[validation]==1.21.5
+feedparser==6.0.10
 graphene-django==3.0.0
 graphene-django==3.0.0
 gunicorn==20.1.0
 gunicorn==20.1.0
 Jinja2==3.1.2
 Jinja2==3.1.2