question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ManyToMany relation with intermediate model breaks import

See original GitHub issue

Let’s say I need to modify the example app to add a field on the relation table between a book and a category:

# models.py
@python_2_unicode_compatible
class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):                            
        return self.name


@python_2_unicode_compatible
class Book(models.Model):
    name = models.CharField('Book name', max_length=100)
    author = models.ForeignKey(Author, blank=True, null=True)
    author_email = models.EmailField('Author email', max_length=75, blank=True)
    imported = models.BooleanField(default=False)
    published = models.DateField('Published', blank=True, null=True)
    price = models.DecimalField(max_digits=10, decimal_places=2, null=True,
                                blank=True)
    categories = models.ManyToManyField(Category, blank=True,
                                        through="IntermediateRelation")

    def __str__(self):
        return self.name


@python_2_unicode_compatible
class IntermediateRelation(models.Model):
    relation_field = models.CharField(max_length=100)
    category = models.ForeignKey(Category)
    book = models.ForeignKey(Book)

    def __str__(self):
        return self.relation_field

Then I update admin.py so that I can assign a category to a book in the admin:

class IntermediateRelationInline(admin.TabularInline):
    model = IntermediateRelation
    extra = 1

class BookAdmin(ImportExportMixin, admin.ModelAdmin):
    list_filter = ['categories', 'author']
    inlines = (IntermediateRelationInline, )

I can export my books, but when I try to import them back, here is what I get:

Traceback:
File "/home/synext/lib/python2.7/site-packages/Django-1.7-py2.7.egg/django/core/handlers/base.py" in get_response
  111.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/synext/lib/python2.7/site-packages/Django-1.7-py2.7.egg/django/utils/decorators.py" in _wrapped_view
  105.                     response = view_func(request, *args, **kwargs)
File "/home/synext/lib/python2.7/site-packages/Django-1.7-py2.7.egg/django/views/decorators/cache.py" in _wrapped_view_func
  52.         response = view_func(request, *args, **kwargs)
File "/home/synext/lib/python2.7/site-packages/Django-1.7-py2.7.egg/django/contrib/admin/sites.py" in inner
  204.             return view(request, *args, **kwargs)
File "../import_export/admin.py" in process_import
  130.                                  raise_errors=True)
File "../import_export/resources.py" in import_data
  355.                     six.reraise(*sys.exc_info())
File "../import_export/resources.py" in import_data
  342.                         self.save_m2m(instance, row, real_dry_run)
File "../import_export/resources.py" in save_m2m
  216.                 self.import_field(field, obj, data)
File "../import_export/resources.py" in import_field
  195.             field.save(obj, data)
File "../import_export/fields.py" in save
  94.             setattr(obj, self.attribute, self.clean(data))
File "/home/synext/lib/python2.7/site-packages/Django-1.7-py2.7.egg/django/db/models/fields/related.py" in __set__
  1170.             raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))

Exception Type: AttributeError at /admin/core/book/process_import/
Exception Value: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use core.IntermediateRelation's Manager instead.

Issue Analytics

  • State:closed
  • Created 9 years ago
  • Comments:9 (2 by maintainers)

github_iconTop GitHub Comments

4reactions
maltem-zacommented, Apr 6, 2016

@amarandon Thank you, that was extremely helpful.

Just a note on your is_m2m_with_intermediate_object and get_intermediate_model functions: _meta.get_field_by_name has been deprecated and will be removed soon.

You can use _meta.get_field instead. Eg:

def is_m2m_with_intermediate_object(self, obj):
    field = obj._meta.get_field(self.attribute)
    return field.many_to_many and not field.rel.through._meta.auto_created

def get_intermediate_model(self, obj):
    field = obj._meta.get_field(self.attribute)
    IntermediateModel = field.rel.through
    from_field_name = field.m2m_field_name()
    to_field_name = field.rel.to.__name__.lower()
    return IntermediateModel, from_field_name, to_field_name
4reactions
amarandoncommented, Oct 17, 2014

I managed to get it working. I’m posting what I’ve got so far for the record:

# -*- coding: utf-8 -*-
import functools
from import_export import resources, widgets, fields
from django.db.models.fields.related import ForeignKey

class IntermediateModelManyToManyWidget(widgets.ManyToManyWidget):

    def __init__(self, *args, **kwargs):
        self.rel = kwargs.pop('rel', None)
        super(IntermediateModelManyToManyWidget, self).__init__(*args,
                                                                **kwargs)

    def clean(self, value):
        ids = [item["uid"] for item in value]
        objects = self.model.objects.filter(**{
            '%s__in' % self.field: ids
        })
        return objects

    def render(self, value, obj):
        return [self.related_object_representation(obj, related_obj)
                for related_obj in value.all()]

    def related_object_representation(self, obj, related_obj):
        result = {
            "uid": related_obj.uid,
            "name": related_obj.name
        }
        if self.rel.through._meta.auto_created:
            return result
        intermediate_own_fields = [
            field for field in self.rel.through._meta.fields
            if field is not self.rel.through._meta.pk
            and not isinstance(field, ForeignKey)
        ]
        for field in intermediate_own_fields:
            result[field.name] = "foo"
        set_name = "{}_set".format(self.rel.through._meta.model_name)
        related_field_name = self.rel.to._meta.model_name
        intermediate_set = getattr(obj, set_name)
        intermediate_obj = intermediate_set.filter(**{
            related_field_name: related_obj
        }).first()
        for field in intermediate_own_fields:
            result[field.name] = getattr(intermediate_obj, field.name)
        return result


