""" Subject taxonomy: curated hierarchical Topics and community Tags. * ``Topic`` — a curated tree (e.g. Mathematics → Algebra → Polynomials) maintained by moderators; courses and problems hang off topics for navigation and prerequisite graphs. * ``Tag`` — flat, community-applied labels for fine-grained discovery (e.g. "induction", "dynamic-programming"). ``usage_count`` is denormalized and maintained by the service layer for fast popular-tag queries. """ from django.conf import settings from django.db import models from apps.core.models import TimeStampedModel, UUIDModel class Topic(UUIDModel, TimeStampedModel): parent = models.ForeignKey( "self", on_delete=models.PROTECT, null=True, blank=True, related_name="children", ) name = models.CharField(max_length=120) slug = models.SlugField(max_length=140, unique=True) description = models.TextField(blank=True) icon = models.CharField( max_length=64, blank=True, help_text="Icon identifier for the frontend." ) order = models.PositiveIntegerField( default=0, help_text="Sort position among siblings." ) class Meta: ordering = ["order", "name"] indexes = [ models.Index(fields=["parent", "order"], name="topic_parent_order_idx"), ] constraints = [ models.CheckConstraint( condition=models.Q(parent__isnull=True) | ~models.Q(parent=models.F("id")), name="topic_not_own_parent", ), ] def __str__(self) -> str: return self.name class Tag(UUIDModel, TimeStampedModel): name = models.CharField(max_length=80) slug = models.SlugField(max_length=100, unique=True) description = models.TextField(blank=True) topic = models.ForeignKey( Topic, on_delete=models.SET_NULL, null=True, blank=True, related_name="tags" ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) is_curated = models.BooleanField( default=False, help_text="Curated tags are protected from renaming/merging." ) usage_count = models.PositiveIntegerField(default=0, db_index=True) class Meta: ordering = ["-usage_count", "name"] def __str__(self) -> str: return self.name