++gD$ST?@cAd-#$h7MmdRk0BE
zFVWkAqT3u37z|)yv>QA;2fgbJE2J_Eh?7i+Uw%Q5|D9TLV(>+shCE>qNECL6#u0=Od)WQQ$H2X@!0A7ukKYNS
zz(w$yCaeOXXlUApVB^zr5`Pfl5}D<(S-P1=T%o<-1+AzyKG#npkl`ws`=p6B!U@hr
z-7u^CseT%+uMO}RNvJjVe7RuZ&aDX*zo1K2On)iDQC{N64Y#UIY-~fNAmdKTXu}Xc
zq3bVY`n)mJC5{eh5nJ;=$@;c2x8kDQQJK9!y3ZRFQ+^@CzFbkUIp3}iCvzUx+qqoN
z_RcR`hPZ{U2a6edQieU37SV(@?TYBJCx{>N#Kkw_iq4<4n6)WDqsI@KF<=$bYxQa>
zaBbC1j~)X?ar_nqnxWeGumKylMHg0xX6zpEkY7rZ`PoP1tUtxh?2@f5Ydjv$KmROi
zon2;id+|ZDFnIZ;pvzL~f5&RcrQNW?p3%H?QK_{_+(97^Ad2Z}2DSYW(xE1YF$o?ps#TIOC#ujO<)}a=k^Ywi&+%
zdxF1oUwXd9)%%)VZ(#sQ>9`>6M177U$Y}Akai$XSO6#VwYi4M5PlltIvu;5T2+Rw`
z=09jOpXAp)k!sfr#oXO7A8eRA^6$Lwe_bNfu6fiwMVs>*yRa22sj~Q94{C}2uQ?sS
zzv=HMz%^Ype{0K6bhYdnsXSS8YF?rV|4omvAW7QDhNa5p(Y}M52ZZ69H&{n}4P&lFK%Z*RW*
z1>$u-c2vi`pruZ^f6cHlESi;RM*$)T@0;RrKW|Ql-ZBhiLHto(P=Dw{|IdW`AQb-;
zd|_2*(NU9sXgcy;Y7gZ*qeuU5657CQzKV^czWSX#-ih3s`Mo1Z$&apV(hDpI>6KG17+y}eN)zcVAi^Z$@xSEhD!`_9ZK
z`>c31o9X|dBdU1mdDrn2X1gRB>`bDLJ|9~Bf>iP|+NAx9L?eCbd%(|s)7h5;
z`?MZeJeZ1HeDryYrdnY1gm<~i+D*{g+v-NF?B=X=i<}0$Joa;StfQ=PZ@PYa
zv>sDO$$oWvctYNElx)7mGoK)~LnRe)Gwa%tiqvwAN%$OV^apd*)L72CWaHsX&4=$W
z4?-6Tm;1^n?JnD-RL}4nkB|btoY{L0+tZ_&l&jwQdTRVP7!{=li^yuTNYl#79wCpO
zci0u|q@n_Ec}aX`S3y6yRCr`&ox~=%m^)QYIQJOQC8QM{t6pkbG1GCogJ1bV-n*MR
zvHnTgv6EK3RQ8GVMHGl(u1k6L--}$hxubKGzLUF)>QW(>SIyQrrbVeP`!DW0&%H$T
zVQ?$DxZbX=)a_6_Ne$IObZ)N5TLtMpOXBf(Yi!5kILY>DQ0Yd8%{jmFh4U&nZrwq0
zuybGgzCg~7!heYVR9H#58w3f5OY-{1&6{Cb$%$PD
zd`epBd{2X6Ca2$*Q^F}RpJ?K?$NUi-HhPD7-yDMdt93ITEGG{ptA^6%&2lvfa_1k=
z%vxX?M~`cInz@s+=Pj4J_uX=NSS>!>*(tp~ir)NmPser~he=WtQ5Ho-k%Y1J6wl{?
r0RsjM7%*VKfB^#r3>Yxr{|5g9%mvQNvlcQ=00000NkvXXu0mjf5bp?*
diff --git a/frontend/vue/src/router.js b/frontend/vue/src/router.js
index 94b4c01ad..997d27666 100644
--- a/frontend/vue/src/router.js
+++ b/frontend/vue/src/router.js
@@ -6,7 +6,6 @@ import ForumsPage from "./pages/ForumsPage.vue";
import SignupPage from "./pages/SignupPage.vue";
import ProfilePage from "./pages/ProfilePage.vue";
import GameLibrary from "./pages/GameLibrary.vue";
-import TournamentPage from './pages/TournamentPage.vue';
const routes = [
{ path: '/', component: HomePage },
@@ -19,7 +18,6 @@ const routes = [
{ path: '/signup', component: SignupPage },
{ path: '/profile', component: ProfilePage },
{ path: '/games', component: GameLibrary },
- { path: '/tournaments', component: TournamentPage },
];
const router = createRouter({
diff --git a/src/chigame/achievements/api/urls.py b/src/chigame/achievements/api/urls.py
index de0461a68..b4a40adef 100644
--- a/src/chigame/achievements/api/urls.py
+++ b/src/chigame/achievements/api/urls.py
@@ -2,8 +2,4 @@
from . import views
-urlpatterns = [
- path("", views.get_achievements),
- path("user-achievements/", views.get_user_achievements),
- path("award-achievement/", views.award_achievement),
-]
+urlpatterns = [path("", views.get_achievements), path("user-achievements/", views.get_user_achievements)]
diff --git a/src/chigame/achievements/api/views.py b/src/chigame/achievements/api/views.py
index 94c3a7460..dcd178488 100644
--- a/src/chigame/achievements/api/views.py
+++ b/src/chigame/achievements/api/views.py
@@ -1,10 +1,7 @@
-from django.http import JsonResponse
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
-from chigame.games.models import Game
-
from ..models import Achievement, UserAchievement
from .serializers import AchievementSerializer, UserAchievementSerializer
@@ -27,7 +24,7 @@ def get_achievements(request):
@api_view(["POST", "GET"])
def get_user_achievements(request):
- # Post an achievement
+ # Awards an achievement to the user
if request.method == "POST":
serializer = UserAchievementSerializer(data=request.data)
if serializer.is_valid():
@@ -39,28 +36,3 @@ def get_user_achievements(request):
user_achievements = UserAchievement.objects.all()
serializer = UserAchievementSerializer(user_achievements, many=True)
return Response(serializer.data)
-
-
-@api_view(["POST"])
-def award_achievement(request):
- # Award an achievement for the demo game
- # Creates or gets a game called Demo Game
- # Same for the achievement and user achievement
- # Assigns the user achievement to the currently logged in user
- if request.method == "POST":
- if request.user.is_authenticated:
- game = Game.objects.get_or_create(
- name="Demo Game",
- description="Game for demonstrating achievements.",
- min_players=1,
- max_players=1,
- complexity=1,
- )[0]
- achievement = Achievement.objects.get_or_create(name="Clicked a Button", rarity=1, game=game)[0]
- user = request.user
- UserAchievement.objects.get_or_create(
- user=user, achievement=achievement, date_earned="2025-04-24T21:45:37.084000Z"
- )
-
- response_data = {"message": "Button press received"}
- return JsonResponse(response_data)
diff --git a/src/chigame/achievements/models.py b/src/chigame/achievements/models.py
index 029865c2f..a0b2e0177 100644
--- a/src/chigame/achievements/models.py
+++ b/src/chigame/achievements/models.py
@@ -29,20 +29,6 @@ def __str__(self):
class Meta:
unique_together = ("name", "game")
- def advance(self, user, amount=1):
- """
- Advance the progress of a user towards this achievement.
- """
- user_achievement, created = UserAchievement.objects.get_or_create(user=user, achievement=self)
- if created:
- amount -= 1
- if user_achievement.progress >= self.threshold:
- return
- user_achievement.progress += amount
- if user_achievement.progress >= self.threshold:
- user_achievement.date_earned = models.DateTimeField(auto_now_add=True)
- user_achievement.save()
-
class UserAchievement(models.Model):
"""
diff --git a/src/chigame/achievements/tests/tests.py b/src/chigame/achievements/tests/tests.py
index 3f4e765a9..593fecddd 100644
--- a/src/chigame/achievements/tests/tests.py
+++ b/src/chigame/achievements/tests/tests.py
@@ -6,7 +6,7 @@
from chigame.achievements.models import UserAchievement
from chigame.achievements.views import get_recent_achievements
-from .factories import AchievementFactory, MatchFactory, UserAchievementFactory, UserFactory
+from .factories import MatchFactory, UserAchievementFactory, UserFactory
@pytest.mark.django_db
@@ -18,25 +18,6 @@ def test_game_users():
assert player in match.game.users.all()
-@pytest.mark.django_db
-def test_achievement_advance():
- """Test that advancing an achievement works correctly"""
- for _ in range(5):
- achievement = AchievementFactory.create()
- user = UserFactory.create()
-
- # Advance the achievement for the user
- achievement.advance(user)
-
- # Check if the user's achievement progress is updated
- user_achievement = UserAchievement.objects.get(user=user, achievement=achievement)
- assert user_achievement.progress == 1
- if 1e-8 > abs(achievement.threshold - 1):
- assert user_achievement.date_earned is None
- else:
- assert user_achievement.date_earned is None
-
-
@pytest.mark.django_db
def test_get_recent_achievements():
user = UserFactory()
diff --git a/src/chigame/achievements/urls.py b/src/chigame/achievements/urls.py
index bb4e26c16..db17f0eb1 100644
--- a/src/chigame/achievements/urls.py
+++ b/src/chigame/achievements/urls.py
@@ -5,5 +5,5 @@
urlpatterns = [
path("demo-game", views.demo_game, name="demo-game"),
path("", views.user_achievements, name="user_achievements"),
- path("/", views.user_achievements, name="user_achievements"),
+ path("/", views.user_achievements, name="user_achievements_by_username"),
]
diff --git a/src/chigame/achievements/views.py b/src/chigame/achievements/views.py
index 1e6301e0c..35261c2b9 100644
--- a/src/chigame/achievements/views.py
+++ b/src/chigame/achievements/views.py
@@ -1,5 +1,5 @@
from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
+from django.shortcuts import get_object_or_404, render
from chigame.games.models import Game
from chigame.users.models import User
@@ -23,18 +23,21 @@ def demo_game(request):
@login_required
-def user_achievements(request, user_id=None):
+def user_achievements(request, username=None):
"""
Display a user's achievements page.
If username is provided, show that user's achievements.
Otherwise, show the logged-in user's achievements.
"""
- if user_id:
- target_user = User.objects.get(id=user_id)
+ if username:
+ # If a username is provided in the URL, get that user's profile
+ target_user = get_object_or_404(User, username=username) # Renamed to avoid confusion with request.user
viewing_own_profile = target_user == request.user
else:
+ # If no username is provided, show the logged-in user's achievements
target_user = request.user
viewing_own_profile = True
+
# Get all games
games = Game.objects.all()
diff --git a/src/chigame/api/serializers.py b/src/chigame/api/serializers.py
index e9ba34115..4b1748691 100644
--- a/src/chigame/api/serializers.py
+++ b/src/chigame/api/serializers.py
@@ -1,7 +1,6 @@
from rest_framework import serializers
from chigame.achievements.models import Achievement, UserAchievement
-from chigame.chat.models import LiveChat
from chigame.games.models import (
Category,
Chat,
@@ -15,7 +14,6 @@
Tournament,
User,
)
-from chigame.leaderboards.models import MetricScore
from chigame.users.models import Group
@@ -125,7 +123,7 @@ class Meta:
class UserAchievementSerializer(serializers.ModelSerializer):
class Meta:
model = UserAchievement
- fields = ["id", "user", "pinned", "date_earned", "last_updated", "progress"]
+ fields = ["id", "user", "pinned", "date_earned", "progress"]
class AchievementSerializer(serializers.ModelSerializer):
@@ -134,30 +132,6 @@ class Meta:
fields = ["id", "name", "description", "rarity", "threshold"]
-class MetricScoreSerializer(serializers.ModelSerializer):
- metric_id = serializers.IntegerField(write_only=True)
- match_id = serializers.IntegerField(write_only=True)
-
- class Meta:
- model = MetricScore
- fields = ["id", "score", "user", "metric", "match", "leaderboard_entry", "metric_id", "match_id"]
- read_only_fields = ["id", "user", "metric", "match", "leaderboard_entry"]
-
- def validate_score(self, value):
- if value < 0:
- raise serializers.ValidationError("Score must be a positive integer.")
- return value
-
-
-class PopUpInfoSerializer(serializers.Serializer):
- min_players = serializers.IntegerField()
- max_players = serializers.IntegerField()
- complexity = serializers.FloatField()
- min_playtime = serializers.IntegerField()
- max_playtime = serializers.IntegerField()
- description = serializers.CharField()
-
-
class GameDataSerializer(serializers.ModelSerializer):
class Meta:
model = GameData
@@ -176,11 +150,3 @@ class GameReviewStatsSerializer(serializers.Serializer):
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, required=False)
popularity = serializers.IntegerField()
read_only_fields = ["id", "created_at", "user", "tournament"]
-
-
-class LiveChatSerializer(serializers.ModelSerializer):
- users = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
-
- class Meta:
- model = LiveChat
- fields = ["id", "name", "users"]
diff --git a/src/chigame/api/tests/factories.py b/src/chigame/api/tests/factories.py
index 090599a6e..81174511a 100644
--- a/src/chigame/api/tests/factories.py
+++ b/src/chigame/api/tests/factories.py
@@ -5,7 +5,7 @@
from factory import Faker, Iterator, LazyAttribute, LazyFunction, Sequence, SubFactory, post_generation
from factory.django import DjangoModelFactory
-from chigame.games.models import Category, Chat, Feedback, Game, Lobby, Match, Mechanic, Review, Tournament
+from chigame.games.models import Category, Chat, Feedback, Game, Lobby, Match, Mechanic, Tournament
from chigame.users.models import User
@@ -170,15 +170,3 @@ class Meta:
user = factory.SubFactory(UserFactory)
rating = 4
comment = "This is a test comment"
-
-
-class ReviewFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = Review
-
- title = factory.Faker("sentence", nb_words=4)
- review = factory.Faker("text", max_nb_chars=200)
- rating = factory.Faker("pydecimal", left_digits=1, right_digits=1, min_value=1, max_value=5)
- is_public = True
- user = factory.SubFactory(UserFactory)
- game = factory.SubFactory(GameFactory)
diff --git a/src/chigame/api/tests/test_api.py b/src/chigame/api/tests/test_api.py
index 5f191612e..3dea11eb3 100644
--- a/src/chigame/api/tests/test_api.py
+++ b/src/chigame/api/tests/test_api.py
@@ -1175,109 +1175,3 @@ def test_endpoint_rejects_malformed_token(self):
self.client.credentials(HTTP_AUTHORIZATION="Bearer invalid_token_string")
response = self.client.post(self.protected_url, self.protected_data, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-
-class GameDataTests(APITestCase):
- def setUp(self):
- self.user1 = UserFactory()
- self.user2 = UserFactory()
-
- self.game1 = GameFactory()
- self.game2 = GameFactory()
-
- self.list_url = reverse("api-game-data-list")
-
- self.test_data = {"game": self.game1.id, "key": "test_key", "value": "test_value"}
-
- self.client.force_authenticate(user=self.user1)
- self.client.post(self.list_url, self.test_data)
- self.client.post(self.list_url, {"game": self.game1.id, "key": "another_key", "value": "another_value"})
- self.client.post(self.list_url, {"game": self.game2.id, "key": "game2_key", "value": "game2_value"})
-
- self.client.force_authenticate(user=self.user2)
- self.client.post(self.list_url, {"game": self.game1.id, "key": "user2_key", "value": "user2_value"})
-
- self.client.force_authenticate(user=None)
-
- def test_unauthenticated_access_rejected(self):
- # list attempt
- response = self.client.get(self.list_url)
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- # create attempt
- response = self.client.post(self.list_url, self.test_data)
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- # access detail endpoint
- detail_url = reverse("api-game-data-detail", args=[self.game1.id, "test_key"])
- response = self.client.get(detail_url)
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_list_gamedata(self):
- self.client.force_authenticate(user=self.user1)
- response = self.client.get(self.list_url)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data["results"]), 3)
-
- def test_filter_by_game(self):
- self.client.force_authenticate(user=self.user1)
- response = self.client.get(f"{self.list_url}?game={self.game1.id}")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data["results"]), 2)
-
- def test_create_gamedata(self):
- self.client.force_authenticate(user=self.user1)
- new_data = {"game": self.game1.id, "key": "new_key", "value": "new_value"}
- response = self.client.post(self.list_url, new_data)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data["key"], "new_key")
- self.assertEqual(response.data["value"], "new_value")
-
- def test_update_existing_by_create(self):
- self.client.force_authenticate(user=self.user1)
- updated_data = {"game": self.game1.id, "key": "test_key", "value": "updated_value"}
- response = self.client.post(self.list_url, updated_data)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- # verify updated value
- detail_url = reverse("api-game-data-detail", args=[self.game1.id, "test_key"])
- response = self.client.get(detail_url)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["value"], "updated_value")
-
- # check the count didn't increase
- response = self.client.get(self.list_url)
- self.assertEqual(len(response.data["results"]), 3)
-
- def test_retrieve_gamedata(self):
- self.client.force_authenticate(user=self.user1)
- detail_url = reverse("api-game-data-detail", args=[self.game1.id, "test_key"])
- response = self.client.get(detail_url)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["key"], "test_key")
- self.assertEqual(response.data["value"], "test_value")
-
- def test_update_gamedata_put(self):
- self.client.force_authenticate(user=self.user1)
- detail_url = reverse("api-game-data-detail", args=[self.game1.id, "test_key"])
- update_data = {"game": self.game1.id, "key": "test_key", "value": "updated_via_put"}
- response = self.client.put(detail_url, update_data)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["value"], "updated_via_put")
-
- def test_update_gamedata_patch(self):
- self.client.force_authenticate(user=self.user1)
- detail_url = reverse("api-game-data-detail", args=[self.game1.id, "test_key"])
- patch_data = {"value": "updated_via_patch"}
- response = self.client.patch(detail_url, patch_data)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["value"], "updated_via_patch")
-
- def test_delete_gamedata(self):
- self.client.force_authenticate(user=self.user1)
- detail_url = reverse("api-game-data-detail", args=[self.game1.id, "test_key"])
- response = self.client.delete(detail_url)
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- response = self.client.get(detail_url)
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
diff --git a/src/chigame/api/tests/test_popups.py b/src/chigame/api/tests/test_popups.py
deleted file mode 100644
index dfa5e6fd7..000000000
--- a/src/chigame/api/tests/test_popups.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from django.urls import reverse
-from rest_framework import status
-from rest_framework.test import APITestCase
-
-from chigame.api.serializers import PopUpInfoSerializer
-from chigame.api.tests.factories import GameFactory, UserFactory
-
-
-class PopupsTests(APITestCase):
- def test_get_popups_200(self):
- game = GameFactory(
- min_players=2,
- max_players=5,
- complexity=3.2,
- min_playtime=15,
- max_playtime=30,
- description="This is a test game",
- )
- url = reverse("api-game-popups", args=[game.id])
- response = self.client.get(url)
- expected = PopUpInfoSerializer(
- {
- "min_players": 2,
- "max_players": 5,
- "complexity": 3.2,
- "min_playtime": 15,
- "max_playtime": 30,
- "description": "This is a test game",
- }
- ).data
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
-
- def test_get_popups_404(self):
- url = reverse("api-game-popups", args=[9999])
- response = self.client.get(url)
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_get_popups_405(self):
- game = GameFactory()
- user = UserFactory()
- self.client.force_authenticate(user=user)
- url = reverse("api-game-popups", args=[game.id])
- response = self.client.post(url, {})
- self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
diff --git a/src/chigame/api/tests/test_review_stats.py b/src/chigame/api/tests/test_review_stats.py
deleted file mode 100644
index 3e072704f..000000000
--- a/src/chigame/api/tests/test_review_stats.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from django.urls import reverse
-from rest_framework import status
-from rest_framework.test import APITestCase
-
-from chigame.api.tests.factories import GameFactory, ReviewFactory, UserFactory
-
-
-class GameReviewStatsTests(APITestCase):
- def test_review_stats_returns_correct_data(self):
- game = GameFactory()
- user = UserFactory()
- self.client.force_authenticate(user=user)
-
- # Create public reviews with ratings
- ReviewFactory.create_batch(3, game=game, user=user, rating=4, is_public=True)
- ReviewFactory(game=game, user=user, rating=2.5, is_public=True)
-
- # Create a review without a rating (should be ignored in average)
- ReviewFactory(game=game, user=user, rating=None, is_public=True)
-
- # Create a private review (should be ignored completely)
- ReviewFactory(game=game, user=user, rating=5, is_public=False)
-
- url = reverse("api-game-review-stats", args=[game.id])
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["popularity"], 5) # all public reviews
- self.assertEqual(float(response.data["average_rating"]), 3.62) # (4+4+4+2.5)/4 truncated to 2 decimal places
-
- def test_review_stats_no_reviews(self):
- game = GameFactory()
- url = reverse("api-game-review-stats", args=[game.id])
- response = self.client.get(url)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["popularity"], 0)
- self.assertIsNone(response.data["average_rating"])
-
- def test_review_stats_unauthenticated_access(self):
- game = GameFactory()
- url = reverse("api-game-review-stats", args=[game.id])
- response = self.client.get(url)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_review_stats_404_for_nonexistent_game(self):
- url = reverse("api-game-review-stats", args=[99999]) # assuming this ID doesn't exist
- response = self.client.get(url)
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_review_stats_405_post_not_allowed(self):
- game = GameFactory()
- user = UserFactory()
- self.client.force_authenticate(user=user)
- url = reverse("api-game-review-stats", args=[game.id])
- response = self.client.post(url, {})
- self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
diff --git a/src/chigame/api/urls.py b/src/chigame/api/urls.py
index 7937701cd..36ebbe36d 100644
--- a/src/chigame/api/urls.py
+++ b/src/chigame/api/urls.py
@@ -6,7 +6,7 @@
game_patterns = [
path("", views.GameListView.as_view(), name="api-game-list"),
path("/", views.GameDetailView.as_view(), name="api-game-detail"),
- path("/categories/", views.GameCategoriesAPIView.as_view(), name="api-game-asdcategories"),
+ path("/categories/", views.GameCategoriesAPIView.as_view(), name="api-game-categories"),
path("/mechanics/", views.GameMechanicsAPIView.as_view(), name="api-game-mechanics"),
path("/reviews/", views.GameReviewListView.as_view(), name="api-game-reviews"),
path("/reviews/create/", views.ReviewCreateView.as_view(), name="api-game-review-create"),
@@ -18,8 +18,6 @@
name="api-user-achievement-assignment",
),
path("/achievements/create/", views.AchievementCreateView.as_view(), name="api-game-achievement-create"),
- path("/scores/", views.MetricScoreView.as_view(), name="api-game-submit-score"),
- path("/popups/", views.GamePopupsAPIView.as_view(), name="api-game-popups"),
path("data/", views.GameDataListView.as_view(), name="api-game-data-list"),
path("/data//", views.GameDataDetailView.as_view(), name="api-game-data-detail"),
path("/review-stats/", views.GameReviewStatsAPIView.as_view(), name="api-game-review-stats"),
@@ -35,12 +33,6 @@
path("/", views.UserDetailView.as_view(), name="api-user-detail"),
path("/groups/", views.UserGroupsView.as_view(), name="api-user-groups"),
path("/friends/", views.UserFriendsAPIView.as_view(), name="api-user-friends"),
- path(
- "/achievements/",
- views.UserAchievementDetailView.as_view(),
- name="api-user-achievements-edit",
- ),
- path("/achievements/", views.UserAchievementListView.as_view(), name="api-user-achievements"),
]
tournament_patterns = [
@@ -64,14 +56,6 @@
path("token/refresh/", TokenRefreshView.as_view(), name="token-refresh"),
]
-
-livechat_patterns = [
- path("create/", views.LiveChatCreateView.as_view(), name="api-livechat-create"),
- path("list/", views.LiveChatListView.as_view(), name="api-livechat-list"),
- path("/add_user/", views.LiveChatAddUserView.as_view(), name="api-livechat-add-user"),
- path("/", views.LiveChatDetailView.as_view(), name="api-livechat-detail"),
-]
-
urlpatterns = [
path("games/", include(game_patterns)),
path("lobbies/", include(lobby_patterns)),
@@ -79,5 +63,4 @@
path("tournaments/", include(tournament_patterns)),
path("groups/", include(group_patterns)),
path("login/", include(login_patterns)),
- path("livechats/", include(livechat_patterns)),
]
diff --git a/src/chigame/api/views.py b/src/chigame/api/views.py
index 43a910103..224661fde 100644
--- a/src/chigame/api/views.py
+++ b/src/chigame/api/views.py
@@ -3,7 +3,6 @@
from django.views import View
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, status
-from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated, IsAuthenticatedOrReadOnly
@@ -20,23 +19,18 @@
GameReviewStatsSerializer,
GameSerializer,
GroupSerializer,
- LiveChatSerializer,
LobbySerializer,
MechanicSerializer,
MessageFeedSerializer,
MessageSerializer,
- MetricScoreSerializer,
- PopUpInfoSerializer,
ReviewSerializer,
UserAchievementSerializer,
UserSerializer,
)
from chigame.api.spam_utils import is_spam
-from chigame.chat.models import LiveChat, LiveChatUser
from chigame.games.models import Feedback, Game, GameData, Lobby, Message, Review, Tournament
from chigame.games.simulation_utils import run_complete_tournament_simulation
-from chigame.leaderboards.models import LeaderboardEntry, Match, Metric, MetricScore
-from chigame.users.models import Group, User, UserProfile
+from chigame.users.models import Group, User
# Helper function to get user from slug
@@ -381,66 +375,6 @@ def create(self, request, *args, **kwargs):
)
-class MetricScoreView(generics.ListCreateAPIView):
- """
- View to handle MetricScore creation and retrieval.
- """
-
- serializer_class = MetricScoreSerializer
- authentication_classes = [SessionAuthentication]
- permission_classes = [IsAuthenticated]
-
- def get_queryset(self):
- game_id = self.kwargs["game_id"]
- return MetricScore.objects.filter(metric__game=game_id)
-
- def perform_create(self, serializer):
- game_id = self.kwargs["game_id"]
-
- user = self.request.user
- user_profile, created = UserProfile.objects.get_or_create(user=user, defaults={})
- game = get_object_or_404(Game, id=game_id)
-
- metric_id = self.request.data.get("metric_id")
- match_id = self.request.data.get("match_id")
-
- metric = get_object_or_404(Metric, id=metric_id, game=game)
- match = get_object_or_404(Match, id=match_id, game=game)
-
- leaderboard = metric.game.leaderboards.first()
- if not leaderboard:
- raise ValidationError("No leaderboard found for this game.")
-
- leaderboard_entry, _ = LeaderboardEntry.objects.get_or_create(
- leaderboard=leaderboard,
- user=user_profile,
- defaults={"rank": 0},
- )
-
- serializer.save(
- user=user_profile,
- metric=metric,
- match=match,
- leaderboard_entry=leaderboard_entry,
- )
-
-
-class GamePopupsAPIView(APIView):
- permission_classes = [IsAuthenticatedOrReadOnly]
-
- def get(self, request, pk):
- game = get_object_or_404(Game, pk=pk)
- data = {
- "min_players": game.min_players,
- "max_players": game.max_players,
- "complexity": float(game.complexity or 0),
- "min_playtime": game.min_playtime or 0,
- "max_playtime": game.max_playtime or 0,
- "description": game.description or "",
- }
- return Response(PopUpInfoSerializer(data).data)
-
-
class GameDataListView(generics.ListCreateAPIView):
"""
API endpoint to list and create game data for the authenticated user.
@@ -564,73 +498,3 @@ def get(self, request, pk):
}
return Response(GameReviewStatsSerializer(data).data)
-
-
-class LiveChatCreateView(generics.CreateAPIView):
- queryset = LiveChat.objects.all()
- serializer_class = LiveChatSerializer
- permission_classes = []
-
- def perform_create(self, serializer):
- user = User.objects.first()
- if not user:
- raise ValueError("No user exists in the database to assign to the LiveChatUser")
-
- chat = serializer.save()
- LiveChatUser.objects.create(user=user, live_chat=chat)
-
-
-class LiveChatAddUserView(APIView):
- permission_classes = []
-
- def post(self, request, chat_id):
- chat = LiveChat.objects.get(id=chat_id)
- user_ids = request.data.get("user_ids", [])
- for uid in user_ids:
- user = User.objects.get(id=uid)
- LiveChatUser.objects.get_or_create(user=user, live_chat=chat)
- return Response({"id": chat.id, "name": chat.name, "users": user_ids})
-
-
-class LiveChatListView(generics.ListAPIView):
- serializer_class = LiveChatSerializer
- permission_classes = []
-
- def get_queryset(self):
- user = User.objects.first()
- return LiveChat.objects.filter(users=user)
-
-
-class LiveChatDetailView(generics.RetrieveAPIView):
- queryset = LiveChat.objects.all()
- serializer_class = LiveChatSerializer
- permission_classes = []
-
-
-class UserAchievementDetailView(generics.RetrieveUpdateDestroyAPIView):
- queryset = UserAchievement.objects.all()
- serializer_class = UserAchievementSerializer
-
- def perform_update(self, serializer):
- serializer.save()
-
-
-class UserAchievementListView(APIView):
- def get(self, request, pk):
- user_id = self.kwargs["pk"]
- user_achievements = UserAchievement.objects.filter(user__id=user_id)
-
- data = [
- {
- "id": achievement.id,
- "achievement": achievement.achievement.name,
- "game": achievement.achievement.game.name,
- "pinned": achievement.pinned,
- "date_earned": achievement.date_earned,
- "last_updated": achievement.last_updated,
- "progress": achievement.progress,
- }
- for achievement in user_achievements
- ]
-
- return Response(data)
diff --git a/src/chigame/chat/consumers.py b/src/chigame/chat/consumers.py
index 541a08902..bd3ca17d8 100644
--- a/src/chigame/chat/consumers.py
+++ b/src/chigame/chat/consumers.py
@@ -1,20 +1,13 @@
-import asyncio
import json
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
-from django.core.cache import cache
from chigame.users.models import User
from .models import LiveChat, LiveChatMessage
from .utils import ProfanityFilter
-# Rate limiting constants
-MESSAGES_PER_SECOND = 1 # Maximum messages allowed per second
-RATE_LIMIT_WINDOW_SECONDS = 1 # Time window for rate limiting in seconds
-RATE_LIMIT_KEY_PREFIX = "chat_rate_limit:"
-
class ChatConsumer(AsyncWebsocketConsumer):
"""
@@ -92,27 +85,8 @@ async def disconnect(self, close_code):
if hasattr(self, "room_group_name"):
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
- async def check_rate_limit(self, user_id):
- """
- Checks if the user has exceeded their message rate limit.
-
- Args:
- user_id (int): The ID of the user.
-
- Returns:
- bool: True if user is within rate limit, False otherwise.
- """
- cache_key = f"{RATE_LIMIT_KEY_PREFIX}{user_id}"
-
- # Initialize the cache key if it does not exist
- if not cache.add(cache_key, 0, RATE_LIMIT_WINDOW_SECONDS):
- # Atomically increment the message count
- if cache.incr(cache_key) >= MESSAGES_PER_SECOND:
- return False
-
- return True
-
- async def save_message(self, chat_id, user_id, message, reply_to_id=None):
+ @database_sync_to_async
+ def save_message(self, chat_id, user_id, message, reply_to_id=None):
"""
Saves the message to the database. This is called when a message is received from the client.
@@ -121,31 +95,26 @@ async def save_message(self, chat_id, user_id, message, reply_to_id=None):
user_id (int): The ID of the user.
message (str): The message to save.
reply_to_id (int, optional): The ID of the message being replied to.
-
- Returns:
- tuple: (username, bool) - The username and whether the message was saved.
"""
- # Check rate limit first
- if not await self.check_rate_limit(user_id):
- return None, False
-
- chat, user = await asyncio.gather(
- database_sync_to_async(LiveChat.objects.get)(id=chat_id),
- database_sync_to_async(User.objects.get)(id=user_id),
- )
+ chat = LiveChat.objects.get(id=chat_id)
+ user = User.objects.get(id=user_id)
reply_to = None
if reply_to_id:
try:
- reply_to = await database_sync_to_async(LiveChatMessage.objects.get)(id=reply_to_id)
+ reply_to = LiveChatMessage.objects.get(id=reply_to_id)
except LiveChatMessage.DoesNotExist:
reply_to = None
# Save the message to the database
- message_obj = await database_sync_to_async(LiveChatMessage.objects.create)(
- live_chat=chat, user=user, content=message, reply_to=reply_to
- )
+ message_obj = LiveChatMessage.objects.create(live_chat=chat, user=user, content=message, reply_to=reply_to)
- # Return the display name (username or email)
- return user.username or user.email, message_obj.id
+ # Return the display name and message ID
+ return {
+ "username": user.username or user.email,
+ "message_id": message_obj.id,
+ "reply_to": reply_to_id,
+ "reply_to_username": reply_to.user.username if reply_to else None,
+ "reply_to_content": reply_to.content if reply_to else None,
+ }
async def receive(self, text_data):
"""
@@ -156,48 +125,28 @@ async def receive(self, text_data):
text_data (str): The message data received from the client.
"""
text_data_json = json.loads(text_data)
-
msg_type = text_data_json.get("type", "send")
if msg_type == "send":
message = text_data_json["message"]
user_id = text_data_json["user_id"]
reply_to_id = text_data_json.get("reply_to")
- # Apply profanity filter to message
- filtered_message = self.profanity_filter.censor_message(message)
+ # Save message to database once when first received from client
+ message_data = await self.save_message(self.chat_id, user_id, message, reply_to_id)
- # Save message to database once when first received from client
- username, message_id = await self.save_message(self.chat_id, user_id, filtered_message, reply_to_id)
-
- # Get reply info if applicable
- reply_to_username = None
- reply_to_content = None
- if reply_to_id:
- try:
- reply_to = await database_sync_to_async(LiveChatMessage.objects.get)(id=reply_to_id)
- reply_to_username = await database_sync_to_async(lambda: reply_to.user.username)()
- reply_to_content = reply_to.content # This is safe — already loaded
- except LiveChatMessage.DoesNotExist:
- reply_to = None
-
- if reply_to_id:
- # Logic to fetch reply details could be added here if needed
- pass
-
- # Send the filtered message to the group
- await self.channel_layer.group_send(
- self.room_group_name,
- {
- "type": "sendMessage",
- "message": filtered_message,
- "user_id": user_id,
- "username": username,
- "message_id": message_id,
- "reply_to": reply_to_id,
- "reply_to_username": reply_to_username,
- "reply_to_content": reply_to_content,
- },
- )
+ await self.channel_layer.group_send(
+ self.room_group_name,
+ {
+ "type": "sendMessage",
+ "message": message,
+ "user_id": user_id,
+ "username": message_data["username"],
+ "message_id": message_data["message_id"],
+ "reply_to": message_data["reply_to"],
+ "reply_to_username": message_data["reply_to_username"],
+ "reply_to_content": message_data["reply_to_content"],
+ },
+ )
async def sendMessage(self, event):
"""
diff --git a/src/chigame/chat/migrations/0001_initial.py b/src/chigame/chat/migrations/0001_initial.py
index d9189aca7..dfac279f5 100644
--- a/src/chigame/chat/migrations/0001_initial.py
+++ b/src/chigame/chat/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.20 on 2025-05-23 07:12
+# Generated by Django 4.2.20 on 2025-05-21 18:00
import chigame.chat.validators
from django.db import migrations, models
@@ -65,4 +65,48 @@ class Migration(migrations.Migration):
("live_chat", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechat")),
],
),
+<<<<<<< HEAD
+=======
+ migrations.CreateModel(
+ name="LiveChatPollVote",
+ fields=[
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ (
+ "option",
+ models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechatpolloption"),
+ ),
+ ("poll", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechatpoll")),
+ ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name="livechatpoll",
+ name="options",
+ field=models.ManyToManyField(blank=True, related_name="polls", to="chat.livechatpolloption"),
+ ),
+ migrations.CreateModel(
+ name="LiveChatMessageReaction",
+ fields=[
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("content", models.CharField(max_length=10, validators=[chigame.chat.validators.validate_emoji])),
+ ("message", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechatmessage")),
+ ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name="livechat",
+ name="users",
+ field=models.ManyToManyField(
+ related_name="live_chats", through="chat.LiveChatUser", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="livechatpollvote",
+ constraint=models.UniqueConstraint(fields=("poll", "option", "user"), name="unique_poll_option_user"),
+ ),
+ migrations.AlterUniqueTogether(
+ name="livechatmessagereaction",
+ unique_together={("user", "message", "content")},
+ ),
+>>>>>>> parent of 7186be2c (Merge branch 'dev' into word-game-versus-css)
]
diff --git a/src/chigame/chat/models.py b/src/chigame/chat/models.py
index 51d701c2f..38ac40ace 100644
--- a/src/chigame/chat/models.py
+++ b/src/chigame/chat/models.py
@@ -31,8 +31,6 @@ class LiveChatMessage(models.Model):
# this is for messaging quoting/replying
reply_to = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True)
- edited = models.BooleanField(default=False)
-
def __str__(self):
return f"Message: [{self.content}] by {self.user} in LiveChat {self.live_chat}"
diff --git a/src/chigame/chat/urls.py b/src/chigame/chat/urls.py
index c346ddbf8..896a09e45 100644
--- a/src/chigame/chat/urls.py
+++ b/src/chigame/chat/urls.py
@@ -6,6 +6,5 @@
path("/", views.chat, name="chat-page"),
path("live-chat-list/", views.live_chat_list, name="live-chat-list"),
path("message//delete", views.delete_message, name="delete-message"),
- path("message//edit", views.edit_message, name="edit-message"),
path("message//react", views.react_to_message, name="react-to-message"),
]
diff --git a/src/chigame/chat/views.py b/src/chigame/chat/views.py
index f3925c984..162f10c6f 100644
--- a/src/chigame/chat/views.py
+++ b/src/chigame/chat/views.py
@@ -12,14 +12,14 @@ def chat(request, chat_id):
messages = LiveChatMessage.objects.filter(live_chat=chat).order_by("sent_at")
# if the chat is public, add the request user to the chat
- if request.user.is_authenticated and chat.public and not chat.users.filter(id=request.user.id).exists():
+ if chat.public and not chat.users.filter(id=request.user.id).exists():
chat.users.add(request.user)
return render(request, "chat/index.html", {"chat": chat, "messages": messages})
def live_chat_list(request):
- chats = LiveChat.objects.filter(public=True)
+ chats = LiveChat.objects.all()
return render(request, "chat/live-chat-list.html", {"chats": chats})
@@ -78,32 +78,3 @@ def react_to_message(request, message_id):
return JsonResponse({"status": "reacted", "content": content}, status=200)
except ValidationError as e:
return JsonResponse({"error": str(e)}, status=400) # not a single emoji
-
-
-def edit_message(request, message_id):
- """
- Edits a message from the database.
-
- Args:
- request: The request object.
- message_id: The id of the message to edit.
-
- Returns:
- A JSON response.
- """
- if not request.user.is_authenticated:
- return JsonResponse({"error": "Unauthorized"}, status=401)
-
- message = get_object_or_404(LiveChatMessage, id=message_id)
-
- if not request.user == message.user:
- return JsonResponse({"error": "Unauthorized"}, status=401)
-
- if request.method == "POST":
- content = request.POST.get("content")
- message.edited = True
- message.content = content
- message.save()
- return JsonResponse({"message": "Message edited successfully", "edited": message.edited}, status=200)
-
- return JsonResponse({"message": "Type of request not allowed"}, status=405)
diff --git a/src/chigame/games/models.py b/src/chigame/games/models.py
index 7dfc030c6..dc07c59b1 100644
--- a/src/chigame/games/models.py
+++ b/src/chigame/games/models.py
@@ -766,9 +766,6 @@ def __str__(self):
return f"{self.user.username} - {self.game.name}: {self.key}"
-# ================ CHECKERS =================
-
-
class Checkers(models.Model):
"""
A game of Checkers stores:
@@ -797,6 +794,7 @@ class CheckersBoard(models.Model):
"""
state = models.JSONField() # store positions/pieces as a 2D array
+ # state_bits = models.IntegerField() # stores positions as bits
def __str__(self):
return f"Board {self.id}"
diff --git a/src/chigame/games/tests.py b/src/chigame/games/tests.py
index 7d9d64261..d3d4daccf 100644
--- a/src/chigame/games/tests.py
+++ b/src/chigame/games/tests.py
@@ -1,140 +1,9 @@
-from datetime import timedelta
-
from django.contrib.auth import get_user_model
from django.test import TestCase
-from django.urls import reverse
-from django.utils import timezone
-from .models import Category, Feedback, Game, Lobby, Match, Mechanic, Person, Player, Tournament
+from .models import Category, Game, Lobby, Match, Mechanic, Person, Player
from .views import get_recommended_games
-
-class FeedbackTests(TestCase):
- def setUp(self):
- # Create users
- self.owner = User.objects.create_user(username="owner", email="owner@example.com", password="pass")
- self.user = User.objects.create_user(username="user", email="user@example.com", password="pass")
- self.other = User.objects.create_user(username="other", email="other@example.com", password="pass")
- self.admin = User.objects.create_superuser(username="admin", email="admin@example.com", password="pass")
-
- assert self.client.login(email="user@example.com", password="pass"), "Login failed for user"
- self.client.logout()
- assert self.client.login(email="admin@example.com", password="pass"), "Login failed for admin"
- self.client.logout()
-
- # Create a game
- self.game = Game.objects.create(
- name="Test Game",
- description="desc",
- min_players=2,
- max_players=4,
- complexity=2.5,
- )
-
- now = timezone.now()
- # Create tournament
- self.tournament = Tournament.objects.create(
- name="Test Tournament",
- game=self.game,
- registration_start_date=now + timedelta(days=1),
- registration_end_date=now + timedelta(days=2),
- tournament_start_date=now + timedelta(days=3),
- tournament_end_date=now + timedelta(days=4),
- max_players=16,
- description="desc",
- rules="rules",
- draw_rules="draw",
- num_winner=1,
- archived=False,
- created_by=self.owner,
- )
-
- # URLs
- self.submit_url = reverse("submit-feedback", args=[self.tournament.id])
- self.feedback_list_url = reverse("tournament-feedback-list", args=[self.tournament.id])
- self.user_feedback_url = reverse("user-feedback-list")
-
- def test_feedback_form_validation(self):
- self.client.login(email="user@example.com", password="pass")
- # Empty comment
- response = self.client.post(self.submit_url, {"content": "", "rating": 3})
- self.assertEqual(response.status_code, 302) # Redirects with error message
-
- # Rating out of range
- response = self.client.post(self.submit_url, {"content": "Nice!", "rating": 6})
- self.assertEqual(response.status_code, 302) # Redirects with error message
-
- def test_feedback_submission_and_appearance(self):
- logged_in = self.client.login(email="user@example.com", password="pass")
- self.assertTrue(logged_in)
-
- # Match this key to your view's form processing
- self.client.post(self.submit_url, {"content": "Great!", "rating": 5})
-
- feedback = Feedback.objects.filter(user=self.user, tournament=self.tournament).first()
- self.assertIsNotNone(feedback)
-
- self.client.logout()
- self.client.login(email="owner@example.com", password="pass")
- response = self.client.get(self.feedback_list_url)
- self.assertContains(response, "Great!")
-
- def test_unauthorized_edit_delete(self):
- self.client.login(email="user@example.com", password="pass")
- feedback = Feedback.objects.create(user=self.user, tournament=self.tournament, comment="Test", rating=4)
- self.client.logout()
-
- self.client.login(email="other@example.com", password="pass")
- update_url = reverse("update-feedback", args=[feedback.id])
- delete_url = reverse("delete-feedback", args=[feedback.id])
-
- response = self.client.post(update_url, {"content": "Hack", "rating": 3})
- self.assertEqual(response.status_code, 403)
-
- response = self.client.post(delete_url)
- self.assertEqual(response.status_code, 403)
-
- def test_admin_can_delete_feedback(self):
- feedback = Feedback.objects.create(user=self.user, tournament=self.tournament, comment="To delete", rating=3)
- self.client.login(email="admin@example.com", password="pass")
- delete_url = reverse("delete-feedback", args=[feedback.id])
- response = self.client.post(delete_url)
- self.assertRedirects(response, self.user_feedback_url)
- self.assertFalse(Feedback.objects.filter(id=feedback.id).exists())
-
- def test_only_owner_can_see_all_feedback(self):
- Feedback.objects.create(user=self.user, tournament=self.tournament, comment="User feedback", rating=4)
-
- self.client.login(email="owner@example.com", password="pass")
- response = self.client.get(self.feedback_list_url)
- self.assertContains(response, "User feedback")
-
- self.client.logout()
- self.client.login(email="user@example.com", password="pass")
- response = self.client.get(self.feedback_list_url)
- self.assertEqual(response.status_code, 302) # Redirects because user isn't the owner
-
- def test_user_can_only_see_and_edit_own_feedback(self):
- fb1 = Feedback.objects.create(user=self.user, tournament=self.tournament, comment="Mine", rating=5)
- fb2 = Feedback.objects.create(user=self.other, tournament=self.tournament, comment="Not mine", rating=3)
-
- self.client.login(email="user@example.com", password="pass")
-
- response = self.client.get(self.user_feedback_url)
- self.assertContains(response, "Mine")
- self.assertNotContains(response, "Not mine")
-
- update_url = reverse("update-feedback", args=[fb1.id])
- response = self.client.post(update_url, {"content": "Updated", "rating": 4})
- self.assertRedirects(response, self.user_feedback_url)
- fb1.refresh_from_db()
- self.assertEqual(fb1.comment, "Updated")
-
- update_url_other = reverse("update-feedback", args=[fb2.id])
- response = self.client.post(update_url_other, {"content": "Hack", "rating": 2})
- self.assertEqual(response.status_code, 403)
-
-
User = get_user_model()
diff --git a/src/chigame/games/urls.py b/src/chigame/games/urls.py
index 091904457..49c56867b 100644
--- a/src/chigame/games/urls.py
+++ b/src/chigame/games/urls.py
@@ -24,7 +24,6 @@
path("/favorite/", views.add_to_favorites, name="add-to-favorites"),
path("/unfavorite/", views.remove_from_favorites, name="remove-from-favorites"),
# custom game list handling
- path("gamelists//", views.GameListDetailView.as_view(), name="gamelist-detail"),
path("/gamelists//add/", views.add_to_gamelist, name="add-to-gamelist"),
path("/gamelists//remove/", views.remove_from_gamelist, name="remove-from-gamelist"),
# games
@@ -57,16 +56,13 @@
path("feedback/my-feedback/", views.user_feedback_list, name="user-feedback-list"),
# Word Game
path("wordle/", views.wordle_game_page, name="wordle-game"),
- # tournament feedback
- path("tournaments//feedback/", views.tournament_feedback_list, name="tournament-feedback-list"),
- path("tournaments//feedback/submit/", views.submit_feedback, name="submit-feedback"),
- path("feedback/update//", views.update_feedback_view, name="update-feedback"),
- path("feedback/delete//", views.delete_feedback_view, name="delete-feedback"),
- path("feedback/my-feedback/", views.user_feedback_list, name="user-feedback-list"),
# checkers
path("checkers//", views.checkers_game_view, name="checkers-game"),
path("checkers//update/", views.checkers_game_update_board_state, name="update_board_state"),
path("checkers//state/", views.checkers_game_get_board_state, name="checkers-get-state"),
+ # tournament feedback
+ path("tournaments//feedback/", views.tournament_feedback_list, name="tournament-feedback-list"),
+ path("tournaments//feedback/submit/", views.submit_feedback, name="submit-feedback"),
]
# for an uploaded twine file this makes the files accessible at a url
if settings.DEBUG:
diff --git a/src/chigame/games/views.py b/src/chigame/games/views.py
index 7eb8d774a..44df9cd96 100644
--- a/src/chigame/games/views.py
+++ b/src/chigame/games/views.py
@@ -82,19 +82,6 @@ def get_queryset(self):
return queryset
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- if self.request.user.is_authenticated:
- # Ensure 'Favorites' is always first if it exists or is created.
- favorites_list, _ = GameList.objects.get_or_create(name="Favorites", created_by=self.request.user)
- other_lists = (
- GameList.objects.filter(created_by=self.request.user).exclude(pk=favorites_list.pk).order_by("name")
- )
- context["game_lists"] = [favorites_list] + list(other_lists)
- else:
- context["game_lists"] = []
- return context
-
class GameDetailView(LoginRequiredMixin, FormMixin, DetailView):
model = Game
@@ -804,16 +791,6 @@ def get(self, request, *args, **kwargs):
return self.render_to_response(context)
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- tournament = self.get_object()
-
- # Fetch the user's feedback for this tournament
- user_feedback = Feedback.objects.filter(tournament=tournament, user=self.request.user).first()
- context["user_feedback"] = user_feedback
-
- return context
-
def post(self, request, *args, **kwargs):
tournament = Tournament.objects.get(id=request.POST.get("tournament_id"))
simulation_type = self.get_simulation_type(request, tournament.id)
@@ -1371,6 +1348,125 @@ def test_func(self):
return self.request.user.is_staff
+# Tournament Feedback Views
+@login_required
+def tournament_feedback_list(request, tournament_id):
+ """
+ View to display all feedback for a specific tournament.
+ Only the owner of the tournament can access this view.
+ """
+ tournament = get_object_or_404(Tournament, id=tournament_id)
+
+ # Check if the requesting user is the owner of the tournament
+ if tournament.created_by != request.user:
+ # Redirect or show an error message if the user is not the owner
+ messages.error(request, "You are not authorized to view feedback for this tournament.")
+ return redirect("tournament-list") # Redirect to the tournament list or another appropriate page
+
+ # Retrieve feedback for the tournament
+ feedback_list = Feedback.objects.filter(tournament=tournament).order_by("-created_at")
+ return render(
+ request,
+ "tournaments/tournament_feedback_list.html",
+ {"tournament": tournament, "feedback_list": feedback_list},
+ )
+
+
+@login_required
+def user_feedback_list(request):
+ """
+ View to display all feedback submitted by the logged-in user.
+ """
+ user_feedback = Feedback.objects.filter(user=request.user).order_by("-created_at")
+
+ # Get the first tournament associated with the user's feedback (if any)
+ tournament = user_feedback.first().tournament if user_feedback.exists() else None
+
+ return render(
+ request,
+ "tournaments/tournament_user_feedback.html",
+ {"user_feedback": user_feedback, "tournament": tournament},
+ )
+
+
+@login_required
+def submit_feedback(request, tournament_id):
+ tournament = get_object_or_404(Tournament, id=tournament_id)
+
+ if request.method == "POST":
+ comment = request.POST.get("content") # Match the field name in the model
+ rating = request.POST.get("rating")
+
+ if not comment or not rating:
+ messages.error(request, "All fields are required.")
+ return redirect("tournament-detail", pk=tournament.id)
+
+ Feedback.objects.create(
+ tournament=tournament,
+ user=request.user,
+ comment=comment, # Use 'comment' if that's the field name in the model
+ rating=rating,
+ )
+ print("Submitting feedback by user:", request.user)
+
+ messages.success(request, "Feedback submitted successfully!")
+ return redirect("tournament-detail", pk=tournament.id)
+
+ return render(request, "tournaments/tournament_submit_feedback.html", {"tournament": tournament})
+
+
+@login_required
+def update_feedback_view(request, feedback_id):
+ """
+ View to handle updating feedback.
+ Only the original author or an admin can update feedback.
+ """
+ feedback = get_object_or_404(Feedback, id=feedback_id)
+
+ # Check if the user is authorized to update the feedback
+ if feedback.user != request.user and not request.user.is_staff:
+ return HttpResponseForbidden("You are not authorized to update this feedback.")
+
+ if request.method == "POST":
+ new_comment = request.POST.get("content")
+ new_rating = request.POST.get("rating")
+
+ if not new_comment or not new_rating:
+ messages.error(request, "All fields are required.")
+ return redirect("update-feedback", feedback_id=feedback.id)
+
+ # Update feedback fields
+ feedback.comment = new_comment
+ feedback.rating = new_rating
+ feedback.save()
+
+ messages.success(request, "Feedback updated successfully!")
+ return redirect("user-feedback-list")
+
+ return render(request, "tournaments/tournament_update_feedback.html", {"feedback": feedback})
+
+
+@login_required
+def delete_feedback_view(request, feedback_id):
+ """
+ View to handle deleting feedback.
+ Only the original author or an admin can delete feedback.
+ """
+ feedback = get_object_or_404(Feedback, id=feedback_id)
+
+ # Check if the user is authorized to delete the feedback
+ if feedback.user != request.user and not request.user.is_staff:
+ return HttpResponseForbidden("You are not authorized to delete this feedback.")
+
+ if request.method == "POST":
+ # Delete the feedback
+ feedback.delete()
+ messages.success(request, "Feedback deleted successfully!")
+ return redirect("user-feedback-list")
+
+ return render(request, "tournaments/tournament_delete_feedback.html", {"feedback": feedback})
+
+
# Placeholder Game
@login_required
def coin_flip_game(request, pk):
@@ -1502,125 +1598,6 @@ def remove_from_gamelist(request, pk, list_pk):
return redirect("game-detail", pk=pk)
-# Tournament Feedback Views
-@login_required
-def tournament_feedback_list(request, tournament_id):
- """
- View to display all feedback for a specific tournament.
- Only the owner of the tournament can access this view.
- """
- tournament = get_object_or_404(Tournament, id=tournament_id)
-
- # Check if the requesting user is the owner of the tournament
- if tournament.created_by != request.user:
- # Redirect or show an error message if the user is not the owner
- messages.error(request, "You are not authorized to view feedback for this tournament.")
- return redirect("tournament-list") # Redirect to the tournament list or another appropriate page
-
- # Retrieve feedback for the tournament
- feedback_list = Feedback.objects.filter(tournament=tournament).order_by("-created_at")
- return render(
- request,
- "tournaments/tournament_feedback_list.html",
- {"tournament": tournament, "feedback_list": feedback_list},
- )
-
-
-@login_required
-def user_feedback_list(request):
- """
- View to display all feedback submitted by the logged-in user.
- """
- user_feedback = Feedback.objects.filter(user=request.user).order_by("-created_at")
-
- # Get the first tournament associated with the user's feedback (if any)
- tournament = user_feedback.first().tournament if user_feedback.exists() else None
-
- return render(
- request,
- "tournaments/tournament_user_feedback.html",
- {"user_feedback": user_feedback, "tournament": tournament},
- )
-
-
-@login_required
-def submit_feedback(request, tournament_id):
- tournament = get_object_or_404(Tournament, id=tournament_id)
-
- if request.method == "POST":
- comment = request.POST.get("content") # Match the field name in the model
- rating = request.POST.get("rating")
-
- if not comment or not rating:
- messages.error(request, "All fields are required.")
- return redirect("tournament-detail", pk=tournament.id)
-
- Feedback.objects.create(
- tournament=tournament,
- user=request.user,
- comment=comment, # Use 'comment' if that's the field name in the model
- rating=rating,
- )
- print("Submitting feedback by user:", request.user)
-
- messages.success(request, "Feedback submitted successfully!")
- return redirect("tournament-detail", pk=tournament.id)
-
- return render(request, "tournaments/tournament_submit_feedback.html", {"tournament": tournament})
-
-
-@login_required
-def update_feedback_view(request, feedback_id):
- """
- View to handle updating feedback.
- Only the original author or an admin can update feedback.
- """
- feedback = get_object_or_404(Feedback, id=feedback_id)
-
- # Check if the user is authorized to update the feedback
- if feedback.user != request.user and not request.user.is_staff:
- return HttpResponseForbidden("You are not authorized to update this feedback.")
-
- if request.method == "POST":
- new_comment = request.POST.get("content")
- new_rating = request.POST.get("rating")
-
- if not new_comment or not new_rating:
- messages.error(request, "All fields are required.")
- return redirect("update-feedback", feedback_id=feedback.id)
-
- # Update feedback fields
- feedback.comment = new_comment
- feedback.rating = new_rating
- feedback.save()
-
- messages.success(request, "Feedback updated successfully!")
- return redirect("user-feedback-list")
-
- return render(request, "tournaments/tournament_update_feedback.html", {"feedback": feedback})
-
-
-@login_required
-def delete_feedback_view(request, feedback_id):
- """
- View to handle deleting feedback.
- Only the original author or an admin can delete feedback.
- """
- feedback = get_object_or_404(Feedback, id=feedback_id)
-
- # Check if the user is authorized to delete the feedback
- if feedback.user != request.user and not request.user.is_staff:
- return HttpResponseForbidden("You are not authorized to delete this feedback.")
-
- if request.method == "POST":
- # Delete the feedback
- feedback.delete()
- messages.success(request, "Feedback deleted successfully!")
- return redirect("user-feedback-list")
-
- return render(request, "tournaments/tournament_delete_feedback.html", {"feedback": feedback})
-
-
# =============== Word Game Views ===============
@@ -1640,9 +1617,6 @@ def wordle_game_page(request):
return render(request, "games/wordle.html", {"iframe_url": iframe_url})
-# ============== Checkers ============
-
-
@login_required
def checkers_game_view(request, pk):
game = get_object_or_404(Checkers, id=pk)
@@ -1658,7 +1632,6 @@ def checkers_game_view(request, pk):
if latest_turn:
board = latest_turn.board
else:
- # First time loading, create default board
default_state = [
[0, 2, 0, 2, 0, 2, 0, 2],
[2, 0, 2, 0, 2, 0, 2, 0],
@@ -1669,7 +1642,6 @@ def checkers_game_view(request, pk):
[0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0],
]
- # Save first turn
board = CheckersBoard.objects.create(state=default_state)
CheckersTurn.objects.create(game=game, board=board, turn_number=1, player=game.player_1)
@@ -1715,15 +1687,3 @@ def checkers_game_get_board_state(request, board_id):
return Response({"state": board.state})
except CheckersBoard.DoesNotExist:
return Response({"error": "Board not found"}, status=404)
-
-
-class GameListDetailView(LoginRequiredMixin, DetailView):
- """Display the games in a specific GameList."""
-
- model = GameList
- template_name = "games/gamelist_detail.html"
- context_object_name = "gamelist"
-
- def get_queryset(self):
- # Ensure users can only view their own game lists
- return GameList.objects.filter(created_by=self.request.user)
diff --git a/src/chigame/knowledge_base/migrations/0004_alter_reviewfeedback_guide_id.py b/src/chigame/knowledge_base/migrations/0004_alter_reviewfeedback_guide_id.py
new file mode 100644
index 000000000..c6886f170
--- /dev/null
+++ b/src/chigame/knowledge_base/migrations/0004_alter_reviewfeedback_guide_id.py
@@ -0,0 +1,19 @@
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("knowledge_base", "0003_alter_guide_favorites_alter_guide_likes_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="reviewfeedback",
+ name="guide_id",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, related_name="feedbacks", to="knowledge_base.guide"
+ ),
+ ),
+ ]
diff --git a/src/chigame/knowledge_base/urls.py b/src/chigame/knowledge_base/urls.py
index a31b1ea50..e674daaad 100644
--- a/src/chigame/knowledge_base/urls.py
+++ b/src/chigame/knowledge_base/urls.py
@@ -10,10 +10,8 @@
ContributorMdUpload,
DefaultView,
DownloadGuide,
- FavUnfavGuide,
FeedbackDetail,
GuideDetail,
- LikeUnlikeGuide,
ModeratorGuidesPending,
ModeratorListByGame,
ModeratorSetPublishedGuide,
@@ -26,8 +24,6 @@
urlpatterns = [
path("", DefaultView.as_view(), name="knowledge-base"),
path("guides/", GuideDetail.as_view(), name="knowledge-base-guide-detail"),
- path("guides//set-liked", LikeUnlikeGuide, name="like-unlike-guide"),
- path("guides//set-favorited", FavUnfavGuide, name="fav-unfav-guide"),
path("moderation", ModeratorGuidesPending.as_view(), name="knowledge-base-moderator"),
path("moderation/review/", ReviewPendingGuideView.as_view(), name="moderator-review-guide"),
path("manage-my-guides", ContributorManageGuide.as_view(), name="contributor-manage-guide"),
diff --git a/src/chigame/knowledge_base/views.py b/src/chigame/knowledge_base/views.py
index da2741832..4ff793aa3 100644
--- a/src/chigame/knowledge_base/views.py
+++ b/src/chigame/knowledge_base/views.py
@@ -6,7 +6,7 @@
from django.core.exceptions import PermissionDenied
from django.db.models import CharField, F, Q, Value
from django.db.models.functions import Concat
-from django.http import HttpResponse, JsonResponse
+from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe
from django.views.decorators.http import require_POST
@@ -385,37 +385,3 @@ def ModeratorSetPublishedGuide(request, game_pk, guide_pk):
game.save()
return redirect("moderator-single-game", game_pk) # or wherever you want to redirect
-
-
-@require_POST
-def LikeUnlikeGuide(request, pk):
- guide = get_object_or_404(Guide, pk=pk)
- liked = guide.likes.filter(pk=request.user.pk).exists()
- # if originally like, then unlike it
- if liked:
- guide.likes.remove(request.user)
- # if originally unlike, then like it
- else:
- guide.likes.add(request.user)
- liked = not liked
- like_count = guide.likes.count()
-
- return JsonResponse({"liked": liked, "like_count": like_count})
-
-
-@require_POST
-@login_required
-def FavUnfavGuide(request, pk):
- guide = get_object_or_404(Guide, pk=pk)
- favorited = guide.favorites.filter(pk=request.user.pk).exists()
- # if originally favorite, then unfavorite it
- if favorited:
- guide.favorites.remove(request.user)
- # if originally unfavorite, then favorite it
- else:
- guide.favorites.add(request.user)
-
- favorited = not favorited
- fav_count = guide.favorites.count()
-
- return JsonResponse({"favorited": favorited, "fav_count": fav_count})
diff --git a/src/config/settings/base.py b/src/config/settings/base.py
index c480ef5dc..fb8b235e1 100644
--- a/src/config/settings/base.py
+++ b/src/config/settings/base.py
@@ -344,7 +344,6 @@
"PAGE_SIZE": 10,
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
- "rest_framework.authentication.SessionAuthentication",
],
}
diff --git a/src/sandbox/checkers-prototype/game.js b/src/sandbox/checkers-prototype/game.js
index 7b109f92b..e28cb742d 100644
--- a/src/sandbox/checkers-prototype/game.js
+++ b/src/sandbox/checkers-prototype/game.js
@@ -54,12 +54,14 @@ let vsEasyBot = true;
// ----------------------------------------------------------------------------
// ---INIT FUNCTIONS-----------------------------------------------------------
+
function preload() {
// load in the soundeffects
this.load.audio('slide', 'sfx/slide.mp3');
this.load.audio('hint', 'sfx/bling.mp3');
}
+
function create() {
// Store reference to the scene
const scene = this;
@@ -189,14 +191,14 @@ function create() {
resetDrawOffer();
}
});
- easyBot.textContent = `Easy Bot: ${vsEasyBot ? 'ON' : 'OFF'}`;
+ easyBot.textContent = `Easy Bot: ${vsEasyBot ? 'ON' : 'OFF'}`;
easyBot.addEventListener('click', () => {
vsEasyBot = !vsEasyBot;
easyBot.textContent = `Easy Bot: ${vsEasyBot ? 'ON' : 'OFF'}`;
});
}
-function update() { }
+function update() {}
// ----------------------------------------------------------------------------
// Draw the game board
@@ -370,20 +372,8 @@ function movePiece(piece, moveX, moveY) {
// Move the piece
piece.x = moveX;
piece.y = moveY;
- const newX = MARGIN + moveX * TILE_SIZE + TILE_SIZE / 2;
- const newY = MARGIN + moveY * TILE_SIZE + TILE_SIZE / 2;
-
- // Animate the piece movement
- checkers.scene.scenes[0].tweens.add({
- targets: piece.sprite,
- x: newX,
- y: newY,
- duration: 300, // having done some testing and playing, I think 300 ms is the best
- // https://docs.phaser.io/phaser/concepts/tweens
- // https://rexrainbow.github.io/phaser3-rex-notes/docs/site/ease-function/
- ease: 'Power3',
- });
-
+ piece.sprite.x = MARGIN + piece.x * TILE_SIZE + TILE_SIZE / 2;
+ piece.sprite.y = MARGIN + piece.y * TILE_SIZE + TILE_SIZE / 2;
// Play move sound effect
piece.sprite.scene.sound.play('slide');
@@ -504,7 +494,8 @@ function giveHint() {
// way of calling a specific square on the board. For now, I just have it return
// the row and col on the matrix.
alert(
- `Hint: Move ${currentPlayer === COLORS.red ? 'red' : 'black'} piece at (row ${randomHint.piece.y
+ `Hint: Move ${currentPlayer === COLORS.red ? 'red' : 'black'} piece at (row ${
+ randomHint.piece.y
}, column ${randomHint.piece.x}) to (row ${randomHint.y}, column ${randomHint.x})`
);
} else {
@@ -583,7 +574,8 @@ function giveHint() {
// way of calling a specific square on the board. For now, I just have it return
// the row and col on the matrix.
alert(
- `Hint: Move ${currentPlayer === COLORS.red ? 'red' : 'black'} piece at (row ${randomHint.piece.y
+ `Hint: Move ${currentPlayer === COLORS.red ? 'red' : 'black'} piece at (row ${
+ randomHint.piece.y
}, column ${randomHint.piece.x}) to (row ${randomHint.y}, column ${randomHint.x})`
);
} else {
@@ -607,7 +599,7 @@ function giveHint() {
declineDrawBtn.style.display = 'none';
}
//if black and bot is on, schedule bot move
- if (vsEasyBot && currentPlayer === COLORS.black) {
+ if (vsEasyBot && currentPlayer === COLORS.black){
//delay so user has time to process bot movw after their own
scene.time.delayedCall(300, easyBot, [scene], scene);
}
@@ -700,58 +692,58 @@ document.addEventListener('DOMContentLoaded', () => {
//return arr of legal moves for given player
function getLegalMoves(color) {
- //arr to store legal moves
- const moves = [];
- //moving up or down board?
- const direction = color === COLORS.red ? -1 : 1;
-
- //loop thru pieces
- pieces.forEach(piece => {
- if (piece.color !== color) return; //return for other p;layer peices
- // simple moves
- [-1, 1].forEach(diagonal => { //try L and R diagonals
- const col = piece.x + diagonal; //new col
- const row = piece.y + direction; //new row
- if ( //check if mvoe is valid
- col >= 0 && col < BOARD_SIZE && row >= 0 && row < BOARD_SIZE &&
- !getPiece(col, row) && isValidMove(piece, col, row)) {
- moves.push({ piece, x: col, y: row }); //add move to arr
- }
- });
- // jump moves for captures
- [-2, 2].forEach(jump => {
- const jump_col = piece.x + jump;
- const jump_row = piece.y + 2 * direction;
- if (
- jump_col >= 0 && jump_col < BOARD_SIZE && jump_row >= 0 && jump_row < BOARD_SIZE &&
- !getPiece(jump_col, jump_row) && isValidMove(piece, jump_col, jump_row)) {
- moves.push({ piece, x: jump_col, y: jump_row });
- }
+ //arr to store legal moves
+ const moves = [];
+ //moving up or down board?
+ const direction = color === COLORS.red ? -1 : 1;
+
+ //loop thru pieces
+ pieces.forEach(piece => {
+ if (piece.color !== color) return; //return for other p;layer peices
+ // simple moves
+ [-1, 1].forEach(diagonal => { //try L and R diagonals
+ const col = piece.x + diagonal; //new col
+ const row = piece.y + direction; //new row
+ if ( //check if mvoe is valid
+ col >= 0 && col < BOARD_SIZE && row >= 0 && row < BOARD_SIZE &&
+ !getPiece(col, row) && isValidMove(piece, col, row)) {
+ moves.push({ piece, x: col, y: row }); //add move to arr
+ }
+ });
+ // jump moves for captures
+ [-2, 2].forEach(jump => {
+ const jump_col = piece.x + jump;
+ const jump_row = piece.y + 2 * direction;
+ if (
+ jump_col >= 0 && jump_col < BOARD_SIZE && jump_row >= 0 && jump_row < BOARD_SIZE &&
+ !getPiece(jump_col, jump_row) && isValidMove(piece, jump_col, jump_row)) {
+ moves.push({ piece, x: jump_col, y: jump_row });
+ }
+ });
});
- });
- return moves;
-}
+ return moves;
+ }
-// Easy bot: pick a random legal move and play it
+ // Easy bot: pick a random legal move and play it
function easyBot(scene) {
- //get legal moves
- //check if game over
- //it not do a random legal move
- const legalMoves = getLegalMoves(COLORS.black);
- //if No legal moves
- if (legalMoves.length === 0) {
- console.log('Cant move');
- return;
+ //get legal moves
+ //check if game over
+ //it not do a random legal move
+ const legalMoves = getLegalMoves(COLORS.black);
+ //if No legal moves
+ if (legalMoves.length === 0) {
+ console.log('Cant move');
+ return;
+ }
+ //get random move
+ const move = Phaser.Utils.Array.GetRandom(legalMoves);
+ //execute move
+ movePiece(move.piece, move.x, move.y);
+ // end bot's turn
+ endTurn(scene);
}
- //get random move
- const move = Phaser.Utils.Array.GetRandom(legalMoves);
- //execute move
- movePiece(move.piece, move.x, move.y);
- // end bot's turn
- endTurn(scene);
-}
// Coordinates overlay button
document.addEventListener('DOMContentLoaded', () => {
diff --git a/src/sandbox/live-chat-demo/.gitignore b/src/sandbox/live-chat-demo/.gitignore
new file mode 100644
index 000000000..79bcb9f67
--- /dev/null
+++ b/src/sandbox/live-chat-demo/.gitignore
@@ -0,0 +1 @@
+env_site
diff --git a/src/sandbox/live-chat-demo/README.md b/src/sandbox/live-chat-demo/README.md
deleted file mode 100644
index 1036611de..000000000
--- a/src/sandbox/live-chat-demo/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Live Chat Demo
-
-This is a demo of a live chat Django application. This was the initial implementation of what is now the `/chat` app.
-
-If you'd like to better understand how the `/chat` app works, this is a good place to start. It covers the basics of `consumers.py` as well as channel configuration without all of the additional complexity of the `/chat` app.
-
-## Libraries
-
-`channels` was used to handle the WebSocket connections.
-
-`daphne` was used to run the development server.
-
-## Views
-
-`index` - The main page for the chat.
-
-## How to Run/Demo
-
-1. `python manage.py runserver` - Run the development server
-
-2. Open `http://localhost:8000/` - Open the main page
-
-3. Open multiple tabs to test the chat
-
-4. Send a message to the chat from one tab and see it in the other tabs (or vice versa)
diff --git a/src/sandbox/live-chat-demo/chatdemo/chat/consumers.py b/src/sandbox/live-chat-demo/chatdemo/chat/consumers.py
index e2a35a66b..5eb239a69 100644
--- a/src/sandbox/live-chat-demo/chatdemo/chat/consumers.py
+++ b/src/sandbox/live-chat-demo/chatdemo/chat/consumers.py
@@ -4,40 +4,18 @@
class ChatConsumer(AsyncWebsocketConsumer):
- """
- This is a simple chat consumer that sends and receives messages to and from the group.
- """
-
async def connect(self):
- """
- This is called when the client connects to the WebSocket.
- """
self.roomGroupName = "group_chat_gfg"
await self.channel_layer.group_add(self.roomGroupName, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
- """
- Disconnects the client from the group.
-
- Args:
- close_code (int): The close code of the connection.
- """
await self.channel_layer.group_discard(self.roomGroupName, self.channel_name)
async def receive(self, text_data):
- """
- Receives a message from the client and sends it to the group.
-
- Args:
- text_data (str): The message from the client.
- """
text_data_json = json.loads(text_data)
message = text_data_json["message"]
username = text_data_json["username"]
-
- # this is the file we modify in the chat app to add things such as profanity filters, etc.
-
await self.channel_layer.group_send(
self.roomGroupName,
{
@@ -48,13 +26,6 @@ async def receive(self, text_data):
)
async def sendMessage(self, event):
- """
- Sends a message to the client.
-
- Args:
- event (dict): The event from the group.
- """
message = event["message"]
username = event["username"]
-
await self.send(text_data=json.dumps({"message": message, "username": username}))
diff --git a/src/sandbox/live-chat-demo/chatdemo/chat/models.py b/src/sandbox/live-chat-demo/chatdemo/chat/models.py
index 8bc9d7e99..6b2021999 100644
--- a/src/sandbox/live-chat-demo/chatdemo/chat/models.py
+++ b/src/sandbox/live-chat-demo/chatdemo/chat/models.py
@@ -1,7 +1 @@
-"""
-No models are needed for the demo app as all of the messages are not stored but rather sent to the client.
-
-This is done through Django channels and the `group_send` method.
-
-Check out the `consumers.py` file to see how this is done.
-"""
+# Create your models here.
diff --git a/src/sandbox/live-chat-demo/chatdemo/chat/views.py b/src/sandbox/live-chat-demo/chatdemo/chat/views.py
index 8926c027b..d9bbc69af 100644
--- a/src/sandbox/live-chat-demo/chatdemo/chat/views.py
+++ b/src/sandbox/live-chat-demo/chatdemo/chat/views.py
@@ -1,17 +1,8 @@
from django.shortcuts import redirect, render
-def chatPage(request):
- """
- This is the main page for the chat. This is what you would edit if you would like to add a login system, etc.
-
- Returns:
- HttpResponse: The response object.
- """
+def chatPage(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("login-user")
-
- # if you wanted to implement chats loaded from db (stored) you would implement some sort of arg here
- # you would also add, like in the chat app, messages fetched from that chat
-
- return render(request, "chat/chatPage.html", {})
+ context = {}
+ return render(request, "chat/chatPage.html", context)
diff --git a/src/static/checkers-prototype/game.js b/src/static/checkers-prototype/game.js
index 2403c7607..4421fb33c 100644
--- a/src/static/checkers-prototype/game.js
+++ b/src/static/checkers-prototype/game.js
@@ -55,7 +55,7 @@ async function fetchInitialBoardState() {
}
}
-// ---INIT FUNCTIONS------------------------------------------------------------
+// ---INIT FUNCTIONS-----------------------------------------------------------
function preload() { }
async function create() {
diff --git a/src/static/css/achievements.css b/src/static/css/achievements.css
deleted file mode 100644
index 26d4032a0..000000000
--- a/src/static/css/achievements.css
+++ /dev/null
@@ -1,389 +0,0 @@
-:root {
- --bg-light: #f8f9fa;
- --text-dark: #212529;
- --card-bg: #ffffff;
- --header-bg: #f8f9fa;
- --accent: #0d6efd;
-}
-
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- margin: 0;
- padding: 0;
- background-color: var(--bg-light);
- color: var(--text-dark);
- }
-
- .container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
- }
-
- .user-info {
- display: flex;
- align-items: center;
- margin-bottom: 30px;
- }
-
- .user-avatar {
- width: 75px;
- height: 75px;
- border-radius: 50%;
- background-color: #0d6efd;
- margin-right: 20px;
- overflow: hidden;
- align-items: center;
- justify-content: center;
- display: flex;
- color: white;
- }
-
- .large-avatar {
- font-size: 40px;
- }
-
- .user-stats {
- margin-top: -10px;
- background-color: var(--card-bg);
- padding: 10px;
- border-radius: 5px;
- margin-bottom: 20px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- }
-
- .search-box {
- padding: 10px;
- margin-bottom: 20px;
- width: 100%;
- border: 1px solid #ced4da;
- border-radius: 0.25rem;
- font-size: 16px;
- }
-
- .games-list {
- display: flex;
- flex-wrap: wrap;
- gap: 20px;
- margin-bottom: 30px;
- }
-
- .game-card {
- background-color: var(--card-bg);
- border-radius: 0.25rem;
- padding: 15px;
- width: calc(33.33% - 20px);
- min-width: 300px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- border: 1px solid rgba(0, 0, 0, 0.125);
- }
-
- .game-header {
- display: flex;
- align-items: center;
- margin-bottom: 15px;
- }
-
- .game-icon {
- width: 50px;
- height: 50px;
- margin-right: 15px;
- border-radius: 5px;
- }
-
- .game-title {
- margin: 0;
- font-size: 1.4em;
- }
-
- .progress-bar {
- height: 10px;
- background-color: #333;
- border-radius: 5px;
- margin: 10px 0;
- overflow: hidden;
- }
-
- .progress-fill {
- height: 100%;
- background-color: var(--accent);
- }
-
- .achievement-progress-fill {
- height: 100%;
- background-color: #0d6efd;
- border-radius: 5px;
- }
-
- .achievements-list {
- margin-top: 20px;
- }
-
- .achievement {
- display: flex;
- background-color: rgba(0, 0, 0, 0.03);
- margin-bottom: 10px;
- padding: 10px;
- border-radius: 0.25rem;
- position: relative;
- border: 1px solid rgba(0, 0, 0, 0.125);
- }
-
- .pinned {
- order: -1;
- border-left: 3px solid var(--accent);
- }
-
- .achievement-icon {
- width: 50px;
- height: 50px;
- margin-right: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .achievement-info {
- max-width: calc(100% - 40px);
- overflow: hidden;
- }
-
- .achievement-info h4 {
- margin: 0 0 5px 0;
- font-size: 1.1em;
- max-width: 100px;
- overflow-wrap: break-word;
- }
-
- .achievement-info p {
- margin: 0;
- font-size: 0.9em;
- }
-
- .achievement-date {
- font-size: 0.8em;
- color: #aaa;
- }
-
- .achievement-percent {
- position: absolute;
- right: 10px;
- top: 10px;
- font-size: 0.8em;
- background-color: rgba(0, 0, 0, 0.05);
- padding: 3px 8px;
- border-radius: 10px;
- border: 1px solid rgba(0, 0, 0, 0.125);
- }
-
- .achievement.not-unlocked .achievement-info h4,
- .achievement.not-unlocked .achievement-info p {
- color: #6c757d;
- }
-
- .achievement.hidden .achievement-info p.achievement-desc {
- color: #6c757d;
- font-style: italic;
- max-width: 90%;
- overflow-wrap: break-word;
- }
-
- .not-unlocked .trophy-icon img {
- filter: brightness(0.3);
- /* 60% brightness (70% darker) */
- opacity: 0.6;
- }
-
- .toggle-button {
- background-color: var(--accent);
- color: white;
- border: none;
- padding: 8px 15px;
- border-radius: 0.25rem;
- cursor: pointer;
- margin-left: 10px;
- }
-
- .toggle-button:hover {
- background-color: #0a58ca;
- }
-
- .nav-tabs {
- display: flex;
- list-style: none;
- padding: 0;
- margin: 0 0 20px 0;
- border-bottom: 1px solid #dee2e6;
- }
-
- .nav-tabs li {
- padding: 10px 20px;
- cursor: pointer;
- }
-
- .nav-tabs li.active {
- background-color: var(--accent);
- color: white;
- border-radius: 0.25rem 0.25rem 0 0;
- }
-
- .tab-content>div {
- display: none;
- }
-
- .tab-content>div.active {
- display: block;
- }
-
- /* Trophy icons based on rarity */
- .trophy-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- }
-
- .trophy-icon img {
- width: 32px;
- height: 32px;
- object-fit: contain;
- }
-
- .achievement.hidden .trophy-icon img {
- filter: brightness(0);
- opacity: 0.5;
- }
-
- .achievement {
- display: flex;
- background-color: rgba(0, 0, 0, 0.03);
- margin-bottom: 15px;
- padding: 10px;
- border-radius: 0.25rem;
- position: relative;
- border: 1px solid rgba(0, 0, 0, 0.125);
- align-items: flex-start;
- transition: all 0.3s ease;
- min-height: 80px;
- }
-
- .achievement.expanded {
- padding-bottom: 15px;
- }
-
- .achievement-icon {
- width: 50px;
- height: 50px;
- margin-right: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
-
- /* Texts for expand and collapse */
- .achievement-desc {
- margin: 0;
- font-size: 0.9em;
- overflow: hidden;
- max-width: 90%;
- box-orient: vertical;
- max-height: 3em;
- overflow-wrap: break-word;
- }
-
- .achievement.expanded .achievement-desc {
- max-height: none;
- white-space: normal;
- cursor: pointer;
- word-wrap: break-word;
- overflow-wrap: break-word;
- }
-
- .pin-button {
- background-color: transparent;
- border: none;
- color: #888;
- cursor: pointer;
- position: absolute;
- /* Changed from margin-left */
- right: 6px;
- top: 5px;
- width: 30px;
- height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .pin-button.active {
- transform: rotate(-45deg);
- color: #0d6efd;
- background-color: rgba(13, 110, 253, 0.1);
- border-radius: 50%;
- }
-
- /* Reposition the percentage badge */
- .achievement-percent {
- position: absolute;
- right: 45px;
- top: 10px;
- font-size: 0.8em;
- background-color: rgba(0, 0, 0, 0.05);
- padding: 3px 8px;
- border-radius: 10px;
- border: 1px solid rgba(0, 0, 0, 0.125);
- white-space: nowrap;
- }
-
- /* Responsive styles */
- @media (max-width: 768px) {
- .game-card {
- width: 100%;
- }
- }
-
- .progress-fill-dynamic {
- height: 100%;
- background-color: var(--accent);
- }
-
- .filter-bar {
- display: flex;
- align-items: center;
- margin-bottom: 20px;
- gap: 15px;
- }
-
- .search-input {
- flex-grow: 1;
- margin-bottom: 0;
- height: 38px;
- }
-
- .status-filter-container {
- display: flex;
- align-items: center;
- }
-
- .status-filter-label {
- margin-right: 5px;
- }
-
- .status-filter-select {
- width: auto;
- display: inline-block;
- height: 38px;
- }
-
- .achievement-progress-bar {
- position: absolute;
- bottom: 5px;
- left: 70px;
- right: 10px;
- height: 5px;
- background-color: #e9ecef;
- border-radius: 5px;
- overflow: hidden;
- }
diff --git a/src/static/css/knowledge-base-styles.css b/src/static/css/knowledge-base-styles.css
index f4fc1010b..3a68c2f01 100644
--- a/src/static/css/knowledge-base-styles.css
+++ b/src/static/css/knowledge-base-styles.css
@@ -20,65 +20,54 @@
}
/* Other Knowledge Base Styles */
-
-
.knowledge-base.container {
max-width: 1000px;
}
-/*.knowledge-base .nav a {
+.knowledge-base .nav a {
color: gray;
text-decoration: none;
}
.knowledge-base .nav {
margin-bottom: 20px;
-}*/
+}
.knowledge-base .page-content {
padding: 10px;
}
-.landing-page-feedback-alert-header {
- margin-bottom: 0;
-}
-
-.status-updates-num{
- font-weight: bold;
-}
-
-.view-details-link{
- float:right;
- padding-right:1%;
-}
-
-.landing-page-accepted{
- color:green;
- font-weight: bold;
+/* In Contributor Manage Guide Page */
+.contributor-title {
+ padding-bottom: 10px;
}
-.landing-page-rejected{
- color:red;
- font-weight: bold;
-}
+/* a {
+ color: #7F1616;
+} */
-.landing-page-changes-requested{
- color:#FFA500;
- font-weight: bold;
-}
-/* In Contributor Manage Guide Page */
-.contributor-header-box{
- display: flex;
- justify-content: space-between;
+#contributor-manage-guide {
+ width: 95%;
+ height: fit-content;
+ margin: 20px 0;
+ border-collapse: collapse;
+ border: 1px solid black;
}
-.create-guide-btn {
- margin-bottom:1rem !important;
+#contributor-manage-guide th{
+ background: #BFBFBF;
+ text-align: left;
+ font-size: large;
+ padding:15px
}
-.status-pending{
- margin-left:0.75rem;
+#contributor-manage-guide td{
+ border-top: 1px solid black;
+ border-bottom: 1px solid black;
+ padding: 10px;
+ text-align: left;
+ font-size: medium;
}
@@ -163,6 +152,8 @@ a.upload-button:active {
color: #7F1616;
}
+
+
/* This is added for backend viewer landing page. Pls replace this when styling
in front end. */
.grid-container {
diff --git a/src/static/css/knowledge-base/single-guide.css b/src/static/css/knowledge-base/single-guide.css
index e31e5c84e..cb29ed3b8 100644
--- a/src/static/css/knowledge-base/single-guide.css
+++ b/src/static/css/knowledge-base/single-guide.css
@@ -6,41 +6,3 @@
.guide-detail-header .visit-game-link {
color: #0d6efd;
}
-
-
-.likefav-button {
- background: none;
- border: none;
- font-size: 16pt;
- cursor: pointer;
- padding: 0;
-}
-
-
-.likefav-text {
- font-size: 12pt;
-}
-
-
-.heart-hollow {
- color: gray;
- transition: color 0.5s ease;
-}
-
-.heart-liked {
- color: red;
- transition: color 0.5s ease;
-
-}
-
-
-.star-hollow {
- color: gray;
- transition: color 0.5s ease;
-}
-
-.star-faved {
- color: #D4AF37;
- transition: color 0.5s ease;
-
-}
diff --git a/src/static/js/achievements.js b/src/static/js/achievements.js
deleted file mode 100644
index ffbba2ed9..000000000
--- a/src/static/js/achievements.js
+++ /dev/null
@@ -1,286 +0,0 @@
-document.addEventListener('DOMContentLoaded', function () {
- // Remove any existing click handlers by cloning and replacing tabs
- const tabsContainer = document.querySelector('.nav-tabs');
- if (tabsContainer) {
- const newTabsContainer = tabsContainer.cloneNode(true);
- tabsContainer.parentNode.replaceChild(newTabsContainer, tabsContainer);
- }
-
- // Initialize progress bars
- document.querySelectorAll('[data-progress]').forEach(function (element) {
- const progressValue = element.getAttribute('data-progress');
- if (progressValue) {
- element.style.width = progressValue + '%';
- }
- });
-
- // Direct and simple tab switching implementation
- document.querySelectorAll('.nav-tabs li').forEach(function (tab) {
- tab.addEventListener('click', function (event) {
- event.preventDefault();
- event.stopPropagation();
-
- // Get tab ID
- const tabId = this.getAttribute('data-tab');
-
- // Reset all tabs
- document.querySelectorAll('.nav-tabs li').forEach(function (t) {
- t.classList.remove('active');
- });
-
- // Activate this tab
- this.classList.add('active');
-
- // Hide all content
- document.querySelectorAll('.tab-content > div').forEach(function (content) {
- content.classList.remove('active');
- content.style.display = 'none';
- });
-
- // Show this content - using both class and style for redundancy
- const content = document.getElementById(tabId);
- content.classList.add('active');
- content.style.display = 'block';
- return false; // Ensure no other handlers run
- });
- });
-
- // Initialize progress bars
- const progressElements = document.querySelectorAll('.progress-fill, .progress-fill-dynamic');
- progressElements.forEach(function (element) {
- const progressValue = element.getAttribute('data-progress');
- if (progressValue) {
- element.style.width = progressValue + '%';
- }
- });
-
- // Pin button functionality
- const pinButtons = document.querySelectorAll('.pin-button');
-
- pinButtons.forEach(function (button) {
- button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- const achievementId = this.getAttribute('data-achievement-id');
- const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
-
- let url = `/achievements/toggle-pin/${achievementId}/`;
-
- fetch(url, {
- method: 'POST',
- headers: {
- 'X-CSRFToken': csrftoken,
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- credentials: 'same-origin',
- })
- .then((response) => {
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- return response.json();
- })
- .then((data) => {
- if (data.status === 'success') {
- // Update the all achievements in both tabs with the same id
- const allGamesAchievements = document.querySelectorAll(
- `#all-games .achievement[data-achievement-id="${achievementId}"]`
- );
- allGamesAchievements.forEach(function (achievementElem) {
- const pinBtn = achievementElem.querySelector('.pin-button');
- if (data.pinned) {
- achievementElem.classList.add('pinned');
- if (pinBtn) pinBtn.classList.add('active');
- } else {
- achievementElem.classList.remove('pinned');
- if (pinBtn) pinBtn.classList.remove('active');
- }
- });
-
- // Unpinning
- if (!data.pinned) {
- // Remove the achievement from the pinned achievements tab
- const pinnedAchievements = document.querySelectorAll(
- `#pinned-achievements .achievement[data-achievement-id="${achievementId}"]`
- );
- pinnedAchievements.forEach(function (achievementElem) {
- achievementElem.style.animation = 'fadeOut 0.3s';
- setTimeout(() => {
- achievementElem.remove();
-
- // check if there are any pinned achievements left
- const remainingPinnedAchievements = document.querySelectorAll(
- '#pinned-achievements .achievement'
- );
- if (remainingPinnedAchievements.length === 0) {
- const emptyMessage = document.createElement('div');
- emptyMessage.className = 'empty-pinned-message';
- emptyMessage.innerHTML =
- 'No achievements pinned yet. Pin your favorite achievements to see them here!
';
- const achievementsList = document.querySelector(
- '#pinned-achievements .achievements-list'
- );
- if (
- achievementsList &&
- !achievementsList.querySelector('.empty-pinned-message')
- ) {
- achievementsList.appendChild(emptyMessage);
- }
- }
- }, 300);
- });
- }
- // Pinning
- else {
- // Check if the achievement is already in the pinned achievements tab
- const existingInPinned = document.querySelector(
- `#pinned-achievements .achievement[data-achievement-id="${achievementId}"]`
- );
-
- if (!existingInPinned) {
- // Add the achievement to the pinned achievements tab
- const originalAchievement = document.querySelector(
- `#all-games .achievement[data-achievement-id="${achievementId}"]`
- );
-
- if (originalAchievement) {
- const nameElem = originalAchievement.querySelector('.achievement-info h4');
- const descElem = originalAchievement.querySelector(
- '.achievement-info .achievement-desc'
- );
- const dateElem = originalAchievement.querySelector(
- '.achievement-info .achievement-date'
- );
- const iconImg = originalAchievement.querySelector('.trophy-icon img');
-
- let rarityClass = '';
- if (originalAchievement.classList.contains('common')) rarityClass = 'common';
- else if (originalAchievement.classList.contains('uncommon'))
- rarityClass = 'uncommon';
- else if (originalAchievement.classList.contains('rare')) rarityClass = 'rare';
- else if (originalAchievement.classList.contains('precious'))
- rarityClass = 'precious';
-
- const gameCard = originalAchievement.closest('.game-card');
- const gameName = gameCard
- ? gameCard.querySelector('.game-title').textContent
- : '';
-
- const rarityMatch = iconImg.src.match(/achievement_icons\/(\d)\.png/);
- const rarityNumber = rarityMatch ? rarityMatch[1] : '1';
-
- let unlockedDate = '';
- if (dateElem) {
- const dateMatch = dateElem.textContent.match(/Unlocked on (.+)/);
- unlockedDate = dateMatch ? dateMatch[1] : 'Not unlocked';
- }
- const newPinnedAchievement = `
-
-
-
-
-
-
-
-
${nameElem ? nameElem.textContent : 'Achievement'}
-
- ${descElem ? descElem.textContent : ''} in ${gameName}
-
-
- ${
- unlockedDate && unlockedDate !== 'Not yet unlocked'
- ? 'Unlocked on ' + unlockedDate : 'Not yet unlocked'
- }
-
-
-
- 📌
-
-
- `;
- const emptyMessage = document.querySelector(
- '#pinned-achievements .empty-pinned-message'
- );
- if (emptyMessage) {
- emptyMessage.remove();
- }
- const achievementsList = document.querySelector(
- '#pinned-achievements .achievements-list'
- );
- if (achievementsList) {
- achievementsList.insertAdjacentHTML('beforeend', newPinnedAchievement);
-
- // Add event listener to the new pin button
- const newPinButton = achievementsList.querySelector(
- `.achievement[data-achievement-id="${achievementId}"] .pin-button`
- );
- if (newPinButton) {
- newPinButton.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- const achievementId = this.getAttribute('data-achievement-id');
- const csrftoken = document.querySelector(
- '[name=csrfmiddlewaretoken]'
- ).value;
-
- let url = `/achievements/toggle-pin/${achievementId}/`;
-
- fetch(url, {
- method: 'POST',
- headers: {
- 'X-CSRFToken': csrftoken,
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- credentials: 'same-origin',
- })
- .then((response) => response.json())
- .then((data) => {
- if (data.status === 'success' && !data.pinned) {
- document
- .querySelectorAll(
- `#all-games .achievement[data-achievement-id="${achievementId}"] .pin-button`
- )
- .forEach(function (btn) {
- btn.classList.remove('active');
- btn.closest('.achievement').classList.remove('pinned');
- });
-
- // Remove the achievement from the pinned achievements tab
- const achievementItem = this.closest('.achievement');
- if (achievementItem) {
- achievementItem.style.animation = 'fadeOut 0.3s';
- setTimeout(() => {
- achievementItem.remove();
-
- // check if there are any pinned achievements left
- const remainingPinnedAchievements = document.querySelectorAll(
- '#pinned-achievements .achievement'
- );
- if (remainingPinnedAchievements.length === 0) {
- const emptyMessage = document.createElement('div');
- emptyMessage.className = 'empty-pinned-message';
- emptyMessage.innerHTML =
- 'No achievements pinned yet. Pin your favorite achievements to see them here!
';
- document
- .querySelector('#pinned-achievements .achievements-list')
- .appendChild(emptyMessage);
- }
- }, 300);
- }
- }
- })
- .catch((error) => {});
- });
- }
- }
- }
- }
- }
- }
- })
- .catch((error) => {});
- });
- });
-});
diff --git a/src/static/js/knowledge-base/like-favorite.js b/src/static/js/knowledge-base/like-favorite.js
deleted file mode 100644
index f57695167..000000000
--- a/src/static/js/knowledge-base/like-favorite.js
+++ /dev/null
@@ -1,80 +0,0 @@
-
-document.addEventListener("DOMContentLoaded", function () {
- const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
-
- //like
- const likeSpan = document.getElementById("like-span");
- const likeIcon = document.getElementById("like-icon");
- const likeurl = likeSpan.getAttribute('data-like-url');
- const likeCount = document.getElementById('like-count');
- const likeText = document.getElementById('like-text');
-
- likeSpan.addEventListener("click", function () {
- fetch(likeurl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- 'X-CSRFToken': csrftoken
- },
- mode: 'same-origin', // learn this code from django doc
- body: JSON.stringify({})
- })
- .then(response => {
- if (!response.ok) {
- throw new Error("Network response was not ok"); // double check this
- }
- return response.json();
- })
- .then(data => {
- if (data.liked) {
- likeIcon.classList.remove("heart-hollow");
- likeIcon.classList.add("heart-liked");
- likeText.textContent = `Unlike`;
- } else {
- likeIcon.classList.remove("heart-liked");
- likeIcon.classList.add("heart-hollow");
- likeText.textContent = `Like`;
- }
- likeCount.textContent = `${data.like_count} ♥`;
- })
- });
-
-
- // favorite
- const favSpan = document.getElementById("fav-span");
- const favIcon = document.getElementById("fav-icon");
- const favurl = favSpan.getAttribute('data-fav-url');
- const favCount = document.getElementById('fav-count');
- const favText = document.getElementById('fav-text');
-
- favSpan.addEventListener("click", function () {
- fetch(favurl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- 'X-CSRFToken': csrftoken
- },
- mode: 'same-origin', // learn this code from django doc
- body: JSON.stringify({})
- })
- .then(response => {
- if (!response.ok) {
- throw new Error("Network response was not ok"); // double check this
- }
- return response.json();
- })
- .then(data => {
- if (data.favorited) {
- favIcon.classList.remove("star-hollow");
- favIcon.classList.add("star-faved");
- favText.textContent = `Unfavorite`;
- } else {
- favIcon.classList.remove("star-faved");
- favIcon.classList.add("star-hollow");
- favText.textContent = `Favorite`;
- }
- favCount.textContent = `${data.fav_count} ★`;
- })
- });
-
-});
diff --git a/src/templates/achievements/demo_game.html b/src/templates/achievements/demo_game.html
index 93e8cc763..a3154c7ef 100644
--- a/src/templates/achievements/demo_game.html
+++ b/src/templates/achievements/demo_game.html
@@ -2,111 +2,13 @@
{% block css %}
{{ block.super }}
-
{% endblock css %}
-{% load static %}
-
{% block content %}
-
-
+ Click here
+ Click here
+ Click here
+ Click here
+ Click here
{% endblock content %}
diff --git a/src/templates/achievements/user_achievements.html b/src/templates/achievements/user_achievements.html
index 50df0e14d..b2f66f220 100644
--- a/src/templates/achievements/user_achievements.html
+++ b/src/templates/achievements/user_achievements.html
@@ -3,7 +3,7 @@
{% load static %}
{% block title %}
- ChiGame - {{ user_profile.name }}'s Achievements
+ ChiGame - {{ user_profile.username }}'s Achievements
{% endblock title %}
{% block css %}
{{ block.super }}
@@ -14,7 +14,427 @@
referrerpolicy="no-referrer" />
-
+
{% endblock css %}
{% block content %}
{% csrf_token %}
@@ -23,11 +443,11 @@
{% if user_profile.avatar %}
+ alt="{{ user_profile.username }}'s avatar" />
{% else %}
- {% if user_profile.name %}
- {{ user_profile.name|first|upper }}
+ {% if user_profile.username %}
+ {{ user_profile.username|first|upper }}
{% elif user_profile.email %}
{{ user_profile.email|first|upper }}
{% else %}
@@ -37,7 +457,7 @@
{% endif %}
-
{{ user_profile.name }}'s Achievements
+
{{ user_profile.username }}'s Achievements
Member since {{ join_date }}
{% if not viewing_own_profile %}
@@ -169,5 +589,60 @@ {{ user_achievement.achievement.name }}
{% endblock content %}
{% block inline_javascript %}
{{ block.super }}
-
+
{% endblock inline_javascript %}
diff --git a/src/templates/chat/components/_context_menu.html b/src/templates/chat/components/_context_menu.html
index 11f19cbc9..6a7eace49 100644
--- a/src/templates/chat/components/_context_menu.html
+++ b/src/templates/chat/components/_context_menu.html
@@ -1,15 +1,12 @@
+
-{% csrf_token %}
diff --git a/src/templates/chat/components/_input.html b/src/templates/chat/components/_input.html
index 9a69e6060..43fbcdac1 100644
--- a/src/templates/chat/components/_input.html
+++ b/src/templates/chat/components/_input.html
@@ -1,9 +1,3 @@
-
-
- Replying to : “ ”
- (cancel)
-
-
- {% if message.edited %}(edited)
{% endif %}