class Field(fields.Field):

    def is_m2m_with_intermediate_object(self, obj):
        field, _, _, m2m = obj._meta.get_field_by_name(self.attribute)
        return m2m and field.rel.through._meta.auto_created is False

    def get_intermediate_model(self, obj):
        field = obj._meta.get_field_by_name(self.attribute)[0]
        IntermediateModel = field.rel.through
        from_field_name = field.m2m_field_name()
        to_field_name = field.rel.to.__name__.lower()
        return IntermediateModel, from_field_name, to_field_name

    def remove_old_intermediates(self, obj, data):
        IntermediateModel, from_field_name, to_field_name = \
            self.get_intermediate_model(obj)
        imported_ids = set(import_obj.pk for import_obj in self.clean(data))
        related_objects = getattr(obj, self.attribute).all()
        for related_object in related_objects:
            if related_object.pk not in imported_ids:
                queryset = IntermediateModel.objects.filter(**{
                    from_field_name: obj,
                    to_field_name: related_object
                })
                queryset.delete()

    def ensure_current_intermediates_created(self, obj, data):
        IntermediateModel, from_field_name, to_field_name = \
            self.get_intermediate_model(obj)

        for related_object in self.clean(data):
            attributes = {from_field_name: obj, to_field_name: related_object}
            self.create_if_not_existing(IntermediateModel, attributes)

    @staticmethod
    def create_if_not_existing(IntermediateModel, attributes):
        # Use this instead of get_or_create in case we have duplicate
        # associations. (get_or_create would raise a DoesNotExist exception)
        if not IntermediateModel.objects.filter(**attributes).exists():
            IntermediateModel.objects.create(**attributes)

    def save(self, obj, data):
        """
        Cleans this field value and assign it to provided object.
        """
        if not self.readonly:
            if self.is_m2m_with_intermediate_object(obj):
                self.remove_old_intermediates(obj, data)
                self.ensure_current_intermediates_created(obj, data)
            else:
                setattr(obj, self.attribute, self.clean(data))

    def export(self, obj):
        """
        Returns value from the provided object converted to export
        representation.
        """
        value = self.get_value(obj)
        if value is None:
            return ""
        if isinstance(self.widget, IntermediateModelManyToManyWidget):
            return self.widget.render(value, obj)
        else:
            return self.widget.render(value)


class ModelResource(resources.ModelResource):

    @classmethod
    def widget_from_django_field(cls, f, default=widgets.Widget):
        """
        Returns the widget that would likely be associated with each
        Django type.
        """
        result = default
        internal_type = f.get_internal_type()
        if internal_type in ('ManyToManyField', ):
            result = functools.partial(IntermediateModelManyToManyWidget,
                                       model=f.rel.to, rel=f.rel)
        if internal_type in ('ForeignKey', 'OneToOneField', ):
            result = functools.partial(widgets.ForeignKeyWidget,
                                       model=f.rel.to)
        if internal_type in ('DecimalField', ):
            result = widgets.DecimalWidget
        if internal_type in ('DateTimeField', ):
            result = widgets.DateTimeWidget
        elif internal_type in ('DateField', ):
            result = widgets.DateWidget
        elif internal_type in ('IntegerField', 'PositiveIntegerField',
                               'PositiveSmallIntegerField',
                               'SmallIntegerField', 'AutoField'):
            result = widgets.IntegerWidget
        elif internal_type in ('BooleanField', 'NullBooleanField'):
            result = widgets.BooleanWidget
        return result


    @classmethod
    def field_from_django_field(self, field_name, django_field, readonly):
        """
        Returns a Resource Field instance for the given Django model field.
        """

        FieldWidget = self.widget_from_django_field(django_field)
        widget_kwargs = self.widget_kwargs_for_field(field_name)
        field = Field(attribute=field_name, column_name=field_name,
                      widget=FieldWidget(**widget_kwargs), readonly=readonly)
        return field

Some of this code is specific to the project I’m working on, for instance the related_object_representation method that has a couple of hardcoded fields but it should be possible to adapt.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Keep relationships between many-to-many fields django-tables2
The relationships between the three fields are broken. Is there a way to correct this? And the separator could be a newline and...
Read more >
ManyToMany relation with intermediate model breaks import
Let's say I need to modify the example app to add a field on the relation table between a book and a category:...
Read more >
Models - Django 1.4 documentation
When you set up the intermediary model, you explicitly specify foreign keys to the models that are involved in the ManyToMany relation.
Read more >
Django Rest Framework - ManyToMany relationship through ...
Django Rest Framework - ManyToMany relationship through intermediate model ; from __future__ import ; import models class ; 50) def ; self): return ......
Read more >
django.db.models.fields.related - Django documentation
ManyToManyField (Target) # rel_opts.object_name == "Target" rel_opts = self.remote_field.model._meta # If the field doesn't install a backward relation on ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found