Skip to content

Dev #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ docs/_build
.DS_Store
examples/.DS_Store
examples/db.sqlite3
examples/media/
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img src="https://i.ibb.co/9y2SgmS/f-P5-Or-Gkxk0-Ynj00ct-G.webp" alt="f-P5-Or-Gkxk0-Ynj00ct-G" border="0">
<img src="https://i.ibb.co/9y2SgmS/f-P5-Or-Gkxk0-Ynj00ct-G.webp" alt="f-P5-Or-Gkxk0-Ynj00ct-G">

Features
----------
Expand Down Expand Up @@ -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
}

```
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions django_chunk_file_upload/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class FileManagerModelAdmin(admin.ModelAdmin):
form = ChunkedUploadFileAdminForm
list_display = (
"id",
"name",
"status",
"created_at",
"updated_at",
Expand Down
23 changes: 21 additions & 2 deletions django_chunk_file_upload/app_settings.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)


Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
]
22 changes: 20 additions & 2 deletions django_chunk_file_upload/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions django_chunk_file_upload/permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations


class BasePermission:
safe_methods = ("GET", "POST", "DELETE")

Expand Down
1 change: 0 additions & 1 deletion django_chunk_file_upload/static/js/upload.chunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n %}
{% load static i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
Expand All @@ -13,30 +13,30 @@
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<style>
:root {
--input-padding-x: .75rem;
--input-padding-y: .75rem;
}
:root {
--input-padding-x: .75rem;
--input-padding-y: .75rem;
}

html,
body {
height: 100%;
}
html,
body {
height: 100%;
}

body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}

</style>
{{ form.media }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n admin_tags %}
{% load i18n %}
<div class="dropzone-wrapper">
<div class="loader hide"></div>
{% if widget.is_initial %}
Expand Down
6 changes: 3 additions & 3 deletions django_chunk_file_upload/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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+"):
Expand Down
44 changes: 22 additions & 22 deletions django_chunk_file_upload/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions django_chunk_file_upload/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down
Loading