Files
MathApp/accounts/management/commands/delete_users.py
2026-10-02 02:41:16 +03:30

111 lines
6.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
python manage.py delete_users --real-phones # هر کاربرِ شماره‌دار جز شماره‌های دمو
`--real-phones` یعنی هر کاربری که شماره دارد و شماره‌اش در TEST_LOGIN_PHONES (حساب‌های دمو با کدِ
ثابت) نیست. شماره‌ها از سرور بیرون نمی‌آیند؛ گزارش فقط چهار رقمِ اول و سه رقمِ آخر را نشان می‌دهد.
مهمان‌ها (بی‌شماره) با این گزینه انتخاب نمی‌شوند.
سفارشِ پرداخت‌شده سندِ مالی است؛ کاربری که سفارشِ «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('--real-phones', action='store_true',
help='همه‌ی کاربرهای شماره‌دار جز شماره‌های TEST_LOGIN_PHONES.')
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']))
if options['real_phones']:
from django.conf import settings
users += list(User.objects.exclude(phone=None).exclude(phone='')
.exclude(phone__in=list(settings.TEST_LOGIN_PHONES)))
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)} کاربر و همه‌ی داده‌شان پاک شدند.'))