Skip to content

Closes #8248: User bookmarks #13035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions netbox/extras/api/nested_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from netbox.api.serializers import NestedTagSerializer, WritableNestedSerializer

__all__ = [
'NestedBookmarkSerializer',
'NestedConfigContextSerializer',
'NestedConfigTemplateSerializer',
'NestedCustomFieldSerializer',
Expand Down Expand Up @@ -73,6 +74,14 @@ class Meta:
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')

Expand Down
25 changes: 25 additions & 0 deletions netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .nested_serializers import *

__all__ = (
'BookmarkSerializer',
'ConfigContextSerializer',
'ConfigTemplateSerializer',
'ContentTypeSerializer',
Expand Down Expand Up @@ -190,6 +191,30 @@ class Meta:
]


#
# Bookmarks
#

class BookmarkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
object_type = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('bookmarks').get_query()),
)
object = serializers.SerializerMethodField(read_only=True)
user = NestedUserSerializer()

class Meta:
model = Bookmark
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', 'last_updated',
]

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, instance):
serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX)
return serializer(instance.object, context={'request': self.context['request']}).data


#
# Tags
#
Expand Down
1 change: 1 addition & 0 deletions netbox/extras/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
router.register('custom-links', views.CustomLinkViewSet)
router.register('export-templates', views.ExportTemplateViewSet)
router.register('saved-filters', views.SavedFilterViewSet)
router.register('bookmarks', views.BookmarkViewSet)
router.register('tags', views.TagViewSet)
router.register('image-attachments', views.ImageAttachmentViewSet)
router.register('journal-entries', views.JournalEntryViewSet)
Expand Down
11 changes: 11 additions & 0 deletions netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ class SavedFilterViewSet(NetBoxModelViewSet):
filterset_class = filtersets.SavedFilterFilterSet


#
# Bookmarks
#

class BookmarkViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Bookmark.objects.all()
serializer_class = serializers.BookmarkSerializer
filterset_class = filtersets.BookmarkFilterSet


#
# Tags
#
Expand Down
17 changes: 16 additions & 1 deletion netbox/extras/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ class CustomLinkButtonClassChoices(ButtonColorChoices):
(LINK, 'Link'),
)


#
# Bookmarks
#

class BookmarkOrderingChoices(ChoiceSet):

ORDERING_NEWEST = '-created'
ORDERING_OLDEST = 'created'

CHOICES = (
(ORDERING_NEWEST, 'Newest'),
(ORDERING_OLDEST, 'Oldest'),
)

#
# ObjectChanges
#
Expand All @@ -98,7 +113,7 @@ class ObjectChangeActionChoices(ChoiceSet):


#
# Jounral entries
# Journal entries
#

class JournalEntryKindChoices(ChoiceSet):
Expand Down
41 changes: 41 additions & 0 deletions netbox/extras/dashboard/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from django.urls import NoReverseMatch, resolve, reverse
from django.utils.translation import gettext as _

from extras.choices import BookmarkOrderingChoices
from extras.utils import FeatureQuery
from utilities.forms import BootstrapMixin
from utilities.permissions import get_permission_for_model
Expand All @@ -23,6 +24,7 @@
from .utils import register_widget

__all__ = (
'BookmarksWidget',
'DashboardWidget',
'NoteWidget',
'ObjectCountsWidget',
Expand Down Expand Up @@ -318,3 +320,42 @@ def get_feed(self):
return {
'feed': feed,
}


@register_widget
class BookmarksWidget(DashboardWidget):
default_title = _('Bookmarks')
default_config = {
'order_by': BookmarkOrderingChoices.ORDERING_NEWEST,
}
description = _('Show your personal bookmarks')
template_name = 'extras/dashboard/widgets/bookmarks.html'

class ConfigForm(WidgetConfigForm):
object_types = forms.MultipleChoiceField(
# TODO: Restrict the choices by FeatureQuery('bookmarks')
choices=get_content_type_labels,
required=False
)
order_by = forms.ChoiceField(
choices=BookmarkOrderingChoices
)
max_items = forms.IntegerField(
min_value=1,
required=False
)

def render(self, request):
from extras.models import Bookmark

bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types)
conent_types = ContentType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=conent_types)
if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items]

