Skip to content

Commit e758094

Browse files
committed
feat: allow unranked matches
[pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci
1 parent 3b46d19 commit e758094

13 files changed

Lines changed: 232 additions & 34 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.10 on 2026-02-02 23:27
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('authentication', '0011_alter_user_rating'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='user',
15+
name='accept_ranked_matches',
16+
field=models.BooleanField(default=True, help_text='Allow any user to request ranked matches against you'),
17+
),
18+
migrations.AddField(
19+
model_name='user',
20+
name='accept_unranked_matches',
21+
field=models.BooleanField(default=True, help_text='Allow any user to request unranked matches against you'),
22+
),
23+
]

othello/apps/auth/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ class User(AbstractUser):
77
is_student = models.BooleanField(default=True, null=False)
88
is_imported = models.BooleanField(default=False, null=False)
99
rating = models.DecimalField(max_digits=6, decimal_places=2, default=1200.00)
10+
accept_ranked_matches = models.BooleanField(
11+
default=True, help_text="Allow any user to request ranked matches against you"
12+
)
13+
accept_unranked_matches = models.BooleanField(
14+
default=True, help_text="Allow any user to request unranked matches against you"
15+
)
1016

1117
@property
1218
def has_management_permission(self) -> bool:

othello/apps/auth/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@
1313
path("rating/", views.rating, name="rating"),
1414
path("profile/<str:username>/", views.profile, name="profile"),
1515
path("rankings/", views.rankings, name="rankings"),
16+
path("update_preferences/", views.update_preferences, name="update_preferences"),
1617
]

othello/apps/auth/views.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
22
import logging
33

4+
from django.contrib import messages
45
from django.contrib.auth import get_user_model
56
from django.contrib.auth.decorators import login_required
67
from django.core.paginator import Paginator
78
from django.http import HttpRequest, HttpResponse
8-
from django.shortcuts import get_object_or_404, render
9+
from django.shortcuts import get_object_or_404, redirect, render
910

1011
from ..games.forms import MatchForm
1112
from ..games.models import RatingHistory, Submission
@@ -14,6 +15,22 @@
1415

1516

1617
def index(request: HttpRequest) -> HttpResponse:
18+
if request.user.is_authenticated:
19+
user = request.user
20+
history = RatingHistory.objects.filter(user=user).order_by("changed_at")
21+
ratings = [float(i) for i in history.values_list("rating", flat=True)]
22+
dates = [h.changed_at.strftime("%Y-%m-%d %H:%M") for h in history]
23+
return render(
24+
request,
25+
"auth/index.html",
26+
{
27+
"current_rating": user.rating,
28+
"ratings_json": json.dumps(ratings),
29+
"dates_json": json.dumps(dates),
30+
"accept_ranked_matches": user.accept_ranked_matches,
31+
"accept_unranked_matches": user.accept_unranked_matches,
32+
},
33+
)
1734
return render(request, "auth/index.html")
1835

1936

@@ -82,3 +99,15 @@ def rankings(request: HttpRequest) -> HttpResponse:
8299
"users_with_submissions": users_with_submissions,
83100
},
84101
)
102+
103+
104+
@login_required
105+
def update_preferences(request: HttpRequest) -> HttpResponse:
106+
if request.method == "POST":
107+
accept_ranked = request.POST.get("accept_ranked_matches") == "true"
108+
accept_unranked = request.POST.get("accept_unranked_matches") == "true"
109+
request.user.accept_ranked_matches = accept_ranked
110+
request.user.accept_unranked_matches = accept_unranked
111+
request.user.save(update_fields=["accept_ranked_matches", "accept_unranked_matches"])
112+
messages.success(request, "Preferences updated successfully.")
113+
return redirect("auth:index")

othello/apps/games/consumers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def send_match_update(self, object_id: int) -> None:
107107
"status": match.status,
108108
"player1": match.player1.get_game_name(),
109109
"player2": match.player2.get_game_name(),
110+
"ranked": "Yes" if match.is_ranked else "No",
110111
"num_games": match.num_games,
111112
"score": score,
112113
"created_at": match.created_at.isoformat(),

othello/apps/games/forms.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ class MatchForm(forms.Form):
8686
min_value=1,
8787
max_value=10,
8888
)
89+
is_ranked = forms.BooleanField(
90+
label="Ranked Match:",
91+
initial=True,
92+
required=False,
93+
)
8994

9095
def __init__(self, user, *args, initial_opponent_user=None, **kwargs):
9196
super().__init__(*args, **kwargs)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.10 on 2026-02-02 23:27
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('games', '0033_match_is_tie_match_loser_match_player1_wins_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='match',
15+
name='is_ranked',
16+
field=models.BooleanField(default=True),
17+
),
18+
]

