Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions backend/siarnaq/api/compete/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ def setUp(self):
)
for name in ["team1", "team2", "team3"]:
t = Team.objects.create(episode=self.e1, name=name)
u = User.objects.create_user(
username=name + "_user", email=f"{name}@example.com"
)
u.email_verified = True
u.save()
Submission.objects.create(
episode=self.e1,
team=t,
user=User.objects.create_user(
username=name + "_user", email=f"{name}@example.com"
),
user=u,
accepted=True,
)

Expand Down Expand Up @@ -168,12 +171,15 @@ def setUp(self):
)
for name in ["team1", "team2", "team3"]:
t = Team.objects.create(episode=e1, name=name)
u = User.objects.create_user(
username=name + "_user", email=f"{name}@example.com"
)
u.email_verified = True
u.save()
Submission.objects.create(
episode=e1,
team=t,
user=User.objects.create_user(
username=name + "_user", email=f"{name}@example.com"
),
user=u,
accepted=True,
)

Expand Down Expand Up @@ -493,12 +499,15 @@ def setUp(self):
self.teams = []
for name in ["team1", "team2"]:
t = Team.objects.create(episode=e1, name=name)
u = User.objects.create_user(
username=name + "_user", email=f"{name}@example.com"
)
u.email_verified = True
u.save()
Submission.objects.create(
episode=e1,
team=t,
user=User.objects.create_user(
username=name + "_user", email=f"{name}@example.com"
),
user=u,
accepted=True,
)
self.teams.append(t)
Expand Down
29 changes: 23 additions & 6 deletions backend/siarnaq/api/compete/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,15 @@ def setUp(self):
self.user = User.objects.create_user(
username="user1", email="user1@example.com"
)
self.user.email_verified = True
self.user.save()
self.team = Team.objects.create(episode=self.e1, name="team1")
self.team.members.add(self.user)
other_user = User.objects.create_user(
username="user2", email="user2@example.com"
)
other_user.email_verified = True
other_user.save()
other_team = Team.objects.create(episode=self.e1, name="team2")
other_team.members.add(other_user)
self.admin = User.objects.get(username="admin")
Expand Down Expand Up @@ -195,9 +199,10 @@ def test_create_has_team_not_staff_hidden(self):
self.assertFalse(Submission.objects.exists())

