mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #5288 from netbox-community/5252-table-config
Closes #5252: Introduce an API endpoint for writing user preferences
This commit is contained in:
commit
6edd65c4ed
@ -1019,7 +1019,11 @@ class DeviceView(ObjectView):
|
||||
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'cable', '_path__destination',
|
||||
)
|
||||
consoleport_table = tables.DeviceConsolePortTable(consoleports, orderable=False)
|
||||
consoleport_table = tables.DeviceConsolePortTable(
|
||||
data=consoleports,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
|
||||
consoleport_table.columns.show('pk')
|
||||
|
||||
@ -1029,7 +1033,11 @@ class DeviceView(ObjectView):
|
||||
).prefetch_related(
|
||||
'cable', '_path__destination',
|
||||
)
|
||||
consoleserverport_table = tables.DeviceConsoleServerPortTable(consoleserverports, orderable=False)
|
||||
consoleserverport_table = tables.DeviceConsoleServerPortTable(
|
||||
data=consoleserverports,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_consoleserverport') or \
|
||||
request.user.has_perm('dcim.delete_consoleserverport'):
|
||||
consoleserverport_table.columns.show('pk')
|
||||
@ -1038,7 +1046,11 @@ class DeviceView(ObjectView):
|
||||
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'cable', '_path__destination',
|
||||
)
|
||||
powerport_table = tables.DevicePowerPortTable(powerports, orderable=False)
|
||||
powerport_table = tables.DevicePowerPortTable(
|
||||
data=powerports,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
|
||||
powerport_table.columns.show('pk')
|
||||
|
||||
@ -1046,7 +1058,11 @@ class DeviceView(ObjectView):
|
||||
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'cable', 'power_port', '_path__destination',
|
||||
)
|
||||
poweroutlet_table = tables.DevicePowerOutletTable(poweroutlets, orderable=False)
|
||||
poweroutlet_table = tables.DevicePowerOutletTable(
|
||||
data=poweroutlets,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
|
||||
poweroutlet_table.columns.show('pk')
|
||||
|
||||
@ -1056,7 +1072,11 @@ class DeviceView(ObjectView):
|
||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||
'lag', 'cable', '_path__destination', 'tags',
|
||||
)
|
||||
interface_table = tables.DeviceInterfaceTable(interfaces, orderable=False)
|
||||
interface_table = tables.DeviceInterfaceTable(
|
||||
data=interfaces,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
|
||||
interface_table.columns.show('pk')
|
||||
|
||||
@ -1064,13 +1084,21 @@ class DeviceView(ObjectView):
|
||||
frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'rear_port', 'cable',
|
||||
)
|
||||
frontport_table = tables.DeviceFrontPortTable(frontports, orderable=False)
|
||||
frontport_table = tables.DeviceFrontPortTable(
|
||||
data=frontports,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
|
||||
frontport_table.columns.show('pk')
|
||||
|
||||
# Rear ports
|
||||
rearports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
|
||||
rearport_table = tables.DeviceRearPortTable(rearports, orderable=False)
|
||||
rearport_table = tables.DeviceRearPortTable(
|
||||
data=rearports,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
|
||||
rearport_table.columns.show('pk')
|
||||
|
||||
@ -1078,7 +1106,11 @@ class DeviceView(ObjectView):
|
||||
devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'installed_device__device_type__manufacturer',
|
||||
)
|
||||
devicebay_table = tables.DeviceDeviceBayTable(devicebays, orderable=False)
|
||||
devicebay_table = tables.DeviceDeviceBayTable(
|
||||
data=devicebays,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
|
||||
devicebay_table.columns.show('pk')
|
||||
|
||||
@ -1086,7 +1118,11 @@ class DeviceView(ObjectView):
|
||||
inventoryitems = InventoryItem.objects.restrict(request.user, 'view').filter(
|
||||
device=device
|
||||
).prefetch_related('manufacturer')
|
||||
inventoryitem_table = tables.DeviceInventoryItemTable(inventoryitems, orderable=False)
|
||||
inventoryitem_table = tables.DeviceInventoryItemTable(
|
||||
data=inventoryitems,
|
||||
user=request.user,
|
||||
orderable=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
|
||||
devicebay_table.columns.show('pk')
|
||||
|
||||
|
@ -193,10 +193,6 @@ table.component-list td.subtable td {
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
table.interface-ips th {
|
||||
font-size: 80%;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Reports */
|
||||
table.reports td.method {
|
||||
|
47
netbox/project-static/js/tableconfig.js
Normal file
47
netbox/project-static/js/tableconfig.js
Normal file
@ -0,0 +1,47 @@
|
||||
$(document).ready(function() {
|
||||
$('form.userconfigform input.reset').click(function(event) {
|
||||
// Deselect all columns when the reset button is clicked
|
||||
$('select[name="columns"]').val([]);
|
||||
});
|
||||
|
||||
$('form.userconfigform').submit(function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// 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,
|
||||
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 to update user config (" + status + "): " + error);
|
||||
});
|
||||
});
|
||||
});
|
@ -96,6 +96,7 @@
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"></script>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
var netbox_csrf_token = "{{ csrf_token }}";
|
||||
var loading = $(".loading");
|
||||
$(document).ajaxStart(function() {
|
||||
loading.show();
|
||||
|
@ -485,7 +485,12 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="col-md-2 pull-right noprint">
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceInterfaceTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pull-right col-md-2 noprint">
|
||||
<input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
|
||||
</div>
|
||||
</div>
|
||||
@ -527,6 +532,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Front Ports</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceFrontPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=frontport_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -564,6 +574,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rear Ports</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceRearPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=rearport_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -601,6 +616,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Ports</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceConsolePortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=consoleport_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -638,6 +658,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceConsoleServerPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=consoleserverport_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -675,6 +700,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Ports</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DevicePowerPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=powerport_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -711,6 +741,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DevicePowerOutletTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=poweroutlet_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -748,6 +783,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceDeviceBayTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=devicebay_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -779,6 +819,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Inventory Items</strong>
|
||||
<div class="pull-right noprint">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceInventoryItemTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'responsive_table.html' with table=inventoryitem_table %}
|
||||
<div class="panel-footer noprint">
|
||||
@ -811,6 +856,15 @@
|
||||
</div>
|
||||
</div>
|
||||
{% include 'secrets/inc/private_key_modal.html' %}
|
||||
{% table_config_form interface_table %}
|
||||
{% table_config_form frontport_table %}
|
||||
{% table_config_form rearport_table %}
|
||||
{% table_config_form consoleport_table %}
|
||||
{% table_config_form consoleserverport_table %}
|
||||
{% table_config_form powerport_table %}
|
||||
{% table_config_form poweroutlet_table %}
|
||||
{% table_config_form devicebay_table %}
|
||||
{% table_config_form inventoryitem_table %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
@ -864,4 +918,5 @@ $(".cable-toggle").click(function() {
|
||||
</script>
|
||||
<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script src="{% static 'js/tableconfig.js' %}?v{{ settings.VERSION }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -1,12 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right noprint">
|
||||
{% block buttons %}{% endblock %}
|
||||
{% if request.user.is_authenticated and table_config_form %}
|
||||
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#tableconfig" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#ObjectTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
{% if permissions.add and 'add' in action_buttons %}
|
||||
{% add_button content_type.model_class|validated_viewname:"add" %}
|
||||
@ -71,9 +72,6 @@
|
||||
{% endwith %}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
<div class="clearfix"></div>
|
||||
{% if table_config_form %}
|
||||
{% include 'inc/table_config_form.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if filter_form %}
|
||||
<div class="col-md-3 noprint">
|
||||
@ -82,4 +80,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% table_config_form table table_name="ObjectTable" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/tableconfig.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% load form_helpers %}
|
||||
<div class="modal fade" tabindex="-1" id="tableconfig">
|
||||
<div class="modal fade" tabindex="-1" id="{{ table_name }}_config">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -7,8 +7,7 @@
|
||||
<h4 class="modal-title">Table Configuration</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<form class="form-horizontal userconfigform" data-config-root="tables.{{ table_config_form.table_name }}">
|
||||
{% render_form table_config_form %}
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
@ -18,8 +17,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<input type="submit" class="btn btn-primary" name="set" value="Save" />
|
||||
<input type="submit" class="btn btn-danger" name="clear" value="Reset" />
|
||||
<input type="submit" class="btn btn-primary" value="Save" />
|
||||
<input type="submit" class="btn btn-danger reset" value="Reset" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -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
|
||||
|
@ -1,11 +1,15 @@
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db.models import Count
|
||||
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 utilities.utils import deepmerge
|
||||
from . import serializers
|
||||
|
||||
|
||||
@ -41,3 +45,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 = deepmerge(userconfig.data, request.data)
|
||||
userconfig.save()
|
||||
|
||||
return Response(userconfig.data)
|
||||
|
@ -1,11 +1,10 @@
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from users.models import ObjectPermission
|
||||
from utilities.testing import APIViewTestCases, APITestCase, disable_warnings
|
||||
from utilities.testing import APIViewTestCases, APITestCase
|
||||
from utilities.utils import deepmerge
|
||||
|
||||
|
||||
class AppTest(APITestCase):
|
||||
@ -132,3 +131,56 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
|
||||
'constraints': {'name': 'TEST6'},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class UserConfigTest(APITestCase):
|
||||
|
||||
def test_get(self):
|
||||
"""
|
||||
Retrieve user configuration via GET request.
|
||||
"""
|
||||
userconfig = self.user.config
|
||||
url = reverse('users-api:userconfig-list')
|
||||
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.data, {})
|
||||
|
||||
data = {
|
||||
"a": 123,
|
||||
"b": 456,
|
||||
"c": 789,
|
||||
}
|
||||
userconfig.data = data
|
||||
userconfig.save()
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.data, data)
|
||||
|
||||
def test_patch(self):
|
||||
"""
|
||||
Set user config via PATCH requests.
|
||||
"""
|
||||
userconfig = self.user.config
|
||||
url = reverse('users-api:userconfig-list')
|
||||
|
||||
data = {
|
||||
"a": {
|
||||
"a1": "X",
|
||||
"a2": "Y",
|
||||
},
|
||||
"b": {
|
||||
"b1": "Z",
|
||||
}
|
||||
}
|
||||
response = self.client.patch(url, data=data, format='json', **self.header)
|
||||
self.assertDictEqual(response.data, data)
|
||||
userconfig.refresh_from_db()
|
||||
self.assertDictEqual(userconfig.data, data)
|
||||
|
||||
update_data = {
|
||||
"c": 123
|
||||
}
|
||||
response = self.client.patch(url, data=update_data, format='json', **self.header)
|
||||
new_data = deepmerge(data, update_data)
|
||||
self.assertDictEqual(response.data, new_data)
|
||||
userconfig.refresh_from_db()
|
||||
self.assertDictEqual(userconfig.data, new_data)
|
||||
|
@ -161,6 +161,7 @@ class TableConfigForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
columns = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'size': 10}
|
||||
),
|
||||
@ -168,8 +169,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__
|
||||
|
@ -1,4 +1,5 @@
|
||||
import django_tables2 as tables
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models.fields.related import RelatedField
|
||||
@ -11,10 +12,11 @@ class BaseTable(tables.Table):
|
||||
"""
|
||||
Default table for object lists
|
||||
|
||||
:param add_prefetch: By default, modify the queryset passed to the table upon initialization to automatically
|
||||
prefetch related data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to
|
||||
accommodate PrefixQuerySet.annotate_depth()).
|
||||
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
|
||||
"""
|
||||
# By default, modify the queryset passed to the table upon initialization to automatically prefetch related
|
||||
# data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to accommodate
|
||||
# PrefixQuerySet.annotate_depth()).
|
||||
add_prefetch = True
|
||||
|
||||
class Meta:
|
||||
@ -22,7 +24,7 @@ class BaseTable(tables.Table):
|
||||
'class': 'table table-hover table-headings',
|
||||
}
|
||||
|
||||
def __init__(self, *args, columns=None, **kwargs):
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set default empty_text if none was provided
|
||||
@ -36,25 +38,27 @@ class BaseTable(tables.Table):
|
||||
if column.name not in default_columns:
|
||||
self.columns.hide(column.name)
|
||||
|
||||
# Apply custom column ordering
|
||||
if columns is not None:
|
||||
pk = self.base_columns.pop('pk', None)
|
||||
actions = self.base_columns.pop('actions', None)
|
||||
# Apply custom column ordering for user
|
||||
if user is not None and not isinstance(user, AnonymousUser):
|
||||
columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
|
||||
if columns:
|
||||
pk = self.base_columns.pop('pk', None)
|
||||
actions = self.base_columns.pop('actions', None)
|
||||
|
||||
for name, column in self.base_columns.items():
|
||||
if name in columns:
|
||||
self.columns.show(name)
|
||||
else:
|
||||
self.columns.hide(name)
|
||||
self.sequence = [c for c in columns if c in self.base_columns]
|
||||
for name, column in self.base_columns.items():
|
||||
if name in columns:
|
||||
self.columns.show(name)
|
||||
else:
|
||||
self.columns.hide(name)
|
||||
self.sequence = [c for c in columns if c in self.base_columns]
|
||||
|
||||
# Always include PK and actions column, if defined on the table
|
||||
if pk:
|
||||
self.base_columns['pk'] = pk
|
||||
self.sequence.insert(0, 'pk')
|
||||
if actions:
|
||||
self.base_columns['actions'] = actions
|
||||
self.sequence.append('actions')
|
||||
# Always include PK and actions column, if defined on the table
|
||||
if pk:
|
||||
self.base_columns['pk'] = pk
|
||||
self.sequence.insert(0, 'pk')
|
||||
if actions:
|
||||
self.base_columns['actions'] = actions
|
||||
self.sequence.append('actions')
|
||||
|
||||
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
|
||||
if self.add_prefetch and isinstance(self.data, TableQuerysetData):
|
||||
|
@ -10,6 +10,7 @@ from django.utils.html import strip_tags
|
||||
from django.utils.safestring import mark_safe
|
||||
from markdown import markdown
|
||||
|
||||
from utilities.forms import TableConfigForm
|
||||
from utilities.utils import foreground_color
|
||||
|
||||
register = template.Library()
|
||||
@ -261,3 +262,11 @@ def badge(value, show_empty=False):
|
||||
'value': value,
|
||||
'show_empty': show_empty,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('utilities/templatetags/table_config_form.html')
|
||||
def table_config_form(table, table_name=None):
|
||||
return {
|
||||
'table_name': table_name or table.__class__.__name__,
|
||||
'table_config_form': TableConfigForm(table=table),
|
||||
}
|
||||
|
@ -289,12 +289,8 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
perm_name = get_permission_for_model(model, action)
|
||||
permissions[action] = request.user.has_perm(perm_name)
|
||||
|
||||
# Construct the table based on the user's permissions
|
||||
if request.user.is_authenticated:
|
||||
columns = request.user.config.get(f"tables.{self.table.__name__}.columns")
|
||||
else:
|
||||
columns = None
|
||||
table = self.table(self.queryset, columns=columns)
|
||||
# Construct the objects table
|
||||
table = self.table(self.queryset, user=request.user)
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.columns.show('pk')
|
||||
|
||||
@ -317,23 +313,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 {}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user