diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09d73b7..d02fdc8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies and package run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r requirements-test.txt pip install django~=${{ matrix.django-version }}.0 pip install djangorestframework~=${{ matrix.djangorestframework-version }}.0 diff --git a/README.md b/README.md index a7099b9..4f28890 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ If used with DRF, `django-filter` is an additional requirement. 1. Install using pip: +```shell +pip install drf-attachments[drf] +``` + +or to install without DRF dependencies (no REST endpoints available from scratch) + ```shell pip install drf-attachments ``` @@ -140,6 +146,12 @@ def attachment_context_translations(): } ``` +4. Optionally define a custom DIR as root for your attachments ("attachments" by default) +```python +# settings.py +ATTACHMENT_UPLOAD_ROOT_DIR = "your/custom/attachments/root/" +``` + ## Usage Attachments accept any other Model as content_object and store the uploaded files in their respective directories diff --git a/drf_attachments/models/models.py b/drf_attachments/models/models.py index 436e915..ececbcd 100644 --- a/drf_attachments/models/models.py +++ b/drf_attachments/models/models.py @@ -13,7 +13,7 @@ UUIDField, ) from django.utils.translation import gettext_lazy as _ -from rest_framework.exceptions import ValidationError +from django.core.exceptions import ValidationError from drf_attachments.config import config from drf_attachments.models.fields import DynamicStorageFileField @@ -99,6 +99,16 @@ class Meta: verbose_name_plural = _("attachments") ordering = ("creation_date",) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + try: + # if DRF is installed, use their ValidationError + from rest_framework.exceptions import ValidationError as DrfValidationError + self.validation_error_class = DrfValidationError + except ImportError: + # otherwise use default django ValidationError + self.validation_error_class = ValidationError + def __str__(self): return f"{self.content_type} | {self.object_id} | {self.context_label} | {self.name}" @@ -184,12 +194,7 @@ def validate_context(self): context=self.context, valid_contexts=", ".join(self.valid_contexts), ) - raise ValidationError( - { - "context": error_msg, - }, - code="invalid", - ) + raise self.validation_error_class(error_msg, code="invalid") def validate_file(self): """ @@ -214,7 +219,7 @@ def _validate_file_mime_type(self): mime_type=self.meta["mime_type"], valid_mime_types=", ".join(self.valid_mime_types), ) - raise ValidationError( + raise self.validation_error_class( { "file": error_msg, }, @@ -236,7 +241,7 @@ def _validate_file_extension(self): extension=self.meta["extension"], valid_extensions=", ".join(self.valid_extensions), ) - raise ValidationError( + raise self.validation_error_class( { "file": error_msg, }, @@ -257,7 +262,7 @@ def _validate_file_size(self): size=self.file.size, min_size=self.min_size, ) - raise ValidationError( + raise self.validation_error_class( { "file": error_msg, }, @@ -272,7 +277,7 @@ def _validate_file_size(self): size=self.file.size, max_size=self.max_size, ) - raise ValidationError( + raise self.validation_error_class( { "file": error_msg, }, diff --git a/drf_attachments/storage.py b/drf_attachments/storage.py index 43ca370..c68c0e4 100644 --- a/drf_attachments/storage.py +++ b/drf_attachments/storage.py @@ -33,6 +33,13 @@ def url(self, name): return get_admin_attachment_url(attachment.pk) +def attachment_upload_root_dir(): + """ + Extract ATTACHMENT_UPLOAD_ROOT_DIR from the settings (if defined) + """ + return getattr(settings, "ATTACHMENT_UPLOAD_ROOT_DIR", "attachments") + + def attachment_upload_path(attachment, filename): """ If not defined otherwise, a content_object's attachment files will be uploaded as @@ -47,4 +54,4 @@ def attachment_upload_path(attachment, filename): """ filename, file_extension = os.path.splitext(filename) month_directory = timezone.now().strftime("%Y%m") - return f"attachments/{month_directory}/{str(uuid1())}{file_extension}" + return f"{attachment_upload_root_dir()}/{month_directory}/{str(uuid1())}{file_extension}" diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..140e8e2 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,4 @@ +# load base requirements +-r base.txt + +rest-framework-generic-relations>=2.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f3d053f..b4bb813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,9 +15,7 @@ black>=22.6.0,<22.7 # TestApp dependencies django>=3.2,<4 -djangorestframework>=3.13,<4 python-magic>=0.4.18 -rest-framework-generic-relations>=2.0.0 django-filter>=21.1,<22 # fix importlib version to avoid "AttributeError: 'EntryPoints' object has no attribute 'get'" with flake8 diff --git a/setup.py b/setup.py index c6bb472..42de1e9 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ author_email="abruckner@anexia-it.com", install_requires=[ "python-magic>=0.4.18", - "rest-framework-generic-relations>=2.0.0", "content-disposition>=1.1.0", ], classifiers=[ @@ -39,4 +38,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], + extras_require={ + "drf": ["rest-framework-generic-relations>=2.0.0"], + }, ) diff --git a/tests/testapp/tests/test_api.py b/tests/testapp/tests/test_api.py index 162ef68..e61c25a 100644 --- a/tests/testapp/tests/test_api.py +++ b/tests/testapp/tests/test_api.py @@ -98,18 +98,19 @@ def test_download(self): # get attachment from main serializer response = self.client.get(path=f"/api/attachment/") - attachment_response = response.json()[0] - # TODO: download_url is not provided by main serializer. intentional? + main_attachment_response = response.json()[0] + main_download_url = main_attachment_response["download_url"] + self.assertIsNotNone(main_download_url) + self.assertGreater(len(main_download_url), 0) # get attachment from sub-serializer response = self.client.get(path=f"/api/photo_album/{self.photo_album.pk}/") - attachment_response = response.json()["attachments"][0] - download_url = attachment_response["download_url"] - self.assertIsNotNone(download_url) - self.assertGreater(len(download_url), 0) + sub_attachment_response = response.json()["attachments"][0] + sub_download_url = sub_attachment_response["download_url"] + self.assertEqual(main_download_url, sub_download_url) # download - response = self.client.get(download_url) + response = self.client.get(sub_download_url) # check response self.assertEqual(