mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
Add SavedTableConfig
This commit is contained in:
parent
fbd6d8c7fc
commit
0033c08c25
@ -12,4 +12,5 @@ from .serializers_.configcontexts import *
|
|||||||
from .serializers_.configtemplates import *
|
from .serializers_.configtemplates import *
|
||||||
from .serializers_.savedfilters import *
|
from .serializers_.savedfilters import *
|
||||||
from .serializers_.scripts import *
|
from .serializers_.scripts import *
|
||||||
|
from .serializers_.tableconfigs import *
|
||||||
from .serializers_.tags import *
|
from .serializers_.tags import *
|
||||||
|
22
netbox/extras/api/serializers_/tableconfigs.py
Normal file
22
netbox/extras/api/serializers_/tableconfigs.py
Normal file
@ -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')
|
@ -14,6 +14,7 @@ router.register('custom-field-choice-sets', views.CustomFieldChoiceSetViewSet)
|
|||||||
router.register('custom-links', views.CustomLinkViewSet)
|
router.register('custom-links', views.CustomLinkViewSet)
|
||||||
router.register('export-templates', views.ExportTemplateViewSet)
|
router.register('export-templates', views.ExportTemplateViewSet)
|
||||||
router.register('saved-filters', views.SavedFilterViewSet)
|
router.register('saved-filters', views.SavedFilterViewSet)
|
||||||
|
router.register('table-configs', views.TableConfigViewSet)
|
||||||
router.register('bookmarks', views.BookmarkViewSet)
|
router.register('bookmarks', views.BookmarkViewSet)
|
||||||
router.register('notifications', views.NotificationViewSet)
|
router.register('notifications', views.NotificationViewSet)
|
||||||
router.register('notification-groups', views.NotificationGroupViewSet)
|
router.register('notification-groups', views.NotificationGroupViewSet)
|
||||||
|
@ -131,6 +131,17 @@ class SavedFilterViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.SavedFilterFilterSet
|
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
|
# Bookmarks
|
||||||
#
|
#
|
||||||
|
@ -32,6 +32,7 @@ __all__ = (
|
|||||||
'ObjectTypeFilterSet',
|
'ObjectTypeFilterSet',
|
||||||
'SavedFilterFilterSet',
|
'SavedFilterFilterSet',
|
||||||
'ScriptFilterSet',
|
'ScriptFilterSet',
|
||||||
|
'TableConfigFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
'TaggedItemFilterSet',
|
'TaggedItemFilterSet',
|
||||||
'WebhookFilterSet',
|
'WebhookFilterSet',
|
||||||
@ -326,6 +327,58 @@ class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
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):
|
class BookmarkFilterSet(BaseFilterSet):
|
||||||
created = django_filters.DateTimeFilter()
|
created = django_filters.DateTimeFilter()
|
||||||
object_type_id = MultiValueNumberFilter()
|
object_type_id = MultiValueNumberFilter()
|
||||||
|
@ -21,6 +21,7 @@ __all__ = (
|
|||||||
'JournalEntryBulkEditForm',
|
'JournalEntryBulkEditForm',
|
||||||
'NotificationGroupBulkEditForm',
|
'NotificationGroupBulkEditForm',
|
||||||
'SavedFilterBulkEditForm',
|
'SavedFilterBulkEditForm',
|
||||||
|
'TableConfigBulkEditForm',
|
||||||
'TagBulkEditForm',
|
'TagBulkEditForm',
|
||||||
'WebhookBulkEditForm',
|
'WebhookBulkEditForm',
|
||||||
)
|
)
|
||||||
@ -201,6 +202,34 @@ class SavedFilterBulkEditForm(BulkEditForm):
|
|||||||
nullable_fields = ('description',)
|
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):
|
class WebhookBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
model = Webhook
|
model = Webhook
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ __all__ = (
|
|||||||
'LocalConfigContextFilterForm',
|
'LocalConfigContextFilterForm',
|
||||||
'NotificationGroupFilterForm',
|
'NotificationGroupFilterForm',
|
||||||
'SavedFilterFilterForm',
|
'SavedFilterFilterForm',
|
||||||
|
'TableConfigFilterForm',
|
||||||
'TagFilterForm',
|
'TagFilterForm',
|
||||||
'WebhookFilterForm',
|
'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):
|
class WebhookFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = Webhook
|
model = Webhook
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
@ -37,6 +37,7 @@ __all__ = (
|
|||||||
'NotificationGroupForm',
|
'NotificationGroupForm',
|
||||||
'SavedFilterForm',
|
'SavedFilterForm',
|
||||||
'SubscriptionForm',
|
'SubscriptionForm',
|
||||||
|
'TableConfigForm',
|
||||||
'TagForm',
|
'TagForm',
|
||||||
'WebhookForm',
|
'WebhookForm',
|
||||||
)
|
)
|
||||||
@ -301,6 +302,22 @@ class SavedFilterForm(forms.ModelForm):
|
|||||||
super().__init__(*args, initial=initial, **kwargs)
|
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):
|
class BookmarkForm(forms.ModelForm):
|
||||||
object_type = ContentTypeChoiceField(
|
object_type = ContentTypeChoiceField(
|
||||||
label=_('Object type'),
|
label=_('Object type'),
|
||||||
|
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'JournalEntryFilter',
|
'JournalEntryFilter',
|
||||||
'NotificationGroupFilter',
|
'NotificationGroupFilter',
|
||||||
'SavedFilterFilter',
|
'SavedFilterFilter',
|
||||||
|
'TableConfigFilter',
|
||||||
'TagFilter',
|
'TagFilter',
|
||||||
'WebhookFilter',
|
'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)
|
@strawberry_django.filter(models.Tag, lookups=True)
|
||||||
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
|
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
|
||||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||||
|
@ -32,6 +32,9 @@ class ExtrasQuery:
|
|||||||
saved_filter: SavedFilterType = strawberry_django.field()
|
saved_filter: SavedFilterType = strawberry_django.field()
|
||||||
saved_filter_list: List[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: JournalEntryType = strawberry_django.field()
|
||||||
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
|
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ __all__ = (
|
|||||||
'NotificationType',
|
'NotificationType',
|
||||||
'SavedFilterType',
|
'SavedFilterType',
|
||||||
'SubscriptionType',
|
'SubscriptionType',
|
||||||
|
'TableConfigType',
|
||||||
'TagType',
|
'TagType',
|
||||||
'WebhookType',
|
'WebhookType',
|
||||||
)
|
)
|
||||||
@ -186,6 +187,15 @@ class SubscriptionType(ObjectType):
|
|||||||
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
|
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(
|
@strawberry_django.type(
|
||||||
models.Tag,
|
models.Tag,
|
||||||
exclude=['extras_taggeditem_items', ],
|
exclude=['extras_taggeditem_items', ],
|
||||||
|
69
netbox/extras/migrations/0126_tableconfig.py
Normal file
69
netbox/extras/migrations/0126_tableconfig.py
Normal file
@ -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'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -36,6 +36,7 @@ __all__ = (
|
|||||||
'ImageAttachment',
|
'ImageAttachment',
|
||||||
'JournalEntry',
|
'JournalEntry',
|
||||||
'SavedFilter',
|
'SavedFilter',
|
||||||
|
'TableConfig',
|
||||||
'Webhook',
|
'Webhook',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -524,6 +525,76 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
return qd.urlencode()
|
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):
|
class ImageAttachment(ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
|
@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'ReportResultsTable',
|
'ReportResultsTable',
|
||||||
'ScriptResultsTable',
|
'ScriptResultsTable',
|
||||||
'SubscriptionTable',
|
'SubscriptionTable',
|
||||||
|
'TableConfigTable',
|
||||||
'TaggedItemTable',
|
'TaggedItemTable',
|
||||||
'TagTable',
|
'TagTable',
|
||||||
'WebhookTable',
|
'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):
|
class BookmarkTable(NetBoxTable):
|
||||||
object_type = columns.ContentTypeColumn(
|
object_type = columns.ContentTypeColumn(
|
||||||
verbose_name=_('Object Types'),
|
verbose_name=_('Object Types'),
|
||||||
|
@ -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', detail=False))),
|
||||||
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
||||||
|
|
||||||
|
path('table-configs/', include(get_model_urls('extras', 'tableconfig', detail=False))),
|
||||||
|
path('table-configs/<int:pk>/', 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', detail=False))),
|
||||||
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
|
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
|
||||||
|
|
||||||
|
@ -2,12 +2,14 @@ import importlib
|
|||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from taggit.managers import _TaggableManager
|
from taggit.managers import _TaggableManager
|
||||||
|
|
||||||
from netbox.context import current_request
|
from netbox.context import current_request
|
||||||
from .validators import CustomValidator
|
from .validators import CustomValidator
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'SharedObjectViewMixin',
|
||||||
'image_upload',
|
'image_upload',
|
||||||
'is_report',
|
'is_report',
|
||||||
'is_script',
|
'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:
|
def filename_from_model(model: models.Model) -> str:
|
||||||
"""Standardises how we generate filenames from model class for exports"""
|
"""Standardises how we generate filenames from model class for exports"""
|
||||||
base = model._meta.verbose_name_plural.lower().replace(' ', '_')
|
base = model._meta.verbose_name_plural.lower().replace(' ', '_')
|
||||||
|
@ -18,6 +18,7 @@ from dcim.models import Device, DeviceRole, Platform
|
|||||||
from extras.choices import LogLevelChoices
|
from extras.choices import LogLevelChoices
|
||||||
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
||||||
from extras.dashboard.utils import get_widget_class
|
from extras.dashboard.utils import get_widget_class
|
||||||
|
from extras.utils import SharedObjectViewMixin
|
||||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox.views.generic.mixins import TableMixin
|
from netbox.views.generic.mixins import TableMixin
|
||||||
@ -285,39 +286,22 @@ class ExportTemplateBulkSyncDataView(generic.BulkSyncDataView):
|
|||||||
# Saved filters
|
# 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)
|
@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 = filtersets.SavedFilterFilterSet
|
||||||
filterset_form = forms.SavedFilterFilterForm
|
filterset_form = forms.SavedFilterFilterForm
|
||||||
table = tables.SavedFilterTable
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter)
|
@register_model_view(SavedFilter)
|
||||||
class SavedFilterView(SavedFilterMixin, generic.ObjectView):
|
class SavedFilterView(SharedObjectViewMixin, generic.ObjectView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'add', detail=False)
|
@register_model_view(SavedFilter, 'add', detail=False)
|
||||||
@register_model_view(SavedFilter, 'edit')
|
@register_model_view(SavedFilter, 'edit')
|
||||||
class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView):
|
class SavedFilterEditView(SharedObjectViewMixin, generic.ObjectEditView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
form = forms.SavedFilterForm
|
form = forms.SavedFilterForm
|
||||||
|
|
||||||
@ -328,18 +312,18 @@ class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView):
|
|||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'delete')
|
@register_model_view(SavedFilter, 'delete')
|
||||||
class SavedFilterDeleteView(SavedFilterMixin, generic.ObjectDeleteView):
|
class SavedFilterDeleteView(SharedObjectViewMixin, generic.ObjectDeleteView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'bulk_import', detail=False)
|
@register_model_view(SavedFilter, 'bulk_import', detail=False)
|
||||||
class SavedFilterBulkImportView(SavedFilterMixin, generic.BulkImportView):
|
class SavedFilterBulkImportView(SharedObjectViewMixin, generic.BulkImportView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
model_form = forms.SavedFilterImportForm
|
model_form = forms.SavedFilterImportForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'bulk_edit', path='edit', detail=False)
|
@register_model_view(SavedFilter, 'bulk_edit', path='edit', detail=False)
|
||||||
class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView):
|
class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
filterset = filtersets.SavedFilterFilterSet
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
table = tables.SavedFilterTable
|
table = tables.SavedFilterTable
|
||||||
@ -347,12 +331,61 @@ class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView):
|
|||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
|
||||||
class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView):
|
class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
filterset = filtersets.SavedFilterFilterSet
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
table = tables.SavedFilterTable
|
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
|
# Bookmarks
|
||||||
#
|
#
|
||||||
|
@ -349,6 +349,7 @@ CUSTOMIZATION_MENU = Menu(
|
|||||||
get_model_item('extras', 'customlink', _('Custom Links')),
|
get_model_item('extras', 'customlink', _('Custom Links')),
|
||||||
get_model_item('extras', 'exporttemplate', _('Export Templates')),
|
get_model_item('extras', 'exporttemplate', _('Export Templates')),
|
||||||
get_model_item('extras', 'savedfilter', _('Saved Filters')),
|
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', 'tag', 'Tags'),
|
||||||
get_model_item('extras', 'imageattachment', _('Image Attachments'), actions=()),
|
get_model_item('extras', 'imageattachment', _('Image Attachments'), actions=()),
|
||||||
),
|
),
|
||||||
|
77
netbox/templates/extras/tableconfig.html
Normal file
77
netbox/templates/extras/tableconfig.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Table Config" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Object Type" %}</th>
|
||||||
|
<td>{{ object.object_type }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Table" %}</th>
|
||||||
|
<td>{{ object.table }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "User" %}</th>
|
||||||
|
<td>{{ object.user|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Enabled" %}</th>
|
||||||
|
<td>{% checkmark object.enabled %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Shared" %}</th>
|
||||||
|
<td>{% checkmark object.shared %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Weight" %}</th>
|
||||||
|
<td>{{ object.weight }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Columns" %}</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul>
|
||||||
|
{% for column in object.columns %}
|
||||||
|
<li>{{ column }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Ordering" %}</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul>
|
||||||
|
{% for column in object.ordering %}
|
||||||
|
<li>{{ column }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user