Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into forums/forum-create-st…
Browse files Browse the repository at this point in the history
…yling
  • Loading branch information
bburgess19 committed Dec 5, 2023
2 parents 6deb1b7 + 4e1f6c9 commit 83bbcb2
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 42 deletions.
45 changes: 44 additions & 1 deletion src/chigame/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,52 @@ class GameFilter(django_filters.FilterSet):
# https://www.w3schools.com/django/ref_lookups_icontains.php
# icontains: case-insensitive containment test
name = django_filters.CharFilter(lookup_expr="icontains")

year_published = django_filters.NumberFilter(lookup_expr="exact")
year_published__gte = django_filters.NumberFilter(field_name="year_published", lookup_expr="gte")
year_published__lte = django_filters.NumberFilter(field_name="year_published", lookup_expr="lte")

suggested_age = django_filters.NumberFilter(lookup_expr="exact")
suggested_age__gte = django_filters.NumberFilter(field_name="suggested_age", lookup_expr="gte")
suggested_age__lte = django_filters.NumberFilter(field_name="suggested_age", lookup_expr="lte")

expected_playtime = django_filters.NumberFilter(lookup_expr="exact")
expected_playtime__gte = django_filters.NumberFilter(field_name="expected_playtime", lookup_expr="gte")
expected_playtime__lte = django_filters.NumberFilter(field_name="expected_playtime", lookup_expr="lte")

min_playtime = django_filters.NumberFilter(lookup_expr="exact")
min_playtime__gte = django_filters.NumberFilter(field_name="min_playtime", lookup_expr="gte")
min_playtime__lte = django_filters.NumberFilter(field_name="min_playtime", lookup_expr="lte")

max_playtime = django_filters.NumberFilter(lookup_expr="exact")
max_playtime__gte = django_filters.NumberFilter(field_name="max_playtime", lookup_expr="gte")
max_playtime__lte = django_filters.NumberFilter(field_name="max_playtime", lookup_expr="lte")

min_players = django_filters.NumberFilter(lookup_expr="exact")
min_players__gte = django_filters.NumberFilter(field_name="min_players", lookup_expr="gte")
min_players__lte = django_filters.NumberFilter(field_name="min_players", lookup_expr="lte")

max_players = django_filters.NumberFilter(lookup_expr="exact")
max_players__gte = django_filters.NumberFilter(field_name="max_players", lookup_expr="gte")
max_players__lte = django_filters.NumberFilter(field_name="max_players", lookup_expr="lte")

complexity = django_filters.NumberFilter(lookup_expr="exact")
complexity__gte = django_filters.NumberFilter(field_name="complexity", lookup_expr="gte")
complexity__lte = django_filters.NumberFilter(field_name="complexity", lookup_expr="lte")

BGG_id = django_filters.NumberFilter(lookup_expr="exact")

class Meta:
model = Game
fields = ["name", "min_players", "max_players"]
fields = [
"name",
"year_published",
"suggested_age",
"expected_playtime",
"min_playtime",
"max_playtime",
"complexity",
"min_players",
"max_players",
"BGG_id",
]
72 changes: 52 additions & 20 deletions src/chigame/games/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ class Tournament(models.Model):
num_winner = models.PositiveIntegerField(default=1) # number of possible winners for the tournament
archived = models.BooleanField(default=False) # whether the tournament is archived by the admin

matches = models.ManyToManyField(Match, related_name="matches", blank=True)
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)

Expand All @@ -263,17 +263,21 @@ def status(self):
"""
Returns the status of the tournament.
"""
if self.registration_start_date > timezone.now():
if (
self.registration_start_date > timezone.now()
): # the period when the information of the tournament is displayed
return "preparing"
elif self.registration_end_date > timezone.now(): # the registration period has started but not ended yet
elif (
self.registration_end_date > timezone.now()
): # the registration period has started, users can register for the tournament
return "registration open"
elif (
self.tournament_start_date > timezone.now()
): # the registration period has ended but the tournament has not started yet
): # the period when the registration period has ended but the tournament has not started yet
return "registration closed"
elif self.tournament_end_date > timezone.now(): # the tournament has started but not ended yet
elif self.tournament_end_date > timezone.now(): # the tournament has started, matches are being played
return "tournament in progress"
else: # the tournament has ended
else: # all matches have finished. The tournament has ended (any matches that have not finished are forfeited)
return "tournament ended"

def clean(self): # restriction
Expand All @@ -290,11 +294,7 @@ def clean(self): # restriction
raise ValidationError("The number of winners should be greater than 0.")

# the number of players should be less than or equal to the maximum number of players
if self.pk is not None: # the tournament is being updated
if self.players.count() > self.max_players:
raise ValidationError(
"The number of players should be less than or equal to the maximum number of players."
)
# Note: this is checked when the tournament is created and updated

# the winners should also be players
if self.pk is not None: # the tournament is being updated
Expand Down Expand Up @@ -328,15 +328,15 @@ def clean(self): # restriction
raise ValidationError("The tournament end date should be in the future.")

# the registration start date should be earlier than the registration end date
if self.registration_start_date > self.registration_end_date:
if self.registration_start_date >= self.registration_end_date:
raise ValidationError("The registration start date should be earlier than the registration end date.")

# the tournament start date should be earlier than the tournament end date
if self.tournament_start_date > self.tournament_end_date:
if self.tournament_start_date >= self.tournament_end_date:
raise ValidationError("The tournament start date should be earlier than the tournament end date.")

# the registration end date should be earlier than the tournament start date
if self.registration_end_date > self.tournament_start_date:
if self.registration_end_date >= self.tournament_start_date:
raise ValidationError("The registration end date should be earlier than the tournament start date.")

# Section: archived
Expand Down Expand Up @@ -368,6 +368,24 @@ def set_archive(self, archive):
self.archived = archive
self.save()

def check_and_end_tournament(self):
"""
Checks if the tournament end date has reached and ends the tournament if it has.
Any matches that have not finished are forfeited.
"""
if self.status == "tournament ended":
brackets = self.matches.all()
for bracket in brackets:
assert isinstance(bracket, Match)
bracket_users = bracket.players.all()
bracket_players = [Player.objects.get(user=user, match=bracket) for user in bracket_users]
bracket_with_outcome = any(player.outcome is not None for player in bracket_players)
if not bracket_with_outcome: # the match has not finished
for player in bracket_players:
player.outcome = Player.WITHDRAWAL # forfeit
player.save()
self.end_tournament()

def __str__(self): # may be changed later
return (
"Tournament "
Expand Down Expand Up @@ -427,13 +445,15 @@ def next_round_tournaments_brackets(self) -> list[Match]:

# get the winners of the previous round
for bracket in brackets:
bracket_players = bracket.players.all()
assert isinstance(bracket, Match)
bracket_users = bracket.players.all()
bracket_players = [Player.objects.get(user=user, match=bracket) for user in bracket_users]
bracket_winners = [
player for player in bracket_players if player.outcome == Player.WIN
] # allow multiple winners
# currently only players who win instead of draw can advance to the next round
for winner in bracket_winners:
players.append(winner)
players.append(winner.user)

# check if the number of players is small enough to end the tournament
if len(players) <= self.num_winner:
Expand Down Expand Up @@ -482,15 +502,18 @@ def end_tournament(self) -> None:
brackets = self.matches.all()
for bracket in brackets: # the matches of the previous round
# get the winners of the previous round
bracket_players = bracket.players.all()
assert isinstance(bracket, Match)
bracket_users = bracket.players.all()
bracket_players = [Player.objects.get(user=user, match=bracket) for user in bracket_users]
bracket_winners = [
player for player in bracket_players if player.outcome == Player.WIN
player.user for player in bracket_players if player.outcome == Player.WIN
] # allow multiple winners
# currently only players who win instead of draw can advance to the next round
for winner in bracket_winners:
winners.append(winner)

self.winners.set(winners)
self.matches.clear()
self.save()

# Note: we don't delete the tournament because we want to keep it in the database
Expand All @@ -506,9 +529,13 @@ def tournament_sign_up(self, user: User) -> int:
Returns:
int: 0 if the user has successfully signed up for the tournament,
1 if the user has already joined the tournament,
2 if the tournament is full, 3 if the tournament has already started,
4 if the tournament has already ended
2 if the tournament is full,
3 if the registration period of tournament has already ended
"""
if self.status != "registration open":
# The registration period has ended (the join and withdraw buttons only appear
# during the registration period)
return 3
if user in self.players.all():
# The user has already joined the tournament
return 1
Expand All @@ -533,7 +560,12 @@ def tournament_withdraw(
Returns:
int: 0 if the user has successfully withdrawn from the tournament,
1 if the user has not joined the tournament
3 if the registration period of tournament has already ended
"""
if self.status != "registration open":
# The registration period has ended (the join and withdraw buttons only appear
# during the registration period)
return 3
if user not in self.players.all():
# The user has not joined the tournament
return 1
Expand Down
63 changes: 55 additions & 8 deletions src/chigame/games/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ def get_context_data(self, **kwargs):
# Additional context can be added if needed
return context

def get(self, request, *args, **kwargs):
super().get(request, *args, **kwargs)
for tournament in self.object_list:
tournament.check_and_end_tournament() # check if the tournament has ended
return self.render_to_response(self.get_context_data())

def post(self, request, *args, **kwargs):
# This method is called when the user clicks the "Join Tournament" or
# "Withdraw" button
Expand All @@ -396,6 +402,9 @@ def post(self, request, *args, **kwargs):
elif success == 2:
messages.error(request, "This tournament is full")
return redirect(reverse_lazy("tournament-list"))
elif success == 3:
messages.error(request, "The registration period for this tournament has ended")
return redirect(reverse_lazy("tournament-list"))
else:
raise Exception("Invalid return value")

Expand All @@ -407,6 +416,9 @@ def post(self, request, *args, **kwargs):
elif success == 1:
messages.error(request, "You have not joined this tournament")
return redirect(reverse_lazy("tournament-list"))
elif success == 3:
messages.error(request, "The registration period for this tournament has ended")
return redirect(reverse_lazy("tournament-list"))
else:
raise Exception("Invalid return value")
else:
Expand All @@ -422,6 +434,17 @@ class TournamentDetailView(DetailView):
template_name = "tournaments/tournament_detail.html"
context_object_name = "tournament"

def get(self, request, *args, **kwargs):
super().get(request, *args, **kwargs)
tournament = Tournament.objects.get(id=self.kwargs["pk"])
if tournament.matches.count() == 0 and (
tournament.status == "registration closed" or tournament.status == "tournament in progress"
):
# if the tournament matches have not been created
tournament.create_tournaments_brackets()
tournament.check_and_end_tournament() # check if the tournament has ended
return self.render_to_response(self.get_context_data())

def post(self, request, *args, **kwargs):
# This method is called when the user clicks the "Join Tournament" or
# "Withdraw" button
Expand All @@ -437,6 +460,9 @@ def post(self, request, *args, **kwargs):
elif success == 2:
messages.error(request, "This tournament is full")
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": tournament.pk}))
elif success == 3:
messages.error(request, "The registration period for this tournament has ended")
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": tournament.pk}))
else:
raise Exception("Invalid return value")

Expand All @@ -448,8 +474,20 @@ def post(self, request, *args, **kwargs):
elif success == 1:
messages.error(request, "You have not joined this tournament")
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": tournament.pk}))
elif success == 3:
messages.error(request, "The registration period for this tournament has ended")
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": tournament.pk}))
else:
raise Exception("Invalid return value")

elif request.POST.get("action") == "join_match":
pass # allow players to join their own matches
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": tournament.pk}))

elif request.POST.get("action") == "spectate":
pass # allow anyone to spectate
return redirect(reverse_lazy("tournament-detail", kwargs={"pk": tournament.pk}))

else:
raise ValueError("Invalid action")

Expand Down Expand Up @@ -483,10 +521,9 @@ class TournamentCreateView(CreateView):
# overrides the default behavior of the CreateView class.
def form_valid(self, form):
response = super().form_valid(form)
self.object.create_tournaments_brackets() # This should be changed later
# because the brackets should not be created right after the tournament
# is created. Instead, the brackets should be created when the registration
# deadline is reached. But for now, we keep it this way for testing.
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"))

# Do something with brackets if needed
return response
Expand Down Expand Up @@ -532,17 +569,27 @@ def form_valid(self, form):
# Check if the 'players' field has been modified
form_players = set(form.cleaned_data["players"])
current_players = set(current_tournament.players.all())

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
raise PermissionDenied("You cannot add new players to the tournament after it has started.")
elif len(current_players - form_players) > 0: # if the players have been removed
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 related_match.players.count() == 0: # if the match is empty, delete it
related_match.delete()
# 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
return super().form_valid(form)
Expand Down
8 changes: 4 additions & 4 deletions src/chigame/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ def get_by_actor(self, actor, include_deleted=False, **kwargs):
try:
actor_content_type = ContentType.objects.get(model=actor._meta.model_name)
actor_object_id = actor.pk
queryset = self.get(actor_content_type=actor_content_type, actor_object_id=actor_object_id, **kwargs)
if not include_deleted:
queryset = queryset.is_not_deleted()
return queryset
notification = self.get(actor_content_type=actor_content_type, actor_object_id=actor_object_id, **kwargs)
if not include_deleted and not notification.visible:
raise Notification.DoesNotExist
return notification

except ContentType.DoesNotExist:
raise ValueError(f"The model {actor.label} is not registered in content type")
Expand Down
Loading

0 comments on commit 83bbcb2

Please sign in to comment.