diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index cb9930ae2..217f55b70 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -1,6 +1,7 @@ from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType +from django.utils.safestring import mark_safe from mptt.forms import TreeNodeMultipleChoiceField from taggit.forms import TagField as TagField_ @@ -161,6 +162,17 @@ class TagForm(BootstrapMixin, forms.ModelForm): ] +class TagCSVForm(CSVModelForm): + slug = SlugField() + + class Meta: + model = Tag + fields = Tag.csv_headers + help_texts = { + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), + } + + class AddRemoveTagsForm(forms.Form): def __init__(self, *args, **kwargs): diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index d5792ebda..9bb90f21e 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -24,6 +24,8 @@ class Tag(TagBase, ChangeLoggedModel): objects = RestrictedQuerySet.as_manager() + csv_headers = ['name', 'slug', 'color', 'description'] + def get_absolute_url(self): return reverse('extras:tag', args=[self.slug]) @@ -34,6 +36,14 @@ class Tag(TagBase, ChangeLoggedModel): slug += "_%d" % i return slug + def to_csv(self): + return ( + self.name, + self.slug, + self.color, + self.description + ) + class TaggedItem(GenericTaggedItemBase): tag = models.ForeignKey( diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 9ad29fd80..b3abf5b22 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -10,16 +10,7 @@ from extras.models import ConfigContext, ObjectChange, Tag from utilities.testing import ViewTestCases, TestCase -# TODO: Change base class to PrimaryObjectViewTestCase -# Blocked by #3703 -class TagTestCase( - ViewTestCases.GetObjectViewTestCase, - ViewTestCases.EditObjectViewTestCase, - ViewTestCases.DeleteObjectViewTestCase, - ViewTestCases.ListObjectsViewTestCase, - ViewTestCases.BulkEditObjectsViewTestCase, - ViewTestCases.BulkDeleteObjectsViewTestCase -): +class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = Tag @classmethod @@ -38,6 +29,13 @@ class TagTestCase( 'comments': 'Some comments', } + cls.csv_data = ( + "name,slug,color,description", + "Tag 4,tag-4,ff0000,Fourth tag", + "Tag 5,tag-5,00ff00,Fifth tag", + "Tag 6,tag-6,0000ff,Sixth tag", + ) + cls.bulk_edit_data = { 'color': '00ff00', } diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 3eee303a3..3007e6524 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -9,6 +9,8 @@ urlpatterns = [ # Tags path('tags/', views.TagListView.as_view(), name='tag_list'), + path('tags/add/', views.TagEditView.as_view(), name='tag_add'), + path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'), path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), path('tags//', views.TagView.as_view(), name='tag'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index e80aa1d62..0e6700f06 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -13,14 +13,13 @@ from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.utils import shallow_compare_dict from utilities.views import ( - BulkDeleteView, BulkEditView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin, ) -from . import filters, forms +from . import filters, forms, tables from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem from .reports import get_report, get_reports from .scripts import get_scripts, run_script -from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable # @@ -35,8 +34,7 @@ class TagListView(ObjectListView): ) filterset = filters.TagFilterSet filterset_form = forms.TagFilterForm - table = TagTable - action_buttons = () + table = tables.TagTable class TagView(ObjectView): @@ -52,7 +50,7 @@ class TagView(ObjectView): ) # Generate a table of all items tagged with this Tag - items_table = TaggedItemTable(tagged_items) + items_table = tables.TaggedItemTable(tagged_items) paginate = { 'paginator_class': EnhancedPaginator, 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) @@ -78,13 +76,20 @@ class TagDeleteView(ObjectDeleteView): default_return_url = 'extras:tag_list' +class TagBulkImportView(BulkImportView): + queryset = Tag.objects.all() + model_form = forms.TagCSVForm + table = tables.TagTable + default_return_url = 'extras:tag_list' + + class TagBulkEditView(BulkEditView): queryset = Tag.objects.annotate( items=Count('extras_taggeditem_items', distinct=True) ).order_by( 'name' ) - table = TagTable + table = tables.TagTable form = forms.TagBulkEditForm default_return_url = 'extras:tag_list' @@ -95,7 +100,7 @@ class TagBulkDeleteView(BulkDeleteView): ).order_by( 'name' ) - table = TagTable + table = tables.TagTable default_return_url = 'extras:tag_list' @@ -107,7 +112,7 @@ class ConfigContextListView(ObjectListView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet filterset_form = forms.ConfigContextFilterForm - table = ConfigContextTable + table = tables.ConfigContextTable action_buttons = ('add',) @@ -143,7 +148,7 @@ class ConfigContextEditView(ObjectEditView): class ConfigContextBulkEditView(BulkEditView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet - table = ConfigContextTable + table = tables.ConfigContextTable form = forms.ConfigContextBulkEditForm default_return_url = 'extras:configcontext_list' @@ -155,7 +160,7 @@ class ConfigContextDeleteView(ObjectDeleteView): class ConfigContextBulkDeleteView(BulkDeleteView): queryset = ConfigContext.objects.all() - table = ConfigContextTable + table = tables.ConfigContextTable default_return_url = 'extras:configcontext_list' @@ -197,7 +202,7 @@ class ObjectChangeListView(ObjectListView): queryset = ObjectChange.objects.prefetch_related('user', 'changed_object_type') filterset = filters.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm - table = ObjectChangeTable + table = tables.ObjectChangeTable template_name = 'extras/objectchange_list.html' action_buttons = ('export',) @@ -214,7 +219,7 @@ class ObjectChangeView(ObjectView): ).exclude( pk=objectchange.pk ) - related_changes_table = ObjectChangeTable( + related_changes_table = tables.ObjectChangeTable( data=related_changes[:50], orderable=False ) @@ -267,7 +272,7 @@ class ObjectChangeLogView(View): Q(changed_object_type=content_type, changed_object_id=obj.pk) | Q(related_object_type=content_type, related_object_id=obj.pk) ) - objectchanges_table = ObjectChangeTable( + objectchanges_table = tables.ObjectChangeTable( data=objectchanges, orderable=False ) diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html index 0c20bcbdc..ff54a4800 100644 --- a/netbox/templates/extras/tag.html +++ b/netbox/templates/extras/tag.html @@ -85,7 +85,7 @@ Description - {{ tag.description }} + {{ tag.description|placeholder }} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 765df31cc..0e0e2b981 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -101,6 +101,12 @@
  • + {% if perms.extras.add_tag %} +
    + + +
    + {% endif %} Tags