Skip to content

Commit

Permalink
Fixes #19: Check user permissions before saving
Browse files Browse the repository at this point in the history
  • Loading branch information
minitriga committed Sep 27, 2024
1 parent a5129ff commit ac68780
Show file tree
Hide file tree
Showing 9 changed files with 1,592 additions and 220 deletions.
125 changes: 95 additions & 30 deletions netbox_reorder_rack/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,24 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework import status
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from utilities.permissions import get_permission_for_model


def get_device_name(device):
if device.virtual_chassis:
name = f"{device.virtual_chassis.name}:{device.vc_position}"
elif device.name:
name = device.name
else:
name = str(device.device_type)

return name


class ReorderRackSerializer(serializers.Serializer):
Expand All @@ -26,44 +40,95 @@ class SaveViewSet(PermissionRequiredMixin, viewsets.ViewSet):

def update(self, request, pk):
rack = get_object_or_404(Rack, pk=pk)
permission = get_permission_for_model(Device, "change")

# Validate input using serializer
serializer = ReorderRackSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

try:
serializer = ReorderRackSerializer(request.data)
changes_made = False # Flag to track if any changes were made

with transaction.atomic():
for new in request.data["front"]:
device = rack.devices.filter(pk=new["id"]).first()
current_device = get_object_or_404(Device, pk=new["id"])

if current_device.face != new[
"face"
] or device.position != decimal.Decimal(new["y"]):
device.position = decimal.Decimal(new["y"])
device.face = new["face"]
device.clean()
device.save()

for new in request.data["rear"]:
device = rack.devices.filter(pk=new["id"]).first()
current_device = get_object_or_404(Device, pk=new["id"])
if current_device.face != new[
"face"
] or device.position != decimal.Decimal(new["y"]):
device.position = decimal.Decimal(new["y"])
device.face = new["face"]
device.clean()
device.save()
# other = unracked
for new in request.data["other"]:
device = rack.devices.filter(pk=new["id"]).first()
device.position = None
device.clean()
device.save()
# Update devices in different categories
changes_made |= self._update_device_positions(
request,
rack,
serializer.validated_data["front"],
permission,
"front",
)
changes_made |= self._update_device_positions(
request, rack, serializer.validated_data["rear"], permission, "rear"
)
changes_made |= self._update_device_positions(
request,
rack,
serializer.validated_data["other"],
permission,
"other",
is_other=True,
)

# If no changes were made, return 304 or a custom response
if not changes_made:
return Response(
{"message": "No changes detected."},
status=status.HTTP_304_NOT_MODIFIED,
)

