Skip to content

Commit 5e6e230

Browse files
authored
Legacy search page (#22)
- Break up the upload template for reuse - Make a search page for legacy use while react frontend is made - fix FOUC (Flash of unstyled content) - make all forms have row and column flexing - Add some navigation - Make page widths consistent and sane - Match search behavior of legacy UI
1 parent 399bbe2 commit 5e6e230

File tree

8 files changed

+328
-67
lines changed

8 files changed

+328
-67
lines changed

kirovy/serializers/cnc_map_serializers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,26 @@ class CncMapBaseSerializer(CncNetUserOwnedModelSerializer):
151151
default=None,
152152
)
153153

154+
# TODO: These serializer method fields really ought to be sub serializers
155+
# TODO: Make sure queries are optimized in the views for listing maps.
156+
latest_map_file_hash = serializers.SerializerMethodField()
157+
game_slug = serializers.SerializerMethodField()
158+
created_date = serializers.DateTimeField("%Y-%m-%d", source="created", read_only=True)
159+
154160
class Meta:
155161
model = cnc_map.CncMap
156162
# We return the ID instead of the whole object.
157163
exclude = ["cnc_game", "categories", "parent"]
158164
fields = "__all__"
159165

166+
def get_latest_map_file_hash(self, obj: cnc_map.CncMap) -> t.Optional[str]:
167+
if latest := obj.cncmapfile_set.order_by("-version").first():
168+
return latest.hash_sha1
169+
return None
170+
171+
def get_game_slug(self, obj: cnc_map.CncMap) -> str:
172+
return obj.cnc_game.slug
173+
160174
def create(self, validated_data: t.DictStrAny) -> cnc_map.CncMap:
161175
cnc_map_instance = cnc_map.CncMap(**validated_data)
162176
cnc_map_instance.save()

