Skip to content

Commit

Permalink
fix file field
Browse files Browse the repository at this point in the history
fix multiplechoice field
  • Loading branch information
velis74 committed Dec 13, 2024
1 parent 28992e9 commit e9dee71
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 191 deletions.
2 changes: 1 addition & 1 deletion dynamicforms/mixins/choice/allow_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self, lst, field):
for itm in ast.literal_eval(lst):
self.append(itm)

elif lst is not None and isinstance(lst, list):
elif lst is not None and isinstance(lst, (list, set)):
for itm in lst:
self.append(itm)

Expand Down
46 changes: 46 additions & 0 deletions dynamicforms/mixins/file_field.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
from typing import TYPE_CHECKING
from django.utils.translation import gettext_lazy as _

if TYPE_CHECKING:
from rest_framework.fields import FileField


class FileFieldMixin(object):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
# noinspection PyUnresolvedReferences
self.style.setdefault("no_filter", True)

def to_representation(self: "FileField", value, row_data=None):
if value:
return getattr(value, "url", None)
return None

def to_internal_value(self: "FileField", data):
from django.core.files.base import ContentFile
from rest_framework.exceptions import ValidationError

from dynamicforms.preupload_files import get_cached_file

if isinstance(data, str): # dobimo guid
cached_file = get_cached_file(data, self.context['request'].user.id or -1, True)
if cached_file:
# Naredimo django file iz cache podatkov
return ContentFile(cached_file.content, name=cached_file.name)

# Če datoteke ni v cache-u in imamo obstoječo vrednost na instanci, vrnemo obstoječo vrednost
if self.parent.instance and getattr(self.parent.instance, self.field_name, None):
return str(getattr(self.parent.instance, self.field_name))
raise ValidationError(_("File not found in cache"))

if not self.allow_empty_file and not data:
self.fail("empty")

return super().to_internal_value(data)

def validate_empty_values(self: "FileField", data):
"""Override za boljše rokovanje z empty vrednostmi"""
if data is None:
if self.required:
self.fail("required")
return True, None

if data == "" and self.allow_empty_file:
return True, None

return False, data
131 changes: 103 additions & 28 deletions dynamicforms/preupload_files.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,117 @@
import pathlib
import time

from dataclasses import asdict, dataclass
from typing import Optional
from uuid import uuid4

from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.http import HttpResponse, JsonResponse
from django.core.cache import cache
from django.utils.translation import gettext as __
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.response import Response
from rest_framework.views import APIView

UPLOADED_FILE_NAME_TIMESTAMP_SEPARATOR = "_timestamp_"
UPLOADED_FILE_NAME_UUID_SEPARATOR = "_uuid_"
UPLOADED_FILE_TMP_LOCATION = "df_tmp_files"
# Cache constants
CACHE_EXPIRATION = 60 * 60 # 60 minutes
CACHE_KEY_PREFIX = "file_upload_"

preuploaded_fs = FileSystemStorage(location=f"{settings.MEDIA_ROOT}/{UPLOADED_FILE_TMP_LOCATION}")

@dataclass
class CachedFile:
content: bytes
name: str
content_type: str
size: int
user_id: int
timestamp: int

def preupload_file(request):
# if not DYNAMICFORMS.allow_anonymous_user_to_preupload_files and not request.user.is_authenticated:
# return HttpResponse(status=status.HTTP_401_UNAUTHORIZED)
if request.method == "POST":
uploaded_file: Optional[InMemoryUploadedFile] = request.FILES.get("file", None)
@property
def age_in_seconds(self) -> int:
return int(time.time()) - self.timestamp


def count_user_cached_files(user_id: int) -> int:
"""Vrne število vseh cached datotek za uporabnika"""
count = 0

if hasattr(cache, 'keys'): # we expect redis in production
for key in cache.keys(f"{CACHE_KEY_PREFIX}*"):
file_data = cache.get(key)
if file_data and file_data.get("user_id") == user_id:
count += 1

return count

class FileUploadView(APIView):
def post(self, request):
"""Upload file to cache storage"""
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB
MAX_FILES_PER_USER = 10

uploaded_file = request.FILES.get("file")
if not uploaded_file:
raise ValidationError(dict(file=["required"]))
file_identifier: str = str(uuid4())
raise ValidationError({"file": ["required"]})

