diff --git a/src/chigame/api/filters.py b/src/chigame/api/filters.py
index d28db9303..c333c7e5b 100644
--- a/src/chigame/api/filters.py
+++ b/src/chigame/api/filters.py
@@ -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",
+ ]
diff --git a/src/chigame/games/models.py b/src/chigame/games/models.py
index 2913d104f..870af9f99 100644
--- a/src/chigame/games/models.py
+++ b/src/chigame/games/models.py
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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 "
@@ -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:
@@ -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
@@ -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
@@ -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
diff --git a/src/chigame/games/views.py b/src/chigame/games/views.py
index 4f7bdcb32..ef3844e4f 100644
--- a/src/chigame/games/views.py
+++ b/src/chigame/games/views.py
@@ -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
@@ -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")
@@ -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:
@@ -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
@@ -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")
@@ -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")
@@ -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
@@ -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)
diff --git a/src/chigame/users/models.py b/src/chigame/users/models.py
index d29e8b7b1..9eebc08ee 100644
--- a/src/chigame/users/models.py
+++ b/src/chigame/users/models.py
@@ -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")
diff --git a/src/chigame/users/tables.py b/src/chigame/users/tables.py
index 4ec356f6c..96edbc1b1 100644
--- a/src/chigame/users/tables.py
+++ b/src/chigame/users/tables.py
@@ -1,15 +1,19 @@
import django_tables2 as tables
-from .models import User, UserProfile
+from .models import User
class FriendsTable(tables.Table):
- name = tables.Column(verbose_name="Friend's Name")
+ email = tables.Column(
+ verbose_name="Email",
+ accessor="email", # Access display_name through the User relationship
+ linkify=("users:user-profile", {"pk": tables.A("pk")}),
+ )
class Meta:
- model = UserProfile # Referencing the UserProfile model
+ model = User # Referencing the UserProfile model
template_name = "django_tables2/bootstrap.html"
- fields = [""] # Adjust fields to show relevant information from the UserProfile model
+ fields = ["email"] # Adjust fields to show relevant information from the UserProfile model
class UserTable(tables.Table):
@@ -18,6 +22,6 @@ class UserTable(tables.Table):
class Meta:
model = User
template_name = "django_tables2/bootstrap.html"
- fields = ["name", "email"]
+ fields = ["name", "first_name", "last_name", "email"]
# Add information about top ranking users, total points collected, etc.
diff --git a/src/chigame/users/tests/test_models.py b/src/chigame/users/tests/test_models.py
index 6091865a1..ef4be6d71 100644
--- a/src/chigame/users/tests/test_models.py
+++ b/src/chigame/users/tests/test_models.py
@@ -75,7 +75,12 @@ def test_notificationqueryset_get_by_actor():
Notification.objects.get_by_actor(friendinvitation1)
Notification.objects.get_by_actor(friendinvitation3)
- assert len(Notification.objects.filter_by_actor(friendinvitation2)) == 1
+ notification = Notification.objects.get_by_actor(friendinvitation2)
+ assert notification.actor == friendinvitation2
+
+ notification.delete()
+ with pytest.raises(Notification.DoesNotExist):
+ Notification.objects.get_by_actor(friendinvitation2)
@pytest.mark.django_db
diff --git a/src/chigame/users/urls.py b/src/chigame/users/urls.py
index 4f0da37ce..3545cc73b 100644
--- a/src/chigame/users/urls.py
+++ b/src/chigame/users/urls.py
@@ -5,6 +5,7 @@
act_on_inbox_notification,
cancel_friend_invitation,
decline_friend_invitation,
+ friend_list_view,
notification_detail,
send_friend_invitation,
user_detail_view,
@@ -31,6 +32,7 @@
path("user_history/
{{ player.email }}
{% endif %}
{% if not forloop.last %}vs.{% endif %}
+ {% empty %}
+ Forfeited
{% endfor %}
)
No matches yet.
{% endif %} - {% if user.is_authenticated %} + {% if user.is_authenticated and tournament.status == "registration open" %} {% endif %} + {% if tournament.status == "tournament in progress" %} + {% if user.is_authenticated and user in tournament.get_all_players %} + + {% endif %} + + {% endif %} {% if user.is_staff %}diff --git a/src/templates/tournaments/tournament_list.html b/src/templates/tournaments/tournament_list.html index bad10e776..487902682 100644 --- a/src/templates/tournaments/tournament_list.html +++ b/src/templates/tournaments/tournament_list.html @@ -71,7 +71,7 @@
No player yet.
{% endif %} - {% if user.is_authenticated %} + {% if user.is_authenticated and tournament.status == "registration open" %}