Skip to content

Commit cd7b0ee

Browse files
committed
feat: Map screenshots / images
- rename an endpoint - fix the model for screenshot uploads. TODO: - [] map serializer return link for most recent image and all images - [] map serializer return mapfile links
1 parent 3930de0 commit cd7b0ee

File tree

7 files changed

+89
-18
lines changed

7 files changed

+89
-18
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 4.2.23 on 2025-08-24 20:27
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("kirovy", "0014_cncmapimagefile"),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name="cncmapimagefile",
15+
name="version",
16+
),
17+
migrations.AddField(
18+
model_name="cncmapimagefile",
19+
name="image_order",
20+
field=models.IntegerField(default=0),
21+
),
22+
migrations.AddField(
23+
model_name="cncmapimagefile",
24+
name="is_extracted",
25+
field=models.BooleanField(default=False),
26+
),
27+
migrations.AddIndex(
28+
model_name="cncfileextension",
29+
index=models.Index(fields=["extension_type"], name="kirovy_cncf_extensi_5e0362_idx"),
30+
),
31+
migrations.AddIndex(
32+
model_name="cncmapfile",
33+
index=models.Index(fields=["cnc_map"], name="kirovy_cncm_cnc_map_a1e8af_idx"),
34+
),
35+
migrations.AddIndex(
36+
model_name="cncmapimagefile",
37+
index=models.Index(fields=["cnc_map"], name="kirovy_cncm_cnc_map_078511_idx"),
38+
),
39+
]

kirovy/models/cnc_game.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class ExtensionTypes(models.TextChoices):
5656
blank=False,
5757
)
5858

59+
class Meta:
60+
indexes = [models.Index(fields=["extension_type"])]
61+
5962
def save(self, *args, **kwargs):
6063
self.extension = self.extension.lower() # Force lowercase
6164
is_valid_extension(self.extension) # force validator on save instead from a view.

kirovy/models/cnc_map.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,15 @@ def generate_versioned_name_for_file(self) -> str:
146146
"""
147147
return f"{self.cnc_game.slug}_{self.id.hex}_v{self.next_version_number():02}"
148148

149-
def get_map_directory_path(self) -> pathlib.Path:
149+
def get_map_directory_path(self, upload_type: str = settings.CNC_MAP_DIRECTORY) -> pathlib.Path:
150150
"""Returns the path to the directory where all files related to the map will be store.
151151
152152
:return:
153153
Directory path to put maps and image previews in.
154154
"""
155155
return pathlib.Path(
156156
self.cnc_game.slug,
157-
settings.CNC_MAP_DIRECTORY,
157+
upload_type,
158158
self.id.hex,
159159
)
160160

@@ -199,6 +199,7 @@ class Meta:
199199
constraints = [
200200
models.UniqueConstraint(fields=["cnc_map_id", "version"], name="unique_map_version"),
201201
]
202+
indexes = [models.Index(fields=["cnc_map"])]
202203

203204
def save(self, *args, **kwargs):
204205
if not self.version:
@@ -226,7 +227,7 @@ def generate_upload_to(instance: "CncMapFile", filename: str) -> pathlib.Path:
226227
final_file_name = f"{instance.name}{filename.suffix}"
227228

228229
# e.g. "yr/maps/CNC_NET_MAP_ID_HEX/ra2_CNC_NET_MAP_ID_HEX_v1.map
229-
return pathlib.Path(instance.cnc_map.get_map_directory_path(), final_file_name)
230+
return pathlib.Path(instance.cnc_map.get_map_directory_path(instance.UPLOAD_TYPE), final_file_name)
230231

231232

232233
class CncMapImageFile(file_base.CncNetFileBaseModel):
@@ -241,19 +242,37 @@ class CncMapImageFile(file_base.CncNetFileBaseModel):
241242

242243
width = models.IntegerField()
243244
height = models.IntegerField()
244-
version = models.IntegerField(editable=False)
245245

246246
cnc_map = models.ForeignKey(CncMap, on_delete=models.CASCADE, null=False)
247247

248248
ALLOWED_EXTENSION_TYPES = {game_models.CncFileExtension.ExtensionTypes.IMAGE.value}
249249

250-
UPLOAD_TYPE = settings.CNC_MAP_DIRECTORY
250+
UPLOAD_TYPE = settings.CNC_MAP_IMAGE_DIRECTORY
251+
252+
is_extracted = models.BooleanField(null=False, blank=False, default=False)
253+
"""attr: If true, then this image was extracted from the uploaded map file, usually generated by FinalAlert.
254+
255+
This will always be false for games released after Yuri's Revenge because Generals and beyond do not pack the
256+
preview image into the map files.
257+
"""
258+
259+
image_order = models.IntegerField(null=False, default=0)
260+
"""attr: The order in which images will be returned to the UI.
261+
262+
``0`` will be the "primary" image that we use in the thumbnails for the search screen.
263+
If there are ``order`` collisions, then we fallback to the creation date.
264+
"""
265+
266+
class Meta:
267+
indexes = [models.Index(fields=["cnc_map"])]
251268

252269
def save(self, *args, **kwargs):
270+
self.name = self.cnc_map.map_name
271+
self.cnc_game = self.cnc_map.cnc_game
253272
super().save(*args, **kwargs)
254273

255274
@staticmethod
256-
def generate_upload_to(instance: "CncMapFile", filename: str) -> pathlib.Path:
275+
def generate_upload_to(instance: "CncMapImageFile", filename: str) -> pathlib.Path:
257276
"""Generate the path to upload map files to.
258277
259278
Gets called by :func:`kirovy.models.file_base._generate_upload_to` when ``CncMapImageFile.save`` is called.
@@ -269,7 +288,7 @@ def generate_upload_to(instance: "CncMapFile", filename: str) -> pathlib.Path:
269288
Path to upload map to relative to :attr:`~kirovy.settings.base.MEDIA_ROOT`.
270289
"""
271290
filename = pathlib.Path(filename)
272-
final_file_name = f"{instance.name}{filename.suffix}"
291+
final_file_name = f"{instance.created.isoformat('-')}_{instance.id.hex}{filename.suffix}"
273292

274-
# e.g. "yr/maps/CNC_NET_MAP_ID_HEX/screenshot_of_map.jpg
275-
return pathlib.Path(instance.cnc_map.get_map_directory_path(), final_file_name)
293+
# e.g. "yr/map_images/CNC_NET_MAP_ID_HEX/2020-01-01_somelongid.jpg
294+
return pathlib.Path(instance.cnc_map.get_map_directory_path(instance.UPLOAD_TYPE), final_file_name)

kirovy/settings/_base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
],
9393
"EXCEPTION_HANDLER": "kirovy.exception_handler.kirovy_exception_handler",
9494
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
95+
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
9596
}
9697
"""
9798
attr: Define the default authentication backend for endpoints.
@@ -179,6 +180,14 @@
179180
CNC_MAP_DIRECTORY = "maps"
180181
"""attr: The directory, beneath the game slug, where map files will be stored."""
181182

183+
CNC_MAP_IMAGE_DIRECTORY = "map_images"
184+
"""att: The directory, beneath the game slug, where map images will be stored.
185+
186+
.. note::
187+
188+
Example: ``/data/cncnet_silo/yr/map_images/
189+
"""
190+
182191

183192
### --------------- SERVING FILES ---------------
184193
### This section of settings has to do with serving files

kirovy/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def _get_url_patterns() -> list[_DjangoPath]:
107107
path("client/upload/", map_upload_views.CncnetClientMapUploadView.as_view()),
108108
path("<uuid:pk>/", cnc_map_views.MapRetrieveUpdateView.as_view()),
109109
path("delete/<uuid:pk>/", cnc_map_views.MapDeleteView.as_view()),
110-
path("search/", cnc_map_views.MapListCreateView.as_view()),
110+
path("search/", cnc_map_views.MapListView.as_view()),
111111
# path("img/<uuid:map_id>/"),
112112
# path("img/<uuid:map_id>/", ...),
113113
# path("search/")

kirovy/views/cnc_map_views.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from rest_framework.renderers import TemplateHTMLRenderer
1212
from rest_framework.views import APIView
1313

14-
from kirovy import permissions
14+
from kirovy import permissions, typing as t
1515
from kirovy.models import (
1616
MapCategory,
1717
CncGame,
@@ -119,7 +119,7 @@ def filter_include_map_edits(self, queryset: QuerySet[CncMap], name: str, value:
119119
# return queryset | CncMap.objects.filter(cnc_game__parent_game__in=)
120120

121121

122-
class MapListCreateView(base_views.KirovyListCreateView):
122+
class MapListView(base_views.KirovyListCreateView):
123123
"""
124124
The view for maps.
125125
"""
@@ -169,7 +169,7 @@ def get_queryset(self):
169169
search_param = "search"
170170
"""attr: The query param to use in the URL
171171
172-
Searches the fields defined in :attr:`~kirovy.views.cnc_map_views.MapListCreateView`
172+
Searches the fields defined in :attr:`~kirovy.views.cnc_map_views.MapListView`
173173
"""
174174

175175
search_fields = [
@@ -199,7 +199,7 @@ def get_queryset(self):
199199
class MapRetrieveUpdateView(base_views.KirovyRetrieveUpdateView):
200200
serializer_class = cnc_map_serializers.CncMapBaseSerializer
201201

202-
def get_queryset(self):
202+
def get_queryset(self) -> QuerySet[CncMap]:
203203
"""Get the queryset for map detail views.
204204
205205
Who can view what:
@@ -208,7 +208,7 @@ def get_queryset(self):
208208
- Anyone: Can view published, legacy, or temporary (cncnet client uploaded) maps.
209209
Banned maps will be excluded.
210210
- Registered Users: Can edit their own maps if the map isn't banned.
211-
Can view their own maps even if the map banned.
211+
Can view their own maps even if the map is banned.
212212
The queryset will return a user's banned map, but :class:`kirovy.permissions.CanEdit` will block
213213
any modification attempts.
214214
@@ -223,7 +223,7 @@ def get_queryset(self):
223223
return CncMap.objects.filter()
224224

225225
# Anyone can view legacy maps, temporary maps (for the cncnet client,) and published maps that aren't banned.
226-
queryset = CncMap.objects.filter(
226+
queryset: QuerySet[CncMap] = CncMap.objects.filter(
227227
Q(Q(is_published=True) | Q(is_legacy=True) | Q(is_temporary=True)) & Q(is_banned=False)
228228
)
229229

@@ -290,7 +290,7 @@ def get(self, request: KirovyRequest) -> KirovyResponse:
290290
return KirovyResponse()
291291

292292

293-
class MapLegacySearchUI(MapListCreateView):
293+
class MapLegacySearchUI(MapListView):
294294

295295
permission_classes = [AllowAny]
296296
renderer_classes = [TemplateHTMLRenderer]

requirements-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ black==24.*
55
pre-commit==3.*
66
pytest-django==4.*
77
markdown>=3.4.4, <=4.0
8-
drf-spectacular[sidecar]
8+
# for docs
9+
sphinx

0 commit comments

Comments
 (0)