Opened 4 years ago

Closed 4 years ago

Last modified 4 years ago

#31774 closed New feature (needsinfo)

validate_unique in Model class doesn't validate uniqueness for ImageField.

Reported by: Gaurav Ghildyal Owned by: nobody
Component: Database layer (models, ORM) Version: 3.0
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Steps for reproduction:

  • Consider a model with an ImageField
@final
class ValidateUnique(models.Model):
    logo = models.ImageField(
          _('Official Logo'),
          help_text=_('The official logo..'),
          upload_to='logos/',
          blank=True,
          unique=True,
    )
  • Integrate the above model with the admin interface.
  • Create two instances of the model via the admin interface.
  • Upload an image for a logo in the first one.
  • Upload the same image for a logo in the second one.

Expected Result: The 'logo' field in the form shows an error saying the logo already exists (Fails uniqueness constraint)

Actual Result: An error page with a stacktrace (running with DEBUG=True) throwing an IntegrityError.

Possible cause:

In the Model class in django/db/models/base.py

def _perform_unique_checks(self, unique_checks):
  ...
  ...
           # some fields were skipped, no reason to do the check
            if len(unique_check) != len(lookup_kwargs):
                continue

            qs = model_class._default_manager.filter(**lookup_kwargs)
 ....

In the above code, the lookup_kwargs gets this value

lookup_kwargs = {'logo': <ImageFieldFile: temp.logo.png>}

The queryset fails to return the first model instance because the lookup for ImageField fails. So validation error isn't added to the form and instead IntegrityError exception is raised.

It would work if the lookup value was the the fields 'name' attribute, say if the lookup_kwargs looked like this:

lookup_kwargs = {'logo': '/logos/temp.logo.png/'}

Attachments (3)

ticket_31774.zip (14.5 KB ) - added by Carlton Gibson 4 years ago.
Test project
ticket_31774_s3.tgz (12.4 KB ) - added by Gaurav Ghildyal 4 years ago.
Reproduce with s3 storage (or any other external storage)
ticket_31774_s3_corrected.tgz (6.4 KB ) - added by Gaurav Ghildyal 4 years ago.

Download all attachments as: .zip

Change History (9)

by Carlton Gibson, 4 years ago

Attachment: ticket_31774.zip added

Test project

comment:1 by Carlton Gibson, 4 years ago

Resolution: worksforme
Status: newclosed
Type: UncategorizedNew feature

I'm afraid I can't reproduce this with the given info.

Actual Result: An error page with a stacktrace (running with DEBUG=True) throwing an IntegrityError.

I don't see that.

Rather, uploaded files have a random string appended in order to create a unique name.

db.sqlite3> SELECT * from reproduce_validateunique;                                                                                        
+----+---------------------------------------------------------+
| id | logo                                                    |
+----+---------------------------------------------------------+
| 1  | logos/2020-03-19-View-from-inside-my-laptop.jpg         |
| 2  | logos/2020-03-19-View-from-inside-my-laptop_xB8SLzj.jpg |
| 3  | logos/2020-03-19-View-from-inside-my-laptop_YeNU9Rp.jpg |
+----+---------------------------------------------------------+
3 rows in set
Time: 0.016s

(See Storage.get_alternative_name())

Can you adjust the uploaded sample project to demonstrate this issue?

Possibly, there's a New Feature here. Originally unique was not supported for FileField. Support was added in 1.11, for #27188, supporting uniqueness of the name attribute.
Maybe additional work is needed to push that out to the form layer (but pending the reproduce...)

