Skip to content

Commit 462d2d4

Browse files
authored
Nour/email verification (#955)
1 parent f01a381 commit 462d2d4

33 files changed

+1018
-58
lines changed

backend/siarnaq/api/compete/test_models.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ def setUp(self):
3030
)
3131
for name in ["team1", "team2", "team3"]:
3232
t = Team.objects.create(episode=self.e1, name=name)
33+
u = User.objects.create_user(
34+
username=name + "_user", email=f"{name}@example.com"
35+
)
36+
u.email_verified = True
37+
u.save()
3338
Submission.objects.create(
3439
episode=self.e1,
3540
team=t,
36-
user=User.objects.create_user(
37-
username=name + "_user", email=f"{name}@example.com"
38-
),
41+
user=u,
3942
accepted=True,
4043
)
4144

@@ -168,12 +171,15 @@ def setUp(self):
168171
)
169172
for name in ["team1", "team2", "team3"]:
170173
t = Team.objects.create(episode=e1, name=name)
174+
u = User.objects.create_user(
175+
username=name + "_user", email=f"{name}@example.com"
176+
)
177+
u.email_verified = True
178+
u.save()
171179
Submission.objects.create(
172180
episode=e1,
173181
team=t,
174-
user=User.objects.create_user(
175-
username=name + "_user", email=f"{name}@example.com"
176-
),
182+
user=u,
177183
accepted=True,
178184
)
179185

@@ -493,12 +499,15 @@ def setUp(self):
493499
self.teams = []
494500
for name in ["team1", "team2"]:
495501
t = Team.objects.create(episode=e1, name=name)
502+
u = User.objects.create_user(
503+
username=name + "_user", email=f"{name}@example.com"
504+
)
505+
u.email_verified = True
506+
u.save()
496507
Submission.objects.create(
497508
episode=e1,
498509
team=t,
499-
user=User.objects.create_user(
500-
username=name + "_user", email=f"{name}@example.com"
501-
),
510+
user=u,
502511
accepted=True,
503512
)
504513
self.teams.append(t)

backend/siarnaq/api/compete/test_views.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,15 @@ def setUp(self):
8989
self.user = User.objects.create_user(
9090
username="user1", email="user1@example.com"
9191
)
92+
self.user.email_verified = True
93+
self.user.save()
9294
self.team = Team.objects.create(episode=self.e1, name="team1")
9395
self.team.members.add(self.user)
9496
other_user = User.objects.create_user(
9597
username="user2", email="user2@example.com"
9698
)
99+
other_user.email_verified = True
100+
other_user.save()
97101
other_team = Team.objects.create(episode=self.e1, name="team2")
98102
other_team.members.add(other_user)
99103
self.admin = User.objects.get(username="admin")
@@ -195,9 +199,10 @@ def test_create_has_team_not_staff_hidden(self):
195199
self.assertFalse(Submission.objects.exists())
196200

