Repository: netbox-community/netbox

Issue:
Error on cable edit after B port was deleted / wrong status
### Deployment Type

Self-hosted (Docker)

### NetBox Version

v3.7.5

### Python Version

3.11

### Steps to Reproduce

1. Have a device with front- and rear-Ports (Patch Panel)
2. Have a device with interfaces (switches)
3. Connect switch interface (here gi43) with front-port (here 22)
4. Delete rear ports on patch panel device
5. Go To Connections > Cables
6. Click on edit of cable --> error message

### Expected Behavior

- Edit Button works to connect cable again

### Observed Behavior

- Error Message 
![2024-04-19 19_20_00-Server Error and 5 more pages - Work - Microsoft​ Edge](https://github.com/netbox-community/netbox/assets/13959569/78d69dbd-7b02-4b1a-be0a-3523e51152a6)

Cables/UI: Unable to change from front to rear while editing cable
### NetBox version

v3.5.6

### Feature type

Change to existing functionality

### Proposed functionality

Currently, if someone accidentally connects a cable to the rear port instead of the front port, the entire cable must be deleted and created again. It would be nice to be able to change not only the port number but also the location (front/rear) when editing the cable. This might just be an ui change as the api seems to allow to change it.

### Use case

It is not necessary to delete the cable and enter all information (label, length etc) again. You can just reconnect it.

### Database changes

_No response_

### External dependencies

_No response_


Discussion:
```
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/views/generic/base.py", line 104, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/ahanson/dev/work/netbox/netbox/dcim/views.py", line 3196, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Users/ahanson/dev/work/netbox/netbox/netbox/views/generic/object_views.py", line 175, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Users/ahanson/dev/work/netbox/netbox/netbox/views/generic/base.py", line 26, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Users/ahanson/dev/work/netbox/netbox/utilities/views.py", line 106, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/views/generic/base.py", line 143, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/ahanson/dev/work/netbox/netbox/netbox/views/generic/object_views.py", line 235, in get
    return render(request, self.template_name, {
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/shortcuts.py", line 24, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 175, in render
    return self._render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/test/utils.py", line 112, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/test/utils.py", line 112, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/test/utils.py", line 112, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/test/utils.py", line 112, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/Users/ahanson/dev/work/netbox/venv/lib/python3.10/site-packages/django/template/library.py", line 258, in render
    _dict = self.func(*resolved_args, **resolved_kwargs)
  File "/Users/ahanson/dev/work/netbox/netbox/utilities/templatetags/form_helpers.py", line 55, in render_field
    'label': label or field.label,
AttributeError: 'str' object has no attribute 'label'
```
More broadly , same applies if you need to change termination type (front , rear , interface etc.) of one end you have to delete cable (and lose history)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. **Do not** attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
Giving this one a little bump. Who wants to take a run at it?
This would be very helpful. One of our team members spent a whole day entering cable terminations in the wrong direction, and being able to edit this instead of starting over would be fantastic.

Resolving patch:
diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py
--- a/netbox/dcim/forms/connections.py
+++ b/netbox/dcim/forms/connections.py
@@ -90,14 +90,14 @@ def __new__(mcs, name, bases, attrs):
     class _CableForm(CableForm, metaclass=FormMetaclass):
 
         def __init__(self, *args, initial=None, **kwargs):
-
             initial = initial or {}
+
             if a_type:
-                ct = ContentType.objects.get_for_model(a_type)
-                initial['a_terminations_type'] = f'{ct.app_label}.{ct.model}'
+                a_ct = ContentType.objects.get_for_model(a_type)
+                initial['a_terminations_type'] = f'{a_ct.app_label}.{a_ct.model}'
             if b_type:
-                ct = ContentType.objects.get_for_model(b_type)
-                initial['b_terminations_type'] = f'{ct.app_label}.{ct.model}'
+                b_ct = ContentType.objects.get_for_model(b_type)
+                initial['b_terminations_type'] = f'{b_ct.app_label}.{b_ct.model}'
 
             # TODO: Temporary hack to work around list handling limitations with utils.normalize_querydict()
             for field_name in ('a_terminations', 'b_terminations'):
@@ -108,8 +108,17 @@ def __init__(self, *args, initial=None, **kwargs):
 
             if self.instance and self.instance.pk:
                 # Initialize A/B terminations when modifying an existing Cable instance
-                self.initial['a_terminations'] = self.instance.a_terminations
-                self.initial['b_terminations'] = self.instance.b_terminations
+                if a_type and self.instance.a_terminations and a_ct == ContentType.objects.get_for_model(self.instance.a_terminations[0]):
+                    self.initial['a_terminations'] = self.instance.a_terminations
+                if b_type and self.instance.b_terminations and b_ct == ContentType.objects.get_for_model(self.instance.b_terminations[0]):
+                    self.initial['b_terminations'] = self.instance.b_terminations
+            else:
+                # Need to clear terminations if swapped type - but need to do it only
+                # if not from instance
+                if a_type:
+                    initial.pop('a_terminations', None)
+                if b_type:
+                    initial.pop('b_terminations', None)
 
         def clean(self):
             super().clean()


Test patch: