From 22ddab42d2c6f680b7d05771ec104d3f643df795 Mon Sep 17 00:00:00 2001 From: Two Dev Date: Wed, 28 Aug 2024 09:54:14 +0700 Subject: [PATCH 1/2] chore: fix permissions, remove admin tags on template --- .gitignore | 1 + README.md | 59 +++++++++++++++++-- django_chunk_file_upload/admin.py | 1 + ...r_checksum_idx_filemanager_checksum_idx.py | 18 ++++++ django_chunk_file_upload/models.py | 22 ++++++- django_chunk_file_upload/permissions.py | 3 + .../static/js/upload.chunk.js | 1 - .../forms/widgets/drag_drop_input.html | 2 +- django_chunk_file_upload/typed.py | 6 +- django_chunk_file_upload/views.py | 44 +++++++------- django_chunk_file_upload/widgets.py | 1 + examples/examples/urls.py | 6 ++ 12 files changed, 130 insertions(+), 34 deletions(-) create mode 100644 django_chunk_file_upload/migrations/0003_rename_file_manager_checksum_idx_filemanager_checksum_idx.py diff --git a/.gitignore b/.gitignore index 815e8e5..e0b95ff 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ docs/_build .DS_Store examples/.DS_Store examples/db.sqlite3 +examples/media/ diff --git a/README.md b/README.md index de1d61e..9058ea8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Django Chunk File Upload is an alternative utility that helps you easily edit Django's chunked, drag and drop file uploads. -f-P5-Or-Gkxk0-Ynj00ct-G +f-P5-Or-Gkxk0-Ynj00ct-G Features ---------- @@ -12,6 +12,8 @@ Features - Chunked uploads: optimizing large file transfers. - Prevent uploading existing files with MD5 checksum. - Easy to use any models. +- Image optimizer, resizer, auto convert to webp (supported webp, png, jpg, jpeg). +- Permissions. Quickstart @@ -57,7 +59,9 @@ Change default config: `settings.py` DJANGO_CHUNK_FILE_UPLOAD = { "chunk_size": 1024 * 1024 * 2, # # custom chunk size upload (default: 2MB). "upload_to": "custom_folder/%Y/%m/%d", # custom upload folder. - "is_metadata_storage": True, # save file metadata + "is_metadata_storage": True, # save file metadata, + "remove_file_on_update": True, + "optimize": True, "js": ( "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js", "https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js", @@ -65,7 +69,14 @@ DJANGO_CHUNK_FILE_UPLOAD = { ), # use cdn. "css": ( "custom.css" - ) # custom css path. + ), # custom css path. + "image_optimizer": { + "quality": 82, + "compress_level": 9, + "max_width": 1024, + "max_height": 720, + "to_webp": True, # focus convert image to webp type. + } } ``` @@ -77,14 +88,14 @@ models.py ```python from django.db import models -from django_chunk_file_upload.models import FileManager +from django_chunk_file_upload.models import FileManagerMixin class Tag(models.Model): name = models.CharField(max_length=255) -class YourModel(FileManager): +class YourModel(FileManagerMixin): tags = models.ManyToManyField(Tag) custom_field = models.CharField(max_length=255) @@ -105,16 +116,29 @@ class YourForm(ChunkedUploadFileForm): views.py +Accepted methods: GET, POST, DELETE (UPDATE, PUT does not work with FormData). ```python from django_chunk_file_upload.views import ChunkedUploadView +from django_chunk_file_upload.typed import File +from django_chunk_file_upload.permissions import IsAuthenticated from .forms import YourForm class CustomChunkedUploadView(ChunkedUploadView): form_class = YourForm + permission_classes = (IsAuthenticated,) + + # file_class = File # file class + # file_status = app_settings.status # default: PENDING (Used when using background task, you can change it to COMPLETED.) + # optimize = True # default: True + # remove_file_on_update = True # update image on admin page. # chunk_size = 1024 * 1024 * 2 # custom chunk size upload (default: 2MB). # upload_to = "custom_folder/%Y/%m/%d" # custom upload folder. # template_name = "custom_template.html" # custom template + + # # Run background task like celery when upload is complete + # def background_task(self, instance): + # pass ``` custom_template.html @@ -139,4 +163,29 @@ urlpatterns = [ ] ``` +### Permissions +```python +from django_chunk_file_upload.permissions import AllowAny, IsAuthenticated, IsAdminUser, IsSuperUser +``` + +### File Handlers +```python +from django_chunk_file_upload.typed import ( + ArchiveFile, + AudioFile, + BinaryFile, + DocumentFile, + File, + FontFile, + HyperTextFile, + ImageFile, + JSONFile, + MicrosoftExcelFile, + MicrosoftPowerPointFile, + MicrosoftWordFile, + SeparatedFile, + XMLFile, +) +``` + This package is under development, only supports create view. There are also no features related to image optimization. Use at your own risk. diff --git a/django_chunk_file_upload/admin.py b/django_chunk_file_upload/admin.py index d3ec7a7..eb207bf 100644 --- a/django_chunk_file_upload/admin.py +++ b/django_chunk_file_upload/admin.py @@ -11,6 +11,7 @@ class FileManagerModelAdmin(admin.ModelAdmin): form = ChunkedUploadFileAdminForm list_display = ( "id", + "name", "status", "created_at", "updated_at", diff --git a/django_chunk_file_upload/migrations/0003_rename_file_manager_checksum_idx_filemanager_checksum_idx.py b/django_chunk_file_upload/migrations/0003_rename_file_manager_checksum_idx_filemanager_checksum_idx.py new file mode 100644 index 0000000..21a66ec --- /dev/null +++ b/django_chunk_file_upload/migrations/0003_rename_file_manager_checksum_idx_filemanager_checksum_idx.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-08-28 02:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_chunk_file_upload", "0002_alter_filemanager_options_and_more"), + ] + + operations = [ + migrations.RenameIndex( + model_name="filemanager", + new_name="filemanager_checksum_idx", + old_name="file_manager_checksum_idx", + ), + ] diff --git a/django_chunk_file_upload/models.py b/django_chunk_file_upload/models.py index 5c91fb9..aade7e8 100644 --- a/django_chunk_file_upload/models.py +++ b/django_chunk_file_upload/models.py @@ -7,7 +7,9 @@ from .constants import StatusChoices, TypeChoices -class FileManager(models.Model): +class FileManagerMixin(models.Model): + """File Manager Mixin for Django Models""" + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) file = models.FileField() @@ -34,9 +36,25 @@ class FileManager(models.Model): ) metadata = models.JSONField(default=dict) + class Meta: + abstract = True + + def __str__(self): + return self.name + + @property + def name(self) -> str: + if "name" in self.metadata and self.metadata["name"]: + return self.metadata["name"] + return self.file.name + + +class FileManager(FileManagerMixin): + """File Manager for Django Models""" + class Meta: db_table = "django_chunk_file_upload" - indexes = [models.Index(fields=["checksum"], name="file_manager_checksum_idx")] + indexes = [models.Index(fields=["checksum"], name="%(class)s_checksum_idx")] ordering = ("-created_at",) unique_together = ( "user", diff --git a/django_chunk_file_upload/permissions.py b/django_chunk_file_upload/permissions.py index d6fd688..e28b326 100644 --- a/django_chunk_file_upload/permissions.py +++ b/django_chunk_file_upload/permissions.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class BasePermission: safe_methods = ("GET", "POST", "DELETE") diff --git a/django_chunk_file_upload/static/js/upload.chunk.js b/django_chunk_file_upload/static/js/upload.chunk.js index d27425e..01e4793 100644 --- a/django_chunk_file_upload/static/js/upload.chunk.js +++ b/django_chunk_file_upload/static/js/upload.chunk.js @@ -136,7 +136,6 @@ function deleteFile() { processData: false, contentType: false, error: function (response) { - console.log(response) let errorMessage = response.statusText; if (response.responseJSON) { errorMessage = response.responseJSON.message; diff --git a/django_chunk_file_upload/templates/django_chunk_file_upload/forms/widgets/drag_drop_input.html b/django_chunk_file_upload/templates/django_chunk_file_upload/forms/widgets/drag_drop_input.html index 8585977..82c649b 100644 --- a/django_chunk_file_upload/templates/django_chunk_file_upload/forms/widgets/drag_drop_input.html +++ b/django_chunk_file_upload/templates/django_chunk_file_upload/forms/widgets/drag_drop_input.html @@ -1,4 +1,4 @@ -{% load i18n admin_tags %} +{% load i18n %}
{% if widget.is_initial %} diff --git a/django_chunk_file_upload/typed.py b/django_chunk_file_upload/typed.py index c7d66f8..0dfec0e 100644 --- a/django_chunk_file_upload/typed.py +++ b/django_chunk_file_upload/typed.py @@ -147,7 +147,7 @@ def file(self) -> Union[InMemoryUploadedFile, TemporaryUploadedFile, None]: @property def filename(self) -> str: - return get_filename(self.file.name) + return get_filename(getattr(self.file, "name", "")) or "" @property def repl_filename(self) -> str: @@ -236,11 +236,10 @@ def to_private_attrs(): user = request.user file = request.FILES.get("file") - extension = get_file_extension(file.name) pk = make_uuid(user=user, checksum=request.headers.get("x-file-checksum")) return { "_id": pk, - "_extension": extension, + "_extension": get_file_extension(file.name) if file else None, "_file": file, "_user": user, "_upload_to": upload_to, @@ -291,6 +290,7 @@ def to_metadata(self) -> dict: def to_response(self) -> dict: metadata = {k: v for k, v in self.to_dict().items() if not k.startswith("_")} metadata["message"] = str(self.message) + metadata["name"] = self.filename return metadata def write(self, mode: str = "ab+"): diff --git a/django_chunk_file_upload/views.py b/django_chunk_file_upload/views.py index 4380c83..698f48a 100644 --- a/django_chunk_file_upload/views.py +++ b/django_chunk_file_upload/views.py @@ -62,6 +62,11 @@ def has_change_permission(self, request, obj=None) -> bool: def has_delete_permission(self, request, obj=None) -> bool: return self.check_object_permissions(request) + def is_valid(self, form, file_obj) -> bool: + if form.is_valid() and file_obj.is_valid(): + return True + return False + def get_model(self): return self.form_class.Meta.model @@ -97,18 +102,7 @@ def post(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs): """Override DELETE method from View.""" - form, file_obj = self._get_form_file(request, *args, **kwargs) - if self.has_delete_permission(request): - instance = self.get_instance() - if instance and ( - self.request.user.is_superuser or self.request.user == instance.user - ): - self._delete(instance) - file_obj.message = _("The file deleted successfully.") - return self.ajax_response(None, file_obj, status=200, save=False) - - file_obj.message = _("Permission denied.") - return self.ajax_response(None, file_obj, status=400, save=False) + return self._delete(request, *args, **kwargs) def _get(self, request, *args, **kwargs): if self.has_view_permission(request): @@ -117,11 +111,7 @@ def _get(self, request, *args, **kwargs): def _post(self, request, *args, **kwargs): form, file_obj = self._get_form_file(request, *args, **kwargs) - if ( - self.has_add_permission(self.request) - and file_obj.is_valid() - and form.is_valid() - ): + if self.has_add_permission(self.request) and self.is_valid(form, file_obj): instance = self.get_instance() return self.chunked_upload(instance, form, file_obj) @@ -130,7 +120,7 @@ def _post(self, request, *args, **kwargs): def _update(self, request, *args, **kwargs): form, file_obj = self._get_form_file(request, *args, **kwargs) - if self.has_change_permission(self.request): + if self.has_change_permission(self.request) and self.is_valid(form, file_obj): instance = self.get_instance() if instance: if self.remove_file_on_update: @@ -145,10 +135,20 @@ def _update(self, request, *args, **kwargs): file_obj.message = _("Permission denied.") return self.ajax_response(None, file_obj, status=400, save=False) - def _delete(self, instance): - if instance: - instance.file.delete() - instance.delete() + def _delete(self, request, *args, **kwargs): + form, file_obj = self._get_form_file(request, *args, **kwargs) + if self.has_delete_permission(request): + instance = self.get_instance() + if instance and ( + self.request.user.is_superuser or self.request.user == instance.user + ): + instance.file.delete() + instance.delete() + file_obj.message = _("The file deleted successfully.") + return self.ajax_response(None, file_obj, status=200, save=False) + + file_obj.message = _("Permission denied.") + return self.ajax_response(None, file_obj, status=400, save=False) def _get_form_file( self, request, *args, **kwargs diff --git a/django_chunk_file_upload/widgets.py b/django_chunk_file_upload/widgets.py index 1529498..3833d3d 100644 --- a/django_chunk_file_upload/widgets.py +++ b/django_chunk_file_upload/widgets.py @@ -11,6 +11,7 @@ class DragDropFileInput(forms.ClearableFileInput): def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) + context["widget"]["attrs"]["required"] = False context["widget"]["attrs"]["hidden"] = True context["widget"]["attrs"]["data-id"] = "dropzone" if value and isinstance(value, (FieldFile, ImageField)): diff --git a/examples/examples/urls.py b/examples/examples/urls.py index f7f74ab..43536f5 100644 --- a/examples/examples/urls.py +++ b/examples/examples/urls.py @@ -15,6 +15,8 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path @@ -23,3 +25,7 @@ path("admin/", admin.site.urls), path("file-manager/", include("django_chunk_file_upload.urls")), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) From fea016d3ac0ca63e1475fee2909852253bbd1bf0 Mon Sep 17 00:00:00 2001 From: Two Dev Date: Wed, 28 Aug 2024 10:30:50 +0700 Subject: [PATCH 2/2] chore: update app settings, README. --- README.md | 16 +++++-- django_chunk_file_upload/app_settings.py | 23 +++++++++- .../chunked_upload.html | 46 +++++++++---------- examples/examples/settings.py | 20 +++++++- 4 files changed, 75 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 253cbc4..cdb57c7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Django Chunk File Upload is an alternative utility that helps you easily edit Django's chunked, drag and drop file uploads. -f-P5-Or-Gkxk0-Ynj00ct-G +f-P5-Or-Gkxk0-Ynj00ct-G Features ---------- @@ -76,7 +76,8 @@ DJANGO_CHUNK_FILE_UPLOAD = { "max_width": 1024, "max_height": 720, "to_webp": True, # focus convert image to webp type. - } + }, + "permission_classes": ("django_chunk_file_upload.permissions.AllowAny",) # default: IsAuthenticated } ``` @@ -88,14 +89,14 @@ models.py ```python from django.db import models -from django_chunk_file_upload.models import FileManager +from django_chunk_file_upload.models import FileManagerMixin class Tag(models.Model): name = models.CharField(max_length=255) -class YourModel(FileManager): +class YourModel(FileManagerMixin): tags = models.ManyToManyField(Tag) custom_field = models.CharField(max_length=255) @@ -116,6 +117,7 @@ class YourForm(ChunkedUploadFileForm): views.py +Accepted methods: GET, POST, DELETE (UPDATE, PUT does not work with FormData). ```python from django_chunk_file_upload.views import ChunkedUploadView from django_chunk_file_upload.typed import File @@ -126,12 +128,18 @@ from .forms import YourForm class CustomChunkedUploadView(ChunkedUploadView): form_class = YourForm permission_classes = (IsAuthenticated,) + # file_class = File # file class + # file_status = app_settings.status # default: PENDING (Used when using background task, you can change it to COMPLETED.) # optimize = True # default: True # remove_file_on_update = True # update image on admin page. # chunk_size = 1024 * 1024 * 2 # custom chunk size upload (default: 2MB). # upload_to = "custom_folder/%Y/%m/%d" # custom upload folder. # template_name = "custom_template.html" # custom template + + # # Run background task like celery when upload is complete + # def background_task(self, instance): + # pass ``` custom_template.html diff --git a/django_chunk_file_upload/app_settings.py b/django_chunk_file_upload/app_settings.py index d6114ce..451d696 100644 --- a/django_chunk_file_upload/app_settings.py +++ b/django_chunk_file_upload/app_settings.py @@ -1,11 +1,12 @@ from __future__ import annotations +import importlib from dataclasses import dataclass, field, fields from django.conf import settings +from . import permissions from .constants import StatusChoices -from .permissions import BasePermission, IsAuthenticated @dataclass(kw_only=True) @@ -49,7 +50,9 @@ class _LazySettings(_Settings): is_metadata_storage: bool = False remove_file_on_update: bool = True status: StatusChoices = StatusChoices.PENDING - permission_classes: tuple[BasePermission] = (IsAuthenticated,) + permission_classes: tuple[permissions.BasePermission] = ( + permissions.IsAuthenticated, + ) optimize: bool = True image_optimizer: _ImageSettings = field(default_factory=_ImageSettings) @@ -63,6 +66,22 @@ def from_kwargs(cls, **kwargs) -> "_LazySettings": image_optimizer = kwargs.pop("image_optimizer", {}) or {} if image_optimizer and isinstance(image_optimizer, dict): kwargs["image_optimizer"] = _ImageSettings.from_kwargs(**image_optimizer) + + permission_classes = kwargs.pop("permission_classes") + if permission_classes and isinstance( + permission_classes, (tuple, list, set, str) + ): + if isinstance(permission_classes, str): + permission_classes = [permission_classes] + + perms = [] + for permission_class in permission_classes: + paths = permission_class.split(".") + module = importlib.import_module(".".join(paths[:-1]), "") + permission_class = getattr(module, paths[-1]) + perms.append(permission_class) + + kwargs["permission_classes"] = tuple(perms) return cls(**kwargs) diff --git a/django_chunk_file_upload/templates/django_chunk_file_upload/chunked_upload.html b/django_chunk_file_upload/templates/django_chunk_file_upload/chunked_upload.html index 5b06fa8..5cf1b29 100644 --- a/django_chunk_file_upload/templates/django_chunk_file_upload/chunked_upload.html +++ b/django_chunk_file_upload/templates/django_chunk_file_upload/chunked_upload.html @@ -1,4 +1,4 @@ -{% load i18n %} +{% load static i18n %} @@ -13,30 +13,30 @@ integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> {{ form.media }} diff --git a/examples/examples/settings.py b/examples/examples/settings.py index 6915c6c..24ff087 100644 --- a/examples/examples/settings.py +++ b/examples/examples/settings.py @@ -60,7 +60,7 @@ ROOT_URLCONF = "examples.urls" -TEMPLATES = [ +TEMPLATES = [ # noqa { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], @@ -136,3 +136,21 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +# Django Chunk File Upload +DJANGO_CHUNK_FILE_UPLOAD = { + "chunk_size": 1024 * 1024 * 2, # # custom chunk size upload (default: 2MB). + "upload_to": "custom_folder/%Y/%m/%d", # custom upload folder. + "is_metadata_storage": True, # save file metadata, + "remove_file_on_update": True, + "optimize": True, + "image_optimizer": { + "quality": 82, + "compress_level": 9, + "max_width": 1024, + "max_height": 720, + "to_webp": True, # focus convert image to webp type. + }, + "permission_classes": ("django_chunk_file_upload.permissions.AllowAny"), +}