Skip to content

Create store for Moderator settings #1320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
29 changes: 29 additions & 0 deletions pydis_site/apps/api/migrations/0097_user_mod_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.0.6 on 2024-05-19 19:15

import django.db.models.deletion
import pydis_site.apps.api.models.mixins
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0096_merge_0093_user_alts_0095_user_display_name'),
]

operations = [
migrations.CreateModel(
name='UserModSettings',
fields=[
('moderator', models.OneToOneField(help_text='The moderator for whom these settings belong to', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='mod_settings', serialize=False, to='api.user')),
('pings_disabled_until', models.DateTimeField(help_text='Date and time that moderation pings are disabled until', null=True)),
('pings_schedule_start', models.TimeField(help_text='UTC time that the moderator wishes to receive pings from', null=True)),
('pings_schedule_end', models.DurationField(help_text='Duration after the schedule start time the moderator wishes to receive pings', null=True)),
],
bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
),
migrations.AddConstraint(
model_name='usermodsettings',
constraint=models.CheckConstraint(check=models.Q(models.Q(('pings_schedule_end__isnull', True), ('pings_schedule_start__isnull', True)), models.Q(('pings_schedule_end__isnull', False), ('pings_schedule_start__isnull', False)), _connector='OR'), name='complete_pings_schedule'),
),
]
3 changes: 2 additions & 1 deletion pydis_site/apps/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
Reminder,
Role,
User,
UserAltRelationship
UserAltRelationship,
UserModSettings
)
2 changes: 1 addition & 1 deletion pydis_site/apps/api/models/bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
from .offensive_message import OffensiveMessage
from .reminder import Reminder
from .role import Role
from .user import User, UserAltRelationship
from .user import User, UserAltRelationship, UserModSettings
38 changes: 37 additions & 1 deletion pydis_site/apps/api/models/bot/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ def username(self) -> str:
"""
return str(self)


class UserAltRelationship(ModelReprMixin, ModelTimestampMixin, models.Model):
"""A relationship between a Discord user and its alts."""

Expand Down Expand Up @@ -140,3 +139,40 @@ class Meta:
check=~models.Q(source=models.F("target")),
),
]

class UserModSettings(ModelReprMixin, models.Model):
"""Moderation settings for a Moderator member of staff."""

moderator = models.OneToOneField(
User,
primary_key=True,
on_delete=models.CASCADE,
related_name="mod_settings",
help_text="The moderator for whom these settings belong to"
)

pings_disabled_until = models.DateTimeField(
null=True,
help_text="Date and time that moderation pings are disabled until"
)

pings_schedule_start = models.TimeField(
null=True,
help_text="UTC time that the moderator wishes to receive pings from"
)

pings_schedule_end = models.DurationField(
null=True,
help_text="Duration after the schedule start time the moderator wishes to receive pings"
)

class Meta:
"""Meta options on the moderator preferences."""

constraints = [
models.CheckConstraint(
check=models.Q(pings_schedule_start__isnull=True, pings_schedule_end__isnull=True)
| models.Q(pings_schedule_start__isnull=False, pings_schedule_end__isnull=False),
name="complete_pings_schedule"
)
]
36 changes: 34 additions & 2 deletions pydis_site/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
Reminder,
Role,
User,
UserAltRelationship
UserAltRelationship,
UserModSettings
)

class FrozenFieldsMixin:
Expand Down Expand Up @@ -701,11 +702,24 @@ class UserSerializer(ModelSerializer):
# ID field must be explicitly set as the default id field is read-only.
id = IntegerField(min_value=0)

def to_representation(self, instance: User) -> dict:
"""Serialize the user to a dictionary, serializing the moderator settings."""
ret = super().to_representation(instance)

if hasattr(instance, 'mod_settings') and instance.mod_settings is not None:
ret['mod_settings'] = UserModSettingsSerializer(instance.mod_settings).data
else:
if 'mod_settings' in ret:
del ret['mod_settings']

return ret

class Meta:
"""Metadata defined for the Django REST Framework."""

model = User
fields = ('id', 'name', 'display_name', 'discriminator', 'roles', 'in_guild')
fields = ('id', 'name', 'display_name', 'discriminator', 'roles',
'in_guild', 'mod_settings')
depth = 1
list_serializer_class = UserListSerializer

Expand Down Expand Up @@ -736,6 +750,24 @@ def get_alts(self, user: User) -> list[dict]:
for alt in user.alts.through.objects.filter(source=user)
]

class UserModSettingsSerializer(ModelSerializer):
"""A class to serialize the moderator settings for a user."""

def validate(self, data: dict) -> dict:
"""Validate the moderator settings contain the necessary fields."""
if data.get("pings_schedule_start") and not data.get("pings_schedule_end"):
raise ValidationError("missing pings_schedule_end")

if data.get("pings_schedule_end") and not data.get("pings_schedule_start"):
raise ValidationError("missing pings_schedule_start")

return data

class Meta:
"""Meta settings for the user moderator settings serializer."""

model = UserModSettings
fields = ('moderator', 'pings_disabled_until', 'pings_schedule_start', 'pings_schedule_end')

class NominationEntrySerializer(ModelSerializer):
"""A class providing (de-)serialization of `NominationEntry` instances."""
Expand Down
38 changes: 36 additions & 2 deletions pydis_site/apps/api/viewsets/bot/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@

from pydis_site.apps.api.models.bot.infraction import Infraction
from pydis_site.apps.api.models.bot.metricity import Metricity, NotFoundError
from pydis_site.apps.api.models.bot.user import User, UserAltRelationship
from pydis_site.apps.api.models.bot.user import User, UserAltRelationship, UserModSettings
from pydis_site.apps.api.serializers import (
UserSerializer,
UserAltRelationshipSerializer,
UserModSettingsSerializer,
UserWithAltsSerializer
)

Expand Down Expand Up @@ -335,7 +336,7 @@ class UserViewSet(ModelViewSet):
"""

serializer_class = UserSerializer
queryset = User.objects.all().order_by("id")
queryset = User.objects.select_related("mod_settings").all().order_by("id")
pagination_class = UserListPagination
filter_backends = (DjangoFilterBackend,)
filterset_fields = ('name', 'discriminator', 'display_name')
Expand Down Expand Up @@ -367,6 +368,39 @@ def bulk_patch(self, request: Request) -> Response:

return Response(serializer.data, status=status.HTTP_200_OK)

@action(detail=True, methods=["PATCH"], name='mod-settings-update')
def mod_settings(self, request: Request, pk: str) -> Response:
"""Update the mod settings for a given user."""
user = self.get_object()
maybe_mod_settings = UserModSettings.objects.filter(moderator=user).first()

mod_settings_data = ChainMap({'moderator': user.id}, request.data)

if maybe_mod_settings:
mod_settings = UserModSettingsSerializer(maybe_mod_settings, data=mod_settings_data)
else:
mod_settings = UserModSettingsSerializer(data=mod_settings_data)

mod_settings.is_valid(raise_exception=True)

mod_settings.save()

return Response(mod_settings.data, status=status.HTTP_200_OK)

@mod_settings.mapping.delete
def delete_mod_settings(self, request: Request, pk: str) -> Response:
"""Delete all moderator settings registered for a user."""
user = self.get_object()
maybe_mod_settings = UserModSettings.objects.filter(moderator=user).first()

if not maybe_mod_settings:
return Response(status=status.HTTP_204_NO_CONTENT)

maybe_mod_settings.delete()

return Response(status=status.HTTP_200_OK)


@action(detail=True, methods=['POST'], name="Add alternate account",
url_name='alts', url_path='alts')
def add_alt(self, request: Request, pk: str) -> Response:
Expand Down
Loading