-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix multiplechoice field
- Loading branch information
Showing
9 changed files
with
225 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.