Checking the image contents is not (won't be) supported:

I think by definition ImageFields are unique; even if you upload the same image twice you'll still end up with two unique files.

Jacob on #1898.

comment:2 by Gaurav Ghildyal, 4 years ago

The automatic random string is appended by the storage layer - in this case the default FileSystemStorage class.

I was storing the filles on AWS S3 using the S3Storage from the https://django-storages.readthedocs.io/en/latest/ library which doesn't do that, it just overwrites the file on the actual storage and returns the same name as set by the form, which the validate_unique function fails to catch but the database layer throws an exception due to name conflict.

Maybe asserting the uniqueness should become a part of the storage layer instead of the model layer, but in that case the validate_unique function should exclude ImageField and FileFields from uniqueness validation and documentation should be updated to reflect this?

by Gaurav Ghildyal, 4 years ago

Attachment: ticket_31774_s3.tgz added

Reproduce with s3 storage (or any other external storage)

comment:3 by Carlton Gibson, 4 years ago

Resolution: worksformeneedsinfo

Hi Gaurav,

Thanks for the follow-up. (I wasn't sure about the attachment: it's seemed like the whole project was saved as binary in the manage.py file — how did you create that? How would one run it? — Not something I've seen before, but I was able to look at it.)

I'm going to move this to needsinfo — assuming a storage that didn't create unique names, I can see how this might come up.

The storage issue is a bit of a distraction. The correct validation should be at the form layer:

  • Calling form.is_valid(), on a ModelForm using the image (or file field).
  • In `_post_clean()` we WOULD validate the uniqueness of the field.
  • In a test case `_get_validation_exclusions()` included the logo field — that's because of the blank=True on the model definition. That means _post_clean() will be skipped for it.

Removing the blank=True allows logo not be excluded but I didn't have enough time to verify whether the image/file field would then trigger the expected error in validate_unique(). I may be able to get back to it (it's interesting) but if you could dig down deeper that would help.

  • I'd subclass Storage and override .get_alternative_name() to not do that properly.
  • Then create a ModelForm with just the logo field to verify that _post_clean() triggers the uniqueness error.

If not then we can see why not, and whether it can be adjusted.

in reply to:  3 comment:4 by Gaurav Ghildyal, 4 years ago

Oops, that's my mistake, wrong tar command! Will upload the correct one!

Replying to Carlton Gibson:

Hi Gaurav,

Thanks for the follow-up. (I wasn't sure about the attachment: it's seemed like the whole project was saved as binary in the manage.py file — how did you create that? How would one run it? — Not something I've seen before, but I was able to look at it.)

I'm going to move this to needsinfo — assuming a storage that didn't create unique names, I can see how this might come up.

The storage issue is a bit of a distraction. The correct validation should be at the form layer:

  • Calling form.is_valid(), on a ModelForm using the image (or file field).
  • In `_post_clean()` we WOULD validate the uniqueness of the field.
  • In a test case `_get_validation_exclusions()` included the logo field — that's because of the blank=True on the model definition. That means _post_clean() will be skipped for it.

Removing the blank=True allows logo not be excluded but I didn't have enough time to verify whether the image/file field would then trigger the expected error in validate_unique(). I may be able to get back to it (it's interesting) but if you could dig down deeper that would help.

  • I'd subclass Storage and override .get_alternative_name() to not do that properly.
  • Then create a ModelForm with just the logo field to verify that _post_clean() triggers the uniqueness error.

If not then we can see why not, and whether it can be adjusted.

Version 0, edited 4 years ago by Gaurav Ghildyal (next)

by Gaurav Ghildyal, 4 years ago

in reply to:  3 comment:5 by Gaurav Ghildyal, 4 years ago

Hi Carlton,

Thanks for looking into this. I will dig deeper along the lines of your input.

Why do you think it should be in the form layer? Given that there is a function (_perform_unique_checks ) that catches all uniqueness errors and raises validation errors, it would be nice to fix the uniqueness check there? The added benefit is that Django admin and API validations can be in one common place.

comment:6 by Carlton Gibson, 4 years ago

Yes, you're right: the form calls down to the model, but as I was looking, I'm wondering why the field was excluded from the check? Then, if we can make sure it's included, is it triggered? (If not, is that because the name isn't set yet by the storage... — and so on.) It's just a place to start looking, but from the view's point-of-view I need form.is_valid() to return False.
(Hopefully that makes sense.)

Note: See TracTickets for help on using tickets.
Back to Top