From b78151b5c95ce8bdfe8ea44b863a59a594d65d58 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Fri, 20 Oct 2023 13:45:46 +0200 Subject: [PATCH] Remove dependency django_userforeignkey, dedicated admin download attachment view --- README.md | 8 +------- drf_attachments/admin.py | 29 ++++++++++++++++++++++++++++- drf_attachments/models/models.py | 15 --------------- drf_attachments/rest/fields.py | 21 +++++++++++++++++++++ drf_attachments/rest/serializers.py | 6 ++++-- drf_attachments/storage.py | 4 +++- drf_attachments/utils.py | 13 +++++++++++++ requirements.txt | 4 ++-- setup.py | 1 - tests/core/settings.py | 2 -- 10 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 drf_attachments/rest/fields.py diff --git a/README.md b/README.md index 1958f84..09a0b01 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,14 @@ If used with DRF, `django-filter` is an additional requirement. pip install git+https://github.com/anexia/drf-attachments@main ``` -2. Integrate `drf_attachments` and `django_userforeignkey` into your `settings.py` +2. Integrate `drf_attachments` into your `settings.py` ```python INSTALLED_APPS = [ # ... - 'django_userforeignkey', 'drf_attachments', # ... ] - -MIDDLEWARE = [ - # ... - 'django_userforeignkey.middleware.UserForeignKeyMiddleware', -] ``` 3. Configure attachment settings diff --git a/drf_attachments/admin.py b/drf_attachments/admin.py index 8d1ef64..2ddb67a 100644 --- a/drf_attachments/admin.py +++ b/drf_attachments/admin.py @@ -1,7 +1,11 @@ +from content_disposition import rfc5987_content_disposition from django.contrib import admin +from django.contrib.admin import AdminSite +from django.contrib.admin.views.decorators import staff_member_required from django.contrib.contenttypes.admin import GenericTabularInline from django.forms import ChoiceField, ModelForm -from django.urls import NoReverseMatch, reverse +from django.http import StreamingHttpResponse +from django.urls import NoReverseMatch, reverse, path from django.utils.safestring import mark_safe from drf_attachments.config import config @@ -71,6 +75,29 @@ def content_object(obj): except NoReverseMatch: return entity + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "/download/", + self.admin_site.admin_view(self.download_view), + name="drf_attachments_attachment_download" + ), + ] + return custom_urls + urls + + def download_view(self, request, object_id): + attachment = Attachment.objects.get(pk=object_id) + response = StreamingHttpResponse( + attachment.file, + content_type=attachment.get_mime_type(), + ) + response["Content-Disposition"] = rfc5987_content_disposition( + (attachment.name if attachment.name else str(attachment.pk)) + attachment.get_extension() + ) + + return response + class BaseAttachmentInlineAdmin(GenericTabularInline, AttachmentAdminMixin): model = Attachment diff --git a/drf_attachments/models/models.py b/drf_attachments/models/models.py index 1b0cefe..95ceed6 100644 --- a/drf_attachments/models/models.py +++ b/drf_attachments/models/models.py @@ -7,16 +7,13 @@ CASCADE, CharField, DateTimeField, - FileField, ForeignKey, JSONField, Model, 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.fields import DynamicStorageFileField @@ -113,18 +110,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() diff --git a/drf_attachments/rest/fields.py b/drf_attachments/rest/fields.py new file mode 100644 index 0000000..1380064 --- /dev/null +++ b/drf_attachments/rest/fields.py @@ -0,0 +1,21 @@ +from django.urls import reverse +from rest_framework import serializers + +__all__ = [ + "DownloadURLField", +] + + +class DownloadURLField(serializers.Field): + def __init__(self, *args, **kwargs): + super().__init__(read_only=True, *args, **kwargs) + def get_attribute(self, instance): + request = self.context.get('request') + relative_url = reverse("attachment-download", kwargs={"pk": instance.pk}) + + if request is None: + return relative_url + return request.build_absolute_uri(relative_url) + + def to_representation(self, value): + return value diff --git a/drf_attachments/rest/serializers.py b/drf_attachments/rest/serializers.py index c4f14f6..ae91e78 100644 --- a/drf_attachments/rest/serializers.py +++ b/drf_attachments/rest/serializers.py @@ -3,6 +3,8 @@ from drf_attachments.config import config from drf_attachments.models.models import Attachment +from drf_attachments.rest.fields import DownloadURLField + __all__ = [ "AttachmentSerializer", @@ -19,6 +21,7 @@ class AttachmentSerializer(serializers.ModelSerializer): file = FileField(write_only=True, required=True) content_object = config.get_content_object_field() context = ChoiceField(choices=config.context_choices(values_list=False)) + download_url = DownloadURLField() class Meta: model = Attachment @@ -33,12 +36,11 @@ class Meta: "file", ) - class AttachmentSubSerializer(serializers.ModelSerializer): """Sub serializer for nested data inside other serializers""" # pk is read-only by default - download_url = ReadOnlyField() + download_url = DownloadURLField() name = ReadOnlyField() context = ChoiceField( choices=config.context_choices(include_default=False, values_list=False), diff --git a/drf_attachments/storage.py b/drf_attachments/storage.py index b9aa82c..6783d69 100644 --- a/drf_attachments/storage.py +++ b/drf_attachments/storage.py @@ -10,6 +10,8 @@ "attachment_upload_path", ] +from drf_attachments.utils import get_admin_attachment_url + class AttachmentFileStorage(FileSystemStorage): """ @@ -27,7 +29,7 @@ def url(self, name): if not attachment: return "" - return attachment.download_url + return get_admin_attachment_url(attachment.pk) def attachment_upload_path(attachment, filename): diff --git a/drf_attachments/utils.py b/drf_attachments/utils.py index bc54ece..02fdcd5 100644 --- a/drf_attachments/utils.py +++ b/drf_attachments/utils.py @@ -1,6 +1,7 @@ import os import magic +from django.urls import reverse def get_mime_type(file): @@ -30,3 +31,15 @@ def remove_file(file_path, raise_exceptions=False): # forward the thrown exception raise # just continue if deletion of old file was not possible and no exceptions should be raised + + +def get_api_attachment_url(attachment_pk): + return reverse("attachment-download", kwargs={"pk": attachment_pk}) + +def get_admin_attachment_url(attachment_pk): + return reverse( + "admin:drf_attachments_attachment_download", + kwargs={ + "object_id": attachment_pk + } + ) diff --git a/requirements.txt b/requirements.txt index 02d190d..cc1d8b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,8 +18,8 @@ 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 # fix importlib version to avoid "AttributeError: 'EntryPoints' object has no attribute 'get'" with flake8 -importlib-metadata<5.0 \ No newline at end of file +importlib-metadata<5.0 +content-disposition diff --git a/setup.py b/setup.py index 506f466..417f17a 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ install_requires=[ "python-magic>=0.4.18", "rest-framework-generic-relations>=2.0.0", - "django-userforeignkey>=0.4", ], classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tests/core/settings.py b/tests/core/settings.py index bca64f5..3d5d3c3 100644 --- a/tests/core/settings.py +++ b/tests/core/settings.py @@ -26,7 +26,6 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "django_userforeignkey", "drf_attachments", "testapp", ] @@ -39,7 +38,6 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django_userforeignkey.middleware.UserForeignKeyMiddleware", ] ROOT_URLCONF = "core.urls"