|
@@ -2,6 +2,8 @@ import json
|
|
|
|
|
|
|
|
from django import forms
|
|
from django import forms
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
|
+from django.db import models
|
|
|
|
|
+from django.db.models.fields.related import ManyToManyRel
|
|
|
|
|
|
|
|
from extras.choices import *
|
|
from extras.choices import *
|
|
|
from utilities.forms.fields import CommentField, SlugField
|
|
from utilities.forms.fields import CommentField, SlugField
|
|
@@ -71,26 +73,47 @@ class NetBoxModelForm(
|
|
|
def _post_clean(self):
|
|
def _post_clean(self):
|
|
|
"""
|
|
"""
|
|
|
Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
|
|
Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
|
|
|
|
|
+ Handles both forward and reverse M2M relationships, and supports both simple (single field)
|
|
|
|
|
+ and add/remove (dual field) modes.
|
|
|
"""
|
|
"""
|
|
|
self.instance._m2m_values = {}
|
|
self.instance._m2m_values = {}
|
|
|
- for field in self.instance._meta.local_many_to_many:
|
|
|
|
|
- if field.name in self.cleaned_data:
|
|
|
|
|
- # Standard M2M field (set-based)
|
|
|
|
|
- self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
|
|
|
|
|
- elif f'add_{field.name}' in self.cleaned_data or f'remove_{field.name}' in self.cleaned_data:
|
|
|
|
|
- # Add/remove M2M field pair: compute the effective set
|
|
|
|
|
- current = set(getattr(self.instance, field.name).values_list('pk', flat=True)) \
|
|
|
|
|
|
|
+ for field in self.instance._meta.get_fields():
|
|
|
|
|
+ # Determine the accessor name for this M2M relationship
|
|
|
|
|
+ if isinstance(field, models.ManyToManyField):
|
|
|
|
|
+ name = field.name
|
|
|
|
|
+ elif isinstance(field, ManyToManyRel):
|
|
|
|
|
+ name = field.get_accessor_name()
|
|
|
|
|
+ else:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if name in self.cleaned_data:
|
|
|
|
|
+ # Simple mode: single multi-select field
|
|
|
|
|
+ self.instance._m2m_values[name] = list(self.cleaned_data[name])
|
|
|
|
|
+ elif f'add_{name}' in self.cleaned_data or f'remove_{name}' in self.cleaned_data:
|
|
|
|
|
+ # Add/remove mode: compute the effective set
|
|
|
|
|
+ current = set(getattr(self.instance, name).values_list('pk', flat=True)) \
|
|
|
if self.instance.pk else set()
|
|
if self.instance.pk else set()
|
|
|
add_values = set(
|
|
add_values = set(
|
|
|
- v.pk for v in self.cleaned_data.get(f'add_{field.name}', [])
|
|
|
|
|
|
|
+ v.pk for v in self.cleaned_data.get(f'add_{name}', [])
|
|
|
)
|
|
)
|
|
|
remove_values = set(
|
|
remove_values = set(
|
|
|
- v.pk for v in self.cleaned_data.get(f'remove_{field.name}', [])
|
|
|
|
|
|
|
+ v.pk for v in self.cleaned_data.get(f'remove_{name}', [])
|
|
|
)
|
|
)
|
|
|
- self.instance._m2m_values[field.name] = list((current | add_values) - remove_values)
|
|
|
|
|
|
|
+ self.instance._m2m_values[name] = list((current | add_values) - remove_values)
|
|
|
|
|
|
|
|
return super()._post_clean()
|
|
return super()._post_clean()
|
|
|
|
|
|
|
|
|
|
+ def _save_m2m(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Save many-to-many field values that were computed in _post_clean(). This handles M2M fields
|
|
|
|
|
+ not included in Meta.fields (e.g. those managed via M2MAddRemoveFields).
|
|
|
|
|
+ """
|
|
|
|
|
+ super()._save_m2m()
|
|
|
|
|
+ meta_fields = self._meta.fields
|
|
|
|
|
+ for field_name, values in self.instance._m2m_values.items():
|
|
|
|
|
+ if not meta_fields or field_name not in meta_fields:
|
|
|
|
|
+ getattr(self.instance, field_name).set(values)
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class PrimaryModelForm(OwnerMixin, NetBoxModelForm):
|
|
class PrimaryModelForm(OwnerMixin, NetBoxModelForm):
|
|
|
"""
|
|
"""
|