Просмотр исходного кода

Merge pull request #3755 from netbox-community/3664-configcontext-tags

3664 configcontext tags
Jeremy Stretch 6 лет назад
Родитель
Сommit
44e5a63a2a

+ 7 - 1
netbox/extras/api/serializers.py

@@ -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',
         ]
         ]
 
 
 
 

+ 6 - 1
netbox/extras/filters.py

@@ -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

+ 18 - 3
netbox/extras/forms.py

@@ -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",
+        )
+    )
 
 
 
 
 #
 #

+ 2 - 1
netbox/extras/middleware.py

@@ -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 - 0
netbox/extras/migrations/0034_configcontext_tags.py

@@ -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'),
+        ),
+    ]

+ 5 - 0
netbox/extras/models.py

@@ -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()

+ 1 - 0
netbox/extras/querysets.py

@@ -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')

+ 5 - 0
netbox/extras/tests/test_api.py

@@ -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 - 0
netbox/extras/utils.py

@@ -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

+ 14 - 0
netbox/templates/extras/configcontext.html

@@ -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>

+ 1 - 0
netbox/templates/extras/configcontext_edit.html

@@ -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">

+ 3 - 2
netbox/utilities/utils.py

@@ -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

+ 2 - 1
netbox/utilities/views.py

@@ -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