kirovy/templates/legacy_outer.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<style>html{visibility: hidden;opacity:0;}</style><!-- unFOUC -->
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>{% block title %}CncNet Map DB{% endblock %}</title>
8+
{% load static %}
9+
<link rel="stylesheet" type="text/css" href="{% static 'bc-assets/bc-main.css' %}">
10+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600&amp;display=swap" rel="stylesheet">
11+
<link rel="icon" type="image/png" sizes="32x32" href="https://cncnet.org/favicon-32x32.png">
12+
</head>
13+
<body>
14+
<div id="flex-main">
15+
<div id="flex-container">
16+
<div id="header" class="use-width">
17+
<a class="navbar-brand" href="http://cncnet.org" title="CnCNet Home">
18+
<img src="https://cncnet.org/build/assets/logo-ad41e578.svg" alt="CnCNet logo" loading="lazy" class="logo-full">
19+
<span class="logo-tagline">
20+
Keeping C&amp;C Alive Since 2009
21+
</span>
22+
</a>
23+
<div class="navbar">
24+
<a href="/search" title="CnCNet Legacy Map Search">Search</a>
25+
<a href="/upload-manual" title="CnCNet Legacy Map Upload">Upload</a>
26+
</div>
27+
<h1>{% block header_text %}404{% endblock %}</h1>
28+
</div>
29+
<div id="content">
30+
{% block wrapped_content %}404{% endblock %}
31+
</div>
32+
<div id="footer">
33+
<a href="https://www.digitalocean.com/?refcode=337544e2ec7b&amp;utm_campaign=Referral_Invite&amp;utm_medium=opensource&amp;utm_source=CnCNet" title="Powered by Digital Ocean" target="_blank">
34+
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px" alt="Powered By Digital Ocean">
35+
</a>
36+
</div>
37+
</div>
38+
</div>
39+
</body>
40+
</html>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{% extends "legacy_outer.html" %}
2+
3+
{% block title %}Search CnCNet Maps{% endblock %}
4+
5+
{% block header_text %}CnCNet Map Search{% endblock %}
6+
7+
{% block wrapped_content %}
8+
<div class="form-wrapper limit-w-1000">
9+
<h2>Search Options</h2>
10+
<form action="/search" method="get" enctype="multipart/form-data">
11+
<div>
12+
<div>
13+
<label for="game-slug">Game</label>
14+
<select id="game-slug" name="game_slug" required="required">
15+
<option disabled selected="selected" value=""> -- Select a game -- </option>
16+
<option value="td">Command &amp; Conquer (Tiberian Dawn)</option>
17+
<option value="ra">Red Alert 1</option>
18+
<option value="d2">Dune 2000</option>
19+
<option value="ts">Tiberian Sun</option>
20+
<option value="yr">Yuri's Revenge</option>
21+
</select>
22+
</div>
23+
<div>
24+
<label for="search">Search text</label>
25+
<input type="text" id="search" name="search" required="required">
26+
</div>
27+
</div>
28+
<button class="full-width-flex-row btn" type="submit">Search</button>
29+
</form>
30+
</div>
31+
<div id="search-results">
32+
<div class="callout-note limit-w-1000" {% if results == '' or results|length == 0 %}style="display: none"{% endif %}>
33+
You can download the map files and place in your game directory,
34+
or you can use the <span class="inline-code">/downloadmap {MAP_HASH}</span> in a CnCNet multiplayer lobby.
35+
</div>
36+
<div class="callout-note limit-w-1000" {% if results is None or results|length > 0 %}style="display: none"{% endif %}>
37+
No maps found. Try altering your search.
38+
</div>
39+
<div class="flex-grid">
40+
{% for cnc_map in results %}
41+
<div class="map-result">
42+
{% autoescape on %}
43+
<div>
44+
{{ cnc_map.created_date }}
45+
</div>
46+
<div>
47+
{{ cnc_map.latest_map_file_hash }}
48+
</div>
49+
<div style="flex-grow: 2; text-align: right">
50+
{{ cnc_map.map_name }}
51+
</div>
52+
<div style="flex-grow: 0">
53+
<a href="/{{ cnc_map.game_slug }}/{{ cnc_map.latest_map_file_hash }}">
54+
<i class="bi bi-floppy btn-smol"></i>
55+
</a>
56+
</div>
57+
{% endautoescape %}
58+
</div>
59+
{% endfor %}
60+
</div>
61+
</div>
62+
{% endblock %}
Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,47 @@
1-
<!DOCTYPE html>
2-
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>CncNet Test Map Upload</title>
7-
{% load static %}
8-
<link rel="stylesheet" type="text/css" href="{% static 'bc-assets/bc-main.css' %}">
9-
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600&amp;display=swap" rel="stylesheet">
10-
<link rel="icon" type="image/png" sizes="32x32" href="https://cncnet.org/favicon-32x32.png">
11-
</head>
12-
<body>
13-
<div id="flex-main">
14-
<div id="flex-container">
15-
<div id="header">
16-
<a class="navbar-brand" href="http://cncnet.org" title="CnCNet Home">
17-
<img src="https://cncnet.org/build/assets/logo-ad41e578.svg" alt="CnCNet logo" loading="lazy" class="logo-full">
18-
<span class="logo-tagline">
19-
Keeping C&amp;C Alive Since 2009
20-
</span>
21-
</a>
22-
<h1>CnCNet 5 client upload test form</h1>
23-
</div>
24-
<div id="content">
25-
<p>
26-
Select a <span class="inline-code"><span class="it">&lt;sha1&gt;</span>.zip</span> from your file system.
27-
It should contain the <span class="inline-code">.mpr</span> file for
28-
Red Alert or <span class="inline-code">.ini</span> and
29-
<span class="inline-code">.bin</span> for Tiberian Dawn.
30-
</p>
31-
<p>
32-
The archive will then be extracted, validated and rebuilt for storage.
33-
You will receive a <span class="inline-code">200 OK</span> status code if your
34-
uploaded file was valid.
35-
</p>
1+
{% extends "legacy_outer.html" %}
2+
3+
{% block title %}CncNet Test Map Upload{% endblock %}
4+
5+
{% block header_text %}CnCNet 5 client upload test form{% endblock %}
6+
7+
{% block wrapped_content %}
8+
<div class="limit-w-1000">
9+
<p>
10+
Select a <span class="inline-code"><span class="it">&lt;sha1&gt;</span>.zip</span> from your file system.
11+
It should contain the <span class="inline-code">.mpr</span> file for
12+
Red Alert or <span class="inline-code">.ini</span> and
13+
<span class="inline-code">.bin</span> for Tiberian Dawn.
14+
</p>
15+
<p>
16+
The archive will then be extracted, validated and rebuilt for storage.
17+
You will receive a <span class="inline-code">200 OK</span> status code if your
18+
uploaded file was valid.
19+
</p>
20+
<div class="form-wrapper">
3621
<form action="/upload" method="post" enctype="multipart/form-data">
37-
<label hidden="hidden" for="game-slug">Game</label>
38-
<select id="game-slug" name="game">
39-
<option value="td" selected="selected">Command &amp; Conquer (Tiberian Dawn)</option>
40-
<option value="ra" selected="selected">Red Alert 1</option>
41-
<option value="d2" selected="selected">Dune 2000</option>
42-
<option value="ts" selected="selected">Tiberian Sun</option>
43-
<option value="yr" selected="selected">Yuri's Revenge</option>
44-
</select>
45-
<label hidden="hidden" for="map-file-input">Map zip file</label>
46-
<input id="map-file-input" type="file" name="file" accept="application/zip">
47-
<button type="submit">Upload</button>
22+
<div>
23+
<div>
24+
<label hidden="hidden" for="game-slug">Game</label>
25+
<select id="game-slug" name="game">
26+
<option value="td" selected="selected">Command &amp; Conquer (Tiberian Dawn)</option>
27+
<option value="ra" selected="selected">Red Alert 1</option>
28+
<option value="d2" selected="selected">Dune 2000</option>
29+
<option value="ts" selected="selected">Tiberian Sun</option>
30+
<option value="yr" selected="selected">Yuri's Revenge</option>
31+
</select>
32+
</div>
33+
</div>
34+
<div>
35+
<div>
36+
<label hidden="hidden" for="map-file-input">Map zip file</label>
37+
<input id="map-file-input" type="file" name="file" accept="application/zip">
38+
</div>
39+
</div>
40+
<div>
41+
<button class="btn full-width-flex-row" type="submit">Upload</button>
42+
</div>
4843
</form>
49-
<p>The CnCNet client will function <strong>exactly</strong> like this form.</p>
50-
</div>
51-
<div id="footer">
52-
<a href="https://www.digitalocean.com/?refcode=337544e2ec7b&amp;utm_campaign=Referral_Invite&amp;utm_medium=opensource&amp;utm_source=CnCNet" title="Powered by Digital Ocean" target="_blank">
53-
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px" alt="Powered By Digital Ocean">
54-
</a>
5544
</div>
45+
<p>The CnCNet client will function <strong>exactly</strong> like this form.</p>
5646
</div>
57-
</div>
58-
</body>
59-
</html>
47+
{% endblock %}

