Repository: netbox-community/netbox

Issue:
Contact assignments ignore tags usage limits in list
### NetBox version

v3.6.1

### Python version

3.11

### Steps to Reproduce

1. Create any tag
2. Limit tag use to anything, except Tenant > Contact Assignment
3. Create any object or open existing, that accept contacts (IP, site, device, VM etc.)
4. Open Contacts tab
5. Press "Add Contact"
6. Open list in "Tags" field

### Expected Behavior

List must be limited to tags, that have "Any" or "Contact Assignment" usage.

### Observed Behavior

List contains all existing tags, including one, created in steps 1-2 (it can not be saved, at least)


Discussion:

Resolving patch:
diff --git a/netbox/extras/forms/mixins.py b/netbox/extras/forms/mixins.py
--- a/netbox/extras/forms/mixins.py
+++ b/netbox/extras/forms/mixins.py
@@ -9,6 +9,7 @@
 __all__ = (
     'CustomFieldsMixin',
     'SavedFiltersMixin',
+    'TagsMixin',
 )
 
 
@@ -72,3 +73,19 @@ class SavedFiltersMixin(forms.Form):
             'usable': True,
         }
     )
+
+
+class TagsMixin(forms.Form):
+    tags = DynamicModelMultipleChoiceField(
+        queryset=Tag.objects.all(),
+        required=False,
+        label=_('Tags'),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Limit tags to those applicable to the object type
+        content_type = ContentType.objects.get_for_model(self._meta.model)
+        if content_type and hasattr(self.fields['tags'].widget, 'add_query_param'):
+            self.fields['tags'].widget.add_query_param('for_object_type_id', content_type.pk)
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -4,10 +4,11 @@
 from django.utils.translation import gettext_lazy as _
 
 from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
-from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
+from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
 from extras.models import CustomField, Tag
-from utilities.forms import BootstrapMixin, CSVModelForm, CheckLastUpdatedMixin
+from utilities.forms import CSVModelForm
 from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.mixins import BootstrapMixin, CheckLastUpdatedMixin
 
 __all__ = (
     'NetBoxModelForm',
@@ -17,7 +18,7 @@
 )
 
 
-class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, forms.ModelForm):
+class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
     """
     Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
 
@@ -26,18 +27,6 @@ class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin,
             the rendered form (optional). If not defined, the all fields will be rendered as a single section.
     """
     fieldsets = ()
-    tags = DynamicModelMultipleChoiceField(
-        queryset=Tag.objects.all(),
-        required=False,
-        label=_('Tags'),
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        # Limit tags to those applicable to the object type
-        if (ct := self._get_content_type()) and hasattr(self.fields['tags'].widget, 'add_query_param'):
-            self.fields['tags'].widget.add_query_param('for_object_type_id', ct.pk)
 
     def _get_content_type(self):
         return ContentType.objects.get_for_model(self._meta.model)
diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py
--- a/netbox/tenancy/forms/model_forms.py
+++ b/netbox/tenancy/forms/model_forms.py
@@ -1,10 +1,11 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
+from extras.forms.mixins import TagsMixin
 from extras.models import Tag
 from netbox.forms import NetBoxModelForm
 from tenancy.models import *
-from utilities.forms import BootstrapMixin
+from utilities.forms.mixins import BootstrapMixin
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
 
 __all__ = (
@@ -121,7 +122,7 @@ class Meta:
         }
 
 
-class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
+class ContactAssignmentForm(BootstrapMixin, TagsMixin, forms.ModelForm):
     group = DynamicModelChoiceField(
         label=_('Group'),
         queryset=ContactGroup.objects.all(),
@@ -141,11 +142,6 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
         label=_('Role'),
         queryset=ContactRole.objects.all()
     )
-    tags = DynamicModelMultipleChoiceField(
-        queryset=Tag.objects.all(),
-        required=False,
-        label=_('Tags')
-    )
 
     class Meta:
         model = ContactAssignment


Test patch: