Initial commit - clean project structure
This commit is contained in:
164
content/models.py
Normal file
164
content/models.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Grade(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
icon = models.CharField(max_length=100)
|
||||
color = models.CharField(max_length=10)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Unit(models.Model):
|
||||
grade = models.ForeignKey(Grade, on_delete=models.CASCADE, related_name='units')
|
||||
title = models.CharField(max_length=100)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Node(models.Model):
|
||||
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, related_name='nodes')
|
||||
title = models.CharField(max_length=150)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Lesson(models.Model):
|
||||
node = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='lessons')
|
||||
title = models.CharField(max_length=150)
|
||||
intro_text = models.TextField()
|
||||
mascot_animation = models.CharField(max_length=50, default='explain')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Question(models.Model):
|
||||
TYPE_CHOICES = [('multiple_choice', 'چهارگزینهای'), ('count_objects', 'شمارش اشیاء')]
|
||||
|
||||
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='questions')
|
||||
type = models.CharField(max_length=30, choices=TYPE_CHOICES)
|
||||
prompt = models.TextField()
|
||||
asset_group = models.CharField(max_length=100, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.prompt[:50]
|
||||
|
||||
|
||||
class Choice(models.Model):
|
||||
"""
|
||||
⚠️ اصلاح نسبت به سند اولیه: دیاگرام معماری فقط فیلد `value` رو برای
|
||||
Choice مشخص کرده بود و فیلد تشخیص گزینهی درست (is_correct) رو نداشت
|
||||
(این نکته توی سند models هم صراحتاً بهعنوان یک ایراد/نقص علامتگذاری
|
||||
شده بود: "یا این فیلد سهواً جا افتاده یا تشخیص جای دیگهای انجام میشه").
|
||||
چون بدون این فیلد امکان پیادهسازی POST /api/questions/{id}/answer
|
||||
(که باید correct را برگردونه) وجود نداره، این فیلد اینجا اضافه شده.
|
||||
اگه منطق تشخیص پاسخ درست قراره جای دیگهای (سرویس content-authoring)
|
||||
انجام بشه، این فیلد رو میشه بعداً حذف/جابهجا کرد.
|
||||
"""
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='choices')
|
||||
value = models.CharField(max_length=100)
|
||||
is_correct = models.BooleanField(default=False) # <-- اصلاح/افزوده شده
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class UserLessonCompletion(models.Model):
|
||||
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
||||
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='completions')
|
||||
correct_count = models.PositiveIntegerField()
|
||||
stars = models.PositiveSmallIntegerField()
|
||||
xp_earned = models.PositiveIntegerField(default=0)
|
||||
coins_earned = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"completion-user{self.user_id}-lesson{self.lesson_id}"
|
||||
|
||||
|
||||
class NodeProgress(models.Model):
|
||||
"""
|
||||
وضعیتِ پیشرفتِ هر کاربر روی هر Node (نه Lesson و نه Unit).
|
||||
|
||||
چرا سطح Node و نه Lesson/Unit:
|
||||
- Lesson خیلی ریزه: هر Lesson خودش با UserLessonCompletion دقیقاً
|
||||
«انجامشده / نشده» ثبت میشه؛ مفهوم «نصفهرها» برای یک درسِ چند
|
||||
دقیقهای معنی نداره (یا تمومش کرده یا نه).
|
||||
- Unit خیلی کلیه (چند Node/چند هفته درس)؛ اگه status روی این سطح
|
||||
باشه، کاربری که یک هفته از ۳ هفتهی یک Unit رو غیرفعال بوده هم
|
||||
«نصفهرها» میشه که گمراهکنندهست.
|
||||
- Node (چند Lesson مرتبط، معادل «مبحث»/skill) دقیقاً سطحیه که
|
||||
اپهای مشابه (Duolingo: پیشرفتِ هر skill/unit روی نقشهی مهارت،
|
||||
Khan Academy: mastery روی هر topic/skill) روش status نشون
|
||||
میدن؛ برای اپِ ریاضیِ ابتدایی هم واحدِ معنادار «مبحث» همینه.
|
||||
|
||||
استاتوسها (طبق درخواست: تکمیلشده / نصفهرها / در حال انجام؛ + یک
|
||||
حالت پایهی چهارم که در اپهای مشابه هم هست):
|
||||
- not_started: هنوز هیچ فعالیتی (نه پاسخ به سوال، نه تکمیل درس)
|
||||
روی هیچکدوم از Lessonهای این Node نداشته. (معادل 'locked'/
|
||||
'unlocked بدون فعالیت' در نقشهی مهارتِ HomeView؛ اون یکی برای
|
||||
UI نمایش نقشهست، این یکی برای ردیابی/گزارشگیریه — عمداً جدا
|
||||
نگه داشته شده تا منطق قفل/باز نقشه به این وابسته نشه.)
|
||||
- in_progress: حداقل یک فعالیت داشته ولی هنوز همهی Lessonهای
|
||||
Node تکمیل نشدن.
|
||||
- completed: همهی Lessonهای Node تکمیل شدن.
|
||||
- abandoned («نصفهرها»): در حال انجام بوده ولی به مدت
|
||||
NODE_PROGRESS_ABANDON_DAYS (پیشفرض ۱۴ روز — بلندتر از حالت
|
||||
معمولِ اپهای مشابه چون با توجه به شرایط اینترنت/دیتای موبایل در
|
||||
ایران، وقفهی چند روزهی کاربر لزوماً به معنی رهاکردنِ درس نیست؛
|
||||
تشخیصش با management command زمانبندیشده، نه سیگنال، چون به
|
||||
گذر زمانِ بدون رویداد نیاز داره) فعالیتی نداشته. اگه دوباره
|
||||
فعالیت کنه، برمیگرده به in_progress.
|
||||
"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('not_started', 'شروعنشده'),
|
||||
('in_progress', 'در حال انجام'),
|
||||
('completed', 'تکمیلشده'),
|
||||
('abandoned', 'نصفهرها'),
|
||||
]
|
||||
|
||||
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
||||
node = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='progress_records')
|
||||
status = models.CharField(max_length=15, choices=STATUS_CHOICES, default='not_started')
|
||||
lessons_completed_count = models.PositiveIntegerField(default=0)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
# آخرین فعالیت (پاسخ به سوال یا تکمیل درس) روی این Node — مبنای تشخیص
|
||||
# abandoned. عمداً بهجای auto_now دستی ست میشه (فقط داخل content/services.py).
|
||||
last_activity_at = models.DateTimeField(null=True, blank=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['user_id', 'node'], name='unique_node_progress_per_user')
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'last_activity_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"node-progress-user{self.user_id}-node{self.node_id}-{self.status}"
|
||||
|
||||
|
||||
class UserAnswer(models.Model):
|
||||
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers')
|
||||
choice = models.ForeignKey(Choice, on_delete=models.CASCADE, related_name='answers')
|
||||
correct = models.BooleanField()
|
||||
xp_earned = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"answer-user{self.user_id}-q{self.question_id}"
|
||||
Reference in New Issue
Block a user