From 0033c08c2509f3ac9e0e5971540e6ce36ceb42f5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Apr 2025 13:41:39 -0400 Subject: [PATCH] Add SavedTableConfig --- netbox/extras/api/serializers.py | 1 + .../extras/api/serializers_/tableconfigs.py | 22 +++++ netbox/extras/api/urls.py | 1 + netbox/extras/api/views.py | 11 +++ netbox/extras/filtersets.py | 53 ++++++++++++ netbox/extras/forms/bulk_edit.py | 29 +++++++ netbox/extras/forms/filtersets.py | 31 +++++++ netbox/extras/forms/model_forms.py | 17 ++++ netbox/extras/graphql/filters.py | 15 ++++ netbox/extras/graphql/schema.py | 3 + netbox/extras/graphql/types.py | 10 +++ netbox/extras/migrations/0126_tableconfig.py | 69 +++++++++++++++ netbox/extras/models/models.py | 71 ++++++++++++++++ netbox/extras/tables/tables.py | 31 +++++++ netbox/extras/urls.py | 3 + netbox/extras/utils.py | 20 +++++ netbox/extras/views.py | 83 +++++++++++++------ netbox/netbox/navigation/menu.py | 1 + netbox/templates/extras/tableconfig.html | 77 +++++++++++++++++ 19 files changed, 523 insertions(+), 25 deletions(-) create mode 100644 netbox/extras/api/serializers_/tableconfigs.py create mode 100644 netbox/extras/migrations/0126_tableconfig.py create mode 100644 netbox/templates/extras/tableconfig.html diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 5e799b504..07540c50d 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -12,4 +12,5 @@ from .serializers_.configcontexts import * from .serializers_.configtemplates import * from .serializers_.savedfilters import * from .serializers_.scripts import * +from .serializers_.tableconfigs import * from .serializers_.tags import * diff --git a/netbox/extras/api/serializers_/tableconfigs.py b/netbox/extras/api/serializers_/tableconfigs.py new file mode 100644 index 000000000..6a59048df --- /dev/null +++ b/netbox/extras/api/serializers_/tableconfigs.py @@ -0,0 +1,22 @@ +from core.models import ObjectType +from extras.models import TableConfig +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'TableConfigSerializer', +) + + +class TableConfigSerializer(ValidatedModelSerializer): + object_type = ContentTypeField( + queryset=ObjectType.objects.all() + ) + + class Meta: + model = TableConfig + fields = [ + 'id', 'url', 'display_url', 'display', 'object_type', 'table', 'name', 'slug', 'description', 'user', + 'weight', 'enabled', 'shared', 'columns', 'ordering', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 88121b640..101808753 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -14,6 +14,7 @@ router.register('custom-field-choice-sets', views.CustomFieldChoiceSetViewSet) router.register('custom-links', views.CustomLinkViewSet) router.register('export-templates', views.ExportTemplateViewSet) router.register('saved-filters', views.SavedFilterViewSet) +router.register('table-configs', views.TableConfigViewSet) router.register('bookmarks', views.BookmarkViewSet) router.register('notifications', views.NotificationViewSet) router.register('notification-groups', views.NotificationGroupViewSet) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 49a44f5f1..6e9225f73 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -131,6 +131,17 @@ class SavedFilterViewSet(NetBoxModelViewSet): filterset_class = filtersets.SavedFilterFilterSet +# +# Table Configs +# + +class TableConfigViewSet(NetBoxModelViewSet): + metadata_class = ContentTypeMetadata + queryset = TableConfig.objects.all() + serializer_class = serializers.TableConfigSerializer + filterset_class = filtersets.TableConfigFilterSet + + # # Bookmarks # diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 89dd4c9f1..614885169 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -32,6 +32,7 @@ __all__ = ( 'ObjectTypeFilterSet', 'SavedFilterFilterSet', 'ScriptFilterSet', + 'TableConfigFilterSet', 'TagFilterSet', 'TaggedItemFilterSet', 'WebhookFilterSet', @@ -326,6 +327,58 @@ class SavedFilterFilterSet(ChangeLoggedModelFilterSet): return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user))) +class TableConfigFilterSet(ChangeLoggedModelFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_type' + ) + object_type = ContentTypeFilter( + field_name='object_type' + ) + user_id = django_filters.ModelMultipleChoiceFilter( + queryset=User.objects.all(), + label=_('User (ID)'), + ) + user = django_filters.ModelMultipleChoiceFilter( + field_name='user__username', + queryset=User.objects.all(), + to_field_name='username', + label=_('User (name)'), + ) + usable = django_filters.BooleanFilter( + method='_usable' + ) + + class Meta: + model = TableConfig + fields = ('id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight') + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) + + def _usable(self, queryset, name, value): + """ + Return only TableConfigs that are both enabled and are shared (or belong to the current user). + """ + user = self.request.user if self.request else None + if not user or user.is_anonymous: + if value: + return queryset.filter(enabled=True, shared=True) + return queryset.filter(Q(enabled=False) | Q(shared=False)) + if value: + return queryset.filter(enabled=True).filter(Q(shared=True) | Q(user=user)) + return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user))) + + class BookmarkFilterSet(BaseFilterSet): created = django_filters.DateTimeFilter() object_type_id = MultiValueNumberFilter() diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 7a78dba8b..c854a6c81 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -21,6 +21,7 @@ __all__ = ( 'JournalEntryBulkEditForm', 'NotificationGroupBulkEditForm', 'SavedFilterBulkEditForm', + 'TableConfigBulkEditForm', 'TagBulkEditForm', 'WebhookBulkEditForm', ) @@ -201,6 +202,34 @@ class SavedFilterBulkEditForm(BulkEditForm): nullable_fields = ('description',) +class TableConfigBulkEditForm(BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=TableConfig.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + weight = forms.IntegerField( + label=_('Weight'), + required=False + ) + enabled = forms.NullBooleanField( + label=_('Enabled'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + shared = forms.NullBooleanField( + label=_('Shared'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + + nullable_fields = ('description',) + + class WebhookBulkEditForm(NetBoxModelBulkEditForm): model = Webhook diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 056ca62a5..358d43041 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -31,6 +31,7 @@ __all__ = ( 'LocalConfigContextFilterForm', 'NotificationGroupFilterForm', 'SavedFilterFilterForm', + 'TableConfigFilterForm', 'TagFilterForm', 'WebhookFilterForm', ) @@ -249,6 +250,36 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): ) +class TableConfigFilterForm(SavedFiltersMixin, FilterForm): + fieldsets = ( + FieldSet('q', 'filter_id'), + FieldSet('object_type', 'enabled', 'shared', 'weight', name=_('Attributes')), + ) + object_type = ContentTypeMultipleChoiceField( + label=_('Object types'), + queryset=ObjectType.objects.public(), + required=False + ) + enabled = forms.NullBooleanField( + label=_('Enabled'), + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + shared = forms.NullBooleanField( + label=_('Shared'), + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + weight = forms.IntegerField( + label=_('Weight'), + required=False + ) + + class WebhookFilterForm(NetBoxModelFilterSetForm): model = Webhook fieldsets = ( diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 594b7d9d0..d148a4b3a 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -37,6 +37,7 @@ __all__ = ( 'NotificationGroupForm', 'SavedFilterForm', 'SubscriptionForm', + 'TableConfigForm', 'TagForm', 'WebhookForm', ) @@ -301,6 +302,22 @@ class SavedFilterForm(forms.ModelForm): super().__init__(*args, initial=initial, **kwargs) +class TableConfigForm(forms.ModelForm): + slug = SlugField() + object_type = ContentTypeChoiceField( + label=_('Object type'), + queryset=ObjectType.objects.all() + ) + + fieldsets = ( + FieldSet('name', 'slug', 'object_type', 'description', 'weight', 'enabled', 'shared', name=_('Table Config')), + ) + + class Meta: + model = TableConfig + exclude = ('user',) + + class BookmarkForm(forms.ModelForm): object_type = ContentTypeChoiceField( label=_('Object type'), diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index b8db143e4..eefd5152d 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -34,6 +34,7 @@ __all__ = ( 'JournalEntryFilter', 'NotificationGroupFilter', 'SavedFilterFilter', + 'TableConfigFilter', 'TagFilter', 'WebhookFilter', ) @@ -262,6 +263,20 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): ) +@strawberry_django.filter(models.TableConfig, lookups=True) +class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + user_id: ID | None = strawberry_django.filter_field() + weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + shared: FilterLookup[bool] | None = strawberry_django.filter_field() + + @strawberry_django.filter(models.Tag, lookups=True) class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py index 7d2d11bf1..947ff0b00 100644 --- a/netbox/extras/graphql/schema.py +++ b/netbox/extras/graphql/schema.py @@ -32,6 +32,9 @@ class ExtrasQuery: saved_filter: SavedFilterType = strawberry_django.field() saved_filter_list: List[SavedFilterType] = strawberry_django.field() + table_config: TableConfigType = strawberry_django.field() + table_config_list: List[TableConfigType] = strawberry_django.field() + journal_entry: JournalEntryType = strawberry_django.field() journal_entry_list: List[JournalEntryType] = strawberry_django.field() diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index f4a1a397f..44a3fb29f 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -38,6 +38,7 @@ __all__ = ( 'NotificationType', 'SavedFilterType', 'SubscriptionType', + 'TableConfigType', 'TagType', 'WebhookType', ) @@ -186,6 +187,15 @@ class SubscriptionType(ObjectType): user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None +@strawberry_django.type( + models.TableConfig, + filters=TableConfigFilter, + pagination=True +) +class TableConfigType(ObjectType): + user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None + + @strawberry_django.type( models.Tag, exclude=['extras_taggeditem_items', ], diff --git a/netbox/extras/migrations/0126_tableconfig.py b/netbox/extras/migrations/0126_tableconfig.py new file mode 100644 index 000000000..1658fff76 --- /dev/null +++ b/netbox/extras/migrations/0126_tableconfig.py @@ -0,0 +1,69 @@ +# Generated by Django 5.2b1 on 2025-04-04 17:35 + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0014_remove_redundant_indexes'), + ('extras', '0125_exporttemplate_file_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='TableConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('table', models.CharField(max_length=100)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(max_length=100)), + ('description', models.CharField(blank=True, max_length=200)), + ('weight', models.PositiveSmallIntegerField(default=100)), + ('enabled', models.BooleanField(default=True)), + ('shared', models.BooleanField(default=True)), + ( + 'columns', + django.contrib.postgres.fields.ArrayField( + base_field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=100), size=None + ), + size=None, + ), + ), + ( + 'ordering', + django.contrib.postgres.fields.ArrayField( + base_field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=100), size=None + ), + blank=True, + null=True, + size=None, + ), + ), + ( + 'object_type', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name='table_configs', to='core.objecttype' + ), + ), + ( + 'user', + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + 'verbose_name': 'table config', + 'verbose_name_plural': 'table configs', + 'ordering': ('weight', 'name'), + }, + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 76fad1082..e0a986b03 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -36,6 +36,7 @@ __all__ = ( 'ImageAttachment', 'JournalEntry', 'SavedFilter', + 'TableConfig', 'Webhook', ) @@ -524,6 +525,76 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): return qd.urlencode() +class TableConfig(ChangeLoggedModel): + """ + A saved configuration of columns and ordering which applies to a specific table. + """ + object_type = models.ForeignKey( + to='core.ObjectType', + on_delete=models.CASCADE, + related_name='table_configs', + help_text=_("The table's object type"), + ) + table = models.CharField( + verbose_name=_('table'), + max_length=100, + ) + name = models.CharField( + verbose_name=_('name'), + max_length=100, + ) + slug = models.SlugField( + verbose_name=_('slug'), + max_length=100, + ) + description = models.CharField( + verbose_name=_('description'), + max_length=200, + blank=True, + ) + user = models.ForeignKey( + to=settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + weight = models.PositiveSmallIntegerField( + verbose_name=_('weight'), + default=100 + ) + enabled = models.BooleanField( + verbose_name=_('enabled'), + default=True + ) + shared = models.BooleanField( + verbose_name=_('shared'), + default=True + ) + columns = ArrayField( + ArrayField(base_field=models.CharField(max_length=100)), + ) + ordering = ArrayField( + ArrayField(base_field=models.CharField(max_length=100)), + blank=True, + null=True + ) + + class Meta: + ordering = ('weight', 'name') + verbose_name = _('table config') + verbose_name_plural = _('table configs') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('extras:tableconfig', args=[self.pk]) + + @property + def docs_url(self): + return f'{settings.STATIC_URL}docs/models/extras/tableconfig/' + + class ImageAttachment(ChangeLoggedModel): """ An uploaded image which is associated with an object. diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 60b207058..3e24b18d1 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -27,6 +27,7 @@ __all__ = ( 'ReportResultsTable', 'ScriptResultsTable', 'SubscriptionTable', + 'TableConfigTable', 'TaggedItemTable', 'TagTable', 'WebhookTable', @@ -281,6 +282,36 @@ class SavedFilterTable(NetBoxTable): ) +class TableConfigTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) + object_type = columns.ContentTypesColumn( + verbose_name=_('Object Type'), + ) + table = tables.Column( + verbose_name=_('Table Name') + ) + enabled = columns.BooleanColumn( + verbose_name=_('Enabled'), + ) + shared = columns.BooleanColumn( + verbose_name=_('Shared'), + false_mark=None + ) + + class Meta(NetBoxTable.Meta): + model = TableConfig + fields = ( + 'pk', 'id', 'name', 'slug', 'object_type', 'table', 'description', 'user', 'weight', 'enabled', 'shared', + 'created', 'last_updated', + ) + default_columns = ( + 'pk', 'name', 'object_type', 'table', 'user', 'description', 'enabled', 'shared', + ) + + class BookmarkTable(NetBoxTable): object_type = columns.ContentTypeColumn( verbose_name=_('Object Types'), diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 32633493f..cceb3c5a5 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -19,6 +19,9 @@ urlpatterns = [ path('export-templates/', include(get_model_urls('extras', 'exporttemplate', detail=False))), path('export-templates//', include(get_model_urls('extras', 'exporttemplate'))), + path('table-configs/', include(get_model_urls('extras', 'tableconfig', detail=False))), + path('table-configs//', include(get_model_urls('extras', 'tableconfig'))), + path('saved-filters/', include(get_model_urls('extras', 'savedfilter', detail=False))), path('saved-filters//', include(get_model_urls('extras', 'savedfilter'))), diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index 155597c30..aba429766 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -2,12 +2,14 @@ import importlib from django.core.exceptions import ImproperlyConfigured from django.db import models +from django.db.models import Q from taggit.managers import _TaggableManager from netbox.context import current_request from .validators import CustomValidator __all__ = ( + 'SharedObjectViewMixin', 'image_upload', 'is_report', 'is_script', @@ -16,6 +18,24 @@ __all__ = ( ) +class SharedObjectViewMixin: + + def get_queryset(self, request): + """ + Return only shared SavedFilters, or those owned by the current user, unless + this is a superuser. + """ + queryset = super().get_queryset(request) + user = request.user + if user.is_superuser: + return queryset + if user.is_anonymous: + return queryset.filter(shared=True) + return queryset.filter( + Q(shared=True) | Q(user=user) + ) + + def filename_from_model(model: models.Model) -> str: """Standardises how we generate filenames from model class for exports""" base = model._meta.verbose_name_plural.lower().replace(' ', '_') diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2833cec0d..30f49698e 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -18,6 +18,7 @@ from dcim.models import Device, DeviceRole, Platform from extras.choices import LogLevelChoices from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class +from extras.utils import SharedObjectViewMixin from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.views import generic from netbox.views.generic.mixins import TableMixin @@ -285,39 +286,22 @@ class ExportTemplateBulkSyncDataView(generic.BulkSyncDataView): # Saved filters # -class SavedFilterMixin: - - def get_queryset(self, request): - """ - Return only shared SavedFilters, or those owned by the current user, unless - this is a superuser. - """ - queryset = SavedFilter.objects.all() - user = request.user - if user.is_superuser: - return queryset - if user.is_anonymous: - return queryset.filter(shared=True) - return queryset.filter( - Q(shared=True) | Q(user=user) - ) - - @register_model_view(SavedFilter, 'list', path='', detail=False) -class SavedFilterListView(SavedFilterMixin, generic.ObjectListView): +class SavedFilterListView(SharedObjectViewMixin, generic.ObjectListView): + queryset = SavedFilter.objects.all() filterset = filtersets.SavedFilterFilterSet filterset_form = forms.SavedFilterFilterForm table = tables.SavedFilterTable @register_model_view(SavedFilter) -class SavedFilterView(SavedFilterMixin, generic.ObjectView): +class SavedFilterView(SharedObjectViewMixin, generic.ObjectView): queryset = SavedFilter.objects.all() @register_model_view(SavedFilter, 'add', detail=False) @register_model_view(SavedFilter, 'edit') -class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView): +class SavedFilterEditView(SharedObjectViewMixin, generic.ObjectEditView): queryset = SavedFilter.objects.all() form = forms.SavedFilterForm @@ -328,18 +312,18 @@ class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView): @register_model_view(SavedFilter, 'delete') -class SavedFilterDeleteView(SavedFilterMixin, generic.ObjectDeleteView): +class SavedFilterDeleteView(SharedObjectViewMixin, generic.ObjectDeleteView): queryset = SavedFilter.objects.all() @register_model_view(SavedFilter, 'bulk_import', detail=False) -class SavedFilterBulkImportView(SavedFilterMixin, generic.BulkImportView): +class SavedFilterBulkImportView(SharedObjectViewMixin, generic.BulkImportView): queryset = SavedFilter.objects.all() model_form = forms.SavedFilterImportForm @register_model_view(SavedFilter, 'bulk_edit', path='edit', detail=False) -class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView): +class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView): queryset = SavedFilter.objects.all() filterset = filtersets.SavedFilterFilterSet table = tables.SavedFilterTable @@ -347,12 +331,61 @@ class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView): @register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False) -class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView): +class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView): queryset = SavedFilter.objects.all() filterset = filtersets.SavedFilterFilterSet table = tables.SavedFilterTable +# +# Table configs +# + +@register_model_view(TableConfig, 'list', path='', detail=False) +class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView): + queryset = TableConfig.objects.all() + filterset = filtersets.TableConfigFilterSet + filterset_form = forms.TableConfigFilterForm + table = tables.TableConfigTable + + +@register_model_view(TableConfig) +class TableConfigView(SharedObjectViewMixin, generic.ObjectView): + queryset = TableConfig.objects.all() + + +@register_model_view(TableConfig, 'add', detail=False) +@register_model_view(TableConfig, 'edit') +class TableConfigEditView(SharedObjectViewMixin, generic.ObjectEditView): + queryset = TableConfig.objects.all() + form = forms.TableConfigForm + + def alter_object(self, obj, request, url_args, url_kwargs): + if not obj.pk: + obj.user = request.user + return obj + + +@register_model_view(TableConfig, 'delete') +class TableConfigDeleteView(SharedObjectViewMixin, generic.ObjectDeleteView): + queryset = TableConfig.objects.all() + + +@register_model_view(TableConfig, 'bulk_edit', path='edit', detail=False) +class TableConfigBulkEditView(SharedObjectViewMixin, generic.BulkEditView): + queryset = TableConfig.objects.all() + filterset = filtersets.TableConfigFilterSet + table = tables.TableConfigTable + form = forms.TableConfigBulkEditForm + + +@register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False) +class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView): + queryset = TableConfig.objects.all() + filterset = filtersets.TableConfigFilterSet + table = tables.TableConfigTable + + # # Bookmarks # diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 778f0d67c..a52d771f1 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -349,6 +349,7 @@ CUSTOMIZATION_MENU = Menu( get_model_item('extras', 'customlink', _('Custom Links')), get_model_item('extras', 'exporttemplate', _('Export Templates')), get_model_item('extras', 'savedfilter', _('Saved Filters')), + get_model_item('extras', 'tableconfig', _('Table Configs'), actions=('add',)), get_model_item('extras', 'tag', 'Tags'), get_model_item('extras', 'imageattachment', _('Image Attachments'), actions=()), ), diff --git a/netbox/templates/extras/tableconfig.html b/netbox/templates/extras/tableconfig.html new file mode 100644 index 000000000..c2d6a4867 --- /dev/null +++ b/netbox/templates/extras/tableconfig.html @@ -0,0 +1,77 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} +{% load i18n %} + +{% block content %} +
+
+
+

{% trans "Table Config" %}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Object Type" %}{{ object.object_type }}
{% trans "Table" %}{{ object.table }}
{% trans "User" %}{{ object.user|placeholder }}
{% trans "Enabled" %}{% checkmark object.enabled %}
{% trans "Shared" %}{% checkmark object.shared %}
{% trans "Weight" %}{{ object.weight }}
+
+ {% plugin_left_page object %} +
+
+
+

{% trans "Columns" %}

+
+
    + {% for column in object.columns %} +
  • {{ column }}
  • + {% endfor %} +
+
+
+
+

{% trans "Ordering" %}

+
+
    + {% for column in object.ordering %} +
  • {{ column }}
  • + {% endfor %} +
+
+
+ {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %}