Updating Many2Many relationship from both objects (related_name)

I have two models with two update forms. Now I want to achieve two things:

1.be able to edit a certificate and set all the servers this certificate is used on
2. be able to update a server and set all the certificates which are used on this server

class Certificate(models.Model):
    internal_name = models.CharField(max_length=1024)
    pem_representation = models.TextField(unique=True)
    servers = models.ManyToManyField(
        Server, related_name='certificates', blank=True)

class Server(models.Model):
    name = models.CharField(max_length=1024, unique=True)
class CertificateUpdateForm(forms.ModelForm):
    class Meta:
        model = models.Certificate
        fields = ['internal_name', 'pem_representation', 'servers']

class ServerUpdateForm(forms.ModelForm):
    class Meta:
        model = models.Server
        fields = ['name', 'certificates']

Without the field “certificates” in ServerUpdateForm I get no error but when updating via the form the changes for the certificates just aren’t recognized. The error message I get with this code is:

Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/usr/lib64/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/usr/lib64/python3.8/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File " venv/lib64/python3.8/site-packages/django/utils/autoreload.py", line 53, in wrapper
    fn(*args, **kwargs)
  File " venv/lib64/python3.8/site-packages/django/core/management/commands/runserver.py", line 118, in inner_run
    self.check(display_num_errors=True)
  File " venv/lib64/python3.8/site-packages/django/core/management/base.py", line 392, in check
    all_issues = checks.run_checks(
  File " venv/lib64/python3.8/site-packages/django/core/checks/registry.py", line 70, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
  File " venv/lib64/python3.8/site-packages/django/core/checks/urls.py", line 13, in check_url_config
    return check_resolver(resolver)
  File " venv/lib64/python3.8/site-packages/django/core/checks/urls.py", line 23, in check_resolver
    return check_method()
  File " venv/lib64/python3.8/site-packages/django/urls/resolvers.py", line 408, in check
    for pattern in self.url_patterns:
  File " venv/lib64/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File " venv/lib64/python3.8/site-packages/django/urls/resolvers.py", line 589, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File " venv/lib64/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File " venv/lib64/python3.8/site-packages/django/urls/resolvers.py", line 582, in urlconf_module
    return import_module(self.urlconf_name)
  File "/usr/lib64/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File " certmanager/certmanager/urls.py", line 26, in <module>
    path('servers/', include('servers.urls')),
  File " venv/lib64/python3.8/site-packages/django/urls/conf.py", line 34, in include
    urlconf_module = import_module(urlconf_module)
  File "/usr/lib64/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File " certmanager/servers/urls.py", line 3, in <module>
    from . import views
  File " certmanager/servers/views.py", line 9, in <module>
    from . import forms, models
  File " certmanager/servers/forms.py", line 12, in <module>
    class ServerUpdateForm(forms.ModelForm):
  File " venv/lib64/python3.8/site-packages/django/forms/models.py", line 268, in __new__
    raise FieldError(message)
django.core.exceptions.FieldError: Unknown field(s) (certificates) specified for Server

How can I update the M2M relation from both objects?

Can you post the views you are using to use the forms and save the data?

Sure, here are the both views:

class CertificateUpdate(PermissionRequiredMixin, generic.UpdateView):
    model = models.Certificate
    template_name = 'certificates/update.html'
    form_class = forms.CertificateUpdateForm
    success_url = reverse_lazy('certificates:certificate-index')
    permission_required = ('certificates.change_certificate')

    def get_context_data(self, **kwargs):
        context = super(CertificateUpdate, self).get_context_data(**kwargs)
        context['all_servers'] = Server.objects.all()
        context['selected_servers'] = self.object.servers.all()

        return context

class ServerUpdate(PermissionRequiredMixin, generic.UpdateView):
    model = models.Server
    template_name = 'servers/update.html'
    form_class = forms.ServerUpdateForm
    queryset = models.Server.objects.all()
    success_url = reverse_lazy('servers:server-index')
    permission_required = ('servers.change_server')

    def get_context_data(self, **kwargs):
        context = super(ServerUpdate, self).get_context_data(**kwargs)
        context['all_certificates'] = Certificate.objects.all()
        context['selected_certificates'] = self.object.certificates.all()

        return context

So the short answer is from what I can determine, Django will not “automatically” manage the back-links of an M2M relationship. The few related topics I’ve found on line on places such as StackOverflow, reddit, and the mailing lists all seem to say that Django doesn’t do that - you need to add a field to your form, and then use the contents of that field in a custom save method to create / manage the M2M data.

I’d love to be shown wrong by one of the more knowledgeable people around here.

Ken

1 Like

Thank you for your time and effort. You actually pointed out how it can be achieved, nevertheless I’d appreciate another, more beautiful way (built-in).
Here is what I did after reading your suggestion - of course I’d like to hear if this can be done in a better way.

class ServerUpdate(PermissionRequiredMixin, generic.UpdateView):
    model = models.Server
    template_name = 'servers/update.html'
    form_class = forms.ServerUpdateForm
    queryset = models.Server.objects.all()
    success_url = reverse_lazy('servers:server-index')
    permission_required = ('servers.change_server')

    def get_context_data(self, **kwargs):
        context = super(ServerUpdate, self).get_context_data(**kwargs)
        context['all_certificates'] = Certificate.objects.all()
        context['selected_certificates'] = self.object.certificates.all()

        return context

    def post(self, request, *args, **kwargs):
        selected_certificates = request.POST.getlist('certificates')
        self.object = self.get_object()
        self.object.certificates.set(selected_certificates)

        return super().post(request, *args, **kwargs)