From 2845dd488e3790789bf1db02c1f68b9858a882dd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 21 Oct 2020 09:05:33 -0400 Subject: [PATCH 01/10] Add a REST API endpoint for updating user preferences --- netbox/users/api/urls.py | 3 +++ netbox/users/api/views.py | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/netbox/users/api/urls.py b/netbox/users/api/urls.py index 4176cc806..df2e8c25a 100644 --- a/netbox/users/api/urls.py +++ b/netbox/users/api/urls.py @@ -12,5 +12,8 @@ router.register('groups', views.GroupViewSet) # Permissions router.register('permissions', views.ObjectPermissionViewSet) +# User preferences +router.register('config', views.UserConfigViewSet, basename='userconfig') + app_name = 'users-api' urlpatterns = router.urls diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index b799bee19..ef6e94fff 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -1,10 +1,15 @@ +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group, User from django.db.models import Count +from django.utils.decorators import method_decorator +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework.routers import APIRootView +from rest_framework.viewsets import ViewSet from netbox.api.views import ModelViewSet from users import filters -from users.models import ObjectPermission +from users.models import ObjectPermission, UserConfig from utilities.querysets import RestrictedQuerySet from . import serializers @@ -41,3 +46,36 @@ class ObjectPermissionViewSet(ModelViewSet): queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users') serializer_class = serializers.ObjectPermissionSerializer filterset_class = filters.ObjectPermissionFilterSet + + +# +# User preferences +# + +class UserConfigViewSet(ViewSet): + """ + An API endpoint via which a user can update his or her own UserConfig data (but no one else's). + """ + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return UserConfig.objects.filter(user=self.request.user) + + def list(self, request): + """ + Return the UserConfig for the currently authenticated User. + """ + userconfig = self.get_queryset().first() + + return Response(userconfig.data) + + def patch(self, request): + """ + Update the UserConfig for the currently authenticated User. + """ + # TODO: How can we validate this data? + userconfig = self.get_queryset().first() + userconfig.data.update(request.data) + userconfig.save() + + return Response(userconfig.data) From 4de7fcd758cb6133073096401757b24dbc21afc4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 21 Oct 2020 14:52:50 -0400 Subject: [PATCH 02/10] Convert table config updates to use REST API --- netbox/project-static/js/tableconfig.js | 25 +++++++++++++++++++++ netbox/templates/base.html | 1 + netbox/templates/inc/table_config_form.html | 2 +- netbox/templates/utilities/obj_list.html | 5 +++++ netbox/utilities/forms/forms.py | 6 +++++ netbox/utilities/views.py | 17 -------------- 6 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 netbox/project-static/js/tableconfig.js diff --git a/netbox/project-static/js/tableconfig.js b/netbox/project-static/js/tableconfig.js new file mode 100644 index 000000000..ddea201a1 --- /dev/null +++ b/netbox/project-static/js/tableconfig.js @@ -0,0 +1,25 @@ +$(document).ready(function() { + $('form.tableconfigform').submit(function(event) { + event.preventDefault(); + let table_name = this.getAttribute('data-table-name'); + let data = {"tables": {}}; + data['tables'][table_name] = {}; + data['tables'][table_name]['columns'] = $('#id_columns').val(); + $.ajax({ + url: netbox_api_path + 'users/config/', + async: true, + contentType: 'application/json', + dataType: 'json', + type: 'PATCH', + beforeSend: function(xhr, settings) { + xhr.setRequestHeader("X-CSRFToken", netbox_csrf_token); + }, + data: JSON.stringify(data), + }).done(function () { + // Reload the page + window.location.reload(true); + }).fail(function (xhr, status, error) { + alert("Failed: " + error); + }); + }); +}); diff --git a/netbox/templates/base.html b/netbox/templates/base.html index 20042d151..b355d8d01 100644 --- a/netbox/templates/base.html +++ b/netbox/templates/base.html @@ -96,6 +96,7 @@ onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"> +{% endblock %} diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index bc68f29f6..47be72eea 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -168,8 +168,14 @@ class TableConfigForm(BootstrapMixin, forms.Form): ) def __init__(self, table, *args, **kwargs): + self.table = table + super().__init__(*args, **kwargs) # Initialize columns field based on table attributes self.fields['columns'].choices = table.configurable_columns self.fields['columns'].initial = table.visible_columns + + @property + def table_name(self): + return self.table.__class__.__name__ diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index f527aeeba..e7e7412bf 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -317,23 +317,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): return render(request, self.template_name, context) - @method_decorator(login_required) - def post(self, request): - - # Update the user's table configuration - table = self.table(self.queryset) - form = TableConfigForm(table=table, data=request.POST) - preference_name = f"tables.{self.table.__name__}.columns" - - if form.is_valid(): - if 'set' in request.POST: - request.user.config.set(preference_name, form.cleaned_data['columns'], commit=True) - elif 'clear' in request.POST: - request.user.config.clear(preference_name, commit=True) - messages.success(request, "Your preferences have been updated.") - - return redirect(request.get_full_path()) - def extra_context(self): return {} From ff3d0b72b9fb9ed3d0050ddb7da2367e69208679 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 21 Oct 2020 16:06:37 -0400 Subject: [PATCH 03/10] Tweak the table config form for generic use --- netbox/project-static/js/tableconfig.js | 29 ++++++++++++++++----- netbox/templates/inc/table_config_form.html | 3 +-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/netbox/project-static/js/tableconfig.js b/netbox/project-static/js/tableconfig.js index ddea201a1..29b5e46d5 100644 --- a/netbox/project-static/js/tableconfig.js +++ b/netbox/project-static/js/tableconfig.js @@ -1,10 +1,27 @@ $(document).ready(function() { - $('form.tableconfigform').submit(function(event) { + $('form.userconfigform').submit(function(event) { event.preventDefault(); - let table_name = this.getAttribute('data-table-name'); - let data = {"tables": {}}; - data['tables'][table_name] = {}; - data['tables'][table_name]['columns'] = $('#id_columns').val(); + + // Derive an array from the dotted path to the config root + let path = this.getAttribute('data-config-root').split('.'); + let data = {}; + let pointer = data; + + // Construct a nested JSON object from the path + let node; + for (node of path) { + pointer[node] = {}; + pointer = pointer[node]; + } + + // Assign the form data to the child node + let field; + $.each($(this).find('[id^="id_"]:input'), function(index, value) { + field = $(value); + pointer[field.attr("name")] = field.val(); + }); + + // Make the REST API request $.ajax({ url: netbox_api_path + 'users/config/', async: true, @@ -19,7 +36,7 @@ $(document).ready(function() { // Reload the page window.location.reload(true); }).fail(function (xhr, status, error) { - alert("Failed: " + error); + alert("Failed to update user config (" + status + "): " + error); }); }); }); diff --git a/netbox/templates/inc/table_config_form.html b/netbox/templates/inc/table_config_form.html index 59baecd8b..c4bc3f41d 100644 --- a/netbox/templates/inc/table_config_form.html +++ b/netbox/templates/inc/table_config_form.html @@ -7,8 +7,7 @@