mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-27 01:36:11 -06:00
object edit updates, basic review request submittion
This commit is contained in:
parent
59c99ddbfe
commit
c72dabd450
@ -204,6 +204,11 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:device_list'
|
url_name='dcim:device_list'
|
||||||
)
|
)
|
||||||
|
# TODO: Remove this and just piggy back on the defaults
|
||||||
|
# once adjusted.
|
||||||
|
actions = columns.ActionsColumn(
|
||||||
|
actions=('edit', 'suggest', 'delete', 'changelog')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = models.Device
|
model = models.Device
|
||||||
|
@ -12,7 +12,7 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView, suggest_form_factory
|
||||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
|
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
|
||||||
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
@ -1916,6 +1916,14 @@ class DeviceEditView(generic.ObjectEditView):
|
|||||||
template_name = 'dcim/device_edit.html'
|
template_name = 'dcim/device_edit.html'
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(Device, 'suggest')
|
||||||
|
class DeviceSuggestView(generic.ObjectEditView):
|
||||||
|
queryset = Device.objects.all()
|
||||||
|
form = suggest_form_factory(Device, forms.DeviceForm)
|
||||||
|
template_name = 'dcim/device_edit.html'
|
||||||
|
is_suggest_view = True
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device, 'delete')
|
@register_model_view(Device, 'delete')
|
||||||
class DeviceDeleteView(generic.ObjectDeleteView):
|
class DeviceDeleteView(generic.ObjectDeleteView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.all()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
from django import forms as DjangoForms
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
|
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
@ -901,3 +903,16 @@ class RenderMarkdownView(View):
|
|||||||
rendered = render_markdown(form.cleaned_data['text'])
|
rendered = render_markdown(form.cleaned_data['text'])
|
||||||
|
|
||||||
return HttpResponse(rendered)
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def suggest_form_factory(obj_cls, form_cls):
|
||||||
|
return type(
|
||||||
|
f'dyn_suggest_{form_cls.__name__}',
|
||||||
|
(form_cls, ),
|
||||||
|
{
|
||||||
|
'reviewer': DjangoForms.ModelChoiceField(
|
||||||
|
# TODO: Use obj_cls to get the right qs here.
|
||||||
|
queryset=User.objects.filter(is_active=True, is_superuser=True),
|
||||||
|
required=True,)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -210,6 +210,7 @@ class ActionsColumn(tables.Column):
|
|||||||
empty_values = ()
|
empty_values = ()
|
||||||
actions = {
|
actions = {
|
||||||
'edit': ActionsItem('Edit', 'pencil', 'change', 'warning'),
|
'edit': ActionsItem('Edit', 'pencil', 'change', 'warning'),
|
||||||
|
'suggest': ActionsItem('Suggest edit', 'pencil', 'suggest', 'warning'),
|
||||||
'delete': ActionsItem('Delete', 'trash-can-outline', 'delete', 'danger'),
|
'delete': ActionsItem('Delete', 'trash-can-outline', 'delete', 'danger'),
|
||||||
'changelog': ActionsItem('Changelog', 'history'),
|
'changelog': ActionsItem('Changelog', 'history'),
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@ -9,6 +10,9 @@ from django.urls import reverse
|
|||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from netbox.staging import checkout
|
||||||
|
|
||||||
|
from extras.models.staging import ReviewRequest, Branch, Notification
|
||||||
from extras.signals import clear_webhooks
|
from extras.signals import clear_webhooks
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
@ -161,11 +165,19 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
form = None
|
form = None
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# Determine required permission based on whether we are editing an existing object
|
# Determine required permission based on whether we
|
||||||
self._permission_action = 'change' if kwargs else 'add'
|
# are suggesting an edit, editing an existing object,
|
||||||
|
# or creating a new instance.
|
||||||
|
if self.is_suggest:
|
||||||
|
self._permission_action = 'suggest'
|
||||||
|
else:
|
||||||
|
self._permission_action = 'change' if kwargs else 'add'
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_suggest(self):
|
||||||
|
return getattr(self, 'is_suggest_view', False)
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
# self._permission_action is set by dispatch() to either "add" or "change" depending on whether
|
# self._permission_action is set by dispatch() to either "add" or "change" depending on whether
|
||||||
# we are modifying an existing object or creating a new one.
|
# we are modifying an existing object or creating a new one.
|
||||||
@ -222,6 +234,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
'model': model,
|
'model': model,
|
||||||
'object': obj,
|
'object': obj,
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'is_suggest': self.is_suggest,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
'prerequisite_model': get_prerequisite_model(self.queryset),
|
'prerequisite_model': get_prerequisite_model(self.queryset),
|
||||||
**self.get_extra_context(request, obj),
|
**self.get_extra_context(request, obj),
|
||||||
@ -250,15 +263,39 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
if self.is_suggest:
|
||||||
object_created = form.instance.pk is None
|
utc_timestamp = int(datetime.utcnow().timestamp())
|
||||||
obj = form.save()
|
branch_name = f'{request.user}_{obj.__class__.__name__}_{utc_timestamp}'
|
||||||
|
branch = Branch.objects.create(name=branch_name, user=request.user)
|
||||||
|
owner = request.user
|
||||||
|
reviewer = form.cleaned_data.get('reviewer')
|
||||||
|
with checkout(branch):
|
||||||
|
obj = form.save()
|
||||||
|
|
||||||
# Check that the new object conforms with any assigned object-level permissions
|
# TODO: Remove branch if this atomic transaction fails.
|
||||||
if not self.queryset.filter(pk=obj.pk).exists():
|
with transaction.atomic():
|
||||||
raise PermissionsViolation()
|
ReviewRequest.objects.create(
|
||||||
|
owner=owner,
|
||||||
|
reviewer=reviewer,
|
||||||
|
branch=branch
|
||||||
|
)
|
||||||
|
Notification.objects.create(
|
||||||
|
user=reviewer,
|
||||||
|
title=f'{owner.get_full_name()} is requesting a review on a modifition on {obj}',
|
||||||
|
content=f'{owner.get_full_name()} has made modifications to {obj} and is requesting \
|
||||||
|
that you review and approve them.',
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with transaction.atomic():
|
||||||
|
object_created = form.instance.pk is None
|
||||||
|
obj = form.save()
|
||||||
|
|
||||||
|
# Check that the new object conforms with any assigned object-level permissions
|
||||||
|
if not self.queryset.filter(pk=obj.pk).exists():
|
||||||
|
raise PermissionsViolation()
|
||||||
|
|
||||||
msg = '{} {}'.format(
|
msg = '{} {}'.format(
|
||||||
|
'Created Review Request' if self.is_suggest else
|
||||||
'Created' if object_created else 'Modified',
|
'Created' if object_created else 'Modified',
|
||||||
self.queryset.model._meta.verbose_name
|
self.queryset.model._meta.verbose_name
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,13 @@ Context:
|
|||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if object.pk %}Editing {{ object|meta:"verbose_name" }} {{ object }}{% else %}Add a new {{ object|meta:"verbose_name" }}{% endif %}
|
{% if is_suggest %}
|
||||||
|
Suggesting an edit for {{ object|meta:"verbose_name" }} {{ object }}
|
||||||
|
{% elif object.pk %}
|
||||||
|
Editing {{ object|meta:"verbose_name" }} {{ object }}
|
||||||
|
{% else %}
|
||||||
|
Add a new {{ object|meta:"verbose_name" }}
|
||||||
|
{% endif %}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
@ -99,6 +105,13 @@ Context:
|
|||||||
|
|
||||||
{% endblock form %}
|
{% endblock form %}
|
||||||
|
|
||||||
|
{% if is_suggest and form.reviewer %}
|
||||||
|
<div class="field-group mb-5">
|
||||||
|
<h5 class="text-center">Reviewer</h5>
|
||||||
|
{% render_field form.reviewer %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-end my-3">
|
<div class="text-end my-3">
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
{% if object.pk %}
|
{% if object.pk %}
|
||||||
|
Loading…
Reference in New Issue
Block a user