Skip to content

Commit

Permalink
SIWKKCB-89: DRF support optional
Browse files Browse the repository at this point in the history
  • Loading branch information
anx-abruckner committed Oct 11, 2024
1 parent 8b1bcca commit 55de63b
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ If used with DRF, `django-filter` is an additional requirement.
1. Install using pip:

```shell
pip install git+https://github.com/anexia/drf-attachments@main
pip install git+https://github.com/anexia/drf-attachments@main[drf]
```

or to install without DRF dependencies (no REST endpoints available from scratch)

```
pip install git+https://github.com/anexia-it/drf-attachments@main
```

2. Integrate `drf_attachments` and `django_userforeignkey` into your `settings.py`
Expand Down Expand Up @@ -69,6 +75,12 @@ class PhotoAlbumAdmin(admin.ModelAdmin):
]
```

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/"
```

`ReadOnlyAttachmentInlineAdmin` is useful when attachments should be provided only by REST API. You may consider
extending the classes in order to handle additional permission checks.

Expand Down
41 changes: 16 additions & 25 deletions drf_attachments/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import (
CASCADE,
CharField,
Expand All @@ -14,9 +15,6 @@
UUIDField,
)
from django.utils.translation import gettext_lazy as _
from django_userforeignkey.request import get_current_request
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse

from drf_attachments.config import config
from drf_attachments.models.managers import AttachmentManager
Expand Down Expand Up @@ -101,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}"

Expand All @@ -112,18 +120,6 @@ def is_image(self):
and self.meta["mime_type"].startswith("image")
)

@property
def download_url(self):
# Note: The attachment-download URL is auto-generated by the AttachmentViewSet
relative_url = reverse("attachment-download", kwargs={"pk": self.id})

# Make sure NOT to throw an exception in any case, otherwise the serializer will not provide the property
request = get_current_request()
if not request:
return relative_url

return request.build_absolute_uri(relative_url)

@property
def default_context(self):
return config.default_context()
Expand Down Expand Up @@ -202,12 +198,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):
"""
Expand All @@ -232,7 +223,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,
},
Expand All @@ -254,7 +245,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,
},
Expand All @@ -275,7 +266,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,
},
Expand All @@ -290,7 +281,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,
},
Expand Down
7 changes: 7 additions & 0 deletions drf_attachments/models/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ def delete(self):

return result

def get_names_list(self):
"""
Return the names of all files within the queryset as list
"""
names = list(self.values_list("name", flat=True))
return ", ".join(names)

def __filter_by_callable(self, callable_) -> QuerySet:
if callable_:
return callable_(self)
Expand Down
17 changes: 15 additions & 2 deletions drf_attachments/rest/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django_userforeignkey.request import get_current_request
from rest_framework import serializers
from rest_framework.fields import ChoiceField, FileField, ReadOnlyField
from rest_framework.fields import ChoiceField, FileField, ReadOnlyField, SerializerMethodField
from rest_framework.reverse import reverse

from drf_attachments.config import config
from drf_attachments.models.models import Attachment
Expand All @@ -16,6 +18,7 @@ class AttachmentSerializer(serializers.ModelSerializer):
to their own respective serializers.
"""

download_url = SerializerMethodField(read_only=True)
file = FileField(write_only=True, required=True)
content_object = config.get_content_object_field()
context = ChoiceField(choices=config.context_choices(values_list=False))
Expand All @@ -33,12 +36,17 @@ class Meta:
"file",
)

def get_download_url(self, obj):
request = get_current_request()
relative_url = reverse("attachment-download", kwargs={"pk": obj.id})
return request.build_absolute_uri(relative_url)


class AttachmentSubSerializer(serializers.ModelSerializer):
"""Sub serializer for nested data inside other serializers"""

# pk is read-only by default
download_url = ReadOnlyField()
download_url = SerializerMethodField(read_only=True)
name = ReadOnlyField()
context = ChoiceField(
choices=config.context_choices(include_default=False, values_list=False),
Expand All @@ -53,3 +61,8 @@ class Meta:
"name",
"context",
)

def get_download_url(self, obj):
request = get_current_request()
relative_url = reverse("attachment-download", kwargs={"pk": obj.id})
return request.build_absolute_uri(relative_url)
12 changes: 10 additions & 2 deletions drf_attachments/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.utils import timezone
from rest_framework.reverse import reverse

__all__ = [
"AttachmentFileStorage",
Expand All @@ -27,7 +28,14 @@ def url(self, name):
if not attachment:
return ""

return attachment.download_url
return reverse("attachment-download", kwargs={"pk": attachment.id})


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):
Expand All @@ -44,4 +52,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}"
4 changes: 4 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# load base requirements
-r base.txt

rest-framework-generic-relations>=2.0.0
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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-userforeignkey>=0.4
django-filter>=21.1,<22

Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
author_email="[email protected]",
install_requires=[
"python-magic>=0.4.18",
"rest-framework-generic-relations>=2.0.0",
"django-userforeignkey>=0.4",
],
classifiers=[
Expand All @@ -39,4 +38,7 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
extras_require={
"drf": ["rest-framework-generic-relations>=2.0.0"],
},
)
15 changes: 8 additions & 7 deletions tests/testapp/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 55de63b

Please sign in to comment.