django-import-export: Import foreign keys that do not exist (creating new values in Foreign key)

I am trying to import values which do not already exist my foreign key. I would like import/export to add these new values to the ForeignKey model.

I’ve tried adding “null/blank” to the models, but it still will not create (import) new values in the foreign key. The error I get is “DoesNotExist: x matching query does not exist.”

Here is a sample of my Admin.py (which only imports to a foreign key if the values being imported already exist).

store_name = fields.Field(column_name='store_name', attribute='Store', widget=widgets.ForeignKeyWidget(Store, 'store_name'))
def clean(self, value):
        val = super(ForeignKeyWidget, self).clean(value)
        object, created = Store.objects.get_or_create(store_name='')
class Meta:
    model = Product
    fields = ('id', 'second_name', 'product_name', store',)
    export_order = ('id', 'second_name', 'product_name')
    skip_unchanged = False
    report_skipped = False
    widgets = {
            'published': {'format': '%d.%m.%Y'},
            }

Can anyone advise please?

Thanks

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 19 (6 by maintainers)

Most upvoted comments

I’ve written it this way:

class ForeignKeyWidgetWithCreation(ForeignKeyWidget):
    def __init__(
            self,
            model,
            field='pk',
            create=False,
            *args, **kwargs):
        self.model = model
        self.field = field
        self.create = create
        # super(ForeignKeyWidgetWithCreation, self).__init__(*args, **kwargs)

    def clean(self, value):
        val = super(ForeignKeyWidgetWithCreation, self).clean(value)
        if self.create:
            instance, new = self.model.objects.get_or_create(**{
                self.field: val
            })
            val = getattr(instance, self.field)
        return self.model.objects.get(**{self.field: val}) if val else None

Usage example

class SomeModelResource(Resource):
    field1 = fields.Field(
        attribute='field1',
        widget=ForeignKeyWidgetWithCreation(
            OtherModel,
            field='name',
            create=True))

@int-ua, I used your post as a basis to implement my Many-to-many widget with creation. @patently-offensive, your answer was helpful too.

Many-to-Many Widget with object creation

from import_export.widgets import ManyToManyWidget
from django.db.models import QuerySet

class ManyToManyWidgetWithCreation(ManyToManyWidget):
    """A many-to-many widget that creates any objects that don't already exist."""

    def __init__(self, model, field="pk", create=False, **kwargs):
        self.model = model
        self.field = field
        self.create = create
        super(ManyToManyWidgetWithCreation, self).__init__(model, field=field, **kwargs)

        def clean(self, value, **kwargs):

            # If no value was passed then we don't have anything to clean.

            if not value:
                return self.model.objects.none()

            # Call the super method. This will return a QuerySet containing any pre-existing objects.
            # Any missing objects will be

            cleaned_value: QuerySet = super(ManyToManyWidgetWithCreation, self).clean(value, **kwargs)

            # Value will be a string that is separated by `self.separator`.
            # Each entry in the list will be a reference to an object. If the object exists it will
            # appear in the cleaned_value results. If the number of objects in the cleaned_value
            # results matches the number of objects in the delimited list then all objects already
            # exist and we can just return those results.

            object_list = value.split(self.separator)

            if len(cleaned_value.all()) == len(object_list):
                return cleaned_value

            # If we are creating new objects then loop over each object in the list and
            # use get_or_create to, um, get or create the object.

            if self.create:
                for object_value in object_list:
                    _instance, _new = self.model.objects.get_or_create(**{self.field: object_value})

            # Use `filter` to re-locate all the objects in the list.

            model_objects = self.model.objects.filter(
                **{f"{self.field}__in": object_list}
            )

            return model_objects

Usage

In admin.py:

from import_export.resources import ModelResource

class MyThingModelResource(ModelResource):

    categories = fields.Field(
        column_name="categories",
        attribute="categories",
        widget=ManyToManyWidgetWithCreation(
            model=models.Category,
            field="name",
            separator="|",
            create=True,
        ),
        default="",
    )

This approach should:

  • Handle delimited lists of objects
  • Create any missing objects when create=True
  • Error in the same way as a normal ManyToManyWidget when create=False
  • Handle lists of objects where some are missing and some are pre-existing

There are some cases where this might not work, for example, if a list of ID’s are passed or if field values are non-string-based.

Does anyone have this working in a many to many context, in particular a model with an m2m relationship with itself?

For example, I have a Product model, with an m2m relationship to itself to represent products that are competitive with each other. Example data:

product_name | competes_with Product A | Product C Product B | Product C | Product B, Product D

I need Product C to be created while importing row 1 and related to Product A and Product D to be created with importing row 3 and linked to Product C.

The below will create new values, but will only create an M2M relationship if the foreign key value already exists. In the above example:

  • C will be created, but not related to A
  • B will be related to C on importing C
  • D will be created, but not related to C
class M2MCreateWithForeignKey(widgets.ManyToManyWidget):
    def __init__(self, model, separator=', ', field='pk', defaults=None, create=False):
        self.model = model
        self.separator = separator
        self.field = field
        self.defaults = defaults
        self.create = create

    def clean(self, value):
        values = filter(None, value.split(self.separator))

        if self.create:
            for v in values:
                self.model.objects.get_or_create(defaults=self.defaults, **{self.field:v})
            return super(M2MCreateWithForeignKey, self).clean(value)
        else:
            try:
                val = super(M2MCreateWithForeignKey, self).clean(value)
            except ObjectDoesNotExist:
                model_name = self.model.__name__
                error = "Imported data includes non-existant %s(s) (%s) "\
                    "\nThis model does not support creating a new %s when importing data."\
                    % (model_name, value, model_name)
                logger.error(error)
                return None
            else:
                return val

Yes, use debugger, insert this in line before one where exception is raised and then inspect variables:

import pdb; pdb.set_trace()