Skip to content

Commit 7397b3b

Browse files
committed
feat: Add icon upload and icon_url support for achievements
1 parent df86fdc commit 7397b3b

File tree

2 files changed

+139
-6
lines changed

2 files changed

+139
-6
lines changed

api/dashboard/achievement/achievement_serializer.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,38 @@
11
from rest_framework import serializers
2+
from django.conf import settings
23
from db.achievement import Achievement, UserAchievementsLog
34
# from db.user import User
45

56
class AchievementSerializer(serializers.ModelSerializer):
7+
icon_url = serializers.SerializerMethodField()
68

79
class Meta:
810
model = Achievement
911
fields = '__all__'
12+
13+
def get_icon_url(self, obj):
14+
if obj.icon:
15+
# Check if it's already a full URL
16+
if obj.icon.startswith('http://') or obj.icon.startswith('https://'):
17+
return obj.icon
18+
# Return full media URL for uploaded files
19+
return f"{settings.MEDIA_URL}{obj.icon}"
20+
return None
1021

1122
class AchievementBasicSerializer(serializers.ModelSerializer):
1223
achievement_name = serializers.CharField(source='name')
24+
icon_url = serializers.SerializerMethodField()
1325

1426
class Meta:
1527
model = Achievement
16-
fields = ['id', 'achievement_name', 'description', 'icon', 'level_id', 'tags', 'template_id']
28+
fields = ['id', 'achievement_name', 'description', 'icon', 'icon_url', 'level_id', 'tags', 'template_id']
29+
30+
def get_icon_url(self, obj):
31+
if obj.icon:
32+
if obj.icon.startswith('http://') or obj.icon.startswith('https://'):
33+
return obj.icon
34+
return f"{settings.MEDIA_URL}{obj.icon}"
35+
return None
1736

1837
class UserAchievementsSerializer(serializers.ModelSerializer):
1938
achievement = AchievementBasicSerializer(source='achievement_id', read_only=True)

api/dashboard/achievement/achievement_views.py

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def get(self, request):
4747

4848

4949
class AchievementCreateAPIView(APIView):
50+
from rest_framework.parsers import MultiPartParser, FormParser
51+
parser_classes = [MultiPartParser, FormParser]
52+
5053
def post(self, request):
5154
user_id = JWTUtils.fetch_user_id(request)
5255

@@ -62,14 +65,31 @@ def post(self, request):
6265
).get_failure_response()
6366

6467
data = request.data
65-
required_fields = ["name", "description", "icon", "tags", "type", "has_vc"]
66-
68+
# Icon can be either a file upload or a text URL
69+
icon_file = request.FILES.get("icon")
70+
icon_url = data.get("icon", "") if not icon_file else ""
71+
72+
required_fields = ["name", "description", "tags", "type", "has_vc"]
6773
missing_fields = [field for field in required_fields if field not in data]
6874
if missing_fields:
6975
return CustomResponse(
7076
general_message=f"Missing required fields: {', '.join(missing_fields)}"
7177
).get_failure_response()
7278

79+
# Parse has_vc from string to boolean (FormData sends strings)
80+
has_vc_value = data.get("has_vc")
81+
if isinstance(has_vc_value, str):
82+
has_vc_value = has_vc_value.lower() in ("true", "1", "yes")
83+
84+
# Parse tags from JSON string to list (FormData sends strings)
85+
tags_value = data.get("tags", [])
86+
if isinstance(tags_value, str):
87+
import json
88+
try:
89+
tags_value = json.loads(tags_value)
90+
except json.JSONDecodeError:
91+
tags_value = []
92+
7393
if Achievement.objects.filter(name=data["name"]).exists():
7494
return CustomResponse(
7595
general_message="Name already exists"
@@ -84,15 +104,51 @@ def post(self, request):
84104
general_message="Invalid level_id"
85105
).get_failure_response()
86106

