mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-27 01:36:11 -06:00
actions on review request view, more tweaks
This commit is contained in:
parent
e10aa0afdb
commit
17ffd5d776
@ -62,7 +62,6 @@ class Branch(ChangeLoggedModel):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for change in self.staged_changes.all():
|
for change in self.staged_changes.all():
|
||||||
change.apply()
|
change.apply()
|
||||||
self.staged_changes.all().delete()
|
|
||||||
|
|
||||||
|
|
||||||
class StagedChange(ChangeLoggedModel):
|
class StagedChange(ChangeLoggedModel):
|
||||||
@ -187,6 +186,9 @@ class ReviewRequest(ChangeLoggedModel):
|
|||||||
related_name='assigned_review_requests'
|
related_name='assigned_review_requests'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: Make sure its corresponding branch and staged
|
||||||
|
# changes are deleted when this entry is deleted.
|
||||||
|
# atm this is not happening.
|
||||||
branch = models.ForeignKey(
|
branch = models.ForeignKey(
|
||||||
to=Branch,
|
to=Branch,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -11,7 +11,8 @@ from django_rq.queues import get_connection
|
|||||||
from rq import Worker
|
from rq import Worker
|
||||||
|
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from extras.choices import ChangeActionChoices
|
from extras.choices import ChangeActionChoices, \
|
||||||
|
ReviewRequestStateChoices, ReviewRequestStatusChoices
|
||||||
from extras.models.staging import ReviewRequest
|
from extras.models.staging import ReviewRequest
|
||||||
from utilities.htmx import is_htmx
|
from utilities.htmx import is_htmx
|
||||||
from utilities.templatetags.builtins.filters import render_markdown
|
from utilities.templatetags.builtins.filters import render_markdown
|
||||||
@ -917,7 +918,7 @@ class ReviewRequestListView(generic.ObjectListView):
|
|||||||
table = tables.ReviewRequestTable
|
table = tables.ReviewRequestTable
|
||||||
actions = ('bulk_delete')
|
actions = ('bulk_delete')
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, _):
|
||||||
return ReviewRequest.objects.filter(
|
return ReviewRequest.objects.filter(
|
||||||
Q(owner__id=self.request.user.id) |
|
Q(owner__id=self.request.user.id) |
|
||||||
Q(reviewer__id=self.request.user.id)
|
Q(reviewer__id=self.request.user.id)
|
||||||
@ -925,8 +926,9 @@ class ReviewRequestListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
@register_model_view(ReviewRequest)
|
@register_model_view(ReviewRequest)
|
||||||
class ReviewRequestEditView(generic.ObjectView):
|
class ReviewRequestView(generic.ObjectView):
|
||||||
queryset = ReviewRequest.objects.all()
|
queryset = ReviewRequest.objects.all()
|
||||||
|
actions = ('delete', )
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
data = []
|
data = []
|
||||||
@ -959,6 +961,46 @@ class ReviewRequestEditView(generic.ObjectView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ReviewRequest, 'approve')
|
||||||
|
class ReviewRequestDeleteView(View):
|
||||||
|
queryset = ReviewRequest.objects.all()
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# TODO: Should this logic go into a form that can be
|
||||||
|
# reused in the API?
|
||||||
|
id = kwargs.get('pk', 0)
|
||||||
|
qs = ReviewRequest.objects.filter(id=id)
|
||||||
|
rr = get_object_or_404(qs)
|
||||||
|
if rr.reviewer != request.user:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
rr.branch.merge()
|
||||||
|
rr.status = ReviewRequestStatusChoices.STATUS_CLOSED
|
||||||
|
rr.state = ReviewRequestStateChoices.STATE_APPROVED
|
||||||
|
rr.save()
|
||||||
|
return redirect(rr.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ReviewRequest, 'deny')
|
||||||
|
class ReviewRequestDeleteView(View):
|
||||||
|
queryset = ReviewRequest.objects.all()
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
id = kwargs.get('pk', 0)
|
||||||
|
qs = ReviewRequest.objects.filter(id=id)
|
||||||
|
rr = get_object_or_404(qs)
|
||||||
|
if rr.reviewer != request.user:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
rr.status = ReviewRequestStatusChoices.STATUS_CLOSED
|
||||||
|
rr.state = ReviewRequestStateChoices.STATE_DENIED
|
||||||
|
rr.save()
|
||||||
|
return redirect(rr.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ReviewRequest, 'delete')
|
||||||
|
class ReviewRequestDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = ReviewRequest.objects.all()
|
||||||
|
|
||||||
|
|
||||||
def suggest_form_factory(obj_cls, form_cls):
|
def suggest_form_factory(obj_cls, form_cls):
|
||||||
return type(
|
return type(
|
||||||
f'dyn_suggest_{form_cls.__name__}',
|
f'dyn_suggest_{form_cls.__name__}',
|
||||||
|
@ -179,6 +179,8 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
return getattr(self, 'is_suggest_view', False)
|
return getattr(self, 'is_suggest_view', False)
|
||||||
|
|
||||||
def _create_review_request(self, request, obj, form):
|
def _create_review_request(self, request, obj, form):
|
||||||
|
# TODO: Validate that owner and reviewer can't be the same
|
||||||
|
# user unless owner is a super user.
|
||||||
obj_cls_name = self.queryset.model._meta.verbose_name
|
obj_cls_name = self.queryset.model._meta.verbose_name
|
||||||
owner = request.user
|
owner = request.user
|
||||||
now_utc = int(datetime.utcnow().timestamp())
|
now_utc = int(datetime.utcnow().timestamp())
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
{% extends 'generic/object.html' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load perms %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block title %}{{ object }}{% endblock %}
|
{% block title %}{{ object }}{% endblock %}
|
||||||
|
|
||||||
{# ObjectChange does not support the default add/edit/delete controls #}
|
{# ObjectChange does not support the default add/edit/delete controls #}
|
||||||
{% block controls %}{% endblock %}
|
{% block controls %}
|
||||||
|
<div class="controls">
|
||||||
|
<div class="control-group">
|
||||||
|
{% if object.status != 'closed' and request.user|is_reviewer:object %}
|
||||||
|
{% approve_button object %}
|
||||||
|
{% deny_button object %}
|
||||||
|
{% endif %}
|
||||||
|
{% if object.status != 'closed' and request.user|is_owner:object %}
|
||||||
|
{% delete_button object %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
{% block subtitle %}{% endblock %}
|
{% block subtitle %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -66,6 +66,9 @@ Context:
|
|||||||
{% if request.user|can_change:object %}
|
{% if request.user|can_change:object %}
|
||||||
{% edit_button object %}
|
{% edit_button object %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.user|can_suggest:object %}
|
||||||
|
{% suggest_button object %}
|
||||||
|
{% endif %}
|
||||||
{% if request.user|can_delete:object %}
|
{% if request.user|can_delete:object %}
|
||||||
{% delete_button object %}
|
{% delete_button object %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
4
netbox/utilities/templates/buttons/approve.html
Normal file
4
netbox/utilities/templates/buttons/approve.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<form action="{{ url }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="approve" class="btn btn-sm btn-success">Approve Change</button>
|
||||||
|
</form>
|
4
netbox/utilities/templates/buttons/deny.html
Normal file
4
netbox/utilities/templates/buttons/deny.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<form action="{{ url }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="deny" class="btn btn-sm btn-danger">Deny Change</button>
|
||||||
|
</form>
|
3
netbox/utilities/templates/buttons/suggest.html
Normal file
3
netbox/utilities/templates/buttons/suggest.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<a href="{{ url }}" class="btn btn-sm btn-warning" role="button">
|
||||||
|
<span class="mdi mdi-pencil" aria-hidden="true"></span> Suggest Change
|
||||||
|
</a>
|
@ -36,6 +36,36 @@ def edit_button(instance):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('buttons/suggest.html')
|
||||||
|
def suggest_button(instance):
|
||||||
|
viewname = get_viewname(instance, 'suggest')
|
||||||
|
url = reverse(viewname, kwargs={'pk': instance.pk})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('buttons/approve.html')
|
||||||
|
def approve_button(instance):
|
||||||
|
viewname = get_viewname(instance, 'approve')
|
||||||
|
url = reverse(viewname, kwargs={'pk': instance.pk})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('buttons/deny.html')
|
||||||
|
def deny_button(instance):
|
||||||
|
viewname = get_viewname(instance, 'deny')
|
||||||
|
url = reverse(viewname, kwargs={'pk': instance.pk})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('buttons/delete.html')
|
@register.inclusion_tag('buttons/delete.html')
|
||||||
def delete_button(instance):
|
def delete_button(instance):
|
||||||
viewname = get_viewname(instance, 'delete')
|
viewname = get_viewname(instance, 'delete')
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
from django import template
|
from django import template
|
||||||
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
from utilities.utils import get_viewname
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@ -10,6 +13,15 @@ def _check_permission(user, instance, action):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_view_exists(instance):
|
||||||
|
try:
|
||||||
|
viewname = get_viewname(instance, 'suggest')
|
||||||
|
reverse(viewname, kwargs={'pk': instance.pk})
|
||||||
|
return True
|
||||||
|
except NoReverseMatch:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def can_view(user, instance):
|
def can_view(user, instance):
|
||||||
return _check_permission(user, instance, 'view')
|
return _check_permission(user, instance, 'view')
|
||||||
@ -25,6 +37,22 @@ def can_change(user, instance):
|
|||||||
return _check_permission(user, instance, 'change')
|
return _check_permission(user, instance, 'change')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def can_suggest(user, instance):
|
||||||
|
# TODO: View check is temporary until we impl. this everywhere.
|
||||||
|
return _check_view_exists(instance) and _check_permission(user, instance, 'suggest')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def is_owner(user, instance):
|
||||||
|
return instance.owner.id == user.id
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def is_reviewer(user, instance):
|
||||||
|
return instance.reviewer.id == user.id
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def can_delete(user, instance):
|
def can_delete(user, instance):
|
||||||
return _check_permission(user, instance, 'delete')
|
return _check_permission(user, instance, 'delete')
|
||||||
|
Loading…
Reference in New Issue
Block a user