Files
MathApp/accounts/management/commands/delete_users.py
Zareian 39247f6d53 senior's post-demo changes: parent report in the student's own account, Jalali birth date, invoices, math-only tutor, priced quotas
- remove the family app from the root (separate parent accounts, invite codes, parental
  settings); progress.0006 drops its tables, permissions and migration history, and
  Profile.parent_phone, the two family notification types and ?userId= on
  /api/progress/nodes go with it
- GET /api/user/weekly-report: last 7 days next to the 7 before (estimated minutes,
  questions, accuracy, islands, tutor questions) for the "parent report" page
- profile birthDate in Jalali (accounts/jalali.py), ages 5-99, birthYear derived from it
- GET /api/payment/invoices; orders now snapshot list price and plan title
- tutor answers only math: server pre-filter, scope rule in the prompt (tutor-v11),
  refusal marker replaced with fixed text; self-harm gets a help line instead
- streamed answers now record token usage (it was always 0)
- daily tutor quota: AI_DAILY_QUOTA_FREE (default 3) vs Plan.ai_daily_quota
- delete_users management command for wiping test accounts across every app
- docs/ai-cost-and-guardrails.md: measured cost per question and guardrail results

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:38:43 +03:30

99 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
پاک کردنِ کامل‌ِ چند کاربر و همه‌ی داده‌شان در همه‌ی اپ‌ها.
چرا لازم شد (۲۰۲۶-۱۰-۰۲): پیش از رسیدنِ کاربرِ واقعی، حساب‌هایی که تیم با شماره‌ی واقعیِ خودش
ساخته بود باید پاک شوند تا همان شماره‌ها دوباره مسیرِ «کاربرِ تازه» را ببینند. پاک کردنِ ردیفِ
`accounts.User` کافی نیست: اپ‌ها به کاربر فقط ارجاعِ سبک (`user_id`) دارند، نه FK، پس امتیاز،
گفتگو، سفارش و اعلانِ او یتیم می‌ماند و کاربرِ تازه‌ای که همان شناسه را بگیرد آن‌ها را می‌دید
(روی SQLite شناسه دوباره داده نمی‌شود، ولی روی Postgres پس از بازنشانیِ sequence ممکن است).
مدل‌ها خودکار پیدا می‌شوند: هر مدلی که ستونِ `user_id` یا `referred_user_id` دارد. پس اپِ تازه
بی‌ویرایشِ این فایل پوشش داده می‌شود. کدهای OTPِ همان شماره‌ها هم پاک می‌شوند تا سقفِ روزانه‌ی
پیامکِ آن شماره از صفر شروع شود.
پیش‌فرض فقط گزارش است و چیزی پاک نمی‌کند:
python manage.py delete_users --phones 09121234567 09351234567
python manage.py delete_users --ids 17 22 --yes
سفارشِ پرداخت‌شده سندِ مالی است؛ کاربری که سفارشِ «paid» دارد بدونِ --include-paid پاک نمی‌شود.
"""
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from accounts.models import OTPCode, User
from accounts.phone import normalize_phone
USER_COLUMNS = ('user_id', 'referred_user_id')
def user_scoped_models():
"""[(model, column)] برای هر مدلی که به کاربر ارجاع می‌دهد، جز خودِ User."""
found = []
for model in apps.get_models():
if model is User:
continue
names = {f.attname for f in model._meta.concrete_fields}
found += [(model, column) for column in USER_COLUMNS if column in names]
return found
def plan(user_ids):
"""{'app.Model.column': count} — آنچه پاک می‌شود، پیش از پاک شدن."""
counts = {}
for model, column in user_scoped_models():
n = model.objects.filter(**{f'{column}__in': user_ids}).count()
if n:
counts[f'{model._meta.label}.{column}'] = n
return counts
def delete_users(user_ids):
phones = list(User.objects.filter(pk__in=user_ids).exclude(phone=None).values_list('phone', flat=True))
with transaction.atomic():
for model, column in user_scoped_models():
model.objects.filter(**{f'{column}__in': user_ids}).delete()
OTPCode.objects.filter(phone__in=phones).delete()
User.objects.filter(pk__in=user_ids).delete()
class Command(BaseCommand):
help = 'کاربر و همه‌ی داده‌اش را در همه‌ی اپ‌ها پاک می‌کند (پیش‌فرض: فقط گزارش).'
def add_arguments(self, parser):
parser.add_argument('--phones', nargs='*', default=[])
parser.add_argument('--ids', nargs='*', type=int, default=[])
parser.add_argument('--yes', action='store_true', help='واقعاً پاک کن.')
parser.add_argument('--include-paid', action='store_true',
help='کاربرِ دارای سفارشِ پرداخت‌شده را هم پاک کن.')
def handle(self, *args, **options):
phones = [normalize_phone(p) or p for p in options['phones']]
users = list(User.objects.filter(phone__in=phones)) + list(User.objects.filter(pk__in=options['ids']))
users = list({u.pk: u for u in users}.values())
missing = set(phones) - {u.phone for u in users}
if missing:
self.stdout.write(f'این شماره‌ها کاربری ندارند: {len(missing)}')
if not users:
raise CommandError('کاربری برای پاک کردن پیدا نشد.')
from payment.models import Order
paid = set(Order.objects.filter(user_id__in=[u.pk for u in users], status='paid')
.values_list('user_id', flat=True))
if paid and not options['include_paid']:
raise CommandError(f'کاربرهای {sorted(paid)} سفارشِ پرداخت‌شده دارند؛ با --include-paid پاک می‌شوند.')
ids = [u.pk for u in users]
for u in users:
phone = f'{u.phone[:4]}***{u.phone[-3:]}' if u.phone else 'مهمان'
self.stdout.write(f' کاربرِ {u.pk} {phone} ساخته‌شده {u.created_at:%Y-%m-%d}')
for label, n in sorted(plan(ids).items()):
self.stdout.write(f' {label}: {n}')
if not options['yes']:
self.stdout.write(self.style.WARNING('فقط گزارش بود؛ برای پاک کردن --yes بدهید.'))
return
delete_users(ids)
self.stdout.write(self.style.SUCCESS(f'{len(ids)} کاربر و همه‌ی داده‌شان پاک شدند.'))