Search code examples
djangodjango-import-export

Django Validate an Email Foreign Key


I have this profile resource:

class ProfileResource(resources.ModelResource):
    email = fields.Field(attribute='email', 
        widget=CharWidget(), 
        column_name='email')

    class Meta:
        model = Profile
        clean_model_instances = True
        import_id_fields = ('email',)

which validates the email when the profile is created. It works fine but when I use it as a foreign key like:

class InvestmentResource(resources.ModelResource):
    email = fields.Field(attribute='email', 
        widget=ForeignKeyWidget(Profile, field='email'), 
        column_name='email')

    class Meta:
        model = Investment
        clean_model_instances = True
        import_id_fields = ('id',)

    def before_import_row(self, row, row_number=None, **kwargs):
        self.email = row["email"]
        self.profile__firstname = row["firstname"]
        self.profile__lastname = row["lastname"]

    def after_import_instance(self, instance, new, row_number=None, **kwargs):
        """
        Create any missing Profile entries prior to importing rows.
        """
        try:
            profile, created = Profile.objects.get_or_create(email=self.email)

            profile.firstname = self.profile__firstname
            profile.lastname = self.profile__lastname
            profile.save()

            instance.profile = profile

        except Exception as e:
            print(e, file=sys.stderr)

It doesn't validate the email anymore. I tried adding two widgets, ForeignKeyWidget and CharWidget for the email on InvestmentResource, but it didn't work.

How do I then validate the email inside the InvestmentResource?


Solution

  • The issue is that your 'email' field is being treated as a lookup key and not an email address. This can be resolved by customizing ForeignKeyWidget to add email validation.

    What is happening is that you are importing an investment resource, which might be something like:

    email,firstname,lastname
    bob@example.com,bob,smith
    

    You have configured InvestmentResource to use the email as a lookup key for Profile. This means that django-import-export is not going to process it as an email address, but instead as lookup key for Profile, so in the code (ForeignKeyWidget) it will be doing something like:

    Profile.objects.get(email='bob@example.com')
    

    If this is successful, your Investment instance will now have the correct profile as a foreign key reference.

    This will raise a DoesNotExist exception if the associated Profile is not present. Therefore you can argue that an email address sent in the csv must be valid, because it if is not then no lookup will be possible.

    However, if you want to check that an email is syntactically valid before you attempt to load the Profile, then this is easy to do, you need to override ForeignKeyWidget to perform validation first:

    from django.core.exceptions import ValidationError
    from django.core.validators import validate_email
    
    class ValidatingForeignKeyWidget(widgets.ForeignKeyWidget):
    
        def clean(self, value, row=None, *args, **kwargs):
            
            try:
                validate_email(value)
            except ValidationError as e:
                # a quirk of import-export means that the ValidationError 
                # should be re-raised
                raise ValueError(f"invalid email {e}")
        
            try:
                val = super().clean(value)
            except self.model.DoesNotExist:
                raise ValueError(f"{self.model.__name__} with value={value} does not exist")
            return val