mirror of
https://github.com/barkeser2002/offline-db.git
synced 2026-09-25 01:40:07 +03:00
- Implement Follow and UserAnimeList models to allow user following and anime list tracking (watchlist, completed, dropped). - Add `is_public` boolean field to User model for privacy control. - Create serializers and ViewSets (FollowViewSet, UserAnimeListViewSet) with correct object-level permissions (IsOwnerOrReadOnly) to prevent IDOR vulnerabilities. - Add `ActivityFeedViewSet` to aggregate recent badge and watch log activities of followed users. - Add DRF routers in main `urls.py`. - Include `test_social.py` with comprehensive API tests. - Resolve migration conflict automatically using `--merge`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
125 lines
4.6 KiB
Python
125 lines
4.6 KiB
Python
from django.db import models
|
|
from django.contrib.auth.models import AbstractUser
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from django.core.validators import RegexValidator
|
|
|
|
from django.utils.deconstruct import deconstructible
|
|
|
|
@deconstructible
|
|
class UsernameValidator(RegexValidator):
|
|
regex = r'^[\w-]+$'
|
|
message = _("Enter a valid username. This value may contain only letters, numbers, and _/- characters.")
|
|
flags = 0
|
|
|
|
class User(AbstractUser):
|
|
username_validator = UsernameValidator()
|
|
username = models.CharField(
|
|
_("username"),
|
|
max_length=150,
|
|
unique=True,
|
|
help_text=_("Required. 150 characters or fewer. Letters, digits, and _/- only."),
|
|
validators=[username_validator],
|
|
error_messages={
|
|
"unique": _("A user with that username already exists."),
|
|
},
|
|
)
|
|
|
|
is_premium = models.BooleanField(default=False, verbose_name=_("Premium Status"))
|
|
bio = models.TextField(_("bio"), blank=True, max_length=500)
|
|
is_public = models.BooleanField(default=True, verbose_name=_("Public Profile"))
|
|
|
|
def __str__(self):
|
|
return self.username
|
|
|
|
class Wallet(models.Model):
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='wallet')
|
|
balance = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name=_("Balance"))
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username}'s Wallet: {self.balance:.2f}"
|
|
|
|
class WatchLog(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='watch_logs')
|
|
episode = models.ForeignKey('content.Episode', on_delete=models.CASCADE, related_name='watch_logs')
|
|
duration = models.PositiveIntegerField(help_text=_("Duration watched in seconds"))
|
|
watched_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=['user', 'watched_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} watched {self.episode} for {self.duration}s"
|
|
|
|
class Badge(models.Model):
|
|
slug = models.SlugField(unique=True, help_text=_("Unique identifier for the badge logic"))
|
|
name = models.CharField(max_length=100)
|
|
description = models.TextField()
|
|
icon_url = models.URLField(blank=True, null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class UserBadge(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='badges')
|
|
badge = models.ForeignKey(Badge, on_delete=models.CASCADE)
|
|
awarded_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ('user', 'badge')
|
|
verbose_name = _("User Badge")
|
|
verbose_name_plural = _("User Badges")
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.badge.name}"
|
|
|
|
class Notification(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
|
title = models.CharField(max_length=255)
|
|
message = models.TextField()
|
|
link = models.URLField(blank=True, null=True)
|
|
is_read = models.BooleanField(default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['user', 'is_read']),
|
|
models.Index(fields=['user', '-created_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Notification for {self.user.username}: {self.title}"
|
|
|
|
class Follow(models.Model):
|
|
follower = models.ForeignKey(User, related_name='following', on_delete=models.CASCADE)
|
|
following = models.ForeignKey(User, related_name='followers', on_delete=models.CASCADE)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ('follower', 'following')
|
|
|
|
def __str__(self):
|
|
return f"{self.follower.username} follows {self.following.username}"
|
|
|
|
class UserAnimeList(models.Model):
|
|
STATUS_CHOICES = [
|
|
('watchlist', 'Watchlist'),
|
|
('completed', 'Completed'),
|
|
('dropped', 'Dropped'),
|
|
]
|
|
user = models.ForeignKey(User, related_name='anime_lists', on_delete=models.CASCADE)
|
|
anime = models.ForeignKey('content.Anime', related_name='user_lists', on_delete=models.CASCADE)
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='watchlist')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
unique_together = ('user', 'anime')
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.anime.title} ({self.get_status_display()})"
|