diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py index 29ef67943..4271e1748 100644 --- a/netbox/extras/api/nested_serializers.py +++ b/netbox/extras/api/nested_serializers.py @@ -4,6 +4,7 @@ from extras import models from netbox.api.serializers import NestedTagSerializer, WritableNestedSerializer __all__ = [ + 'NestedBookmarkSerializer', 'NestedConfigContextSerializer', 'NestedConfigTemplateSerializer', 'NestedCustomFieldSerializer', @@ -73,6 +74,14 @@ class NestedSavedFilterSerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name', 'slug'] +class NestedBookmarkSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') + + class Meta: + model = models.Bookmark + fields = ['id', 'url', 'display', 'object_id', 'object_type'] + + class NestedImageAttachmentSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 5d2e9c332..8919f849d 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -201,11 +201,12 @@ class BookmarkSerializer(ValidatedModelSerializer): queryset=ContentType.objects.all() ) object = serializers.SerializerMethodField(read_only=True) + user = NestedUserSerializer() class Meta: model = Bookmark fields = [ - 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'created', 'last_updated', + 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', 'last_updated', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 61e648194..8f1782c8f 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -206,6 +206,7 @@ class BookmarkFilterSet(BaseFilterSet): label=_('Search'), ) created = django_filters.DateTimeFilter() + object_type_id = MultiValueNumberFilter() object_type = ContentTypeFilter() user_id = django_filters.ModelMultipleChoiceFilter( queryset=get_user_model().objects.all(), @@ -220,7 +221,7 @@ class BookmarkFilterSet(BaseFilterSet): class Meta: model = Bookmark - fields = ['id', 'object_type_id', 'object_id'] + fields = ['id', 'object_id'] # def search(self, queryset, name, value): # if not value.strip(): diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 4c48aa73e..e09d4de78 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -268,6 +268,58 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): savedfilter.content_types.set([site_ct]) +class BookmarkTest( + APIViewTestCases.GetObjectViewTestCase, + APIViewTestCases.ListObjectsViewTestCase, + APIViewTestCases.CreateObjectViewTestCase, + APIViewTestCases.DeleteObjectViewTestCase +): + model = Bookmark + brief_fields = ['display', 'id', 'object_id', 'object_type', 'url'] + + @classmethod + def setUpTestData(cls): + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + Site(name='Site 4', slug='site-4'), + Site(name='Site 5', slug='site-5'), + Site(name='Site 6', slug='site-6'), + ) + Site.objects.bulk_create(sites) + + def setUp(self): + super().setUp() + + sites = Site.objects.all() + + bookmarks = ( + Bookmark(object=sites[0], user=self.user), + Bookmark(object=sites[1], user=self.user), + Bookmark(object=sites[2], user=self.user), + ) + Bookmark.objects.bulk_create(bookmarks) + + self.create_data = [ + { + 'object_type': 'dcim.site', + 'object_id': sites[3].pk, + 'user': self.user.pk, + }, + { + 'object_type': 'dcim.site', + 'object_id': sites[4].pk, + 'user': self.user.pk, + }, + { + 'object_type': 'dcim.site', + 'object_id': sites[5].pk, + 'user': self.user.pk, + }, + ] + + class ExportTemplateTest(APIViewTestCases.APIViewTestCase): model = ExportTemplate brief_fields = ['display', 'id', 'name', 'url'] diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 7dff14cc0..b4b216244 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -365,6 +365,77 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) +class BookmarkTestCase(TestCase, BaseFilterSetTests): + queryset = Bookmark.objects.all() + filterset = BookmarkFilterSet + + @classmethod + def setUpTestData(cls): + content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + ) + Site.objects.bulk_create(sites) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + + bookmarks = ( + Bookmark( + object=sites[0], + user=users[0], + ), + Bookmark( + object=sites[1], + user=users[1], + ), + Bookmark( + object=sites[2], + user=users[2], + ), + Bookmark( + object=tenants[0], + user=users[0], + ), + Bookmark( + object=tenants[1], + user=users[1], + ), + Bookmark( + object=tenants[2], + user=users[2], + ), + ) + Bookmark.objects.bulk_create(bookmarks) + + def test_object_type(self): + params = {'object_type': 'dcim.site'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'object_type_id': [ContentType.objects.get_for_model(Site).pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_user(self): + users = User.objects.filter(username__startswith='User') + params = {'user': [users[0].username, users[1].username]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'user_id': [users[0].pk, users[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + class ExportTemplateTestCase(TestCase, BaseFilterSetTests): queryset = ExportTemplate.objects.all() filterset = ExportTemplateFilterSet diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 3dcb90875..57efc5be7 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -181,6 +181,54 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class BookmarkTestCase( + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase +): + model = Bookmark + + @classmethod + def setUpTestData(cls): + site_ct = ContentType.objects.get_for_model(Site) + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + Site(name='Site 4', slug='site-4'), + ) + Site.objects.bulk_create(sites) + + cls.form_data = { + 'object_type': site_ct.pk, + 'object_id': sites[3].pk, + } + + def setUp(self): + super().setUp() + + sites = Site.objects.all() + user = self.user + + bookmarks = ( + Bookmark(object=sites[0], user=user), + Bookmark(object=sites[1], user=user), + Bookmark(object=sites[2], user=user), + ) + Bookmark.objects.bulk_create(bookmarks) + + def _get_url(self, action, instance=None): + if action == 'list': + return reverse('users:bookmarks') + return super()._get_url(action, instance) + + def test_list_objects_anonymous(self): + return + + def test_list_objects_with_constrained_permission(self): + return + + class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = ExportTemplate diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 2969f7334..086537b99 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,4 +1,4 @@ -from django.urls import include, path, re_path +from django.urls import include, path from extras import views from utilities.urls import get_model_urls @@ -41,7 +41,7 @@ urlpatterns = [ path('saved-filters//', include(get_model_urls('extras', 'savedfilter'))), # Bookmarks - path('bookmarks/add/', views.BookmarkEditView.as_view(), name='bookmark_add'), + path('bookmarks/add/', views.BookmarkCreateView.as_view(), name='bookmark_add'), path('bookmarks/delete/', views.BookmarkBulkDeleteView.as_view(), name='bookmark_bulk_delete'), path('bookmarks//', include(get_model_urls('extras', 'bookmark'))), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index e7d3bae85..e3ba9c0c3 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -241,8 +241,7 @@ class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView): # Bookmarks # -# @register_model_view(Bookmark, 'edit') -class BookmarkEditView(generic.ObjectEditView): +class BookmarkCreateView(generic.ObjectEditView): form = forms.BookmarkForm def get_queryset(self, request): diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 4ee950ac8..76ceb9f35 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -59,7 +59,7 @@ Context: {# Extra buttons #} {% block extra_controls %}{% endblock %} - {% if request.user.is_authenticated %} + {% if perms.extras.add_bookmark %} {% bookmark_button object %} {% endif %} {% if request.user|can_add:object %}