1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #11826: RSS feed widget (#11976)

* Add feedparser as a dependency

* Introduce RSSFeedWidget

* Clean up widget templates
This commit is contained in:
Jeremy Stretch
2023-03-14 11:59:27 -04:00
committed by GitHub
parent af63ac693e
commit 8bd0a2ef9d
7 changed files with 85 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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