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>
This commit is contained in:
51
accounts/tests_delete_users.py
Normal file
51
accounts/tests_delete_users.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""delete_users: کاربر و همهی دادهاش در همهی اپها، و هیچ چیزِ کاربرِ دیگری."""
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import CommandError, call_command
|
||||
from django.test import TestCase
|
||||
|
||||
from accounts.models import OTPCode, Profile, User
|
||||
from ai_tutor.models import Conversation, Message
|
||||
from notifications.models import Notification
|
||||
from payment.models import Order
|
||||
from progress.models import UserStats
|
||||
|
||||
|
||||
class DeleteUsersTests(TestCase):
|
||||
def setUp(self):
|
||||
self.doomed = User.objects.create(phone='09121110001', is_guest=False)
|
||||
self.kept = User.objects.create(phone='09121110002', is_guest=False)
|
||||
for user in (self.doomed, self.kept):
|
||||
Profile.objects.create(user=user)
|
||||
UserStats.objects.create(user_id=user.id, xp=10)
|
||||
Message.objects.create(conversation=Conversation.objects.create(user_id=user.id), role='user', text='؟')
|
||||
Notification.objects.create(user_id=user.id, type='xp_earned', title='ت')
|
||||
OTPCode.objects.create(phone=user.phone, code='123456', expires_at='2030-01-01T00:00Z')
|
||||
|
||||
def run_command(self, *args):
|
||||
out = StringIO()
|
||||
call_command('delete_users', *args, stdout=out)
|
||||
return out.getvalue()
|
||||
|
||||
def test_dry_run_changes_nothing(self):
|
||||
out = self.run_command('--phones', self.doomed.phone)
|
||||
self.assertIn('progress.UserStats.user_id: 1', out)
|
||||
self.assertTrue(User.objects.filter(pk=self.doomed.pk).exists())
|
||||
|
||||
def test_everything_of_that_user_goes_and_nothing_else(self):
|
||||
self.run_command('--phones', self.doomed.phone, '--yes')
|
||||
self.assertFalse(User.objects.filter(pk=self.doomed.pk).exists())
|
||||
for model in (UserStats, Conversation, Notification):
|
||||
self.assertFalse(model.objects.filter(user_id=self.doomed.pk).exists(), model)
|
||||
self.assertTrue(model.objects.filter(user_id=self.kept.pk).exists(), model)
|
||||
self.assertFalse(OTPCode.objects.filter(phone=self.doomed.phone).exists())
|
||||
self.assertTrue(OTPCode.objects.filter(phone=self.kept.phone).exists())
|
||||
self.assertEqual(Message.objects.count(), 1)
|
||||
|
||||
def test_paid_orders_need_explicit_permission(self):
|
||||
Order.objects.create(user_id=self.doomed.id, amount=1000, status='paid', idempotency_key='k1')
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command('--ids', str(self.doomed.pk), '--yes')
|
||||
self.assertTrue(User.objects.filter(pk=self.doomed.pk).exists())
|
||||
self.run_command('--ids', str(self.doomed.pk), '--yes', '--include-paid')
|
||||
self.assertFalse(Order.objects.filter(user_id=self.doomed.pk).exists())
|
||||
Reference in New Issue
Block a user