def test_create_no_team(self):
self.client.force_authenticate(
User.objects.create_user(username="user3", email="user3@example.com")
)
user3 = User.objects.create_user(username="user3", email="user3@example.com")
user3.email_verified = True
user3.save()
self.client.force_authenticate(user3)
with io.BytesIO(b"abcdefg") as f:
response = self.client.post(
reverse("submission-list", kwargs={"episode_id": "e1"}),
Expand Down Expand Up @@ -379,6 +384,8 @@ def setUp(self):
u = User.objects.create_user(
username=f"user{i}", email=f"user{i}@example.com"
)
u.email_verified = True
u.save()
t = Team.objects.create(episode=self.e1, name=f"team{i}")
t.members.add(u)
self.submissions.append(
Expand All @@ -393,6 +400,8 @@ def setUp(self):
self.staff = User.objects.create_user(
username="staff", email="staff@example.com", is_staff=True
)
self.staff.email_verified = True
self.staff.save()

# Partitions:
# user: admin, not admin
Expand Down Expand Up @@ -1017,6 +1026,8 @@ def setUp(self):
u = User.objects.create_user(
username=f"user{i}", email=f"user{i}@example.com"
)
u.email_verified = True
u.save()
t = Team.objects.create(episode=self.e1, name=f"team{i}")
t.members.add(u)
self.submissions.append(
Expand Down Expand Up @@ -1246,14 +1257,17 @@ def setUp(self):
u = User.objects.create_user(
username=f"user{i}", email=f"user{i}@example.com"
)
t = Team.objects.create(
u.email_verified = True
u.save()
t = Team(
episode=self.e1 if i < 2 else self.e2,
name=f"team{i}",
profile=dict(
auto_accept_reject_ranked=ScrimmageRequestAcceptReject.MANUAL,
auto_accept_reject_unranked=ScrimmageRequestAcceptReject.MANUAL,
),
)
t.save()
t.members.add(u)
self.submissions.append(
Submission.objects.create(
Expand Down Expand Up @@ -1636,9 +1650,12 @@ def test_accept_episode_frozen(self):
"siarnaq.api.compete.managers.SaturnInvokableQuerySet.enqueue", autospec=True
)
def test_regression_issue_516(self, enqueue):
self.teams[0].members.add(
User.objects.create_user(username="another", email="another@example.com")
another_user = User.objects.create_user(
username="another", email="another@example.com"
)
another_user.email_verified = True
another_user.save()
self.teams[0].members.add(another_user)
self.client.force_authenticate(self.users[1])
r = ScrimmageRequest.objects.create(
episode=self.e1,
Expand Down
33 changes: 29 additions & 4 deletions backend/siarnaq/api/compete/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from siarnaq.api.episodes.permissions import IsEpisodeAvailable, IsEpisodeMutable
from siarnaq.api.teams.models import Team, TeamStatus
from siarnaq.api.teams.permissions import IsOnTeam
from siarnaq.api.user.permissions import IsEmailVerified
from siarnaq.gcloud import titan

logger = structlog.get_logger(__name__)
Expand Down Expand Up @@ -129,6 +130,7 @@ class SubmissionViewSet(
serializer_class = SubmissionSerializer
permission_classes = (
IsAuthenticated,
IsEmailVerified,
IsEpisodeMutable | IsAdminUser,
IsOnTeam,
)
Expand Down Expand Up @@ -915,18 +917,25 @@ def get_permissions(self):
case "create":
return [
IsAuthenticated(),
IsEmailVerified(),
IsOnTeam(),
(IsEpisodeMutable | IsAdminUser)(),
HasTeamSubmission(),
]
case "destroy":
return [
IsAuthenticated(),
IsEmailVerified(),
IsOnTeam(),
IsEpisodeAvailable(),
]
case "list" | "retrieve":
return [IsAuthenticated(), IsOnTeam(), IsEpisodeAvailable()]
return [
IsAuthenticated(),
IsEmailVerified(),
IsOnTeam(),
IsEpisodeAvailable(),
]
case _:
return super().get_permissions()

Expand Down Expand Up @@ -1037,7 +1046,12 @@ def create(self, request, *, episode_id):
@action(
detail=False,
methods=["get"],
permission_classes=(IsAuthenticated, IsEpisodeAvailable, IsOnTeam),
permission_classes=(
IsAuthenticated,
IsEmailVerified,
IsEpisodeAvailable,
IsOnTeam,
),
)
def inbox(self, request, *, episode_id):
"""Get all pending scrimmage requests received."""
Expand All @@ -1055,7 +1069,12 @@ def inbox(self, request, *, episode_id):
@action(
detail=False,
methods=["get"],
permission_classes=(IsAuthenticated, IsEpisodeAvailable, IsOnTeam),
permission_classes=(
IsAuthenticated,
IsEmailVerified,
IsEpisodeAvailable,
IsOnTeam,
),
)
def outbox(self, request, *, episode_id):
"""Get all pending scrimmage requests sent."""
Expand All @@ -1080,6 +1099,7 @@ def outbox(self, request, *, episode_id):
methods=["post"],
permission_classes=(
IsAuthenticated,
IsEmailVerified,
IsOnTeam,
IsEpisodeMutable | IsAdminUser,
HasTeamSubmission,
Expand Down Expand Up @@ -1107,7 +1127,12 @@ def accept(self, request, pk=None, *, episode_id):
@action(
detail=True,
methods=["post"],
permission_classes=(IsAuthenticated, IsOnTeam, IsEpisodeAvailable),
permission_classes=(
IsAuthenticated,
IsEmailVerified,
IsOnTeam,
IsEpisodeAvailable,
),
)
def reject(self, request, pk=None, *, episode_id):
"""Reject a scrimmage request."""
Expand Down
4 changes: 3 additions & 1 deletion backend/siarnaq/api/teams/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ def setUp(self):
)

self.team = Team.objects.create(episode=self.episode, name="t1")
self.user = User.objects.create_user(username="u1", email="u1@example.com")
self.user = User.objects.create_user(
username="u1", email="u1@example.com", email_verified=True
)
self.team.members.add(self.user)

# Partitions for: me (patch)
Expand Down
19 changes: 13 additions & 6 deletions backend/siarnaq/api/teams/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
UserPassedSerializer,
)
from siarnaq.api.user.models import User
from siarnaq.api.user.permissions import IsEmailVerified
from siarnaq.gcloud import titan

logger = structlog.get_logger(__name__)
Expand Down Expand Up @@ -107,6 +108,7 @@ def get_permissions(self):
case "create":
return (
IsAuthenticated(),
IsEmailVerified(),
IsEpisodeAvailable(),
(~IsOnTeam)(),
)
Expand All @@ -118,7 +120,7 @@ def get_permissions(self):
@action(
detail=False,
methods=["get", "put", "patch"],
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
serializer_class=TeamPrivateSerializer,
)
def me(self, request, *, episode_id):
Expand Down Expand Up @@ -163,7 +165,7 @@ def me(self, request, *, episode_id):
detail=False,
methods=["post"],
serializer_class=TeamLeaveSerializer,
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
)
def leave(self, request, *, episode_id):
"""Leave a team."""
Expand All @@ -178,7 +180,12 @@ def leave(self, request, *, episode_id):
detail=False,
methods=["post"],
serializer_class=TeamJoinSerializer,
permission_classes=(IsAuthenticated, IsEpisodeAvailable, ~IsOnTeam),
permission_classes=(
IsAuthenticated,
IsEmailVerified,
IsEpisodeAvailable,
~IsOnTeam, # type: ignore[operator]
),
)
def join(self, request, pk=None, *, episode_id):
serializer = self.get_serializer(data=request.data)
Expand Down Expand Up @@ -221,7 +228,7 @@ def join(self, request, pk=None, *, episode_id):
detail=False,
methods=["post"],
serializer_class=TeamAvatarSerializer,
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
)
def avatar(self, request, pk=None, *, episode_id):
"""Update uploaded avatar."""
Expand Down Expand Up @@ -254,7 +261,7 @@ def get_queryset(self):
@action(
detail=True,
methods=["get"],
permission_classes=(IsAuthenticated,),
permission_classes=(IsAuthenticated, IsEmailVerified),
serializer_class=UserPassedSerializer,
)
def check(self, request, pk=None, episode_id=None):
Expand All @@ -280,7 +287,7 @@ def compute(self, request, pk=None, episode_id=None):
detail=False,
methods=["get", "put"],
serializer_class=TeamReportSerializer,
permission_classes=(IsAuthenticated, IsEpisodeAvailable),
permission_classes=(IsAuthenticated, IsEmailVerified, IsEpisodeAvailable),
)
def report(self, request, pk=None, *, episode_id):
"""Retrieve or update team strategy report"""
Expand Down
16 changes: 15 additions & 1 deletion backend/siarnaq/api/user/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from siarnaq.api.teams.models import Team
from siarnaq.api.user.forms import UserCreationForm
from siarnaq.api.user.models import User, UserProfile
from siarnaq.api.user.models import EmailVerificationToken, User, UserProfile


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

def has_delete_permission(self, request, obj=None):
return False


@admin.register(EmailVerificationToken)
class EmailVerificationTokenAdmin(admin.ModelAdmin):
"""Admin interface for email verification tokens."""

list_display = ("user", "token", "created_at")
list_filter = ("created_at",)
search_fields = ("user__username", "user__email", "token")
readonly_fields = ("user", "token", "created_at")
ordering = ("-created_at",)

def has_add_permission(self, request):
return False
18 changes: 18 additions & 0 deletions backend/siarnaq/api/user/migrations/0005_user_email_verified.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2025-11-30 17:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("user", "0004_alter_user_managers"),
]

operations = [
migrations.AddField(
model_name="user",
name="email_verified",
field=models.BooleanField(default=False),
),
]
Loading
Loading