Skip to content

Commit 20d102e

Browse files
committed
- Fix filenames overflowing the default django 100 character length and getting truncated weirdly.
- Fixtures for extensions and games - Fix tests that were broken by enforcing game uploadability. The games in 0001 are default non-uploadable so tests using `game_yuri` broke. Backwards compatible uploads ignore game visibility. - Enforce game uploadability - Fix map uploads for moderators. - TODO: need tests for game endpoints
1 parent dea4087 commit 20d102e

File tree

13 files changed

+238
-25
lines changed

13 files changed

+238
-25
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 4.2.26 on 2025-11-23 00:19
2+
3+
from django.db import migrations, models
4+
import kirovy.models.file_base
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("kirovy", "0019_alter_cncmapimagefile_file"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="cncmapfile",
16+
name="file",
17+
field=models.FileField(max_length=2048, upload_to=kirovy.models.file_base.default_generate_upload_to),
18+
),
19+
migrations.AlterField(
20+
model_name="cncmapimagefile",
21+
name="file",
22+
field=models.ImageField(max_length=2048, upload_to=kirovy.models.file_base.default_generate_upload_to),
23+
),
24+
migrations.AlterField(
25+
model_name="mappreview",
26+
name="file",
27+
field=models.FileField(max_length=2048, upload_to=kirovy.models.file_base.default_generate_upload_to),
28+
),
29+
]

kirovy/models/cnc_map.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class CncMapImageFile(file_base.CncNetFileBaseModel):
240240

241241
UPLOAD_TYPE = settings.CNC_MAP_IMAGE_DIRECTORY
242242

243-
file = models.ImageField(null=False, upload_to=file_base.default_generate_upload_to)
243+
file = models.ImageField(null=False, upload_to=file_base.default_generate_upload_to, max_length=2048)
244244
"""The actual file this object represent."""
245245

246246
is_extracted = models.BooleanField(null=False, blank=False, default=False)

kirovy/models/file_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ class Meta:
3434
name = models.CharField(max_length=255, null=False, blank=False)
3535
"""Filename no extension."""
3636

37-
file = models.FileField(null=False, upload_to=default_generate_upload_to)
38-
"""The actual file this object represent."""
37+
file = models.FileField(null=False, upload_to=default_generate_upload_to, max_length=2048)
38+
"""The actual file this object represent. The max length of 2048 is half of the unix max."""
3939

4040
file_extension = models.ForeignKey(
4141
game_models.CncFileExtension,

kirovy/urls.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@
2424
import kirovy.views.map_image_views
2525
from kirovy.models import CncGame
2626
from kirovy.settings import settings_constants
27-
from kirovy.views import test, cnc_map_views, permission_views, admin_views, map_upload_views, map_image_views
27+
from kirovy.views import (
28+
test,
29+
cnc_map_views,
30+
permission_views,
31+
admin_views,
32+
map_upload_views,
33+
map_image_views,
34+
game_views,
35+
)
2836
from kirovy import typing as t, constants
2937

3038
_DjangoPath = URLPattern | URLResolver
@@ -90,7 +98,7 @@ def _get_url_patterns() -> list[_DjangoPath]:
9098
path("ui-permissions/", permission_views.ListPermissionForAuthUser.as_view()),
9199
path("maps/", include(map_patterns)),
92100
# path("users/<uuid:cnc_user_id>/", ...), # will show which files a user has uploaded.
93-
# path("games/", ...), # get games.,
101+
path("games/", include(game_patterns)),
94102
]
95103
+ backwards_compatible_urls
96104
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # static assets
@@ -123,4 +131,11 @@ def _get_url_patterns() -> list[_DjangoPath]:
123131
# /admin/
124132
admin_patterns = [path("ban/", admin_views.BanView.as_view())]
125133

134+
135+
# /game
136+
game_patterns = [
137+
path("", game_views.KirovyListCreateView.as_view()),
138+
path("<uuid:pk>/", game_views.GameDetailView.as_view()),
139+
]
140+
126141
urlpatterns = _get_url_patterns()

kirovy/views/cnc_map_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def get_queryset(self) -> QuerySet[CncMap]:
224224
# Staff users can see everything.
225225
return CncMap.objects.filter()
226226

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