return Response(
{"message": "POST request received", "data": serializer.data},
{
"message": "Devices reordered successfully",
"data": serializer.data,
},
status=status.HTTP_201_CREATED,
)
except PermissionDenied as e:
return Response(
{"message": "Permission denied", "error": str(e)},
status=status.HTTP_403_FORBIDDEN,
)
except Exception as e:
return Response(
{"message": "Error saving data", "error": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

def _update_device_positions(
self, request, rack, device_data_list, permission, device_type, is_other=False
):
"""Helper method to update device positions based on the category."""
changes_made = False # Local flag to track if changes are made

for device_data in device_data_list:
device = rack.devices.filter(pk=device_data["id"]).first()
current_device = get_object_or_404(
Device.objects.restrict(request.user), pk=device_data["id"]
)

# Update position and face for 'front' and 'rear' devices if changed
if current_device.face != device_data[
"face"
] or device.position != decimal.Decimal(device_data["y"]):
if is_other:
device.position = None # For 'other' devices
else:
device.position = decimal.Decimal(device_data["y"])
device.face = device_data["face"]

self._check_permission(request, device, permission)

# Save the device and mark changes as made
device.clean()
device.save()
changes_made = True

return changes_made # Return whether changes were made

def _check_permission(self, request, device, permission):
"""Helper method to check if the user has permission for the device."""
if not request.user.has_perm(permission, obj=device):
raise PermissionDenied(
_(f"You do not have permissions to edit {get_device_name(device)}.")
)
13 changes: 10 additions & 3 deletions netbox_reorder_rack/static/netbox_reorder_rack/js/rack.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions netbox_reorder_rack/static/netbox_reorder_rack/js/rack.js.map

Large diffs are not rendered by default.

160 changes: 136 additions & 24 deletions netbox_reorder_rack/static_dev/js/rack.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,93 @@
// Variable to track whether changes have been made
import { GridStack } from 'gridstack';
import { Toast } from 'bootstrap';

var changesMade = false;
var gridItemsMap = [];
var grids = [];

function createToast(level, title, message, extra) {
// Set the icon based on the toast level
let iconName = 'mdi-alert'; // default icon
switch (level) {
case 'warning':
iconName = 'mdi-alert';
break;
case 'success':
iconName = 'mdi-check-circle';
break;
case 'info':
iconName = 'mdi-information';
break;
case 'danger':
iconName = 'mdi-alert';
break;
}

// Create the container for the toast
const container = document.createElement('div');
container.setAttribute('class', 'toast-container position-fixed bottom-0 end-0 m-3');

// Create the main toast element
const main = document.createElement('div');
main.setAttribute('class', `toast`);
main.setAttribute('role', 'alert');
main.setAttribute('aria-live', 'assertive');
main.setAttribute('aria-atomic', 'true');

// Create the toast header
const header = document.createElement('div');
header.setAttribute('class', `toast-header bg-${level} text-dark`);

// Add the icon to the header
const icon = document.createElement('i');
icon.setAttribute('class', `mdi ${iconName}`);

// Add the title to the header
const titleElement = document.createElement('strong');
titleElement.setAttribute('class', 'me-auto ms-1');
titleElement.innerText = title;

// Add the close button to the header
const button = document.createElement('button');
button.setAttribute('type', 'button');
button.setAttribute('class', 'btn-close');
button.setAttribute('data-bs-dismiss', 'toast');
button.setAttribute('aria-label', 'Close');

// Create the toast body
const body = document.createElement('div');
body.setAttribute('class', 'toast-body text-dark');
body.innerText = message.trim();

// Assemble the header
header.appendChild(icon);
header.appendChild(titleElement);

// If extra info is provided, add it to the header
if (typeof extra !== 'undefined') {
const extraElement = document.createElement('small');
extraElement.setAttribute('class', 'text-dark');
extraElement.innerText = extra;
header.appendChild(extraElement);
}

// Add the close button to the header
header.appendChild(button);

// Assemble the main toast
main.appendChild(header);
main.appendChild(body);
container.appendChild(main);

// Add the toast container to the body
document.body.appendChild(container);

// Initialize the Bootstrap toast
const toast = new Toast(main);
return toast;
}

// Function to get the items from the grids
function getItems(grids) {
// Initialize the gridItemsMap
Expand Down Expand Up @@ -52,7 +135,6 @@ function initializeGrid(element, acceptWidgets) {

function saveRack(rack_id, desc_units) {
getItems(grids);
console.log(desc_units);
var data = {};

// Get the items from the grids
Expand All @@ -71,7 +153,7 @@ function saveRack(rack_id, desc_units) {
let y = parseInt(item.getAttribute('gs-y')) / 2;

// Get the 'height' attribute of the item and divide by 2
let u_height = parseInt(item.getAttribute('gs-h')) / 2 ;
let u_height = parseInt(item.getAttribute('gs-h')) / 2;

// Get the 'max-row' attribute of the grid and divide by 2
let rack_height = item.gridstackNode.grid.el.getAttribute('gs-max-row') / 2;
Expand Down Expand Up @@ -107,32 +189,50 @@ function saveRack(rack_id, desc_units) {

try {
const res = fetch('/' + basePath + 'api/plugins/reorder/save/' + rack_id + '/', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': netbox_csrf_token,
},
body: JSON.stringify(data),
})
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': netbox_csrf_token,
},
body: JSON.stringify(data),
});

res.then(response => {
if (response.ok) {
changesMade = false;
var button = document.getElementById('saveButton');
button.setAttribute('disabled', 'disabled');

// Get JSON data from response
response.json().then(jsonData => {
console.log(jsonData);
// Do something with the jsonData
});

window.location.href = returnUrl;
}
if (response.ok) {
// Reset changesMade flag and disable save button
changesMade = false;
var button = document.getElementById('saveButton');
button.setAttribute('disabled', 'disabled');

// Get JSON data from response
response.json().then(jsonData => {
console.log(jsonData);
});

// Redirect to the return URL after successful save
window.location.href = returnUrl;

} else if (response.status === 304) {
// Handle the 304 Not Modified status
console.warn('No changes detected.');
const toast = createToast('warning', 'Info', 'No changes were detected.', 'The data has not been modified.');
toast.show();

} else {
// Handle other errors
response.json().then(errorData => {
console.error('Error:', errorData);

// Create and show an error toast notification
const toast = createToast('danger', 'Error', errorData.error, errorData.message);
toast.show();
});
}
});
} catch (error) {
console.error('Error:', error);
console.error('Error:', error);
}

}

let frontGrid = initializeGrid("#grid-front", acceptWidgets);
Expand Down Expand Up @@ -219,7 +319,7 @@ grids.forEach(function (grid, gridIndex) {
// Get the items from the grids
getItems(grids);

// If the widget was dropped non-racked grid from the front or rear grid
// If the widget was dropped non-racked grid from the front or rear grid
} else if ((originGrid === 0 || originGrid === 1) && gridIndex === 2) {
// If the widget is full depth, remove the widget from the other grid
if (newWidget.el.getAttribute('data-full-depth') === "True") {
Expand Down Expand Up @@ -271,3 +371,15 @@ window.addEventListener('beforeunload', function (event) {
event.returnValue = 'Are you sure you want to leave? Changes you made may not be saved.';
}
});

document.getElementById('view-selector').addEventListener('change', function () {
// Get the selected option value
var selectedValue = this.value;

// Construct the new URL with the selected option as a query parameter
var currentUrl = window.location.href.split('?')[0]; // Get the base URL (without parameters)
var newUrl = currentUrl + '?view=' + selectedValue;

// Redirect to the new URL
window.location.href = newUrl;
});
Loading

0 comments on commit ac68780

Please sign in to comment.