POST /api/auth/session is gone (404). Existing is_guest rows keep refresh and upgrade-by-phone until the demo/guest rows are purged; the code that serves them is listed in docs/security-before-production.md §10. Tests that used the endpoint as a fixture now use accounts/testing.guest_session. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
460 lines
23 KiB
Python
460 lines
23 KiB
Python
"""
|
||
یک دیتاست کوچک و واقعی برای تست دستی/API لوکال یا staging میسازه، طوری که
|
||
اجرای مکرر (repeat) هیچ رکورد تکراریای نسازه — همهچیز با get_or_create روی
|
||
یک کلید طبیعی/یکتا (phone، device_id، code، idempotency_key و ...) انجام
|
||
میشه، نه با create ساده.
|
||
|
||
هدف: پوشش کمینهی جریانهای اصلی API در هر ۱۰ اپ (accounts، content،
|
||
progress، ai_tutor، subscription، payment، discount، notifications،
|
||
affiliate، support) با کمترین حجم دادهی لازم برای اینکه هر
|
||
endpoint اصلی بدون نیاز به ساختن دستی پیشنیازهاش قابل تست باشه.
|
||
|
||
⚠️ این دستور فقط برای محیط لوکال/staging است، نه پروداکشن:
|
||
- هیچ سکرت واقعی (کلید API واقعی کاوهنگار، gateway واقعی و ...) نمیسازه؛
|
||
Order/Transaction با provider/gateway_ref_id ساختگی و مقدارِ ثابت ساخته
|
||
میشن (شبیهسازیِ یک پرداختِ موفق، نه فراخوانیِ واقعیِ درگاه).
|
||
- برای جلوگیری از اجرای تصادفی روی یک دیتابیسِ پروداکشن، پیشفرض فقط وقتی
|
||
DEBUG=True اجرا میشه؛ برای اجرای صریح در حالت DEBUG=False (مثلاً یک
|
||
محیط staging که عمداً DEBUG خاموش نگه داشته میشه) باید `--force` بدید.
|
||
|
||
اجرا:
|
||
|
||
python manage.py seed_test_data
|
||
python manage.py seed_test_data --force # وقتی DEBUG=False (staging)
|
||
python manage.py seed_test_data --reset # پاککردن دیتای قبلیِ همین seed و ساختِ دوباره
|
||
|
||
idempotent است: اجرای دوباره (بدون --reset) نه رکورد تکراری میسازه و نه
|
||
خطا میده؛ فقط همون رکوردهای قبلی رو پیدا میکنه (get_or_create) و دوباره
|
||
گزارششون میده. تمام رکوردهای ساختهشده با پیشوند «[SEED]» در فیلدهای
|
||
متنیِ قابلمشاهده (عنوان/نام) مشخص شدن تا با دیتای واقعی اشتباه گرفته
|
||
نشن و پاکسازیِ دستی هم راحت باشه.
|
||
"""
|
||
from datetime import timedelta
|
||
|
||
from django.conf import settings
|
||
from django.core.management.base import BaseCommand, CommandError
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import OTPCode, Profile, Session, User, hash_refresh_token
|
||
|
||
# --- شناسههای ثابتِ دادهی seed --------------------------------------------
|
||
# شمارههای 0912000000X عمداً خارج از بازهی واقعیِ اپراتورهای ایران نیستن؛
|
||
# چون این فقط دیتای لوکال/staging است نه پروداکشن، همین کافیه. با پیشوند
|
||
# یکسان و بازهی اختصاصی، شناساییِ رکوردهای seed در دیتابیس هم ساده میمونه.
|
||
MAIN_PHONE = '09120000001'
|
||
REFERRED_PHONE = '09120000004'
|
||
GUEST_DEVICE_ID = 'seed-guest-device-0001'
|
||
|
||
SEED_TAG = '[SEED]'
|
||
|
||
|
||
class Command(BaseCommand):
|
||
help = (
|
||
'یک دیتاست کوچک و idempotent برای تستِ دستی/API روی محیطِ لوکال یا '
|
||
'staging میسازه (کاربر، محتوای درسی، پیشرفت، اشتراک/پرداخت، تخفیف، '
|
||
'دستیار هوش مصنوعی، اعلان، افیلیت، پشتیبانی، خانواده). اجرای مکرر '
|
||
'رکورد تکراری نمیسازه.'
|
||
)
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument(
|
||
'--force',
|
||
action='store_true',
|
||
help='اجرای دستور حتی وقتی DEBUG=False است (مثلاً staging عمدی).',
|
||
)
|
||
parser.add_argument(
|
||
'--reset',
|
||
action='store_true',
|
||
help='قبل از ساخت، فقط همون رکوردهای seed (بر اساس شماره/شناسههای ثابتِ بالا) حذف میشه.',
|
||
)
|
||
|
||
def handle(self, *args, **options):
|
||
if not settings.DEBUG and not options['force']:
|
||
raise CommandError(
|
||
'DEBUG=False است — این دستور برای دیتای پروداکشن نیست. اگه '
|
||
'واقعاً میخواید روی این محیط (مثلاً staging) اجرا بشه، '
|
||
'--force بدید.'
|
||
)
|
||
|
||
with transaction.atomic():
|
||
if options['reset']:
|
||
self._reset()
|
||
summary = self._seed()
|
||
|
||
self.stdout.write(self.style.SUCCESS('\nseed_test_data با موفقیت تمام شد.'))
|
||
self.stdout.write('\nکاربرهای تستی (برای Authorize در Swagger، فقط خودِ token را بدون "Bearer" وارد کنید):')
|
||
for label, user, token in summary['tokens']:
|
||
self.stdout.write(f' - {label}: user_id={user.pk} phone={user.phone or "-"} token={token}')
|
||
self.stdout.write(
|
||
'\nنکته: این توکنها با ACCESS_TOKEN_TTL_SECONDS فعلی امضا شدن و بعد از انقضا '
|
||
'باید دوباره seed یا از /api/auth/login گرفته بشن.'
|
||
)
|
||
|
||
# -- reset ----------------------------------------------------------
|
||
def _reset(self):
|
||
"""فقط دقیقاً همون رکوردهایی که این دستور میسازه رو (بر اساس کلیدهای ثابتِ بالا) حذف میکنه."""
|
||
from ai_tutor.models import Conversation
|
||
from affiliate.models import AffiliateAccount, Conversion
|
||
from content.models import Grade, NodeProgress, UserAnswer, UserLessonCompletion
|
||
from discount.models import DiscountCode, Offer, Redemption, UserOffer
|
||
from notifications.models import Notification
|
||
from payment.models import Order
|
||
from progress.models import LeaderboardEntry, StreakRecord, UserHearts, UserStats
|
||
from subscription.models import Plan, Subscription
|
||
from support.models import BugReport, Ticket
|
||
|
||
phones = [MAIN_PHONE, REFERRED_PHONE]
|
||
user_ids = list(User.objects.filter(phone__in=phones).values_list('pk', flat=True))
|
||
guest = User.objects.filter(device_id=GUEST_DEVICE_ID, is_guest=True).first()
|
||
if guest:
|
||
user_ids.append(guest.pk)
|
||
|
||
if user_ids:
|
||
UserAnswer.objects.filter(user_id__in=user_ids).delete()
|
||
UserLessonCompletion.objects.filter(user_id__in=user_ids).delete()
|
||
NodeProgress.objects.filter(user_id__in=user_ids).delete()
|
||
UserStats.objects.filter(user_id__in=user_ids).delete()
|
||
LeaderboardEntry.objects.filter(user_id__in=user_ids).delete()
|
||
StreakRecord.objects.filter(user_id__in=user_ids).delete()
|
||
UserHearts.objects.filter(user_id__in=user_ids).delete()
|
||
Conversation.objects.filter(user_id__in=user_ids).delete()
|
||
Subscription.objects.filter(user_id__in=user_ids).delete()
|
||
Order.objects.filter(user_id__in=user_ids).delete()
|
||
UserOffer.objects.filter(user_id__in=user_ids).delete()
|
||
Redemption.objects.filter(user_id__in=user_ids).delete()
|
||
Notification.objects.filter(user_id__in=user_ids).delete()
|
||
AffiliateAccount.objects.filter(user_id__in=user_ids).delete()
|
||
Conversion.objects.filter(referred_user_id__in=user_ids).delete()
|
||
Ticket.objects.filter(user_id__in=user_ids).delete()
|
||
BugReport.objects.filter(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()
|
||
Grade.objects.filter(title__startswith=SEED_TAG).delete() # cascades units/nodes/lessons/questions/choices
|
||
|
||
OTPCode.objects.filter(phone__in=phones).delete()
|
||
Session.objects.filter(user__phone__in=phones).delete()
|
||
if guest:
|
||
Session.objects.filter(user=guest).delete()
|
||
User.objects.filter(phone__in=phones).delete()
|
||
if guest:
|
||
guest.delete()
|
||
|
||
self.stdout.write('رکوردهای قبلیِ seed حذف شدن؛ در حال ساختِ دوباره...')
|
||
|
||
# -- seed -------------------------------------------------------------
|
||
def _seed(self):
|
||
main_user, guest_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)
|
||
self._seed_notifications(main_user)
|
||
self._seed_affiliate(main_user, referred_user)
|
||
self._seed_support(main_user)
|
||
|
||
tokens = [
|
||
('کاربر اصلی (ثبتنامشده)', main_user, issue_access_token(main_user.pk)),
|
||
('کاربر مهمان', guest_user, issue_access_token(guest_user.pk)),
|
||
]
|
||
return {'tokens': tokens}
|
||
|
||
# -- accounts -----------------------------------------------------------
|
||
def _seed_accounts(self):
|
||
main_user, _ = User.objects.get_or_create(
|
||
phone=MAIN_PHONE,
|
||
defaults=dict(name=f'{SEED_TAG} کاربر اصلی', is_guest=False, grade_id=None),
|
||
)
|
||
Profile.objects.get_or_create(
|
||
user=main_user, defaults=dict(birth_year=1394, bio=f'{SEED_TAG} پروفایل تستی')
|
||
)
|
||
Session.objects.get_or_create(
|
||
user=main_user,
|
||
refresh_token=hash_refresh_token(f'seed-refresh-{MAIN_PHONE}'),
|
||
defaults=dict(
|
||
device_id='seed-device-main',
|
||
expires_at=timezone.now() + timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||
revoked=False,
|
||
),
|
||
)
|
||
|
||
guest_user, _ = User.objects.get_or_create(
|
||
device_id=GUEST_DEVICE_ID,
|
||
is_guest=True,
|
||
defaults=dict(name=f'{SEED_TAG} کاربر مهمان'),
|
||
)
|
||
Session.objects.get_or_create(
|
||
user=guest_user,
|
||
refresh_token=hash_refresh_token(f'seed-refresh-{GUEST_DEVICE_ID}'),
|
||
defaults=dict(
|
||
device_id=GUEST_DEVICE_ID,
|
||
expires_at=timezone.now() + timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||
revoked=False,
|
||
),
|
||
)
|
||
|
||
referred_user, _ = User.objects.get_or_create(
|
||
phone=REFERRED_PHONE, defaults=dict(name=f'{SEED_TAG} کاربر معرفیشده', is_guest=False)
|
||
)
|
||
|
||
self.stdout.write(self.style.SUCCESS('accounts: 3 کاربر (اصلی/مهمان/معرفیشده) آماده شد.'))
|
||
return main_user, guest_user, referred_user
|
||
|
||
# -- content + progress --------------------------------------------------
|
||
def _seed_content_and_progress(self, main_user):
|
||
from content.models import Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserAnswer, UserLessonCompletion
|
||
from progress.models import ActiveDay, LeaderboardEntry, StreakRecord, UserHearts, UserStats
|
||
|
||
grade, _ = Grade.objects.get_or_create(
|
||
title=f'{SEED_TAG} پایه سوم', defaults=dict(icon='grade-3', color='#4CAF50')
|
||
)
|
||
unit, _ = Unit.objects.get_or_create(grade=grade, title=f'{SEED_TAG} جمع و تفریق', defaults=dict(order=1))
|
||
|
||
node1, _ = Node.objects.get_or_create(unit=unit, title=f'{SEED_TAG} جمع اعداد یکرقمی', defaults=dict(order=1))
|
||
node2, _ = Node.objects.get_or_create(unit=unit, title=f'{SEED_TAG} تفریق اعداد یکرقمی', defaults=dict(order=2))
|
||
|
||
lesson1, _ = Lesson.objects.get_or_create(
|
||
node=node1,
|
||
title=f'{SEED_TAG} درس اول جمع',
|
||
defaults=dict(intro_text='دو عدد را با هم جمع کن.', mascot_animation='explain'),
|
||
)
|
||
lesson2, _ = Lesson.objects.get_or_create(
|
||
node=node2,
|
||
title=f'{SEED_TAG} درس اول تفریق',
|
||
defaults=dict(intro_text='یک عدد را از عدد دیگر کم کن.', mascot_animation='explain'),
|
||
)
|
||
|
||
question1, _ = Question.objects.get_or_create(
|
||
lesson=lesson1, prompt=f'{SEED_TAG} 2 + 3 = ?', defaults=dict(type='multiple_choice', asset_group='')
|
||
)
|
||
choices1 = [('4', False), ('5', True), ('6', False), ('7', False)]
|
||
correct_choice1 = None
|
||
for value, is_correct in choices1:
|
||
choice, _ = Choice.objects.get_or_create(question=question1, value=value, defaults=dict(is_correct=is_correct))
|
||
if is_correct:
|
||
correct_choice1 = choice
|
||
|
||
question2, _ = Question.objects.get_or_create(
|
||
lesson=lesson2, prompt=f'{SEED_TAG} 9 - 4 = ?', defaults=dict(type='multiple_choice', asset_group='')
|
||
)
|
||
for value, is_correct in [('4', False), ('5', True), ('6', False)]:
|
||
Choice.objects.get_or_create(question=question2, value=value, defaults=dict(is_correct=is_correct))
|
||
|
||
# پیشرفتِ کاربرِ اصلی: درسِ اول تمامشده، Nodeِ اول in_progress
|
||
UserAnswer.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
question=question1,
|
||
idempotency_key='seed-answer-1',
|
||
defaults=dict(choice=correct_choice1, correct=True, xp_earned=10),
|
||
)
|
||
UserLessonCompletion.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
lesson=lesson1,
|
||
idempotency_key='seed-completion-1',
|
||
defaults=dict(correct_count=1, stars=3, xp_earned=10, coins_earned=5),
|
||
)
|
||
NodeProgress.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
node=node1,
|
||
defaults=dict(
|
||
status='in_progress',
|
||
lessons_completed_count=1,
|
||
started_at=timezone.now() - timedelta(days=1),
|
||
last_activity_at=timezone.now(),
|
||
),
|
||
)
|
||
NodeProgress.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
node=node2,
|
||
defaults=dict(status='not_started', lessons_completed_count=0),
|
||
)
|
||
|
||
stats, _ = UserStats.objects.get_or_create(
|
||
user_id=main_user.pk, defaults=dict(xp=10, coins=5, progress_percent=12.5)
|
||
)
|
||
streak, _ = StreakRecord.objects.get_or_create(
|
||
user_id=main_user.pk, defaults=dict(current_streak=1, longest_streak=1)
|
||
)
|
||
ActiveDay.objects.get_or_create(streak=streak, date=timezone.now().date())
|
||
UserHearts.objects.get_or_create(user_id=main_user.pk, defaults=dict(current_hearts=5, max_hearts=5))
|
||
LeaderboardEntry.objects.get_or_create(
|
||
user_id=main_user.pk, period='weekly', defaults=dict(xp_in_period=10, rank=1)
|
||
)
|
||
LeaderboardEntry.objects.get_or_create(
|
||
user_id=main_user.pk, period='all_time', defaults=dict(xp_in_period=10, rank=1)
|
||
)
|
||
|
||
self.stdout.write(self.style.SUCCESS(
|
||
'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
|
||
|
||
conversation, _ = Conversation.objects.get_or_create(
|
||
user_id=main_user.pk, lesson_id=None, chapter_id=None, grade_id=None
|
||
)
|
||
user_msg, _ = Message.objects.get_or_create(
|
||
conversation=conversation,
|
||
role='user',
|
||
text=f'{SEED_TAG} چرا حاصل 2+3 برابر 5 است؟',
|
||
)
|
||
assistant_msg, created = Message.objects.get_or_create(
|
||
conversation=conversation,
|
||
role='assistant',
|
||
text=f'{SEED_TAG} چون وقتی 2 واحد و 3 واحد را کنار هم بگذاریم، 5 واحد میشود.',
|
||
)
|
||
MessageFeedback.objects.get_or_create(message=assistant_msg, defaults=dict(is_helpful=True))
|
||
|
||
self.stdout.write(self.style.SUCCESS('ai_tutor: 1 مکالمه با یک پیامِ کاربر و یک پاسخِ دستیار آماده شد.'))
|
||
|
||
# -- subscription + payment -----------------------------------------------
|
||
def _seed_subscription_and_payment(self, main_user):
|
||
from payment.models import Order, Transaction
|
||
from subscription.models import Plan, Subscription
|
||
|
||
plan_monthly, _ = Plan.objects.get_or_create(
|
||
title=f'{SEED_TAG} ماهانه',
|
||
defaults=dict(price=99000, currency='IRT', discount_percent=0, popular=False, duration_days=30),
|
||
)
|
||
plan_yearly, _ = Plan.objects.get_or_create(
|
||
title=f'{SEED_TAG} سالانه',
|
||
defaults=dict(price=799000, currency='IRT', discount_percent=20, popular=True, duration_days=365),
|
||
)
|
||
|
||
order, _ = Order.objects.get_or_create(
|
||
idempotency_key='seed-order-1',
|
||
defaults=dict(
|
||
user_id=main_user.pk,
|
||
amount=plan_monthly.price,
|
||
currency='IRT',
|
||
status='paid',
|
||
provider='gateway',
|
||
),
|
||
)
|
||
Transaction.objects.get_or_create(
|
||
order=order,
|
||
gateway='zarinpal',
|
||
gateway_ref_id='SEED-REF-0001',
|
||
defaults=dict(status='paid', paid_at=timezone.now()),
|
||
)
|
||
subscription, _ = Subscription.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
defaults=dict(
|
||
plan=plan_monthly,
|
||
status='active',
|
||
expires_at=timezone.now() + timedelta(days=plan_monthly.duration_days),
|
||
order_id=order.pk,
|
||
),
|
||
)
|
||
if order.subscription_id != subscription.pk:
|
||
order.subscription_id = subscription.pk
|
||
order.save(update_fields=['subscription_id'])
|
||
|
||
self.stdout.write(self.style.SUCCESS(
|
||
'subscription/payment: 2 پلن، 1 سفارشِ پرداختشده و 1 اشتراکِ فعال آماده شد.'
|
||
))
|
||
return plan_monthly, plan_yearly
|
||
|
||
# -- discount ---------------------------------------------------------------
|
||
def _seed_discount(self, main_user):
|
||
from discount.models import DiscountCode, Offer
|
||
|
||
DiscountCode.objects.get_or_create(
|
||
code='SEEDTEST10',
|
||
defaults=dict(type='percent', value=10, max_uses=100, expires_at=timezone.now() + timedelta(days=90)),
|
||
)
|
||
Offer.objects.get_or_create(
|
||
code='SEEDTRIAL7',
|
||
defaults=dict(
|
||
name=f'{SEED_TAG} هفت روز رایگان',
|
||
kind='trial',
|
||
duration_days=7,
|
||
auto_assign_on_signup=False,
|
||
is_active=True,
|
||
),
|
||
)
|
||
|
||
self.stdout.write(self.style.SUCCESS('discount: 1 کدتخفیفِ فعال و 1 آفرِ trial آماده شد.'))
|
||
|
||
# -- notifications ------------------------------------------------------------
|
||
def _seed_notifications(self, main_user):
|
||
from notifications.models import Notification
|
||
|
||
Notification.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
type='xp_earned',
|
||
title=f'{SEED_TAG} امتیاز گرفتی',
|
||
defaults=dict(body='10 امتیاز از درس اول جمع گرفتی!', related_id=None, is_read=False),
|
||
)
|
||
Notification.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
type='order_paid',
|
||
title=f'{SEED_TAG} پرداخت موفق',
|
||
defaults=dict(body='اشتراک ماهانهت فعال شد.', related_id=None, is_read=True),
|
||
)
|
||
|
||
self.stdout.write(self.style.SUCCESS('notifications: 2 اعلان (یکی خواندهنشده، یکی خواندهشده) آماده شد.'))
|
||
|
||
# -- affiliate ------------------------------------------------------------------
|
||
def _seed_affiliate(self, main_user, referred_user):
|
||
from affiliate.models import AffiliateAccount, Conversion
|
||
|
||
account, _ = AffiliateAccount.objects.get_or_create(
|
||
user_id=main_user.pk, defaults=dict(referral_code='SEEDREF01', commission_rate=0.1)
|
||
)
|
||
Conversion.objects.get_or_create(
|
||
referred_user_id=referred_user.pk,
|
||
defaults=dict(affiliate_account=account, status='pending', commission_amount=0),
|
||
)
|
||
|
||
self.stdout.write(self.style.SUCCESS('affiliate: 1 حساب افیلیت و 1 کانورژنِ در انتظار آماده شد.'))
|
||
|
||
# -- support ----------------------------------------------------------------------
|
||
def _seed_support(self, main_user):
|
||
from support.models import BugReport, Ticket, TicketMessage
|
||
|
||
ticket, _ = Ticket.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
subject=f'{SEED_TAG} مشکل در نمایش درس',
|
||
defaults=dict(category='content', status='open', priority='normal'),
|
||
)
|
||
TicketMessage.objects.get_or_create(
|
||
ticket=ticket, sender='user', text=f'{SEED_TAG} درسِ اول باز نمیشه.'
|
||
)
|
||
BugReport.objects.get_or_create(
|
||
user_id=main_user.pk,
|
||
title=f'{SEED_TAG} کرش هنگام باز کردن درس',
|
||
defaults=dict(description='بعد از زدن دکمهی شروع، اپ بسته میشه.', status='open'),
|
||
)
|
||
|
||
self.stdout.write(self.style.SUCCESS('support: 1 تیکت با یک پیام، و 1 گزارش باگ آماده شد.'))
|