mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* Replace masonry with gridstack * Initial work on dashboard widgets * Implement function to save dashboard layout * Define a default dashboard * Clean up widgets * Implement widget configuration views & forms * Permit merging dict value with existing dict in user config * Add widget deletion view * Enable HTMX for widget configuration * Implement view to add dashboard widgets * ObjectCountsWidget: Identify models by app_label & name * Add color customization to dashboard widgets * Introduce Dashboard model to store user dashboard layout & config * Clean up utility functions * Remove hard-coded API URL * Use fixed grid cell height * Add modal close button * Clean up dashboard views * Rebuild JS
This commit is contained in:
@ -34,6 +34,7 @@ __all__ = (
|
||||
'ContentTypeSerializer',
|
||||
'CustomFieldSerializer',
|
||||
'CustomLinkSerializer',
|
||||
'DashboardSerializer',
|
||||
'ExportTemplateSerializer',
|
||||
'ImageAttachmentSerializer',
|
||||
'JobResultSerializer',
|
||||
@ -563,3 +564,13 @@ class ContentTypeSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'url', 'display', 'app_label', 'model']
|
||||
|
||||
|
||||
#
|
||||
# User dashboard
|
||||
#
|
||||
|
||||
class DashboardSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Dashboard
|
||||
fields = ('layout', 'config')
|
||||
|
@ -1,3 +1,5 @@
|
||||
from django.urls import include, path
|
||||
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
@ -22,4 +24,7 @@ router.register('job-results', views.JobResultViewSet)
|
||||
router.register('content-types', views.ContentTypeViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
urlpatterns = router.urls
|
||||
urlpatterns = [
|
||||
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
@ -4,6 +4,7 @@ from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
@ -423,3 +424,15 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||
serializer_class = serializers.ContentTypeSerializer
|
||||
filterset_class = filtersets.ContentTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# User dashboard
|
||||
#
|
||||
|
||||
class DashboardView(RetrieveUpdateDestroyAPIView):
|
||||
queryset = Dashboard.objects.all()
|
||||
serializer_class = serializers.DashboardSerializer
|
||||
|
||||
def get_object(self):
|
||||
return Dashboard.objects.filter(user=self.request.user).first()
|
||||
|
@ -5,4 +5,4 @@ class ExtrasConfig(AppConfig):
|
||||
name = "extras"
|
||||
|
||||
def ready(self):
|
||||
from . import lookups, search, signals
|
||||
from . import dashboard, lookups, search, signals
|
||||
|
@ -1,2 +1,47 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Webhook content types
|
||||
HTTP_CONTENT_TYPE_JSON = 'application/json'
|
||||
|
||||
# Dashboard
|
||||
DEFAULT_DASHBOARD = [
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
'height': 3,
|
||||
'title': 'IPAM',
|
||||
'config': {
|
||||
'models': [
|
||||
'ipam.aggregate',
|
||||
'ipam.prefix',
|
||||
'ipam.ipaddress',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
'height': 3,
|
||||
'title': 'DCIM',
|
||||
'config': {
|
||||
'models': [
|
||||
'dcim.site',
|
||||
'dcim.rack',
|
||||
'dcim.device',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.NoteWidget',
|
||||
'width': 4,
|
||||
'height': 3,
|
||||
'config': {
|
||||
'content': 'Welcome to **NetBox**!'
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ChangeLogWidget',
|
||||
'width': 12,
|
||||
'height': 6,
|
||||
},
|
||||
]
|
||||
|
2
netbox/extras/dashboard/__init__.py
Normal file
2
netbox/extras/dashboard/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .utils import *
|
||||
from .widgets import *
|
38
netbox/extras/dashboard/forms.py
Normal file
38
netbox/extras/dashboard/forms.py
Normal file
@ -0,0 +1,38 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from netbox.registry import registry
|
||||
from utilities.forms import BootstrapMixin, add_blank_choice
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
__all__ = (
|
||||
'DashboardWidgetAddForm',
|
||||
'DashboardWidgetForm',
|
||||
)
|
||||
|
||||
|
||||
def get_widget_choices():
|
||||
return registry['widgets'].items()
|
||||
|
||||
|
||||
class DashboardWidgetForm(BootstrapMixin, forms.Form):
|
||||
title = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
color = forms.ChoiceField(
|
||||
choices=add_blank_choice(ButtonColorChoices),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class DashboardWidgetAddForm(DashboardWidgetForm):
|
||||
widget_class = forms.ChoiceField(
|
||||
choices=get_widget_choices,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
'hx-get': reverse_lazy('extras:dashboardwidget_add'),
|
||||
'hx-target': '#widget_add_form',
|
||||
}
|
||||
)
|
||||
)
|
||||
field_order = ('widget_class', 'title', 'color')
|
76
netbox/extras/dashboard/utils.py
Normal file
76
netbox/extras/dashboard/utils.py
Normal file
@ -0,0 +1,76 @@
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from netbox.registry import registry
|
||||
from extras.constants import DEFAULT_DASHBOARD
|
||||
|
||||
__all__ = (
|
||||
'get_dashboard',
|
||||
'get_default_dashboard',
|
||||
'get_widget_class',
|
||||
'register_widget',
|
||||
)
|
||||
|
||||
|
||||
def register_widget(cls):
|
||||
"""
|
||||
Decorator for registering a DashboardWidget class.
|
||||
"""
|
||||
app_label = cls.__module__.split('.', maxsplit=1)[0]
|
||||
label = f'{app_label}.{cls.__name__}'
|
||||
registry['widgets'][label] = cls
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
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 for a given User if one exists, or generate a default dashboard.
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
dashboard = get_default_dashboard()
|
||||
else:
|
||||
try:
|
||||
dashboard = user.dashboard
|
||||
except ObjectDoesNotExist:
|
||||
# Create a dashboard for this user
|
||||
dashboard = get_default_dashboard()
|
||||
dashboard.user = user
|
||||
dashboard.save()
|
||||
|
||||
return dashboard
|
||||
|
||||
|
||||
def get_default_dashboard():
|
||||
from extras.models import Dashboard
|
||||
dashboard = Dashboard(
|
||||
layout=[],
|
||||
config={}
|
||||
)
|
||||
for widget in DEFAULT_DASHBOARD:
|
||||
id = str(uuid.uuid4())
|
||||
dashboard.layout.append({
|
||||
'id': id,
|
||||
'w': widget['width'],
|
||||
'h': widget['height'],
|
||||
'x': widget.get('x'),
|
||||
'y': widget.get('y'),
|
||||
})
|
||||
dashboard.config[id] = {
|
||||
'class': widget['widget'],
|
||||
'title': widget.get('title'),
|
||||
'config': widget.get('config', {}),
|
||||
}
|
||||
|
||||
return dashboard
|
119
netbox/extras/dashboard/widgets.py
Normal file
119
netbox/extras/dashboard/widgets.py
Normal file
@ -0,0 +1,119 @@
|
||||
import uuid
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from utilities.forms import BootstrapMixin
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.utils import content_type_identifier, content_type_name
|
||||
from .utils import register_widget
|
||||
|
||||
__all__ = (
|
||||
'ChangeLogWidget',
|
||||
'DashboardWidget',
|
||||
'NoteWidget',
|
||||
'ObjectCountsWidget',
|
||||
)
|
||||
|
||||
|
||||
def get_content_type_labels():
|
||||
return [
|
||||
(content_type_identifier(ct), content_type_name(ct))
|
||||
for ct in ContentType.objects.order_by('app_label', 'model')
|
||||
]
|
||||
|
||||
|
||||
class DashboardWidget:
|
||||
default_title = None
|
||||
description = None
|
||||
width = 4
|
||||
height = 3
|
||||
|
||||
class ConfigForm(forms.Form):
|
||||
pass
|
||||
|
||||
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.config = config or {}
|
||||
self.title = title or self.default_title
|
||||
self.color = color
|
||||
if width:
|
||||
self.width = width
|
||||
if height:
|
||||
self.height = height
|
||||
self.x, self.y = x, y
|
||||
|
||||
def __str__(self):
|
||||
return self.title or self.__class__.__name__
|
||||
|
||||
def set_layout(self, grid_item):
|
||||
self.width = grid_item['w']
|
||||
self.height = grid_item['h']
|
||||
self.x = grid_item.get('x')
|
||||
self.y = grid_item.get('y')
|
||||
|
||||
def render(self, request):
|
||||
raise NotImplementedError(f"{self.__class__} must define a render() method.")
|
||||
|
||||
@property
|
||||
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):
|
||||
description = _('Display some arbitrary custom content. Markdown is supported.')
|
||||
|
||||
class ConfigForm(BootstrapMixin, forms.Form):
|
||||
content = forms.CharField(
|
||||
widget=forms.Textarea()
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
return render_markdown(self.config.get('content'))
|
||||
|
||||
|
||||
@register_widget
|
||||
class ObjectCountsWidget(DashboardWidget):
|
||||
default_title = _('Objects')
|
||||
description = _('Display a set of NetBox models and the number of objects created for each type.')
|
||||
template_name = 'extras/dashboard/widgets/objectcounts.html'
|
||||
|
||||
class ConfigForm(BootstrapMixin, forms.Form):
|
||||
models = forms.MultipleChoiceField(
|
||||
choices=get_content_type_labels
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
counts = []
|
||||
for content_type_id in self.config['models']:
|
||||
app_label, model_name = content_type_id.split('.')
|
||||
model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class()
|
||||
object_count = model.objects.restrict(request.user, 'view').count
|
||||
counts.append((model, object_count))
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'counts': counts,
|
||||
})
|
||||
|
||||
|
||||
@register_widget
|
||||
class ChangeLogWidget(DashboardWidget):
|
||||
default_title = _('Change Log')
|
||||
description = _('Display the most recent records from the global change log.')
|
||||
template_name = 'extras/dashboard/widgets/changelog.html'
|
||||
width = 12
|
||||
height = 4
|
||||
|
||||
def render(self, request):
|
||||
return render_to_string(self.template_name, {})
|
25
netbox/extras/migrations/0087_dashboard.py
Normal file
25
netbox/extras/migrations/0087_dashboard.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.7 on 2023-02-24 00:56
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('extras', '0086_configtemplate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Dashboard',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('layout', models.JSONField()),
|
||||
('config', models.JSONField()),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
from .change_logging import ObjectChange
|
||||
from .configs import *
|
||||
from .customfields import CustomField
|
||||
from .dashboard import *
|
||||
from .models import *
|
||||
from .search import *
|
||||
from .staging import *
|
||||
@ -15,6 +16,7 @@ __all__ = (
|
||||
'ConfigTemplate',
|
||||
'CustomField',
|
||||
'CustomLink',
|
||||
'Dashboard',
|
||||
'ExportTemplate',
|
||||
'ImageAttachment',
|
||||
'JobResult',
|
||||
|
70
netbox/extras/models/dashboard.py
Normal file
70
netbox/extras/models/dashboard.py
Normal file
@ -0,0 +1,70 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
from extras.dashboard.utils import get_widget_class
|
||||
|
||||
__all__ = (
|
||||
'Dashboard',
|
||||
)
|
||||
|
||||
|
||||
class Dashboard(models.Model):
|
||||
user = models.OneToOneField(
|
||||
to=get_user_model(),
|
||||
on_delete=models.CASCADE,
|
||||
related_name='dashboard'
|
||||
)
|
||||
layout = models.JSONField()
|
||||
config = models.JSONField()
|
||||
|
||||
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,
|
||||
'title': widget.title,
|
||||
'color': widget.color,
|
||||
'config': widget.config,
|
||||
}
|
||||
self.layout.append({
|
||||
'id': id,
|
||||
'h': widget.height,
|
||||
'w': widget.width,
|
||||
'x': x,
|
||||
'y': y,
|
||||
})
|
||||
|
||||
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
|
||||
]
|
11
netbox/extras/templatetags/dashboard.py
Normal file
11
netbox/extras/templatetags/dashboard.py
Normal file
@ -0,0 +1,11 @@
|
||||
from django import template
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def render_widget(context, widget):
|
||||
request = context['request']
|
||||
|
||||
return widget.render(request)
|
@ -87,6 +87,11 @@ urlpatterns = [
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path('changelog/<int:pk>/', include(get_model_urls('extras', 'objectchange'))),
|
||||
|
||||
# User dashboard
|
||||
path('dashboard/widgets/add/', views.DashboardWidgetAddView.as_view(), name='dashboardwidget_add'),
|
||||
path('dashboard/widgets/<uuid:id>/configure/', views.DashboardWidgetConfigView.as_view(), name='dashboardwidget_config'),
|
||||
path('dashboard/widgets/<uuid:id>/delete/', views.DashboardWidgetDeleteView.as_view(), name='dashboardwidget_delete'),
|
||||
|
||||
# Reports
|
||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
|
||||
|
@ -1,14 +1,18 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count, Q
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.http import Http404, HttpResponseForbidden, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import View
|
||||
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
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
from utilities.htmx import is_htmx
|
||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
@ -664,6 +668,130 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.JournalEntryTable
|
||||
|
||||
|
||||
#
|
||||
# Dashboard widgets
|
||||
#
|
||||
|
||||
class DashboardWidgetAddView(LoginRequiredMixin, View):
|
||||
template_name = 'extras/dashboard/widget_add.html'
|
||||
|
||||
def get(self, request):
|
||||
if not is_htmx(request):
|
||||
return redirect('home')
|
||||
|
||||
initial = request.GET or {
|
||||
'widget_class': 'extras.NoteWidget',
|
||||
}
|
||||
widget_form = DashboardWidgetAddForm(initial=initial)
|
||||
widget_name = get_field_value(widget_form, 'widget_class')
|
||||
widget_class = get_widget_class(widget_name)
|
||||
config_form = widget_class.ConfigForm(prefix='config')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'widget_class': widget_class,
|
||||
'widget_form': widget_form,
|
||||
'config_form': config_form,
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
widget_form = DashboardWidgetAddForm(request.POST)
|
||||
config_form = None
|
||||
widget_class = None
|
||||
|
||||
if widget_form.is_valid():
|
||||
widget_class = get_widget_class(widget_form.cleaned_data['widget_class'])
|
||||
config_form = widget_class.ConfigForm(request.POST, prefix='config')
|
||||
|
||||
if config_form.is_valid():
|
||||
data = widget_form.cleaned_data
|
||||
data.pop('widget_class')
|
||||
data['config'] = config_form.cleaned_data
|
||||
widget = widget_class(**data)
|
||||
request.user.dashboard.add_widget(widget)
|
||||
request.user.dashboard.save()
|
||||
messages.success(request, f'Added widget {widget.id}')
|
||||
|
||||
return HttpResponse(headers={
|
||||
'HX-Redirect': reverse('home'),
|
||||
})
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'widget_class': widget_class,
|
||||
'widget_form': widget_form,
|
||||
'config_form': config_form,
|
||||
})
|
||||
|
||||
|
||||
class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
||||
template_name = 'extras/dashboard/widget_config.html'
|
||||
|
||||
def get(self, request, id):
|
||||
if not is_htmx(request):
|
||||
return redirect('home')
|
||||
|
||||
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')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'widget_form': widget_form,
|
||||
'config_form': config_form,
|
||||
'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
|
||||
})
|
||||
|
||||
def post(self, request, id):
|
||||
widget = request.user.dashboard.get_widget(id)
|
||||
widget_form = DashboardWidgetForm(request.POST)
|
||||
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[str(id)].update(data)
|
||||
request.user.dashboard.save()
|
||||
messages.success(request, f'Updated widget {widget.id}')
|
||||
|
||||
return HttpResponse(headers={
|
||||
'HX-Redirect': reverse('home'),
|
||||
})
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'widget_form': widget_form,
|
||||
'config_form': config_form,
|
||||
'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
|
||||
})
|
||||
|
||||
|
||||
class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
||||
template_name = 'generic/object_delete.html'
|
||||
|
||||
def get(self, request, id):
|
||||
if not is_htmx(request):
|
||||
return redirect('home')
|
||||
|
||||
widget = request.user.dashboard.get_widget(id)
|
||||
form = ConfirmationForm(initial=request.GET)
|
||||
|
||||
return render(request, 'htmx/delete_form.html', {
|
||||
'object_type': widget.__class__.__name__,
|
||||
'object': widget,
|
||||
'form': form,
|
||||
'form_url': reverse('extras:dashboardwidget_delete', kwargs={'id': id})
|
||||
})
|
||||
|
||||
def post(self, request, id):
|
||||
form = ConfirmationForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
request.user.dashboard.delete_widget(id)
|
||||
request.user.dashboard.save()
|
||||
messages.success(request, f'Deleted widget {id}')
|
||||
else:
|
||||
messages.error(request, f'Error deleting widget: {form.errors[0]}')
|
||||
|
||||
return redirect(reverse('home'))
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
Reference in New Issue
Block a user