Skip to content

Commit

Permalink
Merge branch 'dev' into api/post-patch-user
Browse files Browse the repository at this point in the history
  • Loading branch information
cuyakonwu authored Dec 7, 2023
2 parents 507dbff + 7303ed3 commit 4b370b4
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 132 deletions.
14 changes: 13 additions & 1 deletion src/chigame/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import serializers

from chigame.games.models import Chat, Game, Lobby, Message, Tournament, User
from chigame.games.models import Category, Chat, Game, Lobby, Mechanic, Message, Tournament, User
from chigame.users.models import Group


Expand Down Expand Up @@ -45,6 +45,18 @@ def create(self, validated_data):
return user


class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"


class MechanicSerializer(serializers.ModelSerializer):
class Meta:
model = Mechanic
fields = "__all__"


class MessageSerializer(serializers.ModelSerializer):
sender = serializers.EmailField(write_only=True)

Expand Down
55 changes: 36 additions & 19 deletions src/chigame/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
from django.urls import path
from django.urls import include, path

from . import views

game_patterns = [
path("", views.GameListView.as_view(), name="api-game-list"),
path("<int:pk>/", views.GameDetailView.as_view(), name="api-game-detail"),
path("<int:pk>/categories/", views.GameCategoriesAPIView.as_view(), name="api-game-categories"),
path("<int:pk>/mechanics/", views.GameMechanicsAPIView.as_view(), name="api-game-mechanics"),
]

lobby_patterns = [
path("", views.LobbyListView.as_view(), name="api-lobby-list"),
path("<int:pk>/", views.LobbyDetailView.as_view(), name="api-lobby-detail"),
]

user_patterns = [
path("", views.UserListView.as_view(), name="api-user-list"),
path("<slug:slug>/", views.UserDetailView.as_view(), name="api-user-detail"),
path("<slug:slug>/groups/", views.UserGroupsView.as_view(), name="api-user-groups"),
path("<int:pk>/friends/", views.UserFriendsAPIView.as_view(), name="api-user-friends"),
]

tournament_patterns = [
path("chat/", views.MessageView.as_view(), name="api-chat-list"),
path("chat/feed/", views.MessageFeedView.as_view(), name="api-chat-detail"),
]

group_patterns = [
path("", views.GroupListView.as_view(), name="api-group-list"),
path("<int:pk>/", views.GroupDetailView.as_view(), name="api-group-detail"),
path("<int:pk>/members/", views.GroupMembersView.as_view(), name="api-group-members"),
]

urlpatterns = [
# GAME API URLS
path("games/", views.GameListView.as_view(), name="api-game-list"),
path("games/<int:pk>/", views.GameDetailView.as_view(), name="api-game-detail"),
# LOBBY API URLS
path("lobbies/", views.LobbyListView.as_view(), name="api-lobby-list"),
path("lobbies/<int:pk>/", views.LobbyDetailView.as_view(), name="api-lobby-detail"),
# USER API URLS
path("users/", views.UserListView.as_view(), name="api-user-list"),
path("users/<slug:slug>/", views.UserDetailView.as_view(), name="api-user-detail"),
path("users/<int:pk>/friends/", views.UserFriendsAPIView.as_view(), name="api-user-friends"),
path("users/<slug:slug>/groups/", views.UserGroupsView.as_view(), name="api-user-groups"),
# CHAT API URLS
path("tournaments/chat/", views.MessageView.as_view(), name="api-chat-list"),
path("tournaments/chat/feed/", views.MessageFeedView.as_view(), name="api-chat-detail"),
# GROUP API URLS
path("groups/", views.GroupListView.as_view(), name="api-group-list"),
path("groups/<int:pk>/", views.GroupDetailView.as_view(), name="api-group-detail"),
path("groups/<int:pk>/members/", views.GroupMembersView.as_view(), name="api-group-members"),
path("games/", include(game_patterns)),
path("lobbies/", include(lobby_patterns)),
path("users/", include(user_patterns)),
path("tournaments/", include(tournament_patterns)),
path("groups/", include(group_patterns)),
]
22 changes: 22 additions & 0 deletions src/chigame/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

from chigame.api.filters import GameFilter
from chigame.api.serializers import (
CategorySerializer,
GameSerializer,
GroupSerializer,
LobbySerializer,
MechanicSerializer,
MessageFeedSerializer,
MessageSerializer,
UserSerializer,
Expand Down Expand Up @@ -42,6 +44,26 @@ class GameDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = GameSerializer


class GameCategoriesAPIView(generics.ListAPIView):
serializer_class = CategorySerializer
pagination_class = PageNumberPagination

def get_queryset(self):
game_id = self.kwargs["pk"]
game = Game.objects.get(id=game_id)
return game.categories.all()


class GameMechanicsAPIView(generics.ListAPIView):
serializer_class = MechanicSerializer
pagination_class = PageNumberPagination

def get_queryset(self):
game_id = self.kwargs["pk"]
game = Game.objects.get(id=game_id)
return game.mechanics.all()


class UserFriendsAPIView(generics.RetrieveAPIView):
serializer_class = UserSerializer

Expand Down
25 changes: 25 additions & 0 deletions src/chigame/games/migrations/0022_tournament_created_by.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.4 on 2023-12-06 01:16

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("games", "0021_alter_tournament_matches"),
]

operations = [
migrations.AddField(
model_name="tournament",
name="created_by",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_tournaments",
to=settings.AUTH_USER_MODEL,
),
),
]
1 change: 1 addition & 0 deletions src/chigame/games/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ class Tournament(models.Model):
matches = models.ManyToManyField(Match, related_name="tournament", blank=True)
winners = models.ManyToManyField(User, related_name="won_tournaments", blank=True) # allow multiple winners
players = models.ManyToManyField(User, related_name="joined_tournaments", blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="created_tournaments")

@property
def status(self):
Expand Down
149 changes: 104 additions & 45 deletions src/chigame/games/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from django.views.generic.edit import FormMixin

from chigame.users.models import User

from .filters import LobbyFilter
from .forms import GameForm, LobbyForm, ReviewForm
from .models import Chat, Game, Lobby, Match, Player, Review, Tournament
Expand Down Expand Up @@ -397,14 +400,16 @@ def wrapper(request, *args, **kwargs):

class TournamentListView(ListView):
model = Tournament
queryset = Tournament.objects.prefetch_related("matches").all()
template_name = "tournaments/tournament_list.html"
context_object_name = "tournament_list"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Additional context can be added if needed
return context
def get_queryset(self):
# If the user is staff, show all tournaments
if self.request.user.is_staff:
return Tournament.objects.prefetch_related("matches").all()

# For non-staff users, show only tournaments they are part of
return Tournament.objects.prefetch_related("matches").filter(players=self.request.user)

def get(self, request, *args, **kwargs):
super().get(request, *args, **kwargs)
Expand Down Expand Up @@ -480,6 +485,12 @@ class TournamentDetailView(DetailView):
def get(self, request, *args, **kwargs):
super().get(request, *args, **kwargs)
tournament = Tournament.objects.get(id=self.kwargs["pk"])
self.object = self.get_object()

if not request.user.is_staff and request.user not in tournament.players.all():
messages.error(request, "You are not authorized to view this tournament.")
return redirect("tournament-list")

if tournament.matches.count() == 0 and (
tournament.status == "registration closed" or tournament.status == "tournament in progress"
):
Expand Down Expand Up @@ -545,7 +556,6 @@ def post(self, request, *args, **kwargs):
raise ValueError("Invalid action")


@method_decorator(staff_required, name="dispatch")
class TournamentCreateView(CreateView):
model = Tournament
template_name = "tournaments/tournament_create.html"
Expand All @@ -563,33 +573,46 @@ class TournamentCreateView(CreateView):
"num_winner",
"players", # This field should be removed in the production version. For testing only.
]
# Note: "winner" is not included in the fields because it is not
# supposed to be set by the user. It will be set automatically
# when the tournament is over.
# Note: the "matches" field is not included in the fields because
# it is not supposed to be set by the user. It will be set automatically
# by the create tournament brackets mechanism.

# This method is called when valid form data has been POSTed. It
# overrides the default behavior of the CreateView class.
def form_valid(self, form):
response = super().form_valid(form)
user = self.request.user

if form.cleaned_data["players"].count() > form.cleaned_data["max_players"]:
messages.error(self.request, "The number of players cannot exceed the maximum number of players")
return redirect(reverse_lazy("tournament-create"))

# Check if the user is not a staff member and has less than one token
if not user.is_staff and user.tokens < 1:
messages.error(self.request, "You do not have enough tokens to create a tournament.")
return redirect("tournament-list")

# If the user is not staff, deduct a token
if not user.is_staff:
user.tokens -= 1
user.save()

# Save the form instance but don't commit to the database yet
tournament = form.save(commit=False)
tournament.created_by = user
tournament.save()

players = form.cleaned_data["players"]
tournament.players.add(*players)

# Auto-create a chat for this respective tournament
chat = Chat(tournament=self.object)
chat = Chat(tournament=tournament)
chat.save()

# Do something with brackets if needed
return response
# (Optional) Insert bracket-related logic here if needed

# Redirect to the tournament's detail page or another appropriate response
self.object = tournament
return HttpResponseRedirect(self.get_success_url())

