""" User models for KeepItGoing. """ import uuid from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): """ Custom user model for KeepItGoing. Uses email as the primary identifier instead of username. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField(unique=True) # Profile fields timezone = models.CharField(max_length=50, default='UTC') avatar = models.ImageField(upload_to='avatars/', null=True, blank=True) # Preferences default_reminder_minutes = models.IntegerField(default=30) email_notifications = models.BooleanField(default=True) push_notifications = models.BooleanField(default=True) # Email verification email_verified = models.BooleanField(default=False) email_verified_at = models.DateTimeField(null=True, blank=True) # Admin approval is_approved = models.BooleanField(default=False) approved_by = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_users', limit_choices_to={'is_staff': True} ) approved_at = models.DateTimeField(null=True, blank=True) # Timestamps created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # Use email as the username field USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] class Meta: db_table = 'users' verbose_name = 'user' verbose_name_plural = 'users' def __str__(self): return self.email @classmethod def from_db(cls, db, field_names, values): instance = super().from_db(db, field_names, values) instance._loaded_default_reminder_minutes = dict(zip(field_names, values)).get('default_reminder_minutes') return instance def save(self, *args, **kwargs): loaded_reminder_minutes = getattr(self, '_loaded_default_reminder_minutes', None) reminder_minutes_changed = ( loaded_reminder_minutes is not None and loaded_reminder_minutes != self.default_reminder_minutes ) super().save(*args, **kwargs) self._loaded_default_reminder_minutes = self.default_reminder_minutes if reminder_minutes_changed: self.reschedule_reminder_notifications() def reschedule_reminder_notifications(self): """ Recompute "before due" reminders for active tasks using the current default_reminder_minutes. Task.reschedule_reminders() only captures this setting at the moment a task's own due_date/due_time is set, so it doesn't apply retroactively on its own - this is what makes a profile-level change to the setting reach existing tasks. Tasks with their own explicit reminder_at override are left alone. """ tasks = self.tasks.filter( due_date__isnull=False, is_deleted=False, reminder_at__isnull=True, ).exclude(status__in=('completed', 'cancelled')) for task in tasks: task.reschedule_reminders() class DeviceToken(models.Model): """ Stores push notification tokens for user devices. """ PLATFORM_CHOICES = [ ('android', 'Android'), ('web', 'Web'), ('desktop', 'Desktop'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='device_tokens') platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES) token = models.TextField() device_name = models.CharField(max_length=255, blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'device_tokens' unique_together = ['user', 'token'] def __str__(self): return f"{self.user.email} - {self.platform} - {self.device_name}" class EmailVerificationToken(models.Model): """ Token for email verification. Uses UUID tokens with expiration for security. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='verification_tokens') token = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() is_used = models.BooleanField(default=False) used_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = 'email_verification_tokens' ordering = ['-created_at'] def is_expired(self): from django.utils import timezone return timezone.now() > self.expires_at def __str__(self): return f"Verification token for {self.user.email}"