othello/apps/games/models.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class Match(models.Model):
128128
Submission, on_delete=models.CASCADE, related_name="matches_as_player2"
129129
)
130130
num_games = models.IntegerField(default=5)
131+
is_ranked = models.BooleanField(default=True)
131132
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
132133
created_at = models.DateTimeField(auto_now_add=True)
133134

@@ -199,36 +200,37 @@ def calculate_results(self) -> None:
199200
user1 = self.player1.user
200201
user2 = self.player2.user
201202

202-
rating1 = Decimal(str(user1.rating))
203-
rating2 = Decimal(str(user2.rating))
203+
if self.is_ranked:
204+
rating1 = Decimal(str(user1.rating))
205+
rating2 = Decimal(str(user2.rating))
204206

205-
change1, change2 = calculate_match_rating_change(
206-
rating1=rating1,
207-
rating2=rating2,
208-
player1_wins=player1_wins,
209-
player2_wins=player2_wins,
210-
ties=ties,
211-
)
207+
change1, change2 = calculate_match_rating_change(
208+
rating1=rating1,
209+
rating2=rating2,
210+
player1_wins=player1_wins,
211+
player2_wins=player2_wins,
212+
ties=ties,
213+
)
212214

213-
user1.rating = rating1 + change1
214-
user2.rating = rating2 + change2
215+
user1.rating = rating1 + change1
216+
user2.rating = rating2 + change2
215217

216-
user1.save(update_fields=["rating"])
217-
user2.save(update_fields=["rating"])
218+
user1.save(update_fields=["rating"])
219+
user2.save(update_fields=["rating"])
218220

219-
RatingHistory.objects.create(
220-
user=user1,
221-
rating=user1.rating,
222-
match=self,
223-
)
224-
RatingHistory.objects.create(
225-
user=user2,
226-
rating=user2.rating,
227-
match=self,
228-
)
221+
RatingHistory.objects.create(
222+
user=user1,
223+
rating=user1.rating,
224+
match=self,
225+
)
226+
RatingHistory.objects.create(
227+
user=user2,
228+
rating=user2.rating,
229+
match=self,
230+
)
229231

230-
task_logger.info(f"User {user1} new rating: {user1.rating}")
231-
task_logger.info(f"User {user2} new rating: {user2.rating}")
232+
task_logger.info(f"User {user1} new rating: {user1.rating}")
233+
task_logger.info(f"User {user2} new rating: {user2.rating}")
232234

233235
def __str__(self) -> str:
234236
return f"{self.player1.get_game_name()} vs {self.player2.get_game_name()} ({self.num_games} games)"

othello/apps/games/views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,23 @@ def request_match(request: HttpRequest) -> HttpResponse:
143143
messages.error(request, "Could not request match because you do not have a submission")
144144
return redirect("games:queue")
145145

146+
opponent_user = cd["opponent"].user
147+
if cd["is_ranked"] and not opponent_user.accept_ranked_matches:
148+
messages.error(
149+
request, f"{opponent_user.username} does not accept ranked match requests."
150+
)
151+
return redirect("games:queue")
152+
elif not cd["is_ranked"] and not opponent_user.accept_unranked_matches:
153+
messages.error(
154+
request, f"{opponent_user.username} does not accept unranked match requests."
155+
)
156+
return redirect("games:queue")
157+
146158
match = Match.objects.create(
147159
player1=user_submission,
148160
player2=cd["opponent"],
149161
num_games=cd["num_games"],
162+
is_ranked=cd["is_ranked"],
150163
)
151164
run_match.delay(match.id)
152165
messages.success(request, f"Match requested against {cd['opponent'].get_game_name()}.")

othello/static/js/games/queue.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ $(document).ready(function() {
4848

4949
if (matchRow.length) {
5050
matchRow.find('td:nth-child(2)').text(data.score);
51-
matchRow.find('td:nth-child(4)').html(statusHtml);
51+
matchRow.find('td:nth-child(5)').html(statusHtml);
5252
applyWinLoseClasses(matchRow);
5353
} else {
5454
const newRow = $(`
5555
<tr class="match" data-match-id="${data.match_id}">
5656
<td>${data.player1}</td>
5757
<td>${data.score}</td>
5858
<td>${data.player2}</td>
59+
<td>${data.ranked}</td>
5960
<td>${statusHtml}</td>
6061
<td>${data.created_at}</td>
6162
</tr>

0 commit comments

Comments
 (0)