mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* Add feedparser as a dependency * Introduce RSSFeedWidget * Clean up widget templates
This commit is contained in:
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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
netbox/templates/extras/dashboard/widgets/rssfeed.html
Normal file
13
netbox/templates/extras/dashboard/widgets/rssfeed.html
Normal 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>
|
||||||
|
|
@ -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
|
||||||
|
Reference in New Issue
Block a user