if uploaded_file.size > MAX_FILE_SIZE:
msg = __("File size exceeds allowed maximum size of {size_mb}MB")
raise ValidationError({"file": [msg.format(size_mb=MAX_FILE_SIZE // (1024 * 1024))]})

user_id = request.user.id or -1

if count_user_cached_files(user_id) >= MAX_FILES_PER_USER:
raise ValidationError({"file": [__("Too many files uploaded. Please submit a form or try again later.")]})

try:
file_extension: str = pathlib.Path(uploaded_file.name).suffix
file_name: str = uploaded_file.name.replace(file_extension, "")
preuploaded_fs.save(
f"{file_name}{UPLOADED_FILE_NAME_UUID_SEPARATOR}{file_identifier}"
f"{UPLOADED_FILE_NAME_TIMESTAMP_SEPARATOR}{int(time.time())}"
f"{file_extension}",
uploaded_file,
file_identifier = str(uuid4())
file_content = uploaded_file.read()

cached_file = CachedFile(
content=file_content,
name=uploaded_file.name,
content_type=uploaded_file.content_type,
size=uploaded_file.size,
user_id=user_id,
timestamp=int(time.time()),
)
return JsonResponse(dict(identifier=file_identifier))
except Exception:
return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR, content=__("File upload failed").encode())
return HttpResponse(status=status.HTTP_501_NOT_IMPLEMENTED)

cache_key = f"{CACHE_KEY_PREFIX}{file_identifier}"
cache.set(cache_key, asdict(cached_file), CACHE_EXPIRATION)

return Response({"identifier": file_identifier})

except Exception as e:
# V produkciji uporabi proper logging
print(f"Error during file upload: {str(e)}")
return Response({"error": __("File upload failed")}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

def delete(self, request, file_identifier=None):
"""Delete file from cache storage"""
if not file_identifier:
raise ValidationError({"file_identifier": ["required"]})

file_identifier = file_identifier.rstrip("/") # remove the trailing slash
cached_file = get_cached_file(file_identifier, request.user.id)
if not cached_file:
raise NotFound(__("File not found"))

cache_key = f"{CACHE_KEY_PREFIX}{file_identifier}"
cache.delete(cache_key)

return Response(status=status.HTTP_204_NO_CONTENT)


def get_cached_file(file_identifier: str, user_id: int, delete_from_cache: bool = False) -> Optional[CachedFile]:
"""Helper function to retrieve file from cache"""
cache_key = f"{CACHE_KEY_PREFIX}{file_identifier}"
file_data = cache.get(cache_key)

if not file_data:
return None

if file_data.get("user_id") != user_id:
raise PermissionDenied(__("You don't have permission to access this file"))

if delete_from_cache:
cache.delete(cache_key)

return CachedFile(**file_data)
2 changes: 1 addition & 1 deletion dynamicforms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
urlpatterns = [
# Progress is used for checking on progress of operation on server
re_path(r"^progress/$", progress.get_progress_value, name="progress"),
re_path(r"^preupload-file/$", preupload_files.preupload_file, name="preupload-file"),
re_path(r"^preupload-file/(?P<file_identifier>[\w-]+/)?$", preupload_files.FileUploadView.as_view(), name="preupload-file"),
]
5 changes: 2 additions & 3 deletions examples/rest/advanced_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from dynamicforms.viewsets import ModelViewSet

from ..models import AdvancedFields, Relation
from .fields.df_file_field import DfFileField, DfPreloadedFileField


class AdvancedFieldsSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -95,8 +94,8 @@ def __init__(self, *args, is_filter: bool = False, **kwds):
queryset=Relation.objects.all(), url_reverse="relation-list", value_field="id", text_field="name"
)
slug_related_field = fields.SlugRelatedField(slug_field="name", queryset=Relation.objects.all())
file_field = DfFileField(max_length=None, allow_empty_file=False, use_url=False, allow_null=True, required=False)
file_field_two = DfPreloadedFileField(allow_empty_file=False, use_url=False, allow_null=True, required=False)
file_field = fields.FileField(max_length=None, allow_empty_file=False, use_url=False, allow_null=True, required=False)
file_field_two = fields.FileField(allow_empty_file=False, use_url=False, allow_null=True, required=False)

# hyperlinked_identity_field = serializers.HyperlinkedIdentityField(view_name='relation-detail', read_only=True)

Expand Down
5 changes: 2 additions & 3 deletions examples/rest/document.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from dynamicforms import serializers
from dynamicforms import fields, serializers
from dynamicforms.viewsets import ModelViewSet

from ..models import Document
from .fields.df_file_field import DfPreloadedFileField


class DocumentsSerializer(serializers.ModelSerializer):
Expand All @@ -13,7 +12,7 @@ class DocumentsSerializer(serializers.ModelSerializer):
"edit": "Editing document object",
}

file = DfPreloadedFileField(allow_empty_file=False, use_url=False, allow_null=True)
file = fields.FileField(allow_empty_file=False, use_url=False, allow_null=True)

class Meta:
model = Document
Expand Down
76 changes: 0 additions & 76 deletions examples/rest/fields/df_file_field.py

This file was deleted.

Loading

0 comments on commit e9dee71

Please sign in to comment.