Skip to content

Commit 399bbe2

Browse files
authored
feat: Screenshot model, legacy upload page, and map details (#21)
- Fix serializer crash when context not provided - Fix base map serializer. - Allow unknown UTF characters in map files for legacy db - Move django pycharm debugger to its own docker compose - Legacy manual upload emulator - Screenshot migration.
1 parent 1adcdf7 commit 399bbe2

File tree

16 files changed

+387
-40
lines changed

16 files changed

+387
-40
lines changed

docker-compose.debugger.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# A copy of the docker compose, specifically for debuggers.
2+
# Will not run django by default, your debugger needs to run ``manage.py``.
3+
services:
4+
db:
5+
container_name: mapdb-postgres-debugger
6+
image: postgres
7+
volumes:
8+
- ${POSTGRES_DATA_DIR}/debugger-db/:/var/lib/postgresql/data
9+
environment:
10+
- POSTGRES_DB=${POSTGRES_DB}
11+
- POSTGRES_USER=${POSTGRES_USER}
12+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
13+
env_file:
14+
- .env
15+
ports:
16+
- "127.0.0.1:${POSTGRES_PORT}:${POSTGRES_PORT}"
17+
command: -p ${POSTGRES_PORT}
18+
19+
debugger-django:
20+
container_name: mapdb-django-debugger
21+
build:
22+
context: .
23+
dockerfile: docker/Dockerfile
24+
target: debugger
25+
volumes:
26+
- .:/cncnet-map-api
27+
- ${HOST_MEDIA_ROOT}:/data/cncnet_silo # django will save user-uploaded files here. MEDIA_ROOT
28+
- ${HOST_STATIC_ROOT}:/data/cncnet_static # django will gather static files here. STATIC_ROOT
29+
- ./data/tmp:/tmp/pytest-of-root # For inspecting files during pytests
30+
ports:
31+
- "8000:8000"
32+
env_file:
33+
- .env
34+
environment:
35+
POSTGRES_TEST_HOST: db # Necessary to connect to docker db. Overrides the .env setting.
36+
depends_on:
37+
- db

docker-compose.yml

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,3 @@ services:
4343
- "80:80"
4444
depends_on:
4545
- django
46-
47-
debugger:
48-
# Use this for launching via a debugger like PyCharm or VSCode.
49-
# Just builds, and doesn't execute anything, your debugger will be in charge of executing.
50-
build:
51-
context: .
52-
dockerfile: docker/Dockerfile
53-
target: debugger
54-
volumes:
55-
- .:/cncnet-map-api
56-
- ./data/tmp:/tmp/pytest-of-root # For inspecting files during pytests
57-
env_file:
58-
- .env
59-
environment:
60-
POSTGRES_TEST_HOST: mapdb-postgres-dev # Necessary to connect to docker db. Overrides the .env setting.
61-
# ports:
62-
# - "80:80"
63-
depends_on:
64-
- db

kirovy/constants/api_codes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class UploadApiCodes(enum.StrEnum):
99
EMPTY_UPLOAD = "where-file"
1010
DUPLICATE_MAP = "duplicate-map"
1111
FILE_EXTENSION_NOT_SUPPORTED = "file-extension-not-supported"
12+
INVALID = "invalid-data-upload"
1213

1314

1415
class LegacyUploadApiCodes(enum.StrEnum):
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Generated by Django 4.2.21 on 2025-05-19 17:59
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import kirovy.models.file_base
7+
import uuid
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
("kirovy", "0013_cncmap_is_mapdb1_compatible_alter_cncmapfile_file"),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name="CncMapImageFile",
19+
fields=[
20+
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
21+
("created", models.DateTimeField(auto_now_add=True, null=True)),
22+
("modified", models.DateTimeField(auto_now=True, null=True)),
23+
("name", models.CharField(max_length=255)),
24+
("file", models.FileField(upload_to=kirovy.models.file_base._generate_upload_to)),
25+
("hash_md5", models.CharField(max_length=32)),
26+
("hash_sha512", models.CharField(max_length=512)),
27+
("hash_sha1", models.CharField(max_length=50, null=True)),
28+
("width", models.IntegerField()),
29+
("height", models.IntegerField()),
30+
("version", models.IntegerField(editable=False)),
31+
("cnc_game", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="kirovy.cncgame")),
32+
("cnc_map", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="kirovy.cncmap")),
33+
(
34+
"file_extension",
35+
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="kirovy.cncfileextension"),
36+
),
37+
(
38+
"last_modified_by",
39+
models.ForeignKey(
40+
null=True,
41+
on_delete=django.db.models.deletion.SET_NULL,
42+
related_name="modified_%(class)s_set",
43+
to=settings.AUTH_USER_MODEL,
44+
),
45+
),
46+
],
47+
options={
48+
"abstract": False,
49+
},
50+
),
51+
]

kirovy/models/cnc_map.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,49 @@ def generate_upload_to(instance: "CncMapFile", filename: str) -> pathlib.Path:
227227

