mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
Initial work on saved filters
This commit is contained in:
parent
ea61a540cd
commit
57009643fb
@ -144,7 +144,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Site
|
model = Site
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
|
@ -13,6 +13,7 @@ __all__ = [
|
|||||||
'NestedImageAttachmentSerializer',
|
'NestedImageAttachmentSerializer',
|
||||||
'NestedJobResultSerializer',
|
'NestedJobResultSerializer',
|
||||||
'NestedJournalEntrySerializer',
|
'NestedJournalEntrySerializer',
|
||||||
|
'NestedSavedFilterSerializer',
|
||||||
'NestedTagSerializer', # Defined in netbox.api.serializers
|
'NestedTagSerializer', # Defined in netbox.api.serializers
|
||||||
'NestedWebhookSerializer',
|
'NestedWebhookSerializer',
|
||||||
]
|
]
|
||||||
@ -58,6 +59,14 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedSavedFilterSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.SavedFilter
|
||||||
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ __all__ = (
|
|||||||
'ReportDetailSerializer',
|
'ReportDetailSerializer',
|
||||||
'ReportSerializer',
|
'ReportSerializer',
|
||||||
'ReportInputSerializer',
|
'ReportInputSerializer',
|
||||||
|
'SavedFilterSerializer',
|
||||||
'ScriptDetailSerializer',
|
'ScriptDetailSerializer',
|
||||||
'ScriptInputSerializer',
|
'ScriptInputSerializer',
|
||||||
'ScriptLogMessageSerializer',
|
'ScriptLogMessageSerializer',
|
||||||
@ -149,6 +150,25 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Saved filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class SavedFilterSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||||
|
content_types = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'content_types', 'name', 'description', 'user', 'weight',
|
||||||
|
'enabled', 'shared', 'parameters', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -5,43 +5,19 @@ from . import views
|
|||||||
router = NetBoxRouter()
|
router = NetBoxRouter()
|
||||||
router.APIRootView = views.ExtrasRootView
|
router.APIRootView = views.ExtrasRootView
|
||||||
|
|
||||||
# Webhooks
|
|
||||||
router.register('webhooks', views.WebhookViewSet)
|
router.register('webhooks', views.WebhookViewSet)
|
||||||
|
|
||||||
# Custom fields
|
|
||||||
router.register('custom-fields', views.CustomFieldViewSet)
|
router.register('custom-fields', views.CustomFieldViewSet)
|
||||||
|
|
||||||
# Custom links
|
|
||||||
router.register('custom-links', views.CustomLinkViewSet)
|
router.register('custom-links', views.CustomLinkViewSet)
|
||||||
|
|
||||||
# Export templates
|
|
||||||
router.register('export-templates', views.ExportTemplateViewSet)
|
router.register('export-templates', views.ExportTemplateViewSet)
|
||||||
|
router.register('saved-filter', views.SavedFilterViewSet)
|
||||||
# Tags
|
|
||||||
router.register('tags', views.TagViewSet)
|
router.register('tags', views.TagViewSet)
|
||||||
|
|
||||||
# Image attachments
|
|
||||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||||
|
|
||||||
# Journal entries
|
|
||||||
router.register('journal-entries', views.JournalEntryViewSet)
|
router.register('journal-entries', views.JournalEntryViewSet)
|
||||||
|
|
||||||
# Config contexts
|
|
||||||
router.register('config-contexts', views.ConfigContextViewSet)
|
router.register('config-contexts', views.ConfigContextViewSet)
|
||||||
|
|
||||||
# Reports
|
|
||||||
router.register('reports', views.ReportViewSet, basename='report')
|
router.register('reports', views.ReportViewSet, basename='report')
|
||||||
|
|
||||||
# Scripts
|
|
||||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||||
|
|
||||||
# Change logging
|
|
||||||
router.register('object-changes', views.ObjectChangeViewSet)
|
router.register('object-changes', views.ObjectChangeViewSet)
|
||||||
|
|
||||||
# Job Results
|
|
||||||
router.register('job-results', views.JobResultViewSet)
|
router.register('job-results', views.JobResultViewSet)
|
||||||
|
|
||||||
# ContentTypes
|
|
||||||
router.register('content-types', views.ContentTypeViewSet)
|
router.register('content-types', views.ContentTypeViewSet)
|
||||||
|
|
||||||
app_name = 'extras-api'
|
app_name = 'extras-api'
|
||||||
|
@ -98,6 +98,17 @@ class ExportTemplateViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.ExportTemplateFilterSet
|
filterset_class = filtersets.ExportTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Saved filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class SavedFilterViewSet(NetBoxModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
serializer_class = serializers.SavedFilterSerializer
|
||||||
|
filterset_class = filtersets.SavedFilterFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -23,6 +23,7 @@ __all__ = (
|
|||||||
'JournalEntryFilterSet',
|
'JournalEntryFilterSet',
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
|
'SavedFilterFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
'WebhookFilterSet',
|
'WebhookFilterSet',
|
||||||
)
|
)
|
||||||
@ -138,6 +139,29 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterFilterSet(BaseFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = ['id', 'content_types', 'name', 'description', 'user', '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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect,
|
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, StaticSelect,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -14,6 +12,7 @@ __all__ = (
|
|||||||
'CustomLinkBulkEditForm',
|
'CustomLinkBulkEditForm',
|
||||||
'ExportTemplateBulkEditForm',
|
'ExportTemplateBulkEditForm',
|
||||||
'JournalEntryBulkEditForm',
|
'JournalEntryBulkEditForm',
|
||||||
|
'SavedFilterBulkEditForm',
|
||||||
'TagBulkEditForm',
|
'TagBulkEditForm',
|
||||||
'WebhookBulkEditForm',
|
'WebhookBulkEditForm',
|
||||||
)
|
)
|
||||||
@ -96,6 +95,30 @@ class ExportTemplateBulkEditForm(BulkEditForm):
|
|||||||
nullable_fields = ('description', 'mime_type', 'file_extension')
|
nullable_fields = ('description', 'mime_type', 'file_extension')
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkEditForm(BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=SavedFilter.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
shared = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
|
||||||
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
class WebhookBulkEditForm(BulkEditForm):
|
class WebhookBulkEditForm(BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Webhook.objects.all(),
|
queryset=Webhook.objects.all(),
|
||||||
|
@ -12,6 +12,7 @@ __all__ = (
|
|||||||
'CustomFieldCSVForm',
|
'CustomFieldCSVForm',
|
||||||
'CustomLinkCSVForm',
|
'CustomLinkCSVForm',
|
||||||
'ExportTemplateCSVForm',
|
'ExportTemplateCSVForm',
|
||||||
|
'SavedFilterCSVForm',
|
||||||
'TagCSVForm',
|
'TagCSVForm',
|
||||||
'WebhookCSVForm',
|
'WebhookCSVForm',
|
||||||
)
|
)
|
||||||
@ -81,6 +82,19 @@ class ExportTemplateCSVForm(CSVModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterCSVForm(CSVModelForm):
|
||||||
|
content_types = CSVMultipleContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
help_text="One or more assigned object types"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = (
|
||||||
|
'name', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookCSVForm(CSVModelForm):
|
class WebhookCSVForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -25,6 +25,7 @@ __all__ = (
|
|||||||
'JournalEntryFilterForm',
|
'JournalEntryFilterForm',
|
||||||
'LocalConfigContextFilterForm',
|
'LocalConfigContextFilterForm',
|
||||||
'ObjectChangeFilterForm',
|
'ObjectChangeFilterForm',
|
||||||
|
'SavedFilterFilterForm',
|
||||||
'TagFilterForm',
|
'TagFilterForm',
|
||||||
'WebhookFilterForm',
|
'WebhookFilterForm',
|
||||||
)
|
)
|
||||||
@ -170,6 +171,33 @@ class ExportTemplateFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterFilterForm(FilterForm):
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q',)),
|
||||||
|
('Attributes', ('content_types', 'enabled', 'shared', 'weight')),
|
||||||
|
)
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('export_templates'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
shared = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookFilterForm(FilterForm):
|
class WebhookFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
@ -20,6 +21,7 @@ __all__ = (
|
|||||||
'ExportTemplateForm',
|
'ExportTemplateForm',
|
||||||
'ImageAttachmentForm',
|
'ImageAttachmentForm',
|
||||||
'JournalEntryForm',
|
'JournalEntryForm',
|
||||||
|
'SavedFilterForm',
|
||||||
'TagForm',
|
'TagForm',
|
||||||
'WebhookForm',
|
'WebhookForm',
|
||||||
)
|
)
|
||||||
@ -108,6 +110,34 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Saved Filter', ('name', 'content_types', 'description', 'weight', 'enabled', 'shared')),
|
||||||
|
('Parameters', ('parameters',)),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
exclude = ('user',)
|
||||||
|
widgets = {
|
||||||
|
'parameters': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
|
|
||||||
|
# Convert any parameters delivered via initial data to a dictionary
|
||||||
|
if initial and 'parameters' in initial:
|
||||||
|
if type(initial['parameters']) is str:
|
||||||
|
# TODO: Make a utility function for this
|
||||||
|
initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
|
||||||
|
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -20,6 +20,9 @@ class ExtrasQuery(graphene.ObjectType):
|
|||||||
image_attachment = ObjectField(ImageAttachmentType)
|
image_attachment = ObjectField(ImageAttachmentType)
|
||||||
image_attachment_list = ObjectListField(ImageAttachmentType)
|
image_attachment_list = ObjectListField(ImageAttachmentType)
|
||||||
|
|
||||||
|
saved_filter = ObjectField(SavedFilterType)
|
||||||
|
saved_filter_list = ObjectListField(SavedFilterType)
|
||||||
|
|
||||||
journal_entry = ObjectField(JournalEntryType)
|
journal_entry = ObjectField(JournalEntryType)
|
||||||
journal_entry_list = ObjectListField(JournalEntryType)
|
journal_entry_list = ObjectListField(JournalEntryType)
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ __all__ = (
|
|||||||
'ImageAttachmentType',
|
'ImageAttachmentType',
|
||||||
'JournalEntryType',
|
'JournalEntryType',
|
||||||
'ObjectChangeType',
|
'ObjectChangeType',
|
||||||
|
'SavedFilterType',
|
||||||
'TagType',
|
'TagType',
|
||||||
'WebhookType',
|
'WebhookType',
|
||||||
)
|
)
|
||||||
@ -71,6 +72,14 @@ class ObjectChangeType(BaseObjectType):
|
|||||||
filterset_class = filtersets.ObjectChangeFilterSet
|
filterset_class = filtersets.ObjectChangeFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterType(ObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.SavedFilter
|
||||||
|
exclude = ('content_types', )
|
||||||
|
filterset_class = filtersets.SavedFilterFilterSet
|
||||||
|
|
||||||
|
|
||||||
class TagType(ObjectType):
|
class TagType(ObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
36
netbox/extras/migrations/0083_savedfilter.py
Normal file
36
netbox/extras/migrations/0083_savedfilter.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 4.1.1 on 2022-10-27 18:18
|
||||||
|
|
||||||
|
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),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0082_exporttemplate_content_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SavedFilter',
|
||||||
|
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)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('weight', models.PositiveSmallIntegerField(default=100)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
('shared', models.BooleanField(default=True)),
|
||||||
|
('parameters', models.JSONField()),
|
||||||
|
('content_types', models.ManyToManyField(related_name='saved_filters', to='contenttypes.contenttype')),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('weight', 'name'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -18,6 +18,7 @@ __all__ = (
|
|||||||
'JournalEntry',
|
'JournalEntry',
|
||||||
'ObjectChange',
|
'ObjectChange',
|
||||||
'Report',
|
'Report',
|
||||||
|
'SavedFilter',
|
||||||
'Script',
|
'Script',
|
||||||
'Tag',
|
'Tag',
|
||||||
'TaggedItem',
|
'TaggedItem',
|
||||||
|
@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, QueryDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'JobResult',
|
'JobResult',
|
||||||
'JournalEntry',
|
'JournalEntry',
|
||||||
'Report',
|
'Report',
|
||||||
|
'SavedFilter',
|
||||||
'Script',
|
'Script',
|
||||||
'Webhook',
|
'Webhook',
|
||||||
)
|
)
|
||||||
@ -350,6 +351,69 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
|
"""
|
||||||
|
A set of predefined keyword parameters that can be reused to filter for specific objects.
|
||||||
|
"""
|
||||||
|
content_types = models.ManyToManyField(
|
||||||
|
to=ContentType,
|
||||||
|
related_name='saved_filters',
|
||||||
|
help_text='The object type(s) to which this filter applies.'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
to=User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=100
|
||||||
|
)
|
||||||
|
enabled = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
shared = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
parameters = models.JSONField()
|
||||||
|
|
||||||
|
clone_fields = (
|
||||||
|
'enabled', 'weight',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('weight', 'name')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:savedfilter', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Verify that `parameters` is a JSON object
|
||||||
|
if type(self.parameters) is not dict:
|
||||||
|
raise ValidationError(
|
||||||
|
{'parameters': 'Filter parameters must be stored as a dictionary of keyword arguments.'}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_params(self):
|
||||||
|
qd = QueryDict(mutable=True)
|
||||||
|
qd.update(self.parameters)
|
||||||
|
return qd.urlencode()
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
|
@ -13,16 +13,13 @@ __all__ = (
|
|||||||
'ExportTemplateTable',
|
'ExportTemplateTable',
|
||||||
'JournalEntryTable',
|
'JournalEntryTable',
|
||||||
'ObjectChangeTable',
|
'ObjectChangeTable',
|
||||||
|
'SavedFilterTable',
|
||||||
'TaggedItemTable',
|
'TaggedItemTable',
|
||||||
'TagTable',
|
'TagTable',
|
||||||
'WebhookTable',
|
'WebhookTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomFieldTable(NetBoxTable):
|
class CustomFieldTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -40,10 +37,6 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class JobResultTable(NetBoxTable):
|
class JobResultTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -61,10 +54,6 @@ class JobResultTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',)
|
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom links
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomLinkTable(NetBoxTable):
|
class CustomLinkTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -82,10 +71,6 @@ class CustomLinkTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window')
|
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window')
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Export templates
|
|
||||||
#
|
|
||||||
|
|
||||||
class ExportTemplateTable(NetBoxTable):
|
class ExportTemplateTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -104,9 +89,24 @@ class ExportTemplateTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
class SavedFilterTable(NetBoxTable):
|
||||||
# Webhooks
|
name = tables.Column(
|
||||||
#
|
linkify=True
|
||||||
|
)
|
||||||
|
content_types = columns.ContentTypesColumn()
|
||||||
|
enabled = columns.BooleanColumn()
|
||||||
|
shared = columns.BooleanColumn()
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = SavedFilter
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared',
|
||||||
|
'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookTable(NetBoxTable):
|
class WebhookTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
@ -139,10 +139,6 @@ class WebhookTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Tags
|
|
||||||
#
|
|
||||||
|
|
||||||
class TagTable(NetBoxTable):
|
class TagTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
|
@ -31,6 +31,14 @@ urlpatterns = [
|
|||||||
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
|
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
|
||||||
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
||||||
|
|
||||||
|
# Saved filters
|
||||||
|
path('saved-filters/', views.SavedFilterListView.as_view(), name='savedfilter_list'),
|
||||||
|
path('saved-filters/add/', views.SavedFilterEditView.as_view(), name='savedfilter_add'),
|
||||||
|
path('saved-filters/import/', views.SavedFilterBulkImportView.as_view(), name='savedfilter_import'),
|
||||||
|
path('saved-filters/edit/', views.SavedFilterBulkEditView.as_view(), name='savedfilter_bulk_edit'),
|
||||||
|
path('saved-filters/delete/', views.SavedFilterBulkDeleteView.as_view(), name='savedfilter_bulk_delete'),
|
||||||
|
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
|
||||||
|
|
||||||
# Webhooks
|
# Webhooks
|
||||||
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
||||||
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count
|
||||||
from django.http import Http404, HttpResponseForbidden
|
from django.http import Http404, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -9,7 +9,6 @@ from django_rq.queues import get_connection
|
|||||||
from rq import Worker
|
from rq import Worker
|
||||||
|
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
|
||||||
from utilities.htmx import is_htmx
|
from utilities.htmx import is_htmx
|
||||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||||
@ -159,6 +158,57 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.ExportTemplateTable
|
table = tables.ExportTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Saved filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class SavedFilterListView(generic.ObjectListView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
|
filterset_form = forms.SavedFilterFilterForm
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SavedFilter)
|
||||||
|
class SavedFilterView(generic.ObjectView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SavedFilter, 'edit')
|
||||||
|
class SavedFilterEditView(generic.ObjectEditView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
form = forms.SavedFilterForm
|
||||||
|
|
||||||
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||||
|
if not obj.pk:
|
||||||
|
obj.user = request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SavedFilter, 'delete')
|
||||||
|
class SavedFilterDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
model_form = forms.SavedFilterCSVForm
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
form = forms.SavedFilterBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Webhooks
|
# Webhooks
|
||||||
#
|
#
|
||||||
|
@ -4,10 +4,11 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django_filters.exceptions import FieldLookupError
|
from django_filters.exceptions import FieldLookupError
|
||||||
from django_filters.utils import get_model_field, resolve_field
|
from django_filters.utils import get_model_field, resolve_field
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from extras.choices import CustomFieldFilterLogicChoices
|
from extras.choices import CustomFieldFilterLogicChoices
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField, SavedFilter
|
||||||
from utilities.constants import (
|
from utilities.constants import (
|
||||||
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
||||||
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||||
@ -80,12 +81,28 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
|
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
|
||||||
# however FilterSet Factory is setup before this which creates the
|
# however FilterSet Factory is setup before this which creates the
|
||||||
# initial filters. This recreates the filters so Empty is picked up correctly.
|
# initial filters. This recreates the filters so Empty is picked up correctly.
|
||||||
self.base_filters = self.__class__.get_filters()
|
self.base_filters = self.__class__.get_filters()
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
# Apply any referenced SavedFilters
|
||||||
|
if 'filter' in data:
|
||||||
|
data = data.copy() # Get a mutable copy
|
||||||
|
saved_filters = SavedFilter.objects.filter(pk__in=data.pop('filter'))
|
||||||
|
for sf in saved_filters:
|
||||||
|
for key, value in sf.parameters.items():
|
||||||
|
# QueryDicts are... fun
|
||||||
|
if type(value) not in (list, tuple):
|
||||||
|
value = [value]
|
||||||
|
if key in data:
|
||||||
|
for v in value:
|
||||||
|
data.appendlist(key, v)
|
||||||
|
else:
|
||||||
|
data.setlist(key, value)
|
||||||
|
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_filter_lookup_dict(existing_filter):
|
def _get_filter_lookup_dict(existing_filter):
|
||||||
|
@ -4,7 +4,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
|
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
|
||||||
from extras.forms.customfields import CustomFieldsMixin
|
from extras.forms.customfields import CustomFieldsMixin
|
||||||
from extras.models import CustomField, Tag
|
from extras.models import CustomField, SavedFilter, Tag
|
||||||
from utilities.forms import BootstrapMixin, CSVModelForm
|
from utilities.forms import BootstrapMixin, CSVModelForm
|
||||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||||
|
|
||||||
@ -128,6 +128,19 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
|
filter = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=SavedFilter.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit saved filters to those applicable to the form's model
|
||||||
|
content_type = ContentType.objects.get_for_model(self.model)
|
||||||
|
self.fields['filter'].widget.add_query_params({
|
||||||
|
'content_type_id': content_type.pk,
|
||||||
|
})
|
||||||
|
|
||||||
def _get_custom_fields(self, content_type):
|
def _get_custom_fields(self, content_type):
|
||||||
return super()._get_custom_fields(content_type).exclude(
|
return super()._get_custom_fields(content_type).exclude(
|
||||||
|
@ -278,6 +278,7 @@ OTHER_MENU = Menu(
|
|||||||
get_model_item('extras', 'customfield', 'Custom Fields'),
|
get_model_item('extras', 'customfield', 'Custom Fields'),
|
||||||
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'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
@ -4,17 +4,17 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import FieldDoesNotExist, ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError
|
from django.db.models import ManyToManyField, ProtectedError
|
||||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||||
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, model_to_dict
|
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django_tables2.export import TableExport
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate, SavedFilter
|
||||||
from extras.signals import clear_webhooks
|
from extras.signals import clear_webhooks
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
@ -330,7 +330,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
return headers, records
|
return headers, records
|
||||||
|
|
||||||
def _update_objects(self, form, request, headers, records):
|
def _update_objects(self, form, request, headers, records):
|
||||||
from utilities.forms import CSVModelChoiceField
|
|
||||||
updated_objs = []
|
updated_objs = []
|
||||||
|
|
||||||
ids = [int(record["id"]) for record in records]
|
ids = [int(record["id"]) for record in records]
|
||||||
|
70
netbox/templates/extras/savedfilter.html
Normal file
70
netbox/templates/extras/savedfilter.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-5">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Saved Filter</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">User</th>
|
||||||
|
<td>{{ object.user|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Enabled</th>
|
||||||
|
<td>{% checkmark object.enabled %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Shared</th>
|
||||||
|
<td>{% checkmark object.enabled %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Weight</th>
|
||||||
|
<td>{{ object.weight }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Assigned Models</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-7">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Parameters
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.parameters }}</pre>
|
||||||
|
</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 %}
|
@ -64,7 +64,7 @@ Context:
|
|||||||
|
|
||||||
{# Applied filters #}
|
{# Applied filters #}
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
{% applied_filters filter_form request.GET %}
|
{% applied_filters model filter_form request.GET %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# "Select all" form #}
|
{# "Select all" form #}
|
||||||
|
@ -10,5 +10,10 @@
|
|||||||
<i class="mdi mdi-tag-off"></i> Clear all
|
<i class="mdi mdi-tag-off"></i> Clear all
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if save_link %}
|
||||||
|
<a href="{{ save_link }}" class="badge rounded-pill bg-success text-decoration-none me-1">
|
||||||
|
<i class="mdi mdi-content-save"></i> Save
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
from urllib.parse import quote
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.template.defaultfilters import date
|
from django.template.defaultfilters import date
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -278,12 +280,13 @@ def table_config_form(table, table_name=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('helpers/applied_filters.html')
|
@register.inclusion_tag('helpers/applied_filters.html', takes_context=True)
|
||||||
def applied_filters(form, query_params):
|
def applied_filters(context, model, form, query_params):
|
||||||
"""
|
"""
|
||||||
Display the active filters for a given filter form.
|
Display the active filters for a given filter form.
|
||||||
"""
|
"""
|
||||||
form.is_valid()
|
user = context['request'].user
|
||||||
|
form.is_valid() # Ensure cleaned_data has been set
|
||||||
|
|
||||||
applied_filters = []
|
applied_filters = []
|
||||||
for filter_name in form.changed_data:
|
for filter_name in form.changed_data:
|
||||||
@ -305,6 +308,14 @@ def applied_filters(form, query_params):
|
|||||||
'link_text': f'{bound_field.label}: {display_value}',
|
'link_text': f'{bound_field.label}: {display_value}',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
save_link = None
|
||||||
|
if len(applied_filters) > 1 and user.has_perm('extras.add_saved_filter') and 'filter' not in context['request'].GET:
|
||||||
|
content_type = ContentType.objects.get_for_model(model).pk
|
||||||
|
parameters = context['request'].GET.urlencode()
|
||||||
|
url = reverse('extras:savedfilter_add')
|
||||||
|
save_link = f"{url}?content_types={content_type}¶meters={quote(parameters)}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'applied_filters': applied_filters,
|
'applied_filters': applied_filters,
|
||||||
|
'save_link': save_link,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user