def get_success_url(self):
return reverse_lazy("tournament-detail", kwargs={"pk": self.object.pk})
return reverse("tournament-detail", kwargs={"pk": self.object.pk})


@method_decorator(staff_required, name="dispatch")
class TournamentUpdateView(UpdateView):
# Note: players should not be allowed to join a tournament after
# it has started, so it is discouraged (but still allowed) to add
Expand All @@ -606,9 +629,21 @@ class TournamentUpdateView(UpdateView):
"rules",
"draw_rules",
"num_winner",
"matches",
"players",
]

def dispatch(self, request, *args, **kwargs):
# Get the tournament object
tournament = self.get_object()

# Check if the current user is the creator of the tournament
if tournament.created_by != request.user and not request.user.is_staff:
messages.error(self.request, "You do not have permission to edit this tournament.")
return redirect("tournament-list", pk=self.kwargs["pk"])

# Continue with the normal flow
return super().dispatch(request, *args, **kwargs)

# Note: the "registration_start_date" and "registration_end_date",
# "tournament_start_date" and "tournament_end_date" fields are not
# included because they are not supposed to be updated once the tournament is created.
Expand All @@ -620,44 +655,68 @@ class TournamentUpdateView(UpdateView):
# but we keep it for now because it is convenient for testing.

def form_valid(self, form):
# Get the current tournament from the database
current_tournament = get_object_or_404(Tournament, pk=self.kwargs["pk"])
# Determine if the user is staff or the creator of the tournament
is_staff = self.request.user.is_staff

# Check if the 'players' field has been modified
form_players = set(form.cleaned_data["players"])
current_players = set(current_tournament.players.all())

# Prevent non-staff from modifying 'players' if there are changes
if form_players != current_players and not is_staff:
messages.error(self.request, "You do not have permission to modify players.")
return redirect("tournament-detail", pk=self.kwargs["pk"])

# Get the set of players before and after the form submission
# the tournament cannot be updated if it has ended
if current_tournament.status == "tournament ended":
messages.error(self.request, "You cannot update a tournament that has ended.")
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": self.kwargs["pk"]}))

# Check if the 'players' field has been modified
form_players = set(form.cleaned_data["players"])
current_players = set(current_tournament.players.all())

# Check if new players are being added
new_players = form_players - current_players
if new_players:
# Allow only staff to add new players if the tournament has started
if now() >= current_tournament.tournament_start_date and not is_staff:
messages.error(self.request, "You cannot add new players to the tournament after it has started.")
return redirect("tournament-detail", pk=self.kwargs["pk"])

# Handle player removal
if len(current_players - form_players) > 0: # Players have been removed
removed_players = current_players - form_players
for player in removed_players:
# Handle multiple matches for a player
related_matches = current_tournament.matches.filter(players__in=[player])
for match in related_matches:
match.players.remove(player)
if match.players.count() == 0: # if the match is empty, delete it
match.delete()

# Check for exceeding maximum number of players
if form.cleaned_data["players"].count() > form.cleaned_data["max_players"]:
messages.error(self.request, "The number of players cannot exceed the maximum number of players")
return redirect(reverse_lazy("tournament-update", kwargs={"pk": self.kwargs["pk"]}))

if len(form_players - current_players) > 0: # if the players have been added
if current_tournament.status != "registration open":
raise PermissionDenied(
"You cannot add new players to the tournament when it is not in the registration period."
)
elif (
len(current_players - form_players) > 0 and current_tournament.status == "tournament in progress"
): # if the players have been removed
removed_players = current_players - form_players # get the players that have been removed
for player in removed_players:
related_match = current_tournament.matches.get(
players__in=[player]
) # get the match that the player is in
assert isinstance(related_match, Match)
related_match.players.remove(player)
# if the match is empty, the match will be displayed as forfeited

# The super class's form_valid method will save the form data to the database
# Save the form data to the database using the superclass's method
# This allows updating of other fields by the creator or staff, regardless of the tournament's status
return super().form_valid(form)

def get_success_url(self):
return reverse_lazy("tournament-detail", kwargs={"pk": self.object.pk})
return reverse("tournament-detail", kwargs={"pk": self.object.pk})


def distribute_tokens():
# Placeholder for future date check
# thirty_days_ago = datetime.now() - timedelta(days=30)
# users = User.objects.filter(tokens__lt=3, last_token_distribution__lt=thirty_days_ago)

users = User.objects.filter(tokens__lt=3)
for user in users:
# Add future logic for updating last_token_distribution
# user.last_token_distribution = datetime.now()
user.tokens += 1
user.save()


@method_decorator(staff_required, name="dispatch")
Expand Down
Loading

0 comments on commit 4b370b4

Please sign in to comment.