diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index e224196ad..c06398f5b 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -2,6 +2,7 @@ ## 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 * [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4c8a0821f..ec12c82c6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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): model = Device field_order = [ diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 29afef1f1..98ba650ab 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -423,9 +423,10 @@ class RackElevationHelperMixin: def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation): link = drawing.add( drawing.a( - href='{}?{}'.format( + href='{}?{}&return_url=/dcim/racks/{}'.format( 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' ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 1f67b93f1..38ba8e6d4 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -40,6 +40,14 @@ DEVICE_LINK = """ """ +DEVICE_ASSIGN_LINK = """ +{% if request.GET %} + {{ record }} +{% else %} + {{ record }} +{% endif %} +""" + REGION_ACTIONS = """ @@ -694,6 +702,28 @@ class DeviceDetailTable(DeviceTable): 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): name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 165ca9e02..fab8a7655 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -169,6 +169,7 @@ urlpatterns = [ 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/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), + path('device/assign/', views.DeviceAssignView.as_view(), name='device_assign'), path('devices//', views.DeviceView.as_view(), name='device'), path('devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), path('devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0bb6658a2..e18a5ef14 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1223,6 +1223,53 @@ class DeviceEditView(DeviceCreateView): 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): permission_required = 'dcim.delete_device' model = Device diff --git a/netbox/templates/dcim/device_assign.html b/netbox/templates/dcim/device_assign.html new file mode 100644 index 000000000..7e2ab1e9d --- /dev/null +++ b/netbox/templates/dcim/device_assign.html @@ -0,0 +1,47 @@ +{% extends 'utilities/obj_edit.html' %} +{% load static %} +{% load form_helpers %} +{% load helpers %} + +{% block content %} +
+ {% csrf_token %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} +
+
+

Assign a Device

+ {% include 'dcim/inc/device_edit_header.html' with active_tab='assign' %} + {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
+
+ {% endif %} +
+
Select Device
+
+ {% render_field form.q %} +
+
+
+
+
+ +
+
+ {% if table %} +
+
+

Search Results

+ {% include 'utilities/obj_table.html' with table_template='panel_table.html' %} +
+
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 1486c1ad5..1f12f9983 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -1,5 +1,13 @@ {% extends 'utilities/obj_edit.html' %} +{% load static %} {% 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 %}
diff --git a/netbox/templates/dcim/inc/device_edit_header.html b/netbox/templates/dcim/inc/device_edit_header.html new file mode 100644 index 000000000..e32300fd1 --- /dev/null +++ b/netbox/templates/dcim/inc/device_edit_header.html @@ -0,0 +1,12 @@ +{% load helpers %} + +