mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 01:06:11 -06:00
Initial work on #8248
This commit is contained in:
parent
1056e513b1
commit
38d27a6fee
@ -31,6 +31,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'BookmarkSerializer',
|
||||||
'ConfigContextSerializer',
|
'ConfigContextSerializer',
|
||||||
'ConfigTemplateSerializer',
|
'ConfigTemplateSerializer',
|
||||||
'ContentTypeSerializer',
|
'ContentTypeSerializer',
|
||||||
@ -190,6 +191,29 @@ class SavedFilterSerializer(ValidatedModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Bookmarks
|
||||||
|
#
|
||||||
|
|
||||||
|
class BookmarkSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
||||||
|
object_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.all()
|
||||||
|
)
|
||||||
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Bookmark
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_type', 'object_id', 'object', '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
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -12,6 +12,7 @@ router.register('custom-fields', views.CustomFieldViewSet)
|
|||||||
router.register('custom-links', views.CustomLinkViewSet)
|
router.register('custom-links', views.CustomLinkViewSet)
|
||||||
router.register('export-templates', views.ExportTemplateViewSet)
|
router.register('export-templates', views.ExportTemplateViewSet)
|
||||||
router.register('saved-filters', views.SavedFilterViewSet)
|
router.register('saved-filters', views.SavedFilterViewSet)
|
||||||
|
router.register('bookmarks', views.BookmarkViewSet)
|
||||||
router.register('tags', views.TagViewSet)
|
router.register('tags', views.TagViewSet)
|
||||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||||
router.register('journal-entries', views.JournalEntryViewSet)
|
router.register('journal-entries', views.JournalEntryViewSet)
|
||||||
|
@ -93,6 +93,17 @@ class SavedFilterViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.SavedFilterFilterSet
|
filterset_class = filtersets.SavedFilterFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Bookmarks
|
||||||
|
#
|
||||||
|
|
||||||
|
class BookmarkViewSet(NetBoxModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
|
queryset = Bookmark.objects.all()
|
||||||
|
serializer_class = serializers.BookmarkSerializer
|
||||||
|
filterset_class = filtersets.BookmarkFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -15,6 +15,7 @@ from .filters import TagFilter
|
|||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'BookmarkFilterSet',
|
||||||
'ConfigContextFilterSet',
|
'ConfigContextFilterSet',
|
||||||
'ConfigRevisionFilterSet',
|
'ConfigRevisionFilterSet',
|
||||||
'ConfigTemplateFilterSet',
|
'ConfigTemplateFilterSet',
|
||||||
@ -199,6 +200,34 @@ class SavedFilterFilterSet(BaseFilterSet):
|
|||||||
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkFilterSet(BaseFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label=_('Search'),
|
||||||
|
)
|
||||||
|
created = django_filters.DateTimeFilter()
|
||||||
|
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_type_id', 'object_id']
|
||||||
|
|
||||||
|
# def search(self, queryset, name, value):
|
||||||
|
# if not value.strip():
|
||||||
|
# return queryset
|
||||||
|
# return queryset.filter(name__icontains=value)
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -14,7 +14,7 @@ from extras.utils import FeatureQuery
|
|||||||
from netbox.config import get_config, PARAMS
|
from netbox.config import get_config, PARAMS
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.models import Tenant, TenantGroup
|
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 (
|
from utilities.forms.fields import (
|
||||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
||||||
SlugField,
|
SlugField,
|
||||||
@ -23,6 +23,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'BookmarkForm',
|
||||||
'ConfigContextForm',
|
'ConfigContextForm',
|
||||||
'ConfigRevisionForm',
|
'ConfigRevisionForm',
|
||||||
'ConfigTemplateForm',
|
'ConfigTemplateForm',
|
||||||
@ -169,6 +170,17 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
|
|||||||
super().__init__(*args, initial=initial, **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):
|
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
35
netbox/extras/migrations/0095_bookmarks.py
Normal file
35
netbox/extras/migrations/0095_bookmarks.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -29,6 +29,7 @@ from utilities.querysets import RestrictedQuerySet
|
|||||||
from utilities.utils import clean_html, render_jinja2
|
from utilities.utils import clean_html, render_jinja2
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'Bookmark',
|
||||||
'ConfigRevision',
|
'ConfigRevision',
|
||||||
'CustomLink',
|
'CustomLink',
|
||||||
'ExportTemplate',
|
'ExportTemplate',
|
||||||
@ -595,6 +596,39 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||||||
return JournalEntryKindChoices.colors.get(self.kind)
|
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):
|
class ConfigRevision(models.Model):
|
||||||
"""
|
"""
|
||||||
An atomic revision of NetBox's configuration.
|
An atomic revision of NetBox's configuration.
|
||||||
|
@ -8,6 +8,7 @@ from netbox.tables import NetBoxTable, columns
|
|||||||
from .template_code import *
|
from .template_code import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'BookmarkTable',
|
||||||
'ConfigContextTable',
|
'ConfigContextTable',
|
||||||
'ConfigRevisionTable',
|
'ConfigRevisionTable',
|
||||||
'ConfigTemplateTable',
|
'ConfigTemplateTable',
|
||||||
@ -167,6 +168,21 @@ class SavedFilterTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class WebhookTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
|
@ -40,6 +40,11 @@ urlpatterns = [
|
|||||||
path('saved-filters/delete/', views.SavedFilterBulkDeleteView.as_view(), name='savedfilter_bulk_delete'),
|
path('saved-filters/delete/', views.SavedFilterBulkDeleteView.as_view(), name='savedfilter_bulk_delete'),
|
||||||
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
|
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
|
||||||
|
|
||||||
|
# Bookmarks
|
||||||
|
path('bookmarks/add/', views.BookmarkEditView.as_view(), name='bookmark_add'),
|
||||||
|
path('bookmarks/delete/', views.BookmarkBulkDeleteView.as_view(), name='bookmark_bulk_delete'),
|
||||||
|
path('bookmarks/<int:pk>/', include(get_model_urls('extras', 'bookmark'))),
|
||||||
|
|
||||||
# Webhooks
|
# Webhooks
|
||||||
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
||||||
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
||||||
|
@ -237,6 +237,36 @@ class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView):
|
|||||||
table = tables.SavedFilterTable
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Bookmarks
|
||||||
|
#
|
||||||
|
|
||||||
|
# @register_model_view(Bookmark, 'edit')
|
||||||
|
class BookmarkEditView(generic.ObjectEditView):
|
||||||
|
form = forms.BookmarkForm
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return Bookmark.objects.filter(user=request.user)
|
||||||
|
|
||||||
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||||
|
obj.user = request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Bookmark, 'delete')
|
||||||
|
class BookmarkDeleteView(generic.ObjectDeleteView):
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return Bookmark.objects.filter(user=request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
table = tables.BookmarkTable
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return Bookmark.objects.filter(user=request.user)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Webhooks
|
# Webhooks
|
||||||
#
|
#
|
||||||
|
@ -18,6 +18,7 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
class NetBoxFeatureSet(
|
class NetBoxFeatureSet(
|
||||||
|
BookmarksMixin,
|
||||||
ChangeLoggingMixin,
|
ChangeLoggingMixin,
|
||||||
CustomFieldsMixin,
|
CustomFieldsMixin,
|
||||||
CustomLinksMixin,
|
CustomLinksMixin,
|
||||||
|
@ -22,6 +22,7 @@ from utilities.utils import serialize_object
|
|||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'BookmarksMixin',
|
||||||
'ChangeLoggingMixin',
|
'ChangeLoggingMixin',
|
||||||
'CloningMixin',
|
'CloningMixin',
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
@ -304,6 +305,18 @@ class ExportTemplatesMixin(models.Model):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarksMixin(models.Model):
|
||||||
|
"""
|
||||||
|
Enables support for user bookmarks.
|
||||||
|
"""
|
||||||
|
images = GenericRelation(
|
||||||
|
to='extras.Bookmark'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class JobsMixin(models.Model):
|
class JobsMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
Enables support for job results.
|
Enables support for job results.
|
||||||
|
@ -59,6 +59,9 @@ Context:
|
|||||||
{# Extra buttons #}
|
{# Extra buttons #}
|
||||||
{% block extra_controls %}{% endblock %}
|
{% block extra_controls %}{% endblock %}
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
{% bookmark_button object %}
|
||||||
|
{% endif %}
|
||||||
{% if request.user|can_add:object %}
|
{% if request.user|can_add:object %}
|
||||||
{% clone_button object %}
|
{% clone_button object %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -23,6 +23,11 @@
|
|||||||
<i class="mdi mdi-account"></i> Profile
|
<i class="mdi mdi-account"></i> Profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'users:bookmarks' %}">
|
||||||
|
<i class="mdi mdi-bookmark"></i> Bookmarks
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'users:preferences' %}">
|
<a class="dropdown-item" href="{% url 'users:preferences' %}">
|
||||||
<i class="mdi mdi-wrench"></i> Preferences
|
<i class="mdi mdi-wrench"></i> Preferences
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'users:profile' %}">Profile</a>
|
<a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'users:profile' %}">Profile</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<a class="nav-link{% if active_tab == 'bookmarks' %} active{% endif %}" href="{% url 'users:bookmarks' %}">Bookmarks</a>
|
||||||
|
</li>
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'users:preferences' %}">Preferences</a>
|
<a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'users:preferences' %}">Preferences</a>
|
||||||
</li>
|
</li>
|
||||||
|
34
netbox/templates/users/bookmarks.html
Normal file
34
netbox/templates/users/bookmarks.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends 'users/base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block title %}Bookmarks{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="post" class="form form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="return_url" value="{% url 'users:bookmarks' %}" />
|
||||||
|
|
||||||
|
{# Table #}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||||
|
{% include 'htmx/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Form buttons #}
|
||||||
|
<div class="noprint bulk-buttons">
|
||||||
|
<div class="bulk-button-group">
|
||||||
|
{% if 'bulk_delete' in actions %}
|
||||||
|
{% bulk_delete_button model query_params=request.GET %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -8,6 +8,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# User
|
# User
|
||||||
path('profile/', views.ProfileView.as_view(), name='profile'),
|
path('profile/', views.ProfileView.as_view(), name='profile'),
|
||||||
|
path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'),
|
||||||
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
|
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
|
||||||
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
|
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
|
||||||
|
|
||||||
|
@ -15,10 +15,11 @@ from django.views.decorators.debug import sensitive_post_parameters
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from social_core.backends.utils import load_backends
|
from social_core.backends.utils import load_backends
|
||||||
|
|
||||||
from extras.models import ObjectChange
|
from extras.models import Bookmark, ObjectChange
|
||||||
from extras.tables import ObjectChangeTable
|
from extras.tables import BookmarkTable, ObjectChangeTable
|
||||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
|
from netbox.views.generic import ObjectListView
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view
|
||||||
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
||||||
@ -228,6 +229,23 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Bookmarks
|
||||||
|
#
|
||||||
|
|
||||||
|
class BookmarkListView(LoginRequiredMixin, ObjectListView):
|
||||||
|
table = BookmarkTable
|
||||||
|
template_name = 'users/bookmarks.html'
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return Bookmark.objects.filter(user=request.user)
|
||||||
|
|
||||||
|
def get_extra_context(self, request):
|
||||||
|
return {
|
||||||
|
'active_tab': 'bookmarks',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# API tokens
|
# API tokens
|
||||||
#
|
#
|
||||||
|
15
netbox/utilities/templates/buttons/bookmark.html
Normal file
15
netbox/utilities/templates/buttons/bookmark.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<form action="{{ form_url }}?return_url={{ return_url }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field, value in form_data.items %}
|
||||||
|
<input type="hidden" name="{{ field }}" value="{{ value }}" />
|
||||||
|
{% endfor %}
|
||||||
|
{% if bookmark %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-info">
|
||||||
|
<i class="mdi mdi-bookmark-minus"></i> Unbookmark
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-info">
|
||||||
|
<i class="mdi mdi-bookmark-check"></i> Bookmark
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
@ -2,11 +2,12 @@ from django import template
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import Bookmark, ExportTemplate
|
||||||
from utilities.utils import get_viewname, prepare_cloned_fields
|
from utilities.utils import get_viewname, prepare_cloned_fields
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'add_button',
|
'add_button',
|
||||||
|
'bookmark_button',
|
||||||
'bulk_delete_button',
|
'bulk_delete_button',
|
||||||
'bulk_edit_button',
|
'bulk_edit_button',
|
||||||
'clone_button',
|
'clone_button',
|
||||||
@ -24,6 +25,37 @@ register = template.Library()
|
|||||||
# Instance buttons
|
# Instance buttons
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@register.inclusion_tag('buttons/bookmark.html', takes_context=True)
|
||||||
|
def bookmark_button(context, instance):
|
||||||
|
# Check if this user has already bookmarked the object
|
||||||
|
content_type = ContentType.objects.get_for_model(instance)
|
||||||
|
bookmark = Bookmark.objects.filter(
|
||||||
|
object_type=content_type,
|
||||||
|
object_id=instance.pk,
|
||||||
|
user=context['request'].user
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# Compile form URL & data
|
||||||
|
if bookmark:
|
||||||
|
form_url = reverse('extras:bookmark_delete', kwargs={'pk': bookmark.pk})
|
||||||
|
form_data = {
|
||||||
|
'confirm': 'true',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
form_url = reverse('extras:bookmark_add')
|
||||||
|
form_data = {
|
||||||
|
'object_type': content_type.pk,
|
||||||
|
'object_id': instance.pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bookmark': bookmark,
|
||||||
|
'form_url': form_url,
|
||||||
|
'form_data': form_data,
|
||||||
|
'return_url': instance.get_absolute_url(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('buttons/clone.html')
|
@register.inclusion_tag('buttons/clone.html')
|
||||||
def clone_button(instance):
|
def clone_button(instance):
|
||||||
url = reverse(get_viewname(instance, 'add'))
|
url = reverse(get_viewname(instance, 'add'))
|
||||||
|
Loading…
Reference in New Issue
Block a user