mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -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(
|
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
'cable', '_path__destination',
|
'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'):
|
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
|
||||||
consoleport_table.columns.show('pk')
|
consoleport_table.columns.show('pk')
|
||||||
|
|
||||||
@ -1029,7 +1033,11 @@ class DeviceView(ObjectView):
|
|||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'cable', '_path__destination',
|
'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 \
|
if request.user.has_perm('dcim.change_consoleserverport') or \
|
||||||
request.user.has_perm('dcim.delete_consoleserverport'):
|
request.user.has_perm('dcim.delete_consoleserverport'):
|
||||||
consoleserverport_table.columns.show('pk')
|
consoleserverport_table.columns.show('pk')
|
||||||
@ -1038,7 +1046,11 @@ class DeviceView(ObjectView):
|
|||||||
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
'cable', '_path__destination',
|
'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'):
|
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
|
||||||
powerport_table.columns.show('pk')
|
powerport_table.columns.show('pk')
|
||||||
|
|
||||||
@ -1046,7 +1058,11 @@ class DeviceView(ObjectView):
|
|||||||
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
'cable', 'power_port', '_path__destination',
|
'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'):
|
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
|
||||||
poweroutlet_table.columns.show('pk')
|
poweroutlet_table.columns.show('pk')
|
||||||
|
|
||||||
@ -1056,7 +1072,11 @@ class DeviceView(ObjectView):
|
|||||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||||
'lag', 'cable', '_path__destination', 'tags',
|
'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'):
|
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
|
||||||
interface_table.columns.show('pk')
|
interface_table.columns.show('pk')
|
||||||
|
|
||||||
@ -1064,13 +1084,21 @@ class DeviceView(ObjectView):
|
|||||||
frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
'rear_port', 'cable',
|
'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'):
|
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
|
||||||
frontport_table.columns.show('pk')
|
frontport_table.columns.show('pk')
|
||||||
|
|
||||||
# Rear ports
|
# Rear ports
|
||||||
rearports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
|
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'):
|
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
|
||||||
rearport_table.columns.show('pk')
|
rearport_table.columns.show('pk')
|
||||||
|
|
||||||
@ -1078,7 +1106,11 @@ class DeviceView(ObjectView):
|
|||||||
devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
'installed_device__device_type__manufacturer',
|
'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'):
|
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
|
||||||
devicebay_table.columns.show('pk')
|
devicebay_table.columns.show('pk')
|
||||||
|
|
||||||
@ -1086,7 +1118,11 @@ class DeviceView(ObjectView):
|
|||||||
inventoryitems = InventoryItem.objects.restrict(request.user, 'view').filter(
|
inventoryitems = InventoryItem.objects.restrict(request.user, 'view').filter(
|
||||||
device=device
|
device=device
|
||||||
).prefetch_related('manufacturer')
|
).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'):
|
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
|
||||||
devicebay_table.columns.show('pk')
|
devicebay_table.columns.show('pk')
|
||||||
|
|
||||||
|
@ -193,10 +193,6 @@ table.component-list td.subtable td {
|
|||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
table.interface-ips th {
|
|
||||||
font-size: 80%;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reports */
|
/* Reports */
|
||||||
table.reports td.method {
|
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>
|
onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||||
|
var netbox_csrf_token = "{{ csrf_token }}";
|
||||||
var loading = $(".loading");
|
var loading = $(".loading");
|
||||||
$(document).ajaxStart(function() {
|
$(document).ajaxStart(function() {
|
||||||
loading.show();
|
loading.show();
|
||||||
|
@ -485,7 +485,12 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Interfaces</strong>
|
<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" />
|
<input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -527,6 +532,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Front Ports</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=frontport_table %}
|
{% include 'responsive_table.html' with table=frontport_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -564,6 +574,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Rear Ports</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=rearport_table %}
|
{% include 'responsive_table.html' with table=rearport_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -601,6 +616,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Console Ports</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=consoleport_table %}
|
{% include 'responsive_table.html' with table=consoleport_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -638,6 +658,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Console Server Ports</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=consoleserverport_table %}
|
{% include 'responsive_table.html' with table=consoleserverport_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -675,6 +700,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Ports</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=powerport_table %}
|
{% include 'responsive_table.html' with table=powerport_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -711,6 +741,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Outlets</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=poweroutlet_table %}
|
{% include 'responsive_table.html' with table=poweroutlet_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -748,6 +783,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device Bays</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=devicebay_table %}
|
{% include 'responsive_table.html' with table=devicebay_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -779,6 +819,11 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Inventory Items</strong>
|
<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>
|
</div>
|
||||||
{% include 'responsive_table.html' with table=inventoryitem_table %}
|
{% include 'responsive_table.html' with table=inventoryitem_table %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
@ -811,6 +856,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'secrets/inc/private_key_modal.html' %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
@ -864,4 +918,5 @@ $(".cable-toggle").click(function() {
|
|||||||
</script>
|
</script>
|
||||||
<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></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/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
|
<script src="{% static 'js/tableconfig.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load buttons %}
|
{% load buttons %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
{% block buttons %}{% endblock %}
|
{% block buttons %}{% endblock %}
|
||||||
{% if request.user.is_authenticated and table_config_form %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if permissions.add and 'add' in action_buttons %}
|
{% if permissions.add and 'add' in action_buttons %}
|
||||||
{% add_button content_type.model_class|validated_viewname:"add" %}
|
{% add_button content_type.model_class|validated_viewname:"add" %}
|
||||||
@ -71,9 +72,6 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
{% if table_config_form %}
|
|
||||||
{% include 'inc/table_config_form.html' %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
@ -82,4 +80,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% table_config_form table table_name="ObjectTable" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'js/tableconfig.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% load form_helpers %}
|
{% 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-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -7,8 +7,7 @@
|
|||||||
<h4 class="modal-title">Table Configuration</h4>
|
<h4 class="modal-title">Table Configuration</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="" method="post" class="form-horizontal">
|
<form class="form-horizontal userconfigform" data-config-root="tables.{{ table_config_form.table_name }}">
|
||||||
{% csrf_token %}
|
|
||||||
{% render_form table_config_form %}
|
{% render_form table_config_form %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
@ -18,8 +17,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<input type="submit" class="btn btn-primary" name="set" value="Save" />
|
<input type="submit" class="btn btn-primary" value="Save" />
|
||||||
<input type="submit" class="btn btn-danger" name="clear" value="Reset" />
|
<input type="submit" class="btn btn-danger reset" value="Reset" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
@ -12,5 +12,8 @@ router.register('groups', views.GroupViewSet)
|
|||||||
# Permissions
|
# Permissions
|
||||||
router.register('permissions', views.ObjectPermissionViewSet)
|
router.register('permissions', views.ObjectPermissionViewSet)
|
||||||
|
|
||||||
|
# User preferences
|
||||||
|
router.register('config', views.UserConfigViewSet, basename='userconfig')
|
||||||
|
|
||||||
app_name = 'users-api'
|
app_name = 'users-api'
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.db.models import Count
|
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.routers import APIRootView
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
from netbox.api.views import ModelViewSet
|
from netbox.api.views import ModelViewSet
|
||||||
from users import filters
|
from users import filters
|
||||||
from users.models import ObjectPermission
|
from users.models import ObjectPermission, UserConfig
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
from utilities.utils import deepmerge
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
@ -41,3 +45,36 @@ class ObjectPermissionViewSet(ModelViewSet):
|
|||||||
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
|
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
|
||||||
serializer_class = serializers.ObjectPermissionSerializer
|
serializer_class = serializers.ObjectPermissionSerializer
|
||||||
filterset_class = filters.ObjectPermissionFilterSet
|
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.auth.models import Group, User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from users.models import ObjectPermission
|
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):
|
class AppTest(APITestCase):
|
||||||
@ -132,3 +131,56 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'constraints': {'name': 'TEST6'},
|
'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(
|
columns = forms.MultipleChoiceField(
|
||||||
choices=[],
|
choices=[],
|
||||||
|
required=False,
|
||||||
widget=forms.SelectMultiple(
|
widget=forms.SelectMultiple(
|
||||||
attrs={'size': 10}
|
attrs={'size': 10}
|
||||||
),
|
),
|
||||||
@ -168,8 +169,14 @@ class TableConfigForm(BootstrapMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, table, *args, **kwargs):
|
def __init__(self, table, *args, **kwargs):
|
||||||
|
self.table = table
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize columns field based on table attributes
|
# Initialize columns field based on table attributes
|
||||||
self.fields['columns'].choices = table.configurable_columns
|
self.fields['columns'].choices = table.configurable_columns
|
||||||
self.fields['columns'].initial = table.visible_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
|
import django_tables2 as tables
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
@ -11,10 +12,11 @@ class BaseTable(tables.Table):
|
|||||||
"""
|
"""
|
||||||
Default table for object lists
|
Default table for object lists
|
||||||
|
|
||||||
:param add_prefetch: By default, modify the queryset passed to the table upon initialization to automatically
|
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
|
||||||
prefetch related data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to
|
|
||||||
accommodate PrefixQuerySet.annotate_depth()).
|
|
||||||
"""
|
"""
|
||||||
|
# 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
|
add_prefetch = True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -22,7 +24,7 @@ class BaseTable(tables.Table):
|
|||||||
'class': 'table table-hover table-headings',
|
'class': 'table table-hover table-headings',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, columns=None, **kwargs):
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Set default empty_text if none was provided
|
# Set default empty_text if none was provided
|
||||||
@ -36,25 +38,27 @@ class BaseTable(tables.Table):
|
|||||||
if column.name not in default_columns:
|
if column.name not in default_columns:
|
||||||
self.columns.hide(column.name)
|
self.columns.hide(column.name)
|
||||||
|
|
||||||
# Apply custom column ordering
|
# Apply custom column ordering for user
|
||||||
if columns is not None:
|
if user is not None and not isinstance(user, AnonymousUser):
|
||||||
pk = self.base_columns.pop('pk', None)
|
columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
|
||||||
actions = self.base_columns.pop('actions', None)
|
if columns:
|
||||||
|
pk = self.base_columns.pop('pk', None)
|
||||||
|
actions = self.base_columns.pop('actions', None)
|
||||||
|
|
||||||
for name, column in self.base_columns.items():
|
for name, column in self.base_columns.items():
|
||||||
if name in columns:
|
if name in columns:
|
||||||
self.columns.show(name)
|
self.columns.show(name)
|
||||||
else:
|
else:
|
||||||
self.columns.hide(name)
|
self.columns.hide(name)
|
||||||
self.sequence = [c for c in columns if c in self.base_columns]
|
self.sequence = [c for c in columns if c in self.base_columns]
|
||||||
|
|
||||||
# Always include PK and actions column, if defined on the table
|
# Always include PK and actions column, if defined on the table
|
||||||
if pk:
|
if pk:
|
||||||
self.base_columns['pk'] = pk
|
self.base_columns['pk'] = pk
|
||||||
self.sequence.insert(0, 'pk')
|
self.sequence.insert(0, 'pk')
|
||||||
if actions:
|
if actions:
|
||||||
self.base_columns['actions'] = actions
|
self.base_columns['actions'] = actions
|
||||||
self.sequence.append('actions')
|
self.sequence.append('actions')
|
||||||
|
|
||||||
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
|
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
|
||||||
if self.add_prefetch and isinstance(self.data, TableQuerysetData):
|
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 django.utils.safestring import mark_safe
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
|
from utilities.forms import TableConfigForm
|
||||||
from utilities.utils import foreground_color
|
from utilities.utils import foreground_color
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@ -261,3 +262,11 @@ def badge(value, show_empty=False):
|
|||||||
'value': value,
|
'value': value,
|
||||||
'show_empty': show_empty,
|
'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)
|
perm_name = get_permission_for_model(model, action)
|
||||||
permissions[action] = request.user.has_perm(perm_name)
|
permissions[action] = request.user.has_perm(perm_name)
|
||||||
|
|
||||||
# Construct the table based on the user's permissions
|
# Construct the objects table
|
||||||
if request.user.is_authenticated:
|
table = self.table(self.queryset, user=request.user)
|
||||||
columns = request.user.config.get(f"tables.{self.table.__name__}.columns")
|
|
||||||
else:
|
|
||||||
columns = None
|
|
||||||
table = self.table(self.queryset, columns=columns)
|
|
||||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||||
table.columns.show('pk')
|
table.columns.show('pk')
|
||||||
|
|
||||||
@ -317,23 +313,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
return render(request, self.template_name, context)
|
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):
|
def extra_context(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user