diff --git a/netbox/extras/dashboard/utils.py b/netbox/extras/dashboard/utils.py index 97d1b8f0c..8281cc522 100644 --- a/netbox/extras/dashboard/utils.py +++ b/netbox/extras/dashboard/utils.py @@ -1,13 +1,14 @@ import uuid +from django.core.exceptions import ObjectDoesNotExist + from netbox.registry import registry from extras.constants import DEFAULT_DASHBOARD -from extras.models import Dashboard __all__ = ( 'get_dashboard', - 'get_default_dashboard_config', - 'get_widget_class_and_config', + 'get_default_dashboard', + 'get_widget_class', 'register_widget', ) @@ -23,32 +24,36 @@ def register_widget(cls): return cls -def get_widget_class_and_config(dashboard, id): - config = dict(dashboard.config[id]) # Copy to avoid mutating userconfig data - widget_class = registry['widgets'].get(config.pop('class')) - return widget_class, config +def get_widget_class(name): + """ + Return a registered DashboardWidget class identified by its name. + """ + try: + return registry['widgets'][name] + except KeyError: + raise ValueError(f"Unregistered widget class: {name}") def get_dashboard(user): """ - Return the dashboard layout for a given User. + Return the Dashboard for a given User if one exists, or generate a default dashboard. """ - if not user.is_anonymous and hasattr(user, 'dashboard'): - dashboard = user.dashboard + if user.is_anonymous: + dashboard = get_default_dashboard() else: - dashboard = get_default_dashboard_config() + try: + dashboard = user.dashboard + except ObjectDoesNotExist: + # Create a dashboard for this user + dashboard = get_default_dashboard() + dashboard.user = user + dashboard.save() - widgets = [] - for grid_item in dashboard.layout: - widget_class, widget_config = get_widget_class_and_config(dashboard, grid_item['id']) - widget = widget_class(id=grid_item['id'], **widget_config) - widget.set_layout(grid_item) - widgets.append(widget) - - return widgets + return dashboard -def get_default_dashboard_config(): +def get_default_dashboard(): + from extras.models import Dashboard dashboard = Dashboard( layout=[], config={} diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 6e6113a9e..cee8f5f67 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -61,6 +61,14 @@ class DashboardWidget: def name(self): return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}' + @property + def form_data(self): + return { + 'title': self.title, + 'color': self.color, + 'config': self.config, + } + @register_widget class NoteWidget(DashboardWidget): diff --git a/netbox/extras/models/dashboard.py b/netbox/extras/models/dashboard.py index f17a3be57..cdbf85b60 100644 --- a/netbox/extras/models/dashboard.py +++ b/netbox/extras/models/dashboard.py @@ -1,6 +1,8 @@ from django.contrib.auth import get_user_model from django.db import models +from extras.dashboard.utils import get_widget_class + __all__ = ( 'Dashboard', ) @@ -18,7 +20,30 @@ class Dashboard(models.Model): class Meta: pass + def get_widget(self, id): + """ + Instantiate and return a widget by its ID + """ + id = str(id) + config = dict(self.config[id]) # Copy to avoid mutating instance data + widget_class = get_widget_class(config.pop('class')) + return widget_class(id=id, **config) + + def get_layout(self): + """ + Return the dashboard's configured layout, suitable for rendering with gridstack.js. + """ + widgets = [] + for grid_item in self.layout: + widget = self.get_widget(grid_item['id']) + widget.set_layout(grid_item) + widgets.append(widget) + return widgets + def add_widget(self, widget, x=None, y=None): + """ + Add a widget to the dashboard, optionally specifying its X & Y coordinates. + """ id = str(widget.id) self.config[id] = { 'class': widget.name, @@ -35,6 +60,10 @@ class Dashboard(models.Model): }) def delete_widget(self, id): + """ + Delete a widget from the dashboard. + """ + id = str(id) del self.config[id] self.layout = [ item for item in self.layout if item['id'] != id diff --git a/netbox/extras/views.py b/netbox/extras/views.py index a53b4f05d..03b630ae8 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -10,7 +10,6 @@ from django_rq.queues import get_connection from rq import Worker from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm -from extras.dashboard.utils import get_widget_class_and_config from netbox.registry import registry from netbox.views import generic from utilities.forms import ConfirmationForm, get_field_value @@ -725,10 +724,9 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View): template_name = 'extras/dashboard/widget_config.html' def get(self, request, id): - id = str(id) - widget_class, config = get_widget_class_and_config(request.user.dashboard, id) - widget_form = DashboardWidgetForm(initial=config) - config_form = widget_class.ConfigForm(initial=config.get('config'), prefix='config') + widget = request.user.dashboard.get_widget(id) + widget_form = DashboardWidgetForm(initial=widget.form_data) + config_form = widget.ConfigForm(initial=widget.form_data.get('config'), prefix='config') if not is_htmx(request): return redirect('home') @@ -740,15 +738,16 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View): }) def post(self, request, id): - id = str(id) - widget_class, config = get_widget_class_and_config(request.user.dashboard, id) + widget = request.user.dashboard.get_widget(id) widget_form = DashboardWidgetForm(request.POST) - config_form = widget_class.ConfigForm(request.POST, prefix='config') + config_form = widget.ConfigForm(request.POST, prefix='config') if widget_form.is_valid() and config_form.is_valid(): data = widget_form.cleaned_data data['config'] = config_form.cleaned_data - request.user.dashboard.config[id].update(data) + print(request.user.dashboard.config) + print(data) + request.user.dashboard.config[str(id)].update(data) request.user.dashboard.save() response = HttpResponse() @@ -766,15 +765,13 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View): template_name = 'generic/object_delete.html' def get(self, request, id): - id = str(id) - widget_class, config = get_widget_class_and_config(request.user.dashboard, id) - widget = widget_class(**config) + widget = request.user.dashboard.get_widget(id) form = ConfirmationForm(initial=request.GET) # If this is an HTMX request, return only the rendered deletion form as modal content if is_htmx(request): return render(request, 'htmx/delete_form.html', { - 'object_type': widget_class.__name__, + 'object_type': widget.__class__.__name__, 'object': widget, 'form': form, 'form_url': reverse('extras:dashboardwidget_delete', kwargs={'id': id}) @@ -785,7 +782,6 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View): }) def post(self, request, id): - id = str(id) form = ConfirmationForm(request.POST) if form.is_valid(): diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index d949e3733..c7255916c 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -32,8 +32,8 @@ class HomeView(View): if settings.LOGIN_REQUIRED and not request.user.is_authenticated: return redirect('login') - # Build custom dashboard from user's config - widgets = get_dashboard(request.user) + # Construct the user's custom dashboard layout + dashboard = get_dashboard(request.user).get_layout() # Check whether a new release is available. (Only for staff/superusers.) new_release = None @@ -48,7 +48,7 @@ class HomeView(View): } return render(request, self.template_name, { - 'widgets': widgets, + 'dashboard': dashboard, 'new_release': new_release, }) diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 08135ea4e..5de4b08c1 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -25,7 +25,7 @@ {% block content-wrapper %} {# Render the user's customized dashboard #}