From 859bda079e6fff28018ceace14e5319d3bb1846f Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 8 Apr 2021 13:55:33 +0200 Subject: [PATCH] initial work on #5952 --- netbox/dcim/constants.py | 11 +++++++ netbox/dcim/forms.py | 58 ++++++++++++++++++++++++++---------- netbox/dcim/models/cables.py | 17 +++++++---- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 0fc69be3b..38478e269 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -59,6 +59,17 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage # Cabling and connections # +# Cable endpoint parent types +CABLE_TERMINATION_PARENT_MODELS = Q( + Q(app_label='circuits', model__in=( + 'circuit', + )) | + Q(app_label='dcim', model__in=( + 'device', + 'powerpanel', + )) +) + # Cable endpoint types CABLE_TERMINATION_MODELS = Q( Q(app_label='circuits', model__in=( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 77061d556..ce0ab141c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3819,10 +3819,13 @@ class CableForm(BootstrapMixin, CustomFieldModelForm): class CableCSVForm(CustomFieldModelCSVForm): # Termination A - side_a_device = CSVModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Side A device' + side_a_parent_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=CABLE_TERMINATION_PARENT_MODELS, + help_text='Side A parent type' + ) + side_a_parent_id = forms.IntegerField( + help_text='Side A parent ID' ) side_a_type = CSVContentTypeField( queryset=ContentType.objects.all(), @@ -3834,10 +3837,13 @@ class CableCSVForm(CustomFieldModelCSVForm): ) # Termination B - side_b_device = CSVModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Side B device' + side_b_parent_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=CABLE_TERMINATION_PARENT_MODELS, + help_text='Side B parent type' + ) + side_b_parent_id = forms.IntegerField( + help_text='Side B parent ID' ) side_b_type = CSVContentTypeField( queryset=ContentType.objects.all(), @@ -3868,13 +3874,30 @@ class CableCSVForm(CustomFieldModelCSVForm): class Meta: model = Cable fields = [ - 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type', - 'status', 'label', 'color', 'length', 'length_unit', + 'side_a_parent_type', 'side_a_parent_id', 'side_a_type', 'side_a_name', + 'side_b_parent_type', 'side_b_parent_id', 'side_b_type', 'side_b_name', + 'type', 'status', 'label', 'color', 'length', 'length_unit', ] help_texts = { 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } + def _get_parent(self, side): + parent_type = self.cleaned_data.get(f'side_{side}_parent_type') + parent_id = self.cleaned_data.get(f'side_{side}_parent_id') + if not parent_type or not parent_id: + return None + + model = parent_type.model_class() + + return model.objects.get(pk=parent_id) + + def _translate_model(self, parent_type): + # TODO: maybe we went overboard with making this generic/extensible? + return { + "powerpanel": "power_panel", + }.get(parent_type.model, parent_type.model) + def _clean_side(self, side): """ Derive a Cable's A/B termination objects. @@ -3883,19 +3906,24 @@ class CableCSVForm(CustomFieldModelCSVForm): """ assert side in 'ab', f"Invalid side designation: {side}" - device = self.cleaned_data.get(f'side_{side}_device') + parent_type = self.cleaned_data.get(f'side_{side}_parent_type') + parent = self._get_parent(side) content_type = self.cleaned_data.get(f'side_{side}_type') name = self.cleaned_data.get(f'side_{side}_name') - if not device or not content_type or not name: + if not parent or not content_type or not name: return None model = content_type.model_class() try: - termination_object = model.objects.get(device=device, name=name) + # the parent is named like the model, we utilize that fact for the query + termination_object = model.objects.get(**{ + "name" if hasattr(model, "name") else "pk": name, + self._translate_model(parent_type): parent + }) if termination_object.cable is not None: - raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected") + raise forms.ValidationError(f"Side {side.upper()}: {parent} {termination_object} is already connected") except ObjectDoesNotExist: - raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}") + raise forms.ValidationError(f"{side.upper()} side termination not found: {parent} {name}") setattr(self.instance, f'termination_{side}', termination_object) return termination_object diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index e8c9211f5..024003a59 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -112,8 +112,9 @@ class Cable(ChangeLoggedModel, CustomFieldModel): objects = RestrictedQuerySet.as_manager() csv_headers = [ - 'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label', - 'color', 'length', 'length_unit', + 'side_a_parent_type', 'side_a_parent_id', 'side_a_type', 'side_a_name', + 'side_b_parent_type', 'side_b_parent_id', 'side_b_type', 'side_b_name', + 'type', 'status', 'label', 'color', 'length', 'length_unit', ] class Meta: @@ -281,12 +282,16 @@ class Cable(ChangeLoggedModel, CustomFieldModel): def to_csv(self): return ( + '{}.{}'.format(self.termination_a.parent._meta.app_label, self.termination_a.parent._meta.model_name), + self.termination_a.parent.id, '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model), - self.termination_a_id, + self.termination_a.name if hasattr(self.termination_a, "name") else self.termination_a_id, + '{}.{}'.format(self.termination_b.parent._meta.app_label, self.termination_b.parent._meta.model_name), + self.termination_b.parent.id, '{}.{}'.format(self.termination_b_type.app_label, self.termination_b_type.model), - self.termination_b_id, - self.get_type_display(), - self.get_status_display(), + self.termination_b.name if hasattr(self.termination_b, "name") else self.termination_b_id, + self.type, + self.status, self.label, self.color, self.length,