kirovy/views/game_views.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.db.models import QuerySet
2+
3+
from kirovy import permissions, typing as t
4+
from kirovy.models import CncGame
5+
from kirovy.views.base_views import KirovyListCreateView, KirovyDefaultPagination, KirovyRetrieveUpdateView
6+
7+
8+
class GamesListView(KirovyListCreateView):
9+
10+
permission_classes = [permissions.IsAdmin | permissions.ReadOnly]
11+
pagination_class: t.Type[KirovyDefaultPagination] | None = KirovyDefaultPagination
12+
13+
def get_queryset(self) -> QuerySet[CncGame]:
14+
if self.request.user.is_staff:
15+
return CncGame.objects.all()
16+
17+
return CncGame.objects.filter(is_visible=True)
18+
19+
20+
class GameDetailView(KirovyRetrieveUpdateView):
21+
22+
permission_classes = [permissions.IsAdmin | permissions.ReadOnly]
23+
pagination_class: t.Type[KirovyDefaultPagination] | None = KirovyDefaultPagination
24+
25+
def get_queryset(self) -> QuerySet[CncGame]:
26+
if self.request.user.is_staff:
27+
return CncGame.objects.all()
28+
29+
return CncGame.objects.filter(is_visible=True)

kirovy/views/map_upload_views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def post(self, request: KirovyRequest, format=None) -> KirovyResponse:
5757
uploaded_file: UploadedFile = request.data["file"]
5858

5959
game = self.get_game_from_request(request)
60-
if not game:
60+
game_supports_uploads = game and (request.user.is_staff or (game.is_visible and game.allow_public_uploads))
61+
if not game_supports_uploads:
62+
# Gaslight the user
6163
raise KirovyValidationError(detail="Game does not exist", code=UploadApiCodes.GAME_DOES_NOT_EXIST)
6264
extension_id = self.get_extension_id_for_upload(uploaded_file)
6365
self.verify_file_size_is_allowed(uploaded_file)
@@ -77,6 +79,7 @@ def post(self, request: KirovyRequest, format=None) -> KirovyResponse:
7779
incomplete_upload=True,
7880
cnc_user_id=request.user.id,
7981
parent_id=parent_map.id if parent_map else None,
82+
last_modified_by_id=request.user.id,
8083
),
8184
context={"request": self.request},
8285
)
@@ -134,6 +137,7 @@ def post(self, request: KirovyRequest, format=None) -> KirovyResponse:
134137
hash_sha512=map_hashes_post_processing.sha512,
135138
hash_sha1=map_hashes_post_processing.sha1,
136139
cnc_user_id=self.request.user.id,
140+
last_modified_by_id=self.request.user.id,
137141
),
138142
context={"request": self.request},
139143
)
@@ -325,7 +329,7 @@ def get_game_from_request(self, request: KirovyRequest) -> CncGame | None:
325329

326330

327331
class CncnetClientMapUploadView(_BaseMapFileUploadView):
328-
"""DO NOT USE THIS FOR NOW. Use"""
332+
"""DO NOT USE THIS FOR NOW. Use CncNetBackwardsCompatibleUploadView"""
329333

330334
permission_classes = [AllowAny]
331335
upload_is_temporary = True