return render_to_string(self.template_name, {
'bookmarks': bookmarks,
})
21 changes: 21 additions & 0 deletions netbox/extras/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .models import *

__all__ = (
'BookmarkFilterSet',
'ConfigContextFilterSet',
'ConfigRevisionFilterSet',
'ConfigTemplateFilterSet',
Expand Down Expand Up @@ -199,6 +200,26 @@ def _usable(self, queryset, name, value):
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))


class BookmarkFilterSet(BaseFilterSet):
created = django_filters.DateTimeFilter()
object_type_id = MultiValueNumberFilter()
object_type = ContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=get_user_model().objects.all(),
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
queryset=get_user_model().objects.all(),
to_field_name='username',
label=_('User (name)'),
)

class Meta:
model = Bookmark
fields = ['id', 'object_id']


class ImageAttachmentFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
Expand Down
14 changes: 13 additions & 1 deletion netbox/extras/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, BootstrapMixin, add_blank_choice
from utilities.forms import BootstrapMixin, add_blank_choice
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
SlugField,
Expand All @@ -23,6 +23,7 @@


__all__ = (
'BookmarkForm',
'ConfigContextForm',
'ConfigRevisionForm',
'ConfigTemplateForm',
Expand Down Expand Up @@ -169,6 +170,17 @@ def __init__(self, *args, initial=None, **kwargs):
super().__init__(*args, initial=initial, **kwargs)


class BookmarkForm(BootstrapMixin, forms.ModelForm):
object_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('bookmarks').get_query()
)

class Meta:
model = Bookmark
fields = ('object_type', 'object_id')


class WebhookForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
Expand Down
35 changes: 35 additions & 0 deletions netbox/extras/migrations/0095_bookmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.1.9 on 2023-06-26 14:27

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('extras', '0094_tag_object_types'),
]

operations = [
migrations.CreateModel(
name='Bookmark',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('object_id', models.PositiveBigIntegerField()),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('created', 'pk'),
},
),
migrations.AddConstraint(
model_name='bookmark',
constraint=models.UniqueConstraint(fields=('object_type', 'object_id', 'user'), name='extras_bookmark_unique_per_object_and_user'),
),
]
34 changes: 34 additions & 0 deletions netbox/extras/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from utilities.utils import clean_html, render_jinja2

__all__ = (
'Bookmark',
'ConfigRevision',
'CustomLink',
'ExportTemplate',
Expand Down Expand Up @@ -595,6 +596,39 @@ def get_kind_color(self):
return JournalEntryKindChoices.colors.get(self.kind)


class Bookmark(ChangeLoggedModel):
"""
An object bookmarked by a User.
"""
object_type = models.ForeignKey(
to=ContentType,
on_delete=models.PROTECT
)
object_id = models.PositiveBigIntegerField()
object = GenericForeignKey(
ct_field='object_type',
fk_field='object_id'
)
user = models.ForeignKey(
to=settings.AUTH_USER_MODEL,
on_delete=models.PROTECT
)

class Meta:
ordering = ('created', 'pk')
constraints = (
models.UniqueConstraint(
fields=('object_type', 'object_id', 'user'),
name='%(app_label)s_%(class)s_unique_per_object_and_user'
),
)

def __str__(self):
if self.object:
return str(self.object)
return super().__str__()


class ConfigRevision(models.Model):
"""
An atomic revision of NetBox's configuration.
Expand Down
16 changes: 16 additions & 0 deletions netbox/extras/tables/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .template_code import *

__all__ = (
'BookmarkTable',
'ConfigContextTable',
'ConfigRevisionTable',
'ConfigTemplateTable',
Expand Down Expand Up @@ -167,6 +168,21 @@ class Meta(NetBoxTable.Meta):
)


class BookmarkTable(NetBoxTable):
object_type = columns.ContentTypeColumn()
object = tables.Column(
linkify=True
)
actions = columns.ActionsColumn(
actions=('delete',)
)

class Meta(NetBoxTable.Meta):
model = Bookmark
fields = ('pk', 'object', 'object_type', 'created')
default_columns = ('object', 'object_type', 'created')


class WebhookTable(NetBoxTable):
name = tables.Column(
linkify=True
Expand Down
Loading