object edit updates, basic review request submittion

This commit is contained in:
rmanyari 2023-03-14 14:13:59 -06:00
parent 59c99ddbfe
commit c72dabd450
6 changed files with 90 additions and 11 deletions

View File

@ -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

View File

@ -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()

View File

@ -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,)
}
)

View File

@ -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'),
} }

View File

@ -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
) )

View File

@ -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 %}