diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 95985684ee1..47c84eba3be 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -3,7 +3,8 @@ import posixpath from django import forms from django.core import checks -from django.core.files.base import File +from django.core.exceptions import FieldError +from django.core.files.base import ContentFile, File from django.core.files.images import ImageFile from django.core.files.storage import Storage, default_storage from django.core.files.utils import validate_file_name @@ -12,6 +13,7 @@ from django.db.models.fields import Field from django.db.models.query_utils import DeferredAttribute from django.db.models.utils import AltersData from django.utils.translation import gettext_lazy as _ +from django.utils.version import PY311 class FieldFile(File, AltersData): @@ -312,6 +314,15 @@ class FileField(Field): def pre_save(self, model_instance, add): file = super().pre_save(model_instance, add) + if file.name is None and file._file is not None: + exc = FieldError( + f"File for {self.name} must have " + "the name attribute specified to be saved." + ) + if PY311 and isinstance(file._file, ContentFile): + exc.add_note("Pass a 'name' argument to ContentFile.") + raise exc + if file and not file._committed: # Commit the file to storage prior to saving the model file.save(file.name, file.file, save=False) diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index f068f3e96b0..d60ccb97309 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -425,6 +425,10 @@ Miscellaneous * The minimum supported version of SQLite is increased from 3.27.0 to 3.31.0. +* :class:`~django.db.models.FileField` now raises a + :class:`~django.core.exceptions.FieldError` when saving a file without a + ``name``. + .. _deprecated-features-5.1: Features deprecated in 5.1 diff --git a/tests/model_fields/test_filefield.py b/tests/model_fields/test_filefield.py index 2259c1e480d..478e9edd36b 100644 --- a/tests/model_fields/test_filefield.py +++ b/tests/model_fields/test_filefield.py @@ -5,13 +5,14 @@ import tempfile import unittest from pathlib import Path -from django.core.exceptions import SuspiciousFileOperation +from django.core.exceptions import FieldError, SuspiciousFileOperation from django.core.files import File, temp from django.core.files.base import ContentFile from django.core.files.uploadedfile import TemporaryUploadedFile from django.db import IntegrityError, models from django.test import TestCase, override_settings from django.test.utils import isolate_apps +from django.utils.version import PY311 from .models import Document @@ -72,6 +73,27 @@ class FileFieldTests(TestCase): with self.assertRaisesMessage(SuspiciousFileOperation, msg): document.save() + def test_save_content_file_without_name(self): + d = Document() + d.myfile = ContentFile(b"") + msg = "File for myfile must have the name attribute specified to be saved." + with self.assertRaisesMessage(FieldError, msg) as cm: + d.save() + + if PY311: + self.assertEqual( + cm.exception.__notes__, ["Pass a 'name' argument to ContentFile."] + ) + + def test_delete_content_file(self): + file = ContentFile(b"", name="foo") + d = Document.objects.create(myfile=file) + d.myfile.delete() + self.assertIsNone(d.myfile.name) + msg = "The 'myfile' attribute has no file associated with it." + with self.assertRaisesMessage(ValueError, msg): + getattr(d.myfile, "file") + def test_defer(self): Document.objects.create(myfile="something.txt") self.assertEqual(Document.objects.defer("myfile")[0].myfile, "something.txt")