107+
# Handle icon file upload
108+
icon_path = icon_url # Default to URL if provided
109+
if icon_file:
110+
import os
111+
from django.conf import settings
112+
113+
# Validate file type
114+
allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']
115+
file_ext = icon_file.name.split('.')[-1].lower()
116+
if file_ext not in allowed_extensions:
117+
return CustomResponse(
118+
general_message=f"Invalid file type. Allowed: {', '.join(allowed_extensions)}"
119+
).get_failure_response()
120+
121+
# Validate file size (max 5MB)
122+
if icon_file.size > 5 * 1024 * 1024:
123+
return CustomResponse(
124+
general_message="File size exceeds 5MB limit"
125+
).get_failure_response()
126+
127+
# Create directory if it doesn't exist
128+
upload_dir = os.path.join(settings.MEDIA_ROOT, 'achievements', 'icons')
129+
os.makedirs(upload_dir, exist_ok=True)
130+
131+
# Generate unique filename
132+
unique_filename = f"{uuid.uuid4()}.{file_ext}"
133+
file_path = os.path.join(upload_dir, unique_filename)
134+
135+
# Save file
136+
with open(file_path, 'wb+') as destination:
137+
for chunk in icon_file.chunks():
138+
destination.write(chunk)
139+
140+
# Store relative path for database
141+
icon_path = f"achievements/icons/{unique_filename}"
142+
87143
achievement = Achievement.objects.create(
88144
id=str(uuid.uuid4()),
89145
name=data["name"],
90146
description=data["description"],
91-
icon=data["icon"],
92-
tags=data["tags"],
147+
icon=icon_path,
148+
tags=tags_value,
93149
type=data["type"],
94150
level_id=level,
95-
has_vc=data["has_vc"],
151+
has_vc=has_vc_value,
96152
template_id=data.get("template_id"),
97153
created_by=user,
98154
updated_by=user,
@@ -106,6 +162,9 @@ def post(self, request):
106162

107163

108164
class AchievementUpdateAPIView(APIView):
165+
from rest_framework.parsers import MultiPartParser, FormParser
166+
parser_classes = [MultiPartParser, FormParser]
167+
109168
def put(self, request, achievement_id=None):
110169
user_id = JWTUtils.fetch_user_id(request)
111170

@@ -135,6 +194,45 @@ def put(self, request, achievement_id=None):
135194
data = request.data.copy()
136195
data["updated_by"] = user_id
137196

197+
# Handle icon file upload
198+
icon_file = request.FILES.get("icon")
199+
if icon_file:
200+
import os
201+
from django.conf import settings
202+
203+
# Validate file type
204+
allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']
205+
file_ext = icon_file.name.split('.')[-1].lower()
206+
if file_ext not in allowed_extensions:
207+
return CustomResponse(
208+
general_message=f"Invalid file type. Allowed: {', '.join(allowed_extensions)}"
209+
).get_failure_response()
210+
211+
# Validate file size (max 5MB)
212+
if icon_file.size > 5 * 1024 * 1024:
213+
return CustomResponse(
214+
general_message="File size exceeds 5MB limit"
215+
).get_failure_response()
216+
217+
# Create directory if it doesn't exist
218+
upload_dir = os.path.join(settings.MEDIA_ROOT, 'achievements', 'icons')
219+
os.makedirs(upload_dir, exist_ok=True)
220+
221+
# Generate unique filename
222+
unique_filename = f"{uuid.uuid4()}.{file_ext}"
223+
file_path = os.path.join(upload_dir, unique_filename)
224+
225+
# Save file
226+
with open(file_path, 'wb+') as destination:
227+
for chunk in icon_file.chunks():
228+
destination.write(chunk)
229+
230+
# Store relative path for database
231+
data["icon"] = f"achievements/icons/{unique_filename}"
232+
elif "icon" not in data or not data["icon"]:
233+
# Keep existing icon if no new file or URL provided
234+
data["icon"] = achievement.icon
235+
138236
if "level_id" in data:
139237
if data["level_id"]:
140238
try:
@@ -147,6 +245,22 @@ def put(self, request, achievement_id=None):
147245
else:
148246
data["level_id"] = None
149247

248+
# Parse has_vc from string to boolean (FormData sends strings)
249+
if "has_vc" in data:
250+
has_vc_value = data.get("has_vc")
251+
if isinstance(has_vc_value, str):
252+
data["has_vc"] = has_vc_value.lower() in ("true", "1", "yes")
253+
254+
# Parse tags from JSON string to list (FormData sends strings)
255+
if "tags" in data:
256+
tags_value = data.get("tags", [])
257+
if isinstance(tags_value, str):
258+
import json
259+
try:
260+
data["tags"] = json.loads(tags_value)
261+
except json.JSONDecodeError:
262+
data["tags"] = []
263+
150264
serializer = achievement_serializer.AchievementSerializer(
151265
achievement, data=data, partial=True
152266
)

0 commit comments

Comments
 (0)