mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 00:47:02 +01:00
refactor
This commit is contained in:
1 parent
5e3474dd59
commit
e4d59a13ba
119 files changed
+15490
-9975
No files matched your search
@@ -0,0 +1,22 @@
|
||||
from api.models.flavor import Category, Flavor
|
||||
from api.models.rating import Rating, Reply
|
||||
from api.models.social import Notification, ProfileComment
|
||||
from api.models.support import Ticket, TicketMessage
|
||||
from api.models.system import Banner, Job, SystemConfig
|
||||
from api.models.user import User, UserIP
|
||||
|
||||
__all__ = [
|
||||
"Banner",
|
||||
"Category",
|
||||
"Flavor",
|
||||
"Job",
|
||||
"Notification",
|
||||
"ProfileComment",
|
||||
"Rating",
|
||||
"Reply",
|
||||
"SystemConfig",
|
||||
"Ticket",
|
||||
"TicketMessage",
|
||||
"User",
|
||||
"UserIP",
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
from django.db import models
|
||||
from django.db.models import Avg
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Categories"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class Flavor(models.Model):
|
||||
category = models.ForeignKey(Category, related_name="flavors", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
image_url = models.URLField(max_length=1000, blank=True, null=True)
|
||||
image_urls = models.JSONField(blank=True, default=list)
|
||||
image = models.ImageField(upload_to="flavors/", blank=True, null=True)
|
||||
shop_url = models.URLField(max_length=500, blank=True, null=True)
|
||||
is_available = models.BooleanField(default=True)
|
||||
is_legacy = models.BooleanField(default=False)
|
||||
external_id = models.BigIntegerField(unique=True, null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "category"],
|
||||
name="unique_flavor_name_per_category",
|
||||
)
|
||||
]
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} ({self.category.name})"
|
||||
|
||||
@property
|
||||
def get_average_rating(self) -> float:
|
||||
result = self.ratings.aggregate(avg=Avg("score"))["avg"]
|
||||
return result or 0.0
|
||||
|
||||
@property
|
||||
def cached_image_url(self) -> str | None:
|
||||
if self.image:
|
||||
return self.image.url
|
||||
return self.image_url
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
user = models.ForeignKey("api.User", related_name="ratings", on_delete=models.CASCADE)
|
||||
flavor = models.ForeignKey("api.Flavor", related_name="ratings", on_delete=models.CASCADE)
|
||||
score = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
|
||||
comment = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "flavor")
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user.username} - {self.flavor.name}: {self.score}"
|
||||
|
||||
|
||||
class Reply(models.Model):
|
||||
user = models.ForeignKey("api.User", related_name="replies", on_delete=models.CASCADE)
|
||||
rating = models.ForeignKey(Rating, related_name="replies", on_delete=models.CASCADE)
|
||||
text = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["created_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Reply by {self.user.username} on {self.rating.id}"
|
||||
@@ -0,0 +1,45 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
("reply", "Reply"),
|
||||
("mention", "Mention"),
|
||||
("follow", "Follow"),
|
||||
("ticket_new", "New Ticket"),
|
||||
("ticket_reply", "Ticket Reply"),
|
||||
("profile_comment", "Profile Comment"),
|
||||
]
|
||||
recipient = models.ForeignKey(
|
||||
"api.User", related_name="notifications", on_delete=models.CASCADE
|
||||
)
|
||||
actor = models.ForeignKey("api.User", related_name="actions", on_delete=models.CASCADE)
|
||||
notification_type = models.CharField(max_length=20, choices=TYPE_CHOICES)
|
||||
rating = models.ForeignKey("api.Rating", on_delete=models.CASCADE, null=True, blank=True)
|
||||
reply = models.ForeignKey("api.Reply", on_delete=models.CASCADE, null=True, blank=True)
|
||||
ticket = models.ForeignKey("api.Ticket", on_delete=models.CASCADE, null=True, blank=True)
|
||||
profile_comment = models.ForeignKey(
|
||||
"api.ProfileComment", on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
is_read = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.notification_type} for {self.recipient.username}"
|
||||
|
||||
|
||||
class ProfileComment(models.Model):
|
||||
profile_owner = models.ForeignKey(
|
||||
"api.User", related_name="profile_comments", on_delete=models.CASCADE
|
||||
)
|
||||
author = models.ForeignKey(
|
||||
"api.User", related_name="authored_profile_comments", on_delete=models.CASCADE
|
||||
)
|
||||
text = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
@@ -0,0 +1,29 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Ticket(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
("open", "Open"),
|
||||
("in_progress", "In Progress"),
|
||||
("resolved", "Resolved"),
|
||||
("closed", "Closed"),
|
||||
]
|
||||
user = models.ForeignKey("api.User", related_name="tickets", on_delete=models.CASCADE)
|
||||
subject = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
|
||||
class TicketMessage(models.Model):
|
||||
ticket = models.ForeignKey(Ticket, related_name="messages", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey("api.User", on_delete=models.CASCADE)
|
||||
text = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["created_at"]
|
||||
@@ -0,0 +1,77 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Banner(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
is_active = models.BooleanField(
|
||||
default=False, help_text="Designates this as the global default banner."
|
||||
)
|
||||
is_enabled = models.BooleanField(default=True, help_text="Allows users to select this banner.")
|
||||
settings = models.JSONField(default=dict, blank=True)
|
||||
schema = models.JSONField(default=list, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_active:
|
||||
Banner.objects.filter(is_active=True).exclude(pk=self.pk).update(is_active=False)
|
||||
elif not Banner.objects.filter(is_active=True).exclude(pk=self.pk).exists():
|
||||
self.is_active = True
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class SystemConfig(models.Model):
|
||||
"""Singleton model for application-wide settings editable via UI."""
|
||||
|
||||
site_name = models.CharField(max_length=100, default="Holy Flavors Archive")
|
||||
maintenance_mode = models.BooleanField(default=False)
|
||||
allow_new_signups = models.BooleanField(default=True)
|
||||
require_email_verification = models.BooleanField(default=True)
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "System Configuration"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_solo(cls) -> "SystemConfig":
|
||||
obj, _ = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
|
||||
class Job(models.Model):
|
||||
"""Tracks background jobs, their status and scheduling."""
|
||||
|
||||
JOB_TYPES = [
|
||||
("sync_flavors", "Sync Holy Flavors"),
|
||||
("cleanup_duplicates", "Cleanup Duplicate Flavors"),
|
||||
("backup_db", "Database Backup"),
|
||||
("seed_legacy", "Seed Legacy Data"),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
("pending", "Pending"),
|
||||
("running", "Running"),
|
||||
("completed", "Completed"),
|
||||
("failed", "Failed"),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100, choices=JOB_TYPES)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
|
||||
last_run = models.DateTimeField(null=True, blank=True)
|
||||
next_run = models.DateTimeField(null=True, blank=True)
|
||||
interval_hours = models.IntegerField(default=0, help_text="Set to 0 to disable periodic run.")
|
||||
|
||||
last_output = models.TextField(blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.get_name_display()} ({self.status})"
|
||||
@@ -0,0 +1,72 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
THEME_CHOICES = [
|
||||
("holy_light", "Holy Light"),
|
||||
("holy_dark", "Holy Dark"),
|
||||
("latte", "Catppuccin Latte"),
|
||||
("frappe", "Catppuccin Frappé"),
|
||||
("macchiato", "Catppuccin Macchiato"),
|
||||
("mocha", "Catppuccin Mocha"),
|
||||
("pink_pastel", "Pink Pastel"),
|
||||
("mint_pastel", "Mint Pastel"),
|
||||
("lavender_pastel", "Lavender Pastel"),
|
||||
("dracula", "Dracula"),
|
||||
("nord", "Nordic Frost"),
|
||||
("gruvbox", "Gruvbox Retro"),
|
||||
("oceanic", "Oceanic Deep"),
|
||||
("t0p_sai", "SAI (T0P)"),
|
||||
("t0p_trench", "Trench (T0P)"),
|
||||
("t0p_blurryface", "Blurryface (T0P)"),
|
||||
("t0p_clancy", "Clancy (T0P)"),
|
||||
]
|
||||
LANGUAGE_CHOICES = [
|
||||
("en", "English"),
|
||||
("de", "Deutsch"),
|
||||
]
|
||||
theme = models.CharField(max_length=20, choices=THEME_CHOICES, default="holy_light")
|
||||
language = models.CharField(max_length=2, choices=LANGUAGE_CHOICES, default="en")
|
||||
avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)
|
||||
pending_email = models.EmailField(max_length=254, blank=True, null=True)
|
||||
email_confirmation_code = models.CharField(max_length=6, blank=True, null=True)
|
||||
following = models.ManyToManyField(
|
||||
"self", symmetrical=False, related_name="followers", blank=True
|
||||
)
|
||||
selected_banner = models.ForeignKey(
|
||||
"api.Banner",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="users",
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.avatar:
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(self.avatar.path)
|
||||
if img.height > 256 or img.width > 256 or img.format != "JPEG":
|
||||
output_size = (256, 256)
|
||||
img.thumbnail(output_size)
|
||||
if img.mode in ("RGBA", "P"):
|
||||
img = img.convert("RGB")
|
||||
img.save(self.avatar.path, "JPEG", quality=90)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.username
|
||||
|
||||
|
||||
class UserIP(models.Model):
|
||||
user = models.ForeignKey(User, related_name="ips", on_delete=models.CASCADE)
|
||||
ip_address = models.GenericIPAddressField()
|
||||
last_login = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "ip_address")
|
||||
ordering = ["-last_login"]
|
||||
Reference in new issue
Block a user