228228
# e.g. "yr/maps/CNC_NET_MAP_ID_HEX/ra2_CNC_NET_MAP_ID_HEX_v1.map
229229
return pathlib.Path(instance.cnc_map.get_map_directory_path(), final_file_name)
230+
231+
232+
class CncMapImageFile(file_base.CncNetFileBaseModel):
233+
"""Represents an image file to display on the website for a map.
234+
235+
.. warning::
236+
237+
``name`` is auto-generated for this file subclass.
238+
"""
239+
240+
objects = CncMapFileManager()
241+
242+
width = models.IntegerField()
243+
height = models.IntegerField()
244+
version = models.IntegerField(editable=False)
245+
246+
cnc_map = models.ForeignKey(CncMap, on_delete=models.CASCADE, null=False)
247+
248+
ALLOWED_EXTENSION_TYPES = {game_models.CncFileExtension.ExtensionTypes.IMAGE.value}
249+
250+
UPLOAD_TYPE = settings.CNC_MAP_DIRECTORY
251+
252+
def save(self, *args, **kwargs):
253+
super().save(*args, **kwargs)
254+
255+
@staticmethod
256+
def generate_upload_to(instance: "CncMapFile", filename: str) -> pathlib.Path:
257+
"""Generate the path to upload map files to.
258+
259+
Gets called by :func:`kirovy.models.file_base._generate_upload_to` when ``CncMapImageFile.save`` is called.
260+
See [the django docs for file fields](https://docs.djangoproject.com/en/5.0/ref/models/fields/#filefield).
261+
``upload_to`` is set in :attr:`kirovy.models.file_base.CncNetFileBaseModel.file`, which calls
262+
``_generate_upload_to``, which calls this function.
263+
264+
:param instance:
265+
Acts as ``self``. The image file object that we are creating an upload path for.
266+
:param filename:
267+
The filename of the uploaded image file.
268+
:return:
269+
Path to upload map to relative to :attr:`~kirovy.settings.base.MEDIA_ROOT`.
270+
"""
271+
filename = pathlib.Path(filename)
272+
final_file_name = f"{instance.name}{filename.suffix}"
273+
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)

kirovy/serializers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def get_fields(self):
3030
"""
3131
fields = super().get_fields()
3232
request: t.Optional[KirovyRequest] = self.context.get("request")
33-
if not all([request, request.user.is_authenticated, request.user.is_staff]):
33+
if not (request and request.user.is_authenticated and request.user.is_staff):
3434
fields.pop("last_modified_by_id", None)
3535
return fields
3636

kirovy/serializers/cnc_map_serializers.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class Meta:
8181
hash_sha512 = serializers.CharField(required=True, allow_blank=False)
8282
hash_sha1 = serializers.CharField(required=True, allow_blank=False)
8383

84-
def create(self, validated_data: t) -> cnc_map.CncMapFile:
84+
def create(self, validated_data: t.DictStrAny) -> cnc_map.CncMapFile:
8585
map_file = cnc_map.CncMapFile(**validated_data)
8686
map_file.save()
8787
return map_file
@@ -106,7 +106,7 @@ class CncMapBaseSerializer(CncNetUserOwnedModelSerializer):
106106
description = serializers.CharField(
107107
required=True,
108108
allow_null=False,
109-
allow_blank=False,
109+
allow_blank=True,
110110
trim_whitespace=True,
111111
min_length=10,
112112
)
@@ -117,11 +117,9 @@ class CncMapBaseSerializer(CncNetUserOwnedModelSerializer):
117117
)
118118
category_ids = serializers.PrimaryKeyRelatedField(
119119
source="categories",
120-
queryset=cnc_map.MapCategory.objects.all(),
121120
pk_field=serializers.UUIDField(),
122121
many=True,
123-
allow_null=False,
124-
allow_empty=False,
122+
read_only=True, # Set it manually.
125123
)
126124
is_published = serializers.BooleanField(
127125
default=False,
@@ -139,9 +137,27 @@ class CncMapBaseSerializer(CncNetUserOwnedModelSerializer):
139137
legacy_upload_date = serializers.DateTimeField(
140138
read_only=True,
141139
)
140+
incomplete_upload = serializers.BooleanField(
141+
default=False,
142+
)
143+
144+
parent_id = serializers.PrimaryKeyRelatedField(
145+
source="parent",
146+
queryset=cnc_map.CncMap.objects.all(),
147+
pk_field=serializers.UUIDField(),
148+
many=False,
149+
allow_null=True,
150+
allow_empty=False,
151+
default=None,
152+
)
142153

143154
class Meta:
144155
model = cnc_map.CncMap
145156
# We return the ID instead of the whole object.
146-
exclude = ["cnc_game", "categories"]
157+
exclude = ["cnc_game", "categories", "parent"]
147158
fields = "__all__"
159+
160+
def create(self, validated_data: t.DictStrAny) -> cnc_map.CncMap:
161+
cnc_map_instance = cnc_map.CncMap(**validated_data)
162+
cnc_map_instance.save()
163+
return cnc_map_instance

kirovy/services/cnc_gen_2_services.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def read_django_file(self, file: File):
6060
# We can't use ConfigParser.read_file because parser expects the file to be read as a string,
6161
# but django uploaded files are read as bytes. So we need to convert to string first.
6262
# If `decode` is crashing in a test, make sure your test file is read in read-mode "rb".
63-
self.read_string(file.read().decode())
63+
self.read_string(file.read().decode(errors="ignore"))
6464
except configparser.ParsingError as e:
6565
raise exceptions.InvalidMapFile(
6666
ParseErrorMsg.CORRUPT_MAP,

kirovy/settings/_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@
169169
"""
170170
str: The directory inside of :attr:`~kirovy.settings._base.STATIC_URL` where we store game-specific and mod-specific
171171
logos and backgrounds. So a Red Alert 2 icon would be in e.g. ``URL/static/game_images/ra2/icons/allies.png``
172+
173+
.. warning::
174+
175+
This is **not** where we store user-uploaded images. Do not store them here.
172176
"""
173177

174178

kirovy/templates/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)