kirovy/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def _get_games_url_patterns() -> list[_DjangoPath]:
5252
return [
5353
path("upload-manual", cnc_map_views.MapLegacyStaticUI.as_view()),
5454
path("upload", map_upload_views.CncNetBackwardsCompatibleUploadView.as_view()),
55+
path("search", cnc_map_views.MapLegacySearchUI.as_view()),
5556
*(
5657
# Make e.g. /yr/map_hash, /ra2/map_hash, etc
5758
path(

kirovy/views/base_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class KirovyListCreateView(_g.ListCreateAPIView):
4848
"""
4949

5050
permission_classes = [permissions.CanUpload | permissions.ReadOnly]
51-
pagination_class: t.Type[KirovyDefaultPagination] = KirovyDefaultPagination
51+
pagination_class: t.Optional[t.Type[KirovyDefaultPagination]] = KirovyDefaultPagination
5252
_paginator: t.Optional[KirovyDefaultPagination]
5353
request: KirovyRequest # Added for type hinting. Populated by DRF ``.setup()``
5454

kirovy/views/cnc_map_views.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
CncMap,
1919
CncMapFile,
2020
)
21+
from kirovy.objects.ui_objects import ResultResponseData, ListResponseData, BaseResponseData
2122
from kirovy.request import KirovyRequest
2223
from kirovy.response import KirovyResponse
2324
from kirovy.serializers import cnc_map_serializers
@@ -54,7 +55,7 @@ class MapListFilters(filters.FilterSet):
5455
cnc_game = filters.ModelMultipleChoiceFilter(
5556
field_name="cnc_game__id", to_field_name="id", queryset=CncGame.objects.filter(is_visible=True)
5657
)
57-
# categories = filters.ModelMultipleChoiceFilter(queryset=MapCategory.objects.filter())
58+
game_slug = filters.CharFilter(field_name="cnc_game__slug")
5859

5960
class Meta:
6061
model = CncMap
@@ -123,6 +124,8 @@ class MapListCreateView(base_views.KirovyListCreateView):
123124
The view for maps.
124125
"""
125126

127+
http_method_names = ["get"]
128+
126129
def get_queryset(self):
127130
"""The default query from which all other map list queries are built.
128131
@@ -152,19 +155,25 @@ def get_queryset(self):
152155
# Prefetch the categories because they're displayed like tags.
153156
# TODO: Since the category list is going to be somewhat small,
154157
# maybe the UI should just cache them and I return IDs instead of objects?
155-
.prefetch_related("categories")
158+
.prefetch_related("categories", "cncmapfile_set")
156159
)
157160
return base_query
158161

159162
filter_backends = [
163+
filters.DjangoFilterBackend, # filter first to reduce the count of rows that we full text search on.
160164
SearchFilter,
161165
OrderingFilter,
162-
filters.DjangoFilterBackend,
163166
]
164167
filterset_class = MapListFilters
165168

169+
search_param = "search"
170+
"""attr: The query param to use in the URL
171+
172+
Searches the fields defined in :attr:`~kirovy.views.cnc_map_views.MapListCreateView`
173+
"""
174+
166175
search_fields = [
167-
"@map_name",
176+
"map_name",
168177
"^description",
169178
]
170179
"""
@@ -279,3 +288,18 @@ class MapLegacyStaticUI(APIView):
279288

280289
def get(self, request: KirovyRequest) -> KirovyResponse:
281290
return KirovyResponse()
291+
292+
293+
class MapLegacySearchUI(MapListCreateView):
294+
295+
permission_classes = [AllowAny]
296+
renderer_classes = [TemplateHTMLRenderer]
297+
template_name = "legacy_search.html"
298+
pagination_class = None
299+
300+
# TODO: Require filters.
301+
def get(self, request, *args, **kwargs) -> KirovyResponse[ListResponseData | None]:
302+
if not request.query_params.get("game_slug"):
303+
return KirovyResponse[None](status=status.HTTP_200_OK)
304+
response = super().get(request, *args, **kwargs)
305+
return response

0 commit comments

Comments
 (0)