Django/DRF + PostgreSQL/Redis/Celery backend with accounts, library, moments, and notifications apps; React/TypeScript/Vite frontend with Instagram-style sidebar nav, feed, profiles, nested comment replies, notifications, and photo/video upload with ffmpeg-generated posters. Co-Authored-By: Claude Sonnet 5 <[email protected]>
124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
from django.conf import settings
|
|
from django.db import models
|
|
|
|
from apps.core.models import Audience, TimeStampedModel
|
|
|
|
|
|
def library_upload_path(instance, filename):
|
|
return f"library/{instance.owner_id}/originals/{filename}"
|
|
|
|
|
|
def rendition_upload_path(instance, filename):
|
|
return f"library/{instance.library_item.owner_id}/renditions/{instance.kind}/{filename}"
|
|
|
|
|
|
class LibraryItem(TimeStampedModel):
|
|
"""A single photo or video backed up to a user's private library.
|
|
Nothing here is visible to anyone but the owner until it is attached to
|
|
a Moment or a shared Album."""
|
|
|
|
class MediaType(models.TextChoices):
|
|
PHOTO = "photo", "Photo"
|
|
VIDEO = "video", "Video"
|
|
|
|
class ProcessingStatus(models.TextChoices):
|
|
PENDING = "pending", "Pending"
|
|
PROCESSING = "processing", "Processing"
|
|
READY = "ready", "Ready"
|
|
FAILED = "failed", "Failed"
|
|
|
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="library_items", on_delete=models.CASCADE)
|
|
file = models.FileField(upload_to=library_upload_path)
|
|
media_type = models.CharField(max_length=10, choices=MediaType.choices)
|
|
original_filename = models.CharField(max_length=255)
|
|
file_size = models.PositiveBigIntegerField(default=0)
|
|
width = models.PositiveIntegerField(null=True, blank=True)
|
|
height = models.PositiveIntegerField(null=True, blank=True)
|
|
duration_seconds = models.FloatField(null=True, blank=True)
|
|
|
|
captured_at = models.DateTimeField(null=True, blank=True)
|
|
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
|
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
|
camera_make = models.CharField(max_length=100, blank=True)
|
|
camera_model = models.CharField(max_length=100, blank=True)
|
|
|
|
checksum = models.CharField(max_length=64, db_index=True, help_text="sha256 of the original file, used for dedup")
|
|
processing_status = models.CharField(
|
|
max_length=12, choices=ProcessingStatus.choices, default=ProcessingStatus.PENDING
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = ("owner", "checksum")
|
|
ordering = ["-captured_at", "-created_at"]
|
|
|
|
def __str__(self):
|
|
return f"{self.original_filename} ({self.owner})"
|
|
|
|
|
|
class MediaRendition(models.Model):
|
|
"""A derived, smaller version of a LibraryItem (thumbnail/feed/large),
|
|
generated asynchronously so the original never has to be served to the
|
|
browser just to render a grid."""
|
|
|
|
class Kind(models.TextChoices):
|
|
THUMBNAIL = "thumbnail", "Thumbnail"
|
|
FEED = "feed", "Feed"
|
|
LARGE = "large", "Large / Fullscreen"
|
|
|
|
library_item = models.ForeignKey(LibraryItem, related_name="renditions", on_delete=models.CASCADE)
|
|
kind = models.CharField(max_length=10, choices=Kind.choices)
|
|
file = models.FileField(upload_to=rendition_upload_path)
|
|
width = models.PositiveIntegerField()
|
|
height = models.PositiveIntegerField()
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ("library_item", "kind")
|
|
|
|
def __str__(self):
|
|
return f"{self.library_item_id} - {self.kind}"
|
|
|
|
|
|
class Album(TimeStampedModel):
|
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="albums", on_delete=models.CASCADE)
|
|
name = models.CharField(max_length=150)
|
|
description = models.TextField(blank=True)
|
|
cover_item = models.ForeignKey(
|
|
LibraryItem, related_name="+", on_delete=models.SET_NULL, null=True, blank=True
|
|
)
|
|
audience = models.CharField(max_length=10, choices=Audience.choices, default=Audience.ONLY_ME)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class AlbumItem(models.Model):
|
|
album = models.ForeignKey(Album, related_name="items", on_delete=models.CASCADE)
|
|
library_item = models.ForeignKey(LibraryItem, related_name="album_items", on_delete=models.CASCADE)
|
|
added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
|
|
added_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ("album", "library_item")
|
|
ordering = ["-added_at"]
|
|
|
|
|
|
class AlbumCollaborator(models.Model):
|
|
class Role(models.TextChoices):
|
|
VIEWER = "viewer", "Viewer"
|
|
CONTRIBUTOR = "contributor", "Contributor"
|
|
ADMIN = "admin", "Admin"
|
|
|
|
class Status(models.TextChoices):
|
|
PENDING = "pending", "Pending"
|
|
ACCEPTED = "accepted", "Accepted"
|
|
|
|
album = models.ForeignKey(Album, related_name="collaborators", on_delete=models.CASCADE)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="album_invites", on_delete=models.CASCADE)
|
|
role = models.CharField(max_length=11, choices=Role.choices, default=Role.VIEWER)
|
|
invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
|
|
status = models.CharField(max_length=8, choices=Status.choices, default=Status.PENDING)
|
|
|
|
class Meta:
|
|
unique_together = ("album", "user")
|