Archived
Backend: - New hermes app with ReadingGroup, GroupMeeting, GroupMembership models - NotificationPreference (per-user: enable/disable, frequency daily/weekly) - PaceNotification model for tracking sent/dismissed notifications - Pace calculation service comparing current progress vs meeting targets - API endpoints: pace status, notification preferences CRUD, reminders, dismiss - Ahead/behind/on_track/completed status determination Frontend: - PaceNotification component for in-app alerts (behind/ahead/on_track statuses) - ReadingPaceBanner — persistent banner in reader view - Notification settings in Settings page (enable/disable, frequency, reminders) - API client module (groupsApi) for all notification endpoints
137 lines
4.1 KiB
Python
137 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from rest_framework import serializers
|
|
|
|
from apps.groups.models import (
|
|
FrequencyChoice,
|
|
GroupMeeting,
|
|
GroupMembership,
|
|
NotificationPreference,
|
|
PaceNotification,
|
|
ReadingGroup,
|
|
)
|
|
from apps.groups.services.pace import PaceStatus
|
|
|
|
|
|
class NotificationPreferenceSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = NotificationPreference
|
|
fields = [
|
|
"pace_notifications_enabled",
|
|
"reminder_enabled",
|
|
"frequency",
|
|
]
|
|
|
|
def validate_frequency(self, value: str) -> str:
|
|
valid = {choice[0] for choice in FrequencyChoice.choices}
|
|
if value not in valid:
|
|
raise serializers.ValidationError(
|
|
f"Frequency must be one of: {', '.join(valid)}"
|
|
)
|
|
return value
|
|
|
|
|
|
class GroupMeetingSerializer(serializers.ModelSerializer):
|
|
group_name = serializers.CharField(source="group.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = GroupMeeting
|
|
fields = [
|
|
"id",
|
|
"group",
|
|
"group_name",
|
|
"title",
|
|
"scheduled_at",
|
|
"target_section",
|
|
"target_section_label",
|
|
"created_at",
|
|
]
|
|
read_only_fields = ["id", "created_at"]
|
|
|
|
def validate_target_section(self, value: int) -> int:
|
|
if value < 0:
|
|
raise serializers.ValidationError("Target section must be non-negative.")
|
|
return value
|
|
|
|
|
|
class ReadingGroupSerializer(serializers.ModelSerializer):
|
|
member_count = serializers.SerializerMethodField()
|
|
meeting_count = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ReadingGroup
|
|
fields = [
|
|
"id",
|
|
"name",
|
|
"description",
|
|
"book",
|
|
"ebook",
|
|
"created_by",
|
|
"created_at",
|
|
"member_count",
|
|
"meeting_count",
|
|
]
|
|
read_only_fields = ["id", "created_by", "created_at"]
|
|
|
|
def get_member_count(self, obj: ReadingGroup) -> int:
|
|
return obj.memberships.count()
|
|
|
|
def get_meeting_count(self, obj: ReadingGroup) -> int:
|
|
return obj.meetings.count()
|
|
|
|
|
|
class GroupMembershipSerializer(serializers.ModelSerializer):
|
|
user_email = serializers.CharField(source="user.email", read_only=True)
|
|
group_name = serializers.CharField(source="group.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = GroupMembership
|
|
fields = ["id", "user", "user_email", "group", "group_name", "role", "joined_at"]
|
|
read_only_fields = ["id", "joined_at"]
|
|
|
|
|
|
class PaceNotificationSerializer(serializers.ModelSerializer):
|
|
status_display = serializers.CharField(source="get_status_display", read_only=True)
|
|
|
|
class Meta:
|
|
model = PaceNotification
|
|
fields = [
|
|
"id",
|
|
"user",
|
|
"meeting",
|
|
"ebook",
|
|
"status",
|
|
"status_display",
|
|
"current_section",
|
|
"target_section",
|
|
"ahead_behind_delta",
|
|
"created_at",
|
|
]
|
|
read_only_fields = ["id", "created_at"]
|
|
|
|
|
|
class PaceStatusSerializer(serializers.Serializer):
|
|
meeting_id = serializers.IntegerField()
|
|
meeting_title = serializers.CharField()
|
|
scheduled_at = serializers.CharField()
|
|
target_section = serializers.IntegerField()
|
|
target_section_label = serializers.CharField()
|
|
current_section = serializers.IntegerField()
|
|
ahead_behind_delta = serializers.IntegerField()
|
|
status = serializers.CharField()
|
|
days_until_meeting = serializers.IntegerField()
|
|
sections_to_catch_up = serializers.IntegerField()
|
|
ebook_id = serializers.IntegerField()
|
|
ebook_title = serializers.CharField()
|
|
|
|
|
|
class ReminderSerializer(serializers.Serializer):
|
|
meeting_id = serializers.IntegerField()
|
|
meeting_title = serializers.CharField()
|
|
scheduled_at = serializers.CharField()
|
|
target_section = serializers.IntegerField()
|
|
target_section_label = serializers.CharField()
|
|
group_name = serializers.CharField()
|
|
hours_until = serializers.IntegerField()
|
|
ebook_id = serializers.IntegerField()
|
|
ebook_title = serializers.CharField() |