197201
def test_create_no_team(self):
198-
self.client.force_authenticate(
199-
User.objects.create_user(username="user3", email="user3@example.com")
200-
)
202+
user3 = User.objects.create_user(username="user3", email="user3@example.com")
203+
user3.email_verified = True
204+
user3.save()
205+
self.client.force_authenticate(user3)
201206
with io.BytesIO(b"abcdefg") as f:
202207
response = self.client.post(
203208
reverse("submission-list", kwargs={"episode_id": "e1"}),
@@ -379,6 +384,8 @@ def setUp(self):
379384
u = User.objects.create_user(
380385
username=f"user{i}", email=f"user{i}@example.com"
381386
)
387+
u.email_verified = True
388+
u.save()
382389
t = Team.objects.create(episode=self.e1, name=f"team{i}")
383390
t.members.add(u)
384391
self.submissions.append(
@@ -393,6 +400,8 @@ def setUp(self):
393400
self.staff = User.objects.create_user(
394401
username="staff", email="staff@example.com", is_staff=True
395402
)
403+
self.staff.email_verified = True
404+
self.staff.save()
396405

397406
# Partitions:
398407
# user: admin, not admin
@@ -1017,6 +1026,8 @@ def setUp(self):
10171026
u = User.objects.create_user(
10181027
username=f"user{i}", email=f"user{i}@example.com"
10191028
)
1029+
u.email_verified = True
1030+
u.save()
10201031
t = Team.objects.create(episode=self.e1, name=f"team{i}")
10211032
t.members.add(u)
10221033
self.submissions.append(
@@ -1246,14 +1257,17 @@ def setUp(self):
12461257
u = User.objects.create_user(
12471258
username=f"user{i}", email=f"user{i}@example.com"
12481259
)
1249-
t = Team.objects.create(
1260+
u.email_verified = True
1261+
u.save()
1262+
t = Team(
12501263
episode=self.e1 if i < 2 else self.e2,
12511264
name=f"team{i}",
12521265
profile=dict(
12531266
auto_accept_reject_ranked=ScrimmageRequestAcceptReject.MANUAL,
12541267
auto_accept_reject_unranked=ScrimmageRequestAcceptReject.MANUAL,
12551268
),
12561269
)
1270+
t.save()
12571271
t.members.add(u)
12581272
self.submissions.append(
12591273
Submission.objects.create(
@@ -1636,9 +1650,12 @@ def test_accept_episode_frozen(self):
16361650
"siarnaq.api.compete.managers.SaturnInvokableQuerySet.enqueue", autospec=True
16371651
)
16381652
def test_regression_issue_516(self, enqueue):
1639-
self.teams[0].members.add(
1640-
User.objects.create_user(username="another", email="another@example.com")
1653+
another_user = User.objects.create_user(
1654+
username="another", email="another@example.com"
16411655
)
1656+
another_user.email_verified = True
1657+
another_user.save()
1658+
self.teams[0].members.add(another_user)
16421659
self.client.force_authenticate(self.users[1])
16431660
r = ScrimmageRequest.objects.create(
16441661
episode=self.e1,

backend/siarnaq/api/compete/views.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from siarnaq.api.episodes.permissions import IsEpisodeAvailable, IsEpisodeMutable
5151
from siarnaq.api.teams.models import Team, TeamStatus
5252
from siarnaq.api.teams.permissions import IsOnTeam
53+
from siarnaq.api.user.permissions import IsEmailVerified
5354
from siarnaq.gcloud import titan
5455

5556
logger = structlog.get_logger(__name__)
@@ -129,6 +130,7 @@ class SubmissionViewSet(
129130
serializer_class = SubmissionSerializer
130131
permission_classes = (
131132
IsAuthenticated,
133+
IsEmailVerified,
132134
IsEpisodeMutable | IsAdminUser,
133135
IsOnTeam,
134136
)
@@ -915,18 +917,25 @@ def get_permissions(self):
915917
case "create":
916918
return [
917919
IsAuthenticated(),
920+
IsEmailVerified(),
918921
IsOnTeam(),
919922
(IsEpisodeMutable | IsAdminUser)(),
920923
HasTeamSubmission(),
921924
]
922925
case "destroy":
923926
return [
924927
IsAuthenticated(),
928+
IsEmailVerified(),
925929
IsOnTeam(),
926930
IsEpisodeAvailable(),
927931
]
928932
case "list" | "retrieve":
929-
return [IsAuthenticated(), IsOnTeam(), IsEpisodeAvailable()]
933+
return [
934+
IsAuthenticated(),
935+
IsEmailVerified(),
936+
IsOnTeam(),
937+
IsEpisodeAvailable(),
938+
]
930939
case _:
931940
return super().get_permissions()
932941

@@ -1037,7 +1046,12 @@ def create(self, request, *, episode_id):
10371046
@action(
10381047
detail=False,
10391048
methods=["get"],
1040-
permission_classes=(IsAuthenticated, IsEpisodeAvailable, IsOnTeam),
1049+
permission_classes=(
1050+
IsAuthenticated,
1051+
IsEmailVerified,
1052+
IsEpisodeAvailable,
1053+
IsOnTeam,
1054+
),
10411055
)
10421056
def inbox(self, request, *, episode_id):
10431057
"""Get all pending scrimmage requests received."""
@@ -1055,7 +1069,12 @@ def inbox(self, request, *, episode_id):
10551069
@action(
10561070
detail=False,
10571071
methods=["get"],
1058-
permission_classes=(IsAuthenticated, IsEpisodeAvailable, IsOnTeam),
1072+
permission_classes=(
1073+
IsAuthenticated,
1074+
IsEmailVerified,
1075+
IsEpisodeAvailable,
1076+
IsOnTeam,
1077+
),
10591078
)
10601079
def outbox(self, request, *, episode_id):
10611080
"""Get all pending scrimmage requests sent."""
@@ -1080,6 +1099,7 @@ def outbox(self, request, *, episode_id):
10801099
methods=["post"],
10811100
permission_classes=(
10821101
IsAuthenticated,
1102+
IsEmailVerified,
10831103
IsOnTeam,
10841104
IsEpisodeMutable | IsAdminUser,
10851105
HasTeamSubmission,
@@ -1107,7 +1127,12 @@ def accept(self, request, pk=None, *, episode_id):
11071127
@action(
11081128
detail=True,
11091129
methods=["post"],
1110-
permission_classes=(IsAuthenticated, IsOnTeam, IsEpisodeAvailable),
1130+
permission_classes=(
1131+
IsAuthenticated,
1132+
IsEmailVerified,
1133+
IsOnTeam,
1134+
IsEpisodeAvailable,
1135+
),
11111136
)
11121137
def reject(self, request, pk=None, *, episode_id):
11131138
"""Reject a scrimmage request."""

backend/siarnaq/api/teams/tests.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@ def setUp(self):
253253
)
254254

255255
self.team = Team.objects.create(episode=self.episode, name="t1")
256-
self.user = User.objects.create_user(username="u1", email="u1@example.com")
256+
self.user = User.objects.create_user(
257+
username="u1", email="u1@example.com", email_verified=True
258+
)
257259
self.team.members.add(self.user)
258260

259261
# Partitions for: me (patch)

backend/siarnaq/api/teams/views.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
UserPassedSerializer,
3636
)
3737
from siarnaq.api.user.models import User
38+
from siarnaq.api.user.permissions import IsEmailVerified
3839
from siarnaq.gcloud import titan
3940

4041
logger = structlog.get_logger(__name__)
@@ -107,6 +108,7 @@ def get_permissions(self):
107108
case "create":
108109
return (
109110
IsAuthenticated(),
111+
IsEmailVerified(),
110112
IsEpisodeAvailable(),
111113
(~IsOnTeam)(),
112114
)
@@ -118,7 +120,7 @@ def get_permissions(self):
118120
@action(
119121
detail=False,
120122
methods=["get", "put", "patch"],
121-
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
123+
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
122124
serializer_class=TeamPrivateSerializer,
123125
)
124126
def me(self, request, *, episode_id):
@@ -163,7 +165,7 @@ def me(self, request, *, episode_id):
163165
detail=False,
164166
methods=["post"],
165167
serializer_class=TeamLeaveSerializer,
166-
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
168+
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
167169
)
168170
def leave(self, request, *, episode_id):
169171
"""Leave a team."""
@@ -178,7 +180,12 @@ def leave(self, request, *, episode_id):
178180
detail=False,
179181
methods=["post"],
180182
serializer_class=TeamJoinSerializer,
181-
permission_classes=(IsAuthenticated, IsEpisodeAvailable, ~IsOnTeam),
183+
permission_classes=(
184+
IsAuthenticated,
185+
IsEmailVerified,
186+
IsEpisodeAvailable,
187+
~IsOnTeam, # type: ignore[operator]
188+
),
182189
)
183190
def join(self, request, pk=None, *, episode_id):
184191
serializer = self.get_serializer(data=request.data)
@@ -221,7 +228,7 @@ def join(self, request, pk=None, *, episode_id):
221228
detail=False,
222229
methods=["post"],
223230
serializer_class=TeamAvatarSerializer,
224-
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
231+
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
225232
)
226233
def avatar(self, request, pk=None, *, episode_id):
227234
"""Update uploaded avatar."""
@@ -254,7 +261,7 @@ def get_queryset(self):
254261
@action(
255262
detail=True,
256263
methods=["get"],
257-
permission_classes=(IsAuthenticated,),
264+
permission_classes=(IsAuthenticated, IsEmailVerified),
258265
serializer_class=UserPassedSerializer,
259266
)
260267
def check(self, request, pk=None, episode_id=None):
@@ -280,7 +287,7 @@ def compute(self, request, pk=None, episode_id=None):
280287
detail=False,
281288
methods=["get", "put"],
282289
serializer_class=TeamReportSerializer,
283-
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
290+
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
284291
)
285292
def report(self, request, pk=None, *, episode_id):
286293
"""Retrieve or update team strategy report"""

backend/siarnaq/api/user/admin.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from siarnaq.api.teams.models import Team
66
from siarnaq.api.user.forms import UserCreationForm
7-
from siarnaq.api.user.models import User, UserProfile
7+
from siarnaq.api.user.models import EmailVerificationToken, User, UserProfile
88

99

1010
class UserProfileInline(admin.StackedInline):
@@ -93,3 +93,17 @@ def get_form(self, request, obj=None, **kwargs):
9393

9494
def has_delete_permission(self, request, obj=None):
9595
return False
96+
97+
98+
@admin.register(EmailVerificationToken)
99+
class EmailVerificationTokenAdmin(admin.ModelAdmin):
100+
"""Admin interface for email verification tokens."""
101+
102+
list_display = ("user", "token", "created_at")
103+
list_filter = ("created_at",)
104+
search_fields = ("user__username", "user__email", "token")
105+
readonly_fields = ("user", "token", "created_at")
106+
ordering = ("-created_at",)
107+
108+
def has_add_permission(self, request):
109+
return False
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.1.2 on 2025-11-30 17:04
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("user", "0004_alter_user_managers"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="user",
15+
name="email_verified",
16+
field=models.BooleanField(default=False),
17+
),
18+
]

0 commit comments

Comments
 (0)