add gamification characters (Duolingo-style mascot catalog)

New progress.Character model: an admin-managed catalog (same pattern as
subscription.Plan) of selectable mascots, each unlocked once the user's
XP crosses unlock_xp. No separate ownership table yet — unlocking is a
pure function of UserStats.xp, kept intentionally minimal since this is
the first of an incremental rollout the senior asked for.

- GET /api/characters — catalog with per-user unlocked/selected flags
- POST /api/characters/select — picks an active character, rejecting
  ones the user hasn't unlocked yet
- UserStats.selected_character_id tracks the pick
- seed_test_data now seeds 4 sample characters (owl/fox/turtle/lion)
  for local/staging testing

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 13:53:17 +03:30
parent b5476e1c5d
commit 37a1741e0e
9 changed files with 266 additions and 6 deletions

View File

@@ -134,6 +134,9 @@ class Command(BaseCommand):
BugReport.objects.filter(user_id__in=user_ids).delete()
FamilyLink.objects.filter(parent_user_id__in=user_ids).delete()
from progress.models import Character
Character.objects.filter(slug__startswith='seed-').delete()
Plan.objects.filter(title__startswith=SEED_TAG).delete()
DiscountCode.objects.filter(code='SEEDTEST10').delete()
Offer.objects.filter(code='SEEDTRIAL7').delete()
@@ -153,6 +156,7 @@ class Command(BaseCommand):
def _seed(self):
main_user, guest_user, parent_user, child_user, referred_user = self._seed_accounts()
self._seed_content_and_progress(main_user)
self._seed_characters()
self._seed_ai_tutor(main_user)
plan_monthly, plan_yearly = self._seed_subscription_and_payment(main_user)
self._seed_discount(main_user)
@@ -303,6 +307,26 @@ class Command(BaseCommand):
'content/progress: 1 پایه، 1 مبحث، 2 Node، 2 درس، 2 سوال و پیشرفتِ کاربرِ اصلی آماده شد.'
))
# -- گیمیفیکیشن: کاراکترها -------------------------------------------------
def _seed_characters(self):
from progress.models import Character
# unlock_xp پلکانی، هماهنگ با xp=10ی کاربرِ اصلی که بالاتر ساخته شد:
# فقط اولین کاراکتر (unlock_xp=0) براش باز است، بقیه هنوز قفلن —
# برای این‌که هم مسیرِ «باز» و هم مسیرِ «قفل» در تست دستی قابل‌دیدن باشه.
characters = [
('seed-owl', f'{SEED_TAG} جغد کنجکاو', 'owl', 0, 1),
('seed-fox', f'{SEED_TAG} روباه باهوش', 'fox', 100, 2),
('seed-turtle', f'{SEED_TAG} لاک‌پشت صبور', 'turtle', 300, 3),
('seed-lion', f'{SEED_TAG} شیر شجاع', 'lion', 600, 4),
]
for slug, name, icon, unlock_xp, order in characters:
Character.objects.get_or_create(
slug=slug, defaults=dict(name=name, icon=icon, unlock_xp=unlock_xp, order=order),
)
self.stdout.write(self.style.SUCCESS(f'progress: {len(characters)} کاراکترِ گیمیفیکیشن آماده شد.'))
# -- ai_tutor -------------------------------------------------------------
def _seed_ai_tutor(self, main_user):
from ai_tutor.models import Conversation, Message, MessageFeedback