tests/fixtures/extension_fixtures.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,37 @@ def extension_mix(db) -> k_models.CncFileExtension:
2727
The extension object for .mix files.
2828
"""
2929
return k_models.CncFileExtension.objects.get(extension="mix")
30+
31+
32+
@pytest.fixture
33+
def create_cnc_file_extension(db):
34+
"""Return a function to create a CNC file extension."""
35+
36+
def _inner(
37+
extension: str = "map",
38+
about: str = "A Generals map file.",
39+
extension_type: k_models.CncFileExtension.ExtensionTypes = k_models.CncFileExtension.ExtensionTypes.MAP,
40+
) -> k_models.CncFileExtension:
41+
"""Create a CNC file extension.
42+
43+
:param extension:
44+
The actual file extension at the end of a filepath. Don't include the `.` prefix.
45+
:param about:
46+
A description of the file extension.
47+
:param extension_type:
48+
The type of file extension.
49+
:return:
50+
A CNC file extension object.
51+
"""
52+
file_extension = k_models.CncFileExtension(extension=extension, about=about, extension_type=extension_type)
53+
file_extension.save()
54+
file_extension.refresh_from_db()
55+
return file_extension
56+
57+
return _inner
58+
59+
60+
@pytest.fixture
61+
def cnc_file_extension(create_cnc_file_extension) -> k_models.CncFileExtension:
62+
"""Convenience wrapper to make a CncFileExtension for a test."""
63+
return create_cnc_file_extension()

tests/fixtures/game_fixtures.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from kirovy import typing as t, constants
3-
from kirovy.models.cnc_game import CncGame
3+
from kirovy.models.cnc_game import CncGame, CncFileExtension
44

55

66
@pytest.fixture
@@ -73,3 +73,66 @@ def game_red_alert(db) -> CncGame:
7373
The game object for Red Alert.
7474
"""
7575
return CncGame.objects.get(slug__iexact=constants.GameSlugs.red_alert)
76+
77+
78+
@pytest.fixture
79+
def create_cnc_game(db, extension_map, extension_mix):
80+
"""Return a function to create a CNC game."""
81+
82+
def _inner(
83+
slug: str = "ra2remaster",
84+
full_name: str = "Command & Conquer: Red Alert 2 - Remastered",
85+
is_visible: bool = True,
86+
allow_public_uploads: bool = True,
87+
compatible_with_parent_maps: bool = False,
88+
parent_game: CncGame | None = None,
89+
is_mod: bool = False,
90+
allowed_extensions: t.List[CncFileExtension] | None = None,
91+
) -> CncGame:
92+
"""Create a CNC game.
93+
94+
:param slug:
95+
The slug for the game.
96+
:param full_name:
97+
The full name of the game.
98+
:param is_visible:
99+
If the game is visible on the website.
100+
:param allow_public_uploads:
101+
If users can upload maps for this game.
102+
:param compatible_with_parent_maps:
103+
If maps from the parent game work in this game.
104+
:param parent_game:
105+
The parent game if this is a mod or expansion.
106+
:param is_mod:
107+
If this is a mod.
108+
:return:
109+
A CNC game object.
110+
"""
111+
if allowed_extensions is None:
112+
allowed_extensions = [extension_mix, extension_map]
113+
game = CncGame(
114+
slug=slug,
115+
full_name=full_name,
116+
is_visible=is_visible,
117+
allow_public_uploads=allow_public_uploads,
118+
compatible_with_parent_maps=compatible_with_parent_maps,
119+
parent_game=parent_game,
120+
is_mod=is_mod,
121+
)
122+
game.save()
123+
game.allowed_extensions.add(*allowed_extensions)
124+
game.refresh_from_db()
125+
return game
126+
127+
return _inner
128+
129+
130+
@pytest.fixture
131+
def cnc_game(create_cnc_game) -> CncGame:
132+
"""Convenience wrapper to make a CncGame for a test."""
133+
return create_cnc_game()
134+
135+
136+
@pytest.fixture
137+
def game_uploadable(create_cnc_game) -> CncGame:
138+
return create_cnc_game(is_visible=True, allow_public_uploads=True)

tests/fixtures/map_fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ def _inner(
7575

7676

7777
@pytest.fixture
78-
def create_cnc_map(db, cnc_map_category, game_yuri, client_user, create_cnc_map_file):
78+
def create_cnc_map(db, cnc_map_category, game_uploadable, client_user, create_cnc_map_file):
7979
"""Return a function to create a CncMap object."""
8080

8181
def _inner(
8282
map_name: str = "Streets Of Gold 2",
8383
description: str = "A fun map. Capture the center airports for a Hind.",
84-
cnc_game: CncGame = game_yuri,
84+
cnc_game: CncGame = game_uploadable,
8585
map_categories: t.List[MapCategory] = None,
8686
user_id: t.Union[UUIDField, str, None, t.NO_VALUE] = t.NO_VALUE,
8787
is_legacy: bool = False,

0 commit comments

Comments
 (0)