|
|
@@ -1,5 +1,4 @@
|
|
|
from django import forms
|
|
|
-from django.db.models import Q
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
|
|
|
@@ -223,55 +222,57 @@ class ModuleBayTemplateImportForm(forms.ModelForm):
|
|
|
|
|
|
class Meta:
|
|
|
model = ModuleBayTemplate
|
|
|
- # module_bay_types must stay last: clean_device_type/clean_module_type narrow its queryset by
|
|
|
- # manufacturer before it is itself cleaned, and Django cleans fields in this order.
|
|
|
fields = [
|
|
|
'device_type', 'module_type', 'name', 'label', 'position', 'enabled', 'description',
|
|
|
'module_bay_types',
|
|
|
]
|
|
|
|
|
|
- def _scope_module_bay_types(self, manufacturer):
|
|
|
- module_bay_types = self.fields['module_bay_types']
|
|
|
- module_bay_types.queryset = module_bay_types.queryset.filter(
|
|
|
- Q(manufacturer__isnull=True) | Q(manufacturer=manufacturer)
|
|
|
- )
|
|
|
-
|
|
|
- def clean_device_type(self):
|
|
|
- if device_type := self.cleaned_data['device_type']:
|
|
|
- self._scope_module_bay_types(device_type.manufacturer)
|
|
|
-
|
|
|
- return device_type
|
|
|
-
|
|
|
- def clean_module_type(self):
|
|
|
- if module_type := self.cleaned_data['module_type']:
|
|
|
- self._scope_module_bay_types(module_type.manufacturer)
|
|
|
-
|
|
|
- return module_type
|
|
|
-
|
|
|
- def clean_module_bay_types(self):
|
|
|
+ def clean(self):
|
|
|
"""
|
|
|
- Collapse to one match per name, preferring a manufacturer-specific match over a global
|
|
|
- one. ModuleBayType's unique constraint is on (manufacturer, name), not name alone, so a
|
|
|
- name can legitimately collide between a global type and one scoped to this template's
|
|
|
- own manufacturer (narrowed by clean_device_type/clean_module_type above); the field's
|
|
|
- default name-based lookup resolves both matches into cleaned_data rather than picking
|
|
|
- one, since it has no way to know which is meant.
|
|
|
-
|
|
|
- If neither device_type nor module_type resolved (so the queryset above was never
|
|
|
- narrowed), a name could in principle collide across two unrelated manufacturers here
|
|
|
- too. That's not reachable with valid data: ModularComponentTemplateModel.clean()
|
|
|
- rejects a template with neither parent, so the form fails in _post_clean() before this
|
|
|
- method's result would ever be saved.
|
|
|
+ Resolve each referenced bay type name against the parent type's own manufacturer, plus
|
|
|
+ bay types having no manufacturer (global), preferring a manufacturer-specific match
|
|
|
+ over a global one. ModuleBayType's unique constraint is on (manufacturer, name), not
|
|
|
+ name alone, so a name can legitimately match both, and the field's name-based lookup
|
|
|
+ resolves every match into cleaned_data rather than picking one.
|
|
|
+
|
|
|
+ This runs in clean() rather than in clean_module_bay_types() so that it does not depend
|
|
|
+ on the parent having been cleaned first, which would make it sensitive to the order of
|
|
|
+ Meta.fields.
|
|
|
"""
|
|
|
- module_bay_types = self.cleaned_data['module_bay_types']
|
|
|
+ super().clean()
|
|
|
+
|
|
|
+ module_bay_types = self.cleaned_data.get('module_bay_types')
|
|
|
+ if not module_bay_types:
|
|
|
+ return
|
|
|
+
|
|
|
+ # If neither parent resolved, leave the field alone: ModularComponentTemplateModel.clean()
|
|
|
+ # rejects a parentless template in _post_clean(), and reporting unresolvable names on top
|
|
|
+ # of that would just be noise.
|
|
|
+ parent = self.cleaned_data.get('device_type') or self.cleaned_data.get('module_type')
|
|
|
+ if parent is None:
|
|
|
+ return
|
|
|
|
|
|
by_name = {}
|
|
|
for module_bay_type in module_bay_types:
|
|
|
+ if module_bay_type.manufacturer_id not in (None, parent.manufacturer_id):
|
|
|
+ continue
|
|
|
existing = by_name.get(module_bay_type.name)
|
|
|
if existing is None or module_bay_type.manufacturer_id is not None:
|
|
|
by_name[module_bay_type.name] = module_bay_type
|
|
|
|
|
|
- return list(by_name.values())
|
|
|
+ # A name matching only some other manufacturer's bay type is rejected rather than
|
|
|
+ # resolved to it.
|
|
|
+ for module_bay_type in module_bay_types:
|
|
|
+ if module_bay_type.name not in by_name:
|
|
|
+ raise forms.ValidationError({
|
|
|
+ 'module_bay_types': forms.ValidationError(
|
|
|
+ self.fields['module_bay_types'].error_messages['invalid_choice'],
|
|
|
+ code='invalid_choice',
|
|
|
+ params={'value': module_bay_type.name},
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ self.cleaned_data['module_bay_types'] = list(by_name.values())
|
|
|
|
|
|
|
|
|
class DeviceBayTemplateImportForm(forms.ModelForm):
|