mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #6323 from netbox-community/feature-2434
Closes #2434: Show 'Create & Assign IP Address' Button when Creating …
This commit is contained in:
@ -1,7 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
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.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import F, Prefetch
|
from django.db.models import F, Prefetch
|
||||||
@ -1910,6 +1913,30 @@ class InterfaceCreateView(generic.ComponentCreateView):
|
|||||||
model_form = forms.InterfaceForm
|
model_form = forms.InterfaceForm
|
||||||
template_name = 'dcim/device_component_add.html'
|
template_name = 'dcim/device_component_add.html'
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
Override inherited post() method to handle request to assign newly created
|
||||||
|
interface objects (first object) to an IP Address object.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger('netbox.dcim.views.InterfaceCreateView')
|
||||||
|
form = self.form(request.POST, initial=request.GET)
|
||||||
|
new_objs = self.validate_form(request, form)
|
||||||
|
|
||||||
|
if form.is_valid() and not form.errors:
|
||||||
|
if '_addanother' in request.POST:
|
||||||
|
return redirect(request.get_full_path())
|
||||||
|
elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and request.user.has_perm('ipam.add_ipaddress'):
|
||||||
|
first_obj = new_objs[0].pk
|
||||||
|
return redirect(f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}')
|
||||||
|
else:
|
||||||
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'component_type': self.queryset.model._meta.verbose_name,
|
||||||
|
'form': form,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class InterfaceEditView(generic.ObjectEditView):
|
class InterfaceEditView(generic.ObjectEditView):
|
||||||
queryset = Interface.objects.all()
|
queryset = Interface.objects.all()
|
||||||
|
@ -1105,25 +1105,47 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
logger = logging.getLogger('netbox.views.ComponentCreateView')
|
logger = logging.getLogger('netbox.views.ComponentCreateView')
|
||||||
form = self.form(request.POST, initial=request.GET)
|
form = self.form(request.POST, initial=request.GET)
|
||||||
|
self.validate_form(request, form)
|
||||||
|
|
||||||
|
if form.is_valid() and not form.errors:
|
||||||
|
if '_addanother' in request.POST:
|
||||||
|
return redirect(request.get_full_path())
|
||||||
|
else:
|
||||||
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'component_type': self.queryset.model._meta.verbose_name,
|
||||||
|
'form': form,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
def validate_form(self, request, form):
|
||||||
|
"""
|
||||||
|
Validate form values and set errors on the form object as they are detected. If
|
||||||
|
no errors are found, signal success messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger('netbox.views.ComponentCreateView')
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
new_components = []
|
new_components = []
|
||||||
data = deepcopy(request.POST)
|
data = deepcopy(request.POST)
|
||||||
|
|
||||||
names = form.cleaned_data['name_pattern']
|
names = form.cleaned_data['name_pattern']
|
||||||
labels = form.cleaned_data.get('label_pattern')
|
labels = form.cleaned_data.get('label_pattern')
|
||||||
|
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
label = labels[i] if labels else None
|
label = labels[i] if labels else None
|
||||||
# Initialize the individual component form
|
# Initialize the individual component form
|
||||||
data['name'] = name
|
data['name'] = name
|
||||||
data['label'] = label
|
data['label'] = label
|
||||||
|
|
||||||
if hasattr(form, 'get_iterative_data'):
|
if hasattr(form, 'get_iterative_data'):
|
||||||
data.update(form.get_iterative_data(i))
|
data.update(form.get_iterative_data(i))
|
||||||
|
|
||||||
component_form = self.model_form(data)
|
component_form = self.model_form(data)
|
||||||
|
|
||||||
if component_form.is_valid():
|
if component_form.is_valid():
|
||||||
new_components.append(component_form)
|
new_components.append(component_form)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for field, errors in component_form.errors.as_data().items():
|
for field, errors in component_form.errors.as_data().items():
|
||||||
# Assign errors on the child form's name/label field to name_pattern/label_pattern on the parent form
|
# Assign errors on the child form's name/label field to name_pattern/label_pattern on the parent form
|
||||||
@ -1135,11 +1157,8 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|||||||
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
|
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
|
||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
||||||
# Create the new components
|
# Create the new components
|
||||||
new_objs = []
|
new_objs = []
|
||||||
for component_form in new_components:
|
for component_form in new_components:
|
||||||
@ -1153,21 +1172,14 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|||||||
messages.success(request, "Added {} {}".format(
|
messages.success(request, "Added {} {}".format(
|
||||||
len(new_components), self.queryset.model._meta.verbose_name_plural
|
len(new_components), self.queryset.model._meta.verbose_name_plural
|
||||||
))
|
))
|
||||||
if '_addanother' in request.POST:
|
# Return the newly created objects so overridden post methods can use the data as needed.
|
||||||
return redirect(request.get_full_path())
|
return new_objs
|
||||||
else:
|
|
||||||
return redirect(self.get_return_url(request))
|
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
msg = "Component creation failed due to object-level permissions violation"
|
msg = "Component creation failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
|
return None
|
||||||
return render(request, self.template_name, {
|
|
||||||
'component_type': self.queryset.model._meta.verbose_name,
|
|
||||||
'form': form,
|
|
||||||
'return_url': self.get_return_url(request),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||||
|
@ -20,6 +20,11 @@
|
|||||||
<div class="col col-md-8 text-end">
|
<div class="col col-md-8 text-end">
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||||
|
{% if component_type == 'interface' and perms.ipam.add_ipaddress %}
|
||||||
|
<button type="submit" name="_assignip" class="btn btn-outline-success">
|
||||||
|
Create & Assign IP Address
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
||||||
Create & Add Another
|
Create & Add Another
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,49 +1,39 @@
|
|||||||
<div
|
<div id="django-messages" class="toast-container position-fixed bottom-0 end-0 m-3">
|
||||||
id="django-messages"
|
{% if messages %}
|
||||||
class="toast-container position-fixed bottom-0 end-0 m-3"
|
{% for message in messages %}
|
||||||
>
|
<div class="django-message toast align-items-center border-0 bg-{% if message.tags %}{{ message.tags }}{% else %}info{% endif %}" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="10000">
|
||||||
{% if messages %} {% for message in messages %}
|
|
||||||
<div
|
|
||||||
class="django-message toast align-items-center border-0 bg-{% if message.tags %}{{ message.tags }}{% else %}info{% endif %}"
|
|
||||||
role="alert"
|
|
||||||
aria-live="assertive"
|
|
||||||
aria-atomic="true"
|
|
||||||
data-bs-delay="10000"
|
|
||||||
>
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="toast-body">
|
<div class="toast-body">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
type="button"
|
|
||||||
class="btn-close me-2 m-auto"
|
|
||||||
data-bs-dismiss="toast"
|
|
||||||
aria-label="Close"
|
|
||||||
></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif form and form.non_field_errors %}
|
{% elif form and form.non_field_errors %}
|
||||||
{% for error in form.non_field_errors.get_json_data %}
|
{% for error in form.non_field_errors.get_json_data %}
|
||||||
<div
|
<div class="django-message toast align-items-center border-0 bg-danger" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="10000">
|
||||||
class="django-message toast align-items-center border-0 bg-danger"
|
|
||||||
role="alert"
|
|
||||||
aria-live="assertive"
|
|
||||||
aria-atomic="true"
|
|
||||||
data-bs-delay="10000"
|
|
||||||
>
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="toast-body">
|
<div class="toast-body">
|
||||||
{{ error.message }}
|
{{ error.message }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
type="button"
|
|
||||||
class="btn-close me-2 m-auto"
|
|
||||||
data-bs-dismiss="toast"
|
|
||||||
aria-label="Close"
|
|
||||||
></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% elif form and form.errors %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="django-message toast align-items-center border-0 bg-danger" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="60000">
|
||||||
|
<div class="toast-header bg-danger">
|
||||||
|
<strong class="me-auto">{{ field.label }}</strong>
|
||||||
|
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body">
|
||||||
|
{{ error|escape }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user