Fixes #1830: Support assgin an existing device to a rack

This commit is contained in:
kobayashi 2020-02-18 02:27:30 -05:00
parent 1a8eea5aa9
commit 7245922ea7
9 changed files with 156 additions and 2 deletions

View File

@ -2,6 +2,7 @@
## Enhancements ## Enhancements
* [#1830](https://github.com/netbox-community/netbox/issues/1830) - Support Assgin existing device to a rack with 'add device' button
* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment * [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment
* [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings * [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings

View File

@ -2137,6 +2137,13 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
] ]
class DeviceAssignForm(BootstrapMixin, forms.Form):
q = forms.CharField(
required=False,
label='Search',
)
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm): class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
model = Device model = Device
field_order = [ field_order = [

View File

@ -423,9 +423,10 @@ class RackElevationHelperMixin:
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation): def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
link = drawing.add( link = drawing.add(
drawing.a( drawing.a(
href='{}?{}'.format( href='{}?{}&return_url=/dcim/racks/{}'.format(
reverse('dcim:device_add'), reverse('dcim:device_add'),
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_}) urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_}),
rack.pk,
), ),
target='_top' target='_top'
) )

View File

@ -40,6 +40,14 @@ DEVICE_LINK = """
</a> </a>
""" """
DEVICE_ASSIGN_LINK = """
{% if request.GET %}
<a href="{% url 'dcim:device_edit' pk=record.pk %}?rack={{ request.GET.rack }}&site={{ request.GET.site }}&face={{ request.GET.face }}&position={{ request.GET.position }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
{% else %}
<a href="{% url 'dcim:device_edit' pk=record.pk %}?rack={{ record.rack.pk }}&site={{ record.site.pk }}&face={{ record.face.pk }}&position={{ record.position.pk }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
{% endif %}
"""
REGION_ACTIONS = """ REGION_ACTIONS = """
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog"> <a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
@ -694,6 +702,28 @@ class DeviceDetailTable(DeviceTable):
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceAssignTable(BaseTable):
pk = ToggleColumn()
name = tables.TemplateColumn(
order_by=('_name',),
template_code=DEVICE_ASSIGN_LINK
)
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.LinkColumn(
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.display_name
)
class Meta(BaseTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
orderable = False
class DeviceImportTable(BaseTable): class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')

View File

@ -169,6 +169,7 @@ urlpatterns = [
path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
path('device/assign/', views.DeviceAssignView.as_view(), name='device_assign'),
path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'), path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'), path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'), path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),

View File

@ -1223,6 +1223,53 @@ class DeviceEditView(DeviceCreateView):
permission_required = 'dcim.change_device' permission_required = 'dcim.change_device'
class DeviceAssignView(PermissionRequiredMixin, View):
"""
Search for Devices to be assigned to a Rack.
"""
permission_required = 'dcim.change_device'
def dispatch(self, request, *args, **kwargs):
# Redirect user if a rack has not been provided
if 'rack' not in request.GET:
return redirect('dcim:device_add')
else:
# Restrict device assignment in the same site
self.site = request.GET['site']
return super().dispatch(request, *args, **kwargs)
def get(self, request):
form = forms.DeviceAssignForm()
return render(request, 'dcim/device_assign.html', {
'form': form,
'return_url': request.GET.get('return_url', ''),
})
def post(self, request):
form = forms.DeviceAssignForm(request.POST)
table = None
if form.is_valid():
devices = Device.objects.filter(site=self.site).prefetch_related(
'tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer'
)
# Limit to 100 results
devices = filters.DeviceFilterSet(request.POST, devices).qs[:100]
table = tables.DeviceAssignTable(devices)
return render(request, 'dcim/device_assign.html', {
'form': form,
'table': table,
'return_url': request.GET.get('return_url', ''),
})
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_device' permission_required = 'dcim.delete_device'
model = Device model = Device

View File

@ -0,0 +1,47 @@
{% extends 'utilities/obj_edit.html' %}
{% load static %}
{% load form_helpers %}
{% load helpers %}
{% block content %}
<form action="{% querystring request %}" method="post" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>Assign a Device</h3>
{% include 'dcim/inc/device_edit_header.html' with active_tab='assign' %}
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Select Device</strong></div>
<div class="panel-body">
{% render_field form.q %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3 text-right">
<button type="submit" class="btn btn-primary">Search</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
{% if table %}
<div class="row">
<div class="col-md-12" style="margin-top: 20px">
<h3>Search Results</h3>
{% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -1,5 +1,13 @@
{% extends 'utilities/obj_edit.html' %} {% extends 'utilities/obj_edit.html' %}
{% load static %}
{% load form_helpers %} {% load form_helpers %}
{% load helpers %}
{% block tabs %}
{% if not obj.pk %}
{% include 'dcim/inc/device_edit_header.html' with active_tab='add' %}
{% endif %}
{% endblock %}
{% block form %} {% block form %}
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -0,0 +1,12 @@
{% load helpers %}
<ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_add' %}{% querystring request %}">New Device</a>
</li>
{% if 'rack' in request.GET %}
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_assign' %}{% querystring request %}">Assign Device</a>
</li>
{% endif %}
</ul>