from django.conf import settings from django.db import models class ReadingGroup(models.Model): """A group of users reading the same book together.""" name = models.CharField(max_length=256, db_index=True) ebook = models.ForeignKey( "books.EBook", on_delete=models.CASCADE, related_name="reading_groups", ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="created_groups", ) description = models.TextField(blank=True, default="") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "groups_reading_group" verbose_name = "Reading Group" verbose_name_plural = "Reading Groups" ordering = ["-created_at"] def __str__(self) -> str: return self.name @property def total_sections(self) -> int: return self.ebook.chapters.count() class GroupMembership(models.Model): class Role(models.TextChoices): MEMBER = "member", "Member" ADMIN = "admin", "Admin" group = models.ForeignKey( ReadingGroup, on_delete=models.CASCADE, related_name="memberships", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="group_memberships", ) role = models.CharField( max_length=16, choices=Role.choices, default=Role.MEMBER, ) joined_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "groups_membership" verbose_name = "Group Membership" verbose_name_plural = "Group Memberships" unique_together = [("group", "user")] ordering = ["joined_at"] def __str__(self) -> str: return f"{self.user} in {self.group.name} ({self.role})" class MemberProgress(models.Model): """Per-member reading progress within a reading group.""" membership = models.OneToOneField( GroupMembership, on_delete=models.CASCADE, related_name="progress", ) group = models.ForeignKey( ReadingGroup, on_delete=models.CASCADE, related_name="member_progress", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="member_progress", ) current_section = models.PositiveIntegerField(default=0) percentage = models.FloatField(default=0.0) time_spent_seconds = models.PositiveIntegerField(default=0) last_position = models.JSONField(blank=True, default=dict) is_public = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "groups_member_progress" verbose_name = "Member Progress" verbose_name_plural = "Member Progress" unique_together = [("group", "user")] indexes = [ models.Index(fields=["group", "-percentage"]), models.Index(fields=["group", "user"]), ] def __str__(self) -> str: return f"{self.user} — {self.group.name} ({self.percentage:.0f}%)" def total_sections(self) -> int: return self.group.total_sections def update_progress( self, current_section: int, percentage: float | None = None, time_spent_delta: int = 0, last_position: dict | None = None, ) -> None: """Update progress with computed percentage if not provided.""" self.current_section = current_section if percentage is not None: self.percentage = min(100.0, max(0.0, percentage)) elif (ts := self.total_sections()) > 0: self.percentage = min(100.0, (current_section / ts) * 100.0) else: self.percentage = 0.0 if time_spent_delta > 0: self.time_spent_seconds += time_spent_delta if last_position is not None: self.last_position = last_position self.save( update_fields=[ "current_section", "percentage", "time_spent_seconds", "last_position", "updated_at", ] )