mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 11:42:52 -06:00
Merge pull request #3755 from netbox-community/3664-configcontext-tags
3664 configcontext tags
This commit is contained in:
commit
44e5a63a2a
@ -173,12 +173,18 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
tags = serializers.SlugRelatedField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
slug_field='slug',
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
|
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
|
||||||
'tenant_groups', 'tenants', 'data',
|
'tenant_groups', 'tenants', 'tags', 'data',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ from django.db.models import Q
|
|||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
|
||||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
|
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
@ -180,6 +179,12 @@ class ConfigContextFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='tags__slug',
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tag (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
|
@ -14,7 +14,6 @@ from utilities.forms import (
|
|||||||
BOOLEAN_WITH_BLANK_CHOICES,
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
|
||||||
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
@ -238,6 +237,14 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
tags = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/extras/tags/"
|
||||||
|
)
|
||||||
|
)
|
||||||
data = JSONField(
|
data = JSONField(
|
||||||
label=''
|
label=''
|
||||||
)
|
)
|
||||||
@ -246,7 +253,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
|
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
|
||||||
'tenants', 'data',
|
'tenants', 'tags', 'data',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'regions': APISelectMultiple(
|
'regions': APISelectMultiple(
|
||||||
@ -266,7 +273,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
),
|
),
|
||||||
'tenants': APISelectMultiple(
|
'tenants': APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenants/"
|
api_url="/api/tenancy/tenants/"
|
||||||
)
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -347,6 +354,14 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
|||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = FilterChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/extras/tags/",
|
||||||
|
value_field="slug",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -9,6 +9,7 @@ from django.db.models.signals import pre_delete, post_save
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||||
|
|
||||||
|
from extras.utils import is_taggable
|
||||||
from utilities.querysets import DummyQuerySet
|
from utilities.querysets import DummyQuerySet
|
||||||
from .choices import ObjectChangeActionChoices
|
from .choices import ObjectChangeActionChoices
|
||||||
from .models import ObjectChange
|
from .models import ObjectChange
|
||||||
@ -41,7 +42,7 @@ def handle_deleted_object(sender, instance, **kwargs):
|
|||||||
copy = deepcopy(instance)
|
copy = deepcopy(instance)
|
||||||
|
|
||||||
# Preserve tags
|
# Preserve tags
|
||||||
if hasattr(instance, 'tags'):
|
if is_taggable(instance):
|
||||||
copy.tags = DummyQuerySet(instance.tags.all())
|
copy.tags = DummyQuerySet(instance.tags.all())
|
||||||
|
|
||||||
# Queue the copy of the object for processing once the request completes
|
# Queue the copy of the object for processing once the request completes
|
||||||
|
18
netbox/extras/migrations/0034_configcontext_tags.py
Normal file
18
netbox/extras/migrations/0034_configcontext_tags.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-12-11 09:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0033_graph_type_to_fk'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='configcontext',
|
||||||
|
name='tags',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='_configcontext_tags_+', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -682,6 +682,11 @@ class ConfigContext(models.Model):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
tags = models.ManyToManyField(
|
||||||
|
to='extras.Tag',
|
||||||
|
related_name='+',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
|
|
||||||
objects = ConfigContextQuerySet.as_manager()
|
objects = ConfigContextQuerySet.as_manager()
|
||||||
|
@ -46,5 +46,6 @@ class ConfigContextQuerySet(QuerySet):
|
|||||||
Q(platforms=obj.platform) | Q(platforms=None),
|
Q(platforms=obj.platform) | Q(platforms=None),
|
||||||
Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
|
Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
|
||||||
Q(tenants=obj.tenant) | Q(tenants=None),
|
Q(tenants=obj.tenant) | Q(tenants=None),
|
||||||
|
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).order_by('weight', 'name')
|
).order_by('weight', 'name')
|
||||||
|
@ -370,6 +370,8 @@ class ConfigContextTest(APITestCase):
|
|||||||
tenantgroup2 = TenantGroup.objects.create(name='Test Tenant Group 2', slug='test-tenant-group-2')
|
tenantgroup2 = TenantGroup.objects.create(name='Test Tenant Group 2', slug='test-tenant-group-2')
|
||||||
tenant1 = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
|
tenant1 = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
|
||||||
tenant2 = Tenant.objects.create(name='Test Tenant 2', slug='test-tenant-2')
|
tenant2 = Tenant.objects.create(name='Test Tenant 2', slug='test-tenant-2')
|
||||||
|
tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
|
||||||
|
tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'name': 'Test Config Context 4',
|
'name': 'Test Config Context 4',
|
||||||
@ -380,6 +382,7 @@ class ConfigContextTest(APITestCase):
|
|||||||
'platforms': [platform1.pk, platform2.pk],
|
'platforms': [platform1.pk, platform2.pk],
|
||||||
'tenant_groups': [tenantgroup1.pk, tenantgroup2.pk],
|
'tenant_groups': [tenantgroup1.pk, tenantgroup2.pk],
|
||||||
'tenants': [tenant1.pk, tenant2.pk],
|
'tenants': [tenant1.pk, tenant2.pk],
|
||||||
|
'tags': [tag1.slug, tag2.slug],
|
||||||
'data': {'foo': 'XXX'}
|
'data': {'foo': 'XXX'}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,6 +405,8 @@ class ConfigContextTest(APITestCase):
|
|||||||
self.assertEqual(tenantgroup2.pk, data['tenant_groups'][1])
|
self.assertEqual(tenantgroup2.pk, data['tenant_groups'][1])
|
||||||
self.assertEqual(tenant1.pk, data['tenants'][0])
|
self.assertEqual(tenant1.pk, data['tenants'][0])
|
||||||
self.assertEqual(tenant2.pk, data['tenants'][1])
|
self.assertEqual(tenant2.pk, data['tenants'][1])
|
||||||
|
self.assertEqual(tag1.slug, data['tags'][0])
|
||||||
|
self.assertEqual(tag2.slug, data['tags'][1])
|
||||||
self.assertEqual(configcontext4.data, data['data'])
|
self.assertEqual(configcontext4.data, data['data'])
|
||||||
|
|
||||||
def test_create_configcontext_bulk(self):
|
def test_create_configcontext_bulk(self):
|
||||||
|
15
netbox/extras/utils.py
Normal file
15
netbox/extras/utils.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from taggit.managers import _TaggableManager
|
||||||
|
from utilities.querysets import DummyQuerySet
|
||||||
|
|
||||||
|
|
||||||
|
def is_taggable(obj):
|
||||||
|
"""
|
||||||
|
Return True if the instance can have Tags assigned to it; False otherwise.
|
||||||
|
"""
|
||||||
|
if hasattr(obj, 'tags'):
|
||||||
|
if issubclass(obj.tags.__class__, _TaggableManager):
|
||||||
|
return True
|
||||||
|
# TaggableManager has been replaced with a DummyQuerySet prior to object deletion
|
||||||
|
if isinstance(obj.tags, DummyQuerySet):
|
||||||
|
return True
|
||||||
|
return False
|
@ -162,6 +162,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% if configcontext.tags.all %}
|
||||||
|
<ul>
|
||||||
|
{% for tag in configcontext.tags.all %}
|
||||||
|
<li><a href="{{ tag.get_absolute_url }}">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
{% render_field form.platforms %}
|
{% render_field form.platforms %}
|
||||||
{% render_field form.tenant_groups %}
|
{% render_field form.tenant_groups %}
|
||||||
{% render_field form.tenants %}
|
{% render_field form.tenants %}
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -6,6 +6,7 @@ from django.core.serializers import serialize
|
|||||||
from django.db.models import Count, OuterRef, Subquery
|
from django.db.models import Count, OuterRef, Subquery
|
||||||
|
|
||||||
from dcim.choices import CableLengthUnitChoices
|
from dcim.choices import CableLengthUnitChoices
|
||||||
|
from extras.utils import is_taggable
|
||||||
|
|
||||||
|
|
||||||
def csv_format(data):
|
def csv_format(data):
|
||||||
@ -103,7 +104,7 @@ def serialize_object(obj, extra=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Include any tags
|
# Include any tags
|
||||||
if hasattr(obj, 'tags'):
|
if is_taggable(obj):
|
||||||
data['tags'] = [tag.name for tag in obj.tags.all()]
|
data['tags'] = [tag.name for tag in obj.tags.all()]
|
||||||
|
|
||||||
# Append any extra data
|
# Append any extra data
|
||||||
@ -201,7 +202,7 @@ def prepare_cloned_fields(instance):
|
|||||||
params[field_name] = field_value
|
params[field_name] = field_value
|
||||||
|
|
||||||
# Copy tags
|
# Copy tags
|
||||||
if hasattr(instance, 'tags'):
|
if is_taggable(instance):
|
||||||
params['tags'] = ','.join([t.name for t in instance.tags.all()])
|
params['tags'] = ','.join([t.name for t in instance.tags.all()])
|
||||||
|
|
||||||
# Concatenate parameters into a URL query string
|
# Concatenate parameters into a URL query string
|
||||||
|
@ -24,6 +24,7 @@ from django_tables2 import RequestConfig
|
|||||||
|
|
||||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||||
from extras.querysets import CustomFieldQueryset
|
from extras.querysets import CustomFieldQueryset
|
||||||
|
from extras.utils import is_taggable
|
||||||
from utilities.exceptions import AbortTransaction
|
from utilities.exceptions import AbortTransaction
|
||||||
from utilities.forms import BootstrapMixin, CSVDataField
|
from utilities.forms import BootstrapMixin, CSVDataField
|
||||||
from utilities.utils import csv_format, prepare_cloned_fields
|
from utilities.utils import csv_format, prepare_cloned_fields
|
||||||
@ -144,7 +145,7 @@ class ObjectListView(View):
|
|||||||
table.columns.show('pk')
|
table.columns.show('pk')
|
||||||
|
|
||||||
# Construct queryset for tags list
|
# Construct queryset for tags list
|
||||||
if hasattr(model, 'tags'):
|
if is_taggable(model):
|
||||||
tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
|
tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
|
||||||
else:
|
else:
|
||||||
tags = None
|
tags = None
|
||||||
|
Loading…
Reference in New Issue
Block a user