Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion netbox/extras/dashboard/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django import forms
from django.conf import settings
from django.core.cache import cache
from django.db.models import Model
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, resolve, reverse
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -42,6 +43,27 @@ def get_object_type_choices():
]


def object_list_widget_supports_model(model: Model) -> bool:
"""Test whether a model is supported by the ObjectListWidget

In theory there could be more than one reason why a model isn't supported by the
ObjectListWidget, although we've only identified one so far--there's no resolve-able 'list' URL
for the model. Add more tests if more conditions arise.
"""
def can_resolve_model_list_view(model: Model) -> bool:
try:
reverse(get_viewname(model, action='list'))
return True
except Exception:
return False

tests = [
can_resolve_model_list_view,
]

return all(test(model) for test in tests)


def get_bookmarks_object_type_choices():
return [
(object_type_identifier(ot), object_type_name(ot))
Expand Down Expand Up @@ -234,6 +256,17 @@ def clean_url_params(self):
raise forms.ValidationError(_("Invalid format. URL parameters must be passed as a dictionary."))
return data

def clean_model(self):
if model_info := self.cleaned_data['model']:
app_label, model_name = model_info.split('.')
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
if not object_list_widget_supports_model(model):
raise forms.ValidationError(
_(f"Invalid model selection: {self['model'].data} is not supported.")
)

return model_info

def render(self, request):
app_label, model_name = self.config['model'].split('.')
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
Expand All @@ -257,7 +290,7 @@ def render(self, request):
parameters['per_page'] = page_size
parameters['embedded'] = True

if parameters:
if parameters and htmx_url is not None:
try:
htmx_url = f'{htmx_url}?{urlencode(parameters, doseq=True)}'
except ValueError:
Expand Down
48 changes: 48 additions & 0 deletions netbox/extras/tests/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.test import tag, TestCase

from extras.dashboard.widgets import ObjectListWidget


class ObjectListWidgetTests(TestCase):
def test_widget_config_form_validates_model(self):
model_info = 'extras.notification'
form = ObjectListWidget.ConfigForm({'model': model_info})
self.assertFalse(form.is_valid())

@tag('regression')
def test_widget_fails_gracefully(self):
"""
Example:
'2829fd9b-5dee-4c9a-81f2-5bd84c350a27': {
'class': 'extras.ObjectListWidget',
'color': 'indigo',
'title': 'Object List',
'config': {
'model': 'extras.notification',
'page_size': None,
'url_params': None
}
}
"""
config = {
# 'class': 'extras.ObjectListWidget', # normally popped off, left for clarity
'color': 'yellow',
'title': 'this should fail',
'config': {
'model': 'extras.notification',
'page_size': None,
'url_params': None,
},
}

class Request:
class User:
def has_perm(self, *args, **kwargs):
return True

user = User()

mock_request = Request()
widget = ObjectListWidget(id='2829fd9b-5dee-4c9a-81f2-5bd84c350a27', **config)
rendered = widget.render(mock_request)
self.assertTrue('Unable to load content. Invalid view name:' in rendered)