|
2 | 2 | Base views with common functionality for all API views in Kirovy |
3 | 3 | """ |
4 | 4 |
|
| 5 | +from abc import ABCMeta |
| 6 | + |
| 7 | +import magic, mimetypes |
| 8 | +from django.core.files.uploadedfile import UploadedFile |
5 | 9 | from rest_framework import ( |
6 | 10 | exceptions as _e, |
7 | 11 | generics as _g, |
8 | 12 | permissions as _p, |
9 | 13 | pagination as _pagination, |
10 | 14 | status, |
11 | 15 | ) |
| 16 | +from rest_framework.generics import get_object_or_404 |
| 17 | +from rest_framework.parsers import MultiPartParser |
12 | 18 | from rest_framework.response import Response |
| 19 | +from rest_framework.views import APIView |
13 | 20 |
|
14 | 21 | import kirovy.objects.ui_objects |
15 | | -from kirovy import permissions, typing as t |
| 22 | +from kirovy import permissions, typing as t, logging |
| 23 | +from kirovy.constants import api_codes |
| 24 | +from kirovy.exceptions.view_exceptions import KirovyValidationError |
| 25 | +from kirovy.models import CncNetFileBaseModel |
| 26 | +from kirovy.models.cnc_game import GameScopedUserOwnedModel, CncFileExtension |
16 | 27 | from kirovy.objects import ui_objects |
| 28 | +from kirovy.permissions import CanUpload, CanEdit |
17 | 29 | from kirovy.request import KirovyRequest |
18 | 30 | from kirovy.response import KirovyResponse |
19 | | -from kirovy.serializers import KirovySerializer |
| 31 | +from kirovy.serializers import KirovySerializer, CncNetUserOwnedModelSerializer |
| 32 | +from kirovy.serializers.cnc_map_serializers import CncMapImageFileSerializer |
| 33 | +from kirovy.utils import file_utils |
20 | 34 |
|
21 | 35 |
|
22 | 36 | class KirovyDefaultPagination(_pagination.LimitOffsetPagination): |
@@ -136,3 +150,73 @@ class KirovyDestroyView(_g.DestroyAPIView): |
136 | 150 |
|
137 | 151 | request: KirovyRequest # Added for type hinting. Populated by DRF ``.setup()`` |
138 | 152 | permission_classes = [permissions.CanDelete | _p.IsAdminUser] |
| 153 | + |
| 154 | + |
| 155 | +class FileUploadBaseView(APIView, metaclass=ABCMeta): |
| 156 | + parser_classes = [MultiPartParser] |
| 157 | + permission_classes: [CanUpload, CanEdit] |
| 158 | + request: KirovyRequest |
| 159 | + file_class: t.ClassVar[t.Type[CncNetFileBaseModel]] |
| 160 | + """attr: The class for the file.""" |
| 161 | + file_parent_class: t.ClassVar[t.Type[GameScopedUserOwnedModel]] |
| 162 | + """attr: The class that the file will be linked to.""" |
| 163 | + |
| 164 | + file_parent_attr_name: t.ClassVar[str] |
| 165 | + """attr: The name of the foreign key to the parent object. |
| 166 | +
|
| 167 | + e.g. ``cnc_game_id``. |
| 168 | + """ |
| 169 | + |
| 170 | + serializer_class = t.ClassVar[t.Type[CncNetUserOwnedModelSerializer]] |
| 171 | + |
| 172 | + def get_parent_object(self, request: KirovyRequest) -> GameScopedUserOwnedModel: |
| 173 | + |
| 174 | + parent_object_id = request.data.get(self.file_parent_attr_name) |
| 175 | + if not parent_object_id: |
| 176 | + raise KirovyValidationError( |
| 177 | + detail="Must specify foreign key to parent object", |
| 178 | + code=api_codes.FileUploadApiCodes.MISSING_FOREIGN_ID, |
| 179 | + additional={"expected_field": self.file_parent_attr_name}, |
| 180 | + ) |
| 181 | + |
| 182 | + parent_object: GameScopedUserOwnedModel = get_object_or_404(self.file_parent_class.objects, id=parent_object_id) |
| 183 | + self.check_object_permissions(request, parent_object) |
| 184 | + |
| 185 | + return parent_object |
| 186 | + |
| 187 | + def post(self, request: KirovyRequest, format=None) -> KirovyResponse: |
| 188 | + uploaded_file: UploadedFile = request.data["file"] |
| 189 | + parent_object = self.get_parent_object(request) |
| 190 | + |
| 191 | + # TODO BEFORE MEGE: Move to helper and 400 instead of 500 |
| 192 | + # TODO: maybe DRY mr mime. |
| 193 | + magic_parser = magic.Magic(mime=True) |
| 194 | + uploaded_file.seek(0) |
| 195 | + mr_mime = magic_parser.from_buffer(uploaded_file.read()) |
| 196 | + uploaded_file.seek(0) |
| 197 | + extension_id = file_utils.get_extension_id_for_upload( |
| 198 | + uploaded_file, |
| 199 | + self.file_class.ALLOWED_EXTENSION_TYPES, |
| 200 | + logger=logging.get_logger(), |
| 201 | + error_detail_upload_type="image", |
| 202 | + ) |
| 203 | + serializer = self.serializer_class( |
| 204 | + data={ |
| 205 | + "cnc_game_id": parent_object.cnc_game_id, |
| 206 | + self.file_parent_attr_name: parent_object.id, |
| 207 | + "name": request.get("name"), |
| 208 | + "file": uploaded_file, |
| 209 | + "file_extension_id": extension_id, |
| 210 | + **self.extra_serializer_data(request), |
| 211 | + } |
| 212 | + ) |
| 213 | + |
| 214 | + def extra_serializer_data(self, request: KirovyRequest) -> t.Dict[str, t.Any]: |
| 215 | + raise NotImplementedError() |
| 216 | + |
| 217 | + |
| 218 | +class MapImageFileUploadView(FileUploadBaseView): |
| 219 | + permission_classes = [permissions.CanEdit] |
| 220 | + serializer = CncMapImageFileSerializer |
| 221 | + |
| 222 | + # TODO: Finish writing the subclass |
0 commit comments