Files
khatibi/scripts/hold_write_verification.py
T
2026-10-22 14:01:08 +03:30

287 lines
12 KiB
Python

#!/usr/bin/env python
"""صحت‌سنجیِ دو نقص گزارش‌شده دربارهٔ ``set_visit_hold`` (روی کپی دیتابیس).
سناریوها روی یک **کپی موقت** از دیتابیس اجرا می‌شوند؛ دیتابیس اصلی دست‌نخورده
می‌ماند و هیچ کد تولیدی‌ای تغییر نمی‌کند — این فقط ابزار اثبات/رد ادعاهاست.
اجرا::
.venv/Scripts/python.exe scripts/hold_write_verification.py
"""
import argparse
import datetime
import os
import shutil
import sys
import tempfile
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'khatibi.settings')
def _copy_db(source_path, target_dir):
target = os.path.join(target_dir, 'verify.sqlite3')
shutil.copy2(source_path, target)
for suffix in ('-wal', '-shm'):
side = source_path + suffix
if os.path.exists(side):
shutil.copy2(side, target + suffix)
return target
def _new_customer(name):
from django.utils import timezone
from admin_panel.models import Customer
today = timezone.localdate()
customer = Customer.objects.create(name_family=name, address='verify')
Customer.objects.filter(pk=customer.pk).update(
installDate=today - datetime.timedelta(days=400))
customer.refresh_from_db()
return customer
def _coordination(customer, *, days, status='finish'):
from django.utils import timezone
from admin_panel.models import Action
when = timezone.now() + datetime.timedelta(days=days)
return Action.objects.create(
customer=customer, actionType=Action.ActionType.HAMAHANGI, status=status,
actionDateTime=when,
finishDateTime=timezone.now() if status == 'finish' else None,
desc='verify',
)
def _show(label, customer):
from django.utils import timezone
from admin_panel.visit_policy import manual_holds_map, appointment_sources_map
tz = timezone.get_current_timezone()
print(' %-44s column=%-18s معتبر=%-5s منابع=%s' % (
label,
customer.visit_hold_until.astimezone(tz).strftime('%Y-%m-%d')
if customer.visit_hold_until else 'None',
'بله' if customer.pk in manual_holds_map() else 'خیر',
sorted((appointment_sources_map().get(customer.pk) or {}).keys()) or '—',
))
def claim_1_silent_failure():
"""نقص ۲ — آیا `_apply_visit_hold` نتیجه را نادیده می‌گیرد؟"""
from django.utils import timezone
from admin_panel.views.actions import _apply_visit_hold
from admin_panel.visit_policy import set_visit_hold
from admin_panel.models import Action
print('\n' + '=' * 92)
print('ادعای ۱ (نقص ۲): نتیجهٔ set_visit_hold در لایهٔ فراخوان دور ریخته می‌شود')
now = timezone.now()
customer = _new_customer('نقص۲')
set_visit_hold(customer, now + datetime.timedelta(days=20))
far = customer.visit_hold_until
_show('پس از ثبت قرار ۲۰ روزه', customer)
written = set_visit_hold(customer, now + datetime.timedelta(days=5))
print(' set_visit_hold(5 روز) ⇒ %s (مقدار بولی واقعی)' % written)
customer.refresh_from_db()
print(' ستون تغییر کرد؟ %s' % ('بله' if customer.visit_hold_until != far else 'خیر'))
result = _apply_visit_hold(
customer,
{'actionType': Action.ActionType.HAMAHANGI,
'holdRaw': now + datetime.timedelta(days=5),
'status': Action.Status.FINISH},
status=Action.Status.FINISH, is_new=False,
)
print(' _apply_visit_hold ⇒ %s' % result)
silent = result.get('reason') != 'forward_only'
print(' نتیجه: نقص ۲ %s' % ('**هنوز وجود دارد** (نتیجه گزارش نمی‌شود)'
if silent else '**رفع شد** (reason=forward_only گزارش می‌شود)'))
def claim_2_no_success_message():
"""آیا کاربر بازخورد می‌بیند؟ (پس از رفع: پیام هشدار با دلیل و تاریخ‌ها)"""
import inspect
from admin_panel.views import actions as views
print('\n' + '=' * 92)
print('بازخورد کاربر در مسیر ویرایش/ایجاد')
edit_src = inspect.getsource(views.customerEditAction)
create_src = inspect.getsource(views.customerNewAction)
helper_src = inspect.getsource(views._report_hold_result)
print(' customerEditAction نتیجهٔ قرار را گزارش می‌کند؟ %s'
% ('_report_hold_result' in edit_src))
print(' customerNewAction نتیجهٔ قرار را گزارش می‌کند؟ %s'
% ('_report_hold_result' in create_src))
print(' پیام هشدار شامل دلیل و تاریخ‌ها است؟ %s'
% ('ذخیره نشد' in helper_src))
print(' پیام موفقیتِ گمراه‌کننده وجود دارد؟ %s'
% ('messages.success' in edit_src or 'messages.success' in create_src))
def claim_3_stale_column_lock():
"""نقص ۳ — قفل شدن ستون بعد از حذف اقدام منبع."""
from django.utils import timezone
from admin_panel.visit_policy import (manual_holds_map, set_visit_hold,
soft_delete_action)
print('\n' + '=' * 92)
print('ادعای ۳ (نقص ۳): حذف اقدام منبع ستون را پاک نمی‌کند و نوشتن را قفل می‌کند')
now = timezone.now()
customer = _new_customer('نقص۳')
action = _coordination(customer, days=60)
set_visit_hold(customer, now + datetime.timedelta(days=60))
customer.refresh_from_db()
_show('قرار ۶۰ روزه ثبت شد', customer)
soft_delete_action(action)
action.refresh_from_db()
customer.refresh_from_db()
print(' اقدام منبع ⇒ status=%s deleted_at=%s' % (action.status, action.deleted_at))
_show('بعد از حذف (ستون کهنه ولی پر)', customer)
print(' در manual_holds_map هست؟ %s' % (customer.pk in manual_holds_map()))
ok = set_visit_hold(customer, now + datetime.timedelta(days=10))
customer.refresh_from_db()
print(' set_visit_hold(10 روز) ⇒ %s' % ok)
_show('پس از تلاش برای تاریخ نزدیک‌تر', customer)
print(' نتیجه: قفل %s' % ('تأیید می‌شود' if ok is False else 'رد می‌شود'))
def claim_4_escape_hatch():
"""آیا مسیر عادیِ رهاسازی وجود دارد؟ (ادعا: هیچ)"""
from django.utils import timezone
from admin_panel.models import Action
from admin_panel.views.actions import _apply_visit_hold
from admin_panel.visit_policy import set_visit_hold, soft_delete_action
print('\n' + '=' * 92)
print('ادعای ۴: «هیچ مسیر عادی‌ای برای رهاسازی وجود ندارد»')
now = timezone.now()
customer = _new_customer('رهاسازی')
action = _coordination(customer, days=60)
set_visit_hold(customer, now + datetime.timedelta(days=60))
soft_delete_action(action)
customer.refresh_from_db()
_show('قفل‌شده پس از حذف اقدام', customer)
# ثبت یک اقدام **غیرهماهنگی** جدید ⇒ is_new=True ⇒ clear_visit_hold
cleared = _apply_visit_hold(
customer,
{'actionType': Action.ActionType.OUT_CALL, 'holdRaw': None,
'status': Action.Status.WAITE},
status=Action.Status.WAITE, is_new=True,
)
customer.refresh_from_db()
print(' ثبت «تماس خروجی» جدید ⇒ clear_visit_hold: %s | ستون=%s'
% (cleared.get('reason') or cleared.get('ok'), customer.visit_hold_until))
ok = set_visit_hold(customer, now + datetime.timedelta(days=10))
customer.refresh_from_db()
_show('قرار ۱۰ روزه پس از رهاسازی', customer)
print(' ⇒ %s' % ('ادعا **رد** می‌شود: مسیر وجود دارد (ولی غیرشهودی و داده‌آلوده)'
if ok else 'ادعا تأیید می‌شود'))
def claim_5_edit_to_visit():
"""مسیری که در گزارش نیامده: ویرایش هماهنگی → «بازدید» ستون را پاک نمی‌کند."""
from django.utils import timezone
from admin_panel.models import Action
from admin_panel.views.actions import _apply_visit_hold
from admin_panel.visit_policy import set_visit_hold
print('\n' + '=' * 92)
print('یافتهٔ اضافی: ویرایش هماهنگی → «بازدید» ⇒ ستون می‌ماند ولی اعتبارش می‌رود')
now = timezone.now()
customer = _new_customer('ویرایش-به-بازدید')
action = _coordination(customer, days=60)
set_visit_hold(customer, now + datetime.timedelta(days=60))
customer.refresh_from_db()
action.actionType = Action.ActionType.BAZDID
action.status = Action.Status.FINISH
action.finishDateTime = now
action.save()
_apply_visit_hold(
customer,
{'actionType': Action.ActionType.BAZDID, 'holdRaw': None,
'status': Action.Status.FINISH},
status=Action.Status.FINISH, is_new=False,
)
customer.refresh_from_db()
_show('پس از ویرایش به بازدید', customer)
ok = set_visit_hold(customer, now + datetime.timedelta(days=7))
print(' set_visit_hold(7 روز) ⇒ %s' % ok)
print(' ⇒ %s' % ('قفل، حتی با ستونِ بی‌اعتبار — شدت نقص ۳ بیشتر از تخمین گزارش است'
if ok is False else 'قفل نشد'))
def real_db_stats():
"""آمار واقعی: چند مشتری ستون پرشدهٔ «بی‌اعتبار» دارند؟"""
from django.utils import timezone
from admin_panel.models import Action, Customer
from admin_panel.visit_policy import manual_holds_map
print('\n' + '=' * 92)
print('آمار دیتابیس واقعی (روی کپی)')
tz = timezone.get_current_timezone()
now = timezone.now()
future = list(Customer.objects.filter(visit_hold_until__gt=now))
valid = set(manual_holds_map())
dead = [c for c in future if c.pk not in valid]
print(' ستون پرشده و آینده: %d | معتبر: %d | بی‌اعتبار (قفل‌کننده): %d'
% (len(future), len(valid), len(dead)))
for c in dead[:10]:
last = Action.objects.filter(
customer=c, actionType=Action.ActionType.HAMAHANGI,
).order_by('-createdDT').first()
print(' id=%-6s idCode=%-10s hold=%-11s آخرین هماهنگی=%s (%s)' % (
c.id, c.idCode,
c.visit_hold_until.astimezone(tz).date(),
last.status if last else '—',
last.createdDT.date() if last else '—'))
print(' هماهنگی حذف/لغوشده با تاریخ آینده (کاندید قفل آینده): %d'
% Action.objects.filter(
actionType=Action.ActionType.HAMAHANGI, status=Action.Status.CANCELL,
actionDateTime__gt=now).count())
def main():
parser = argparse.ArgumentParser(description='صحت‌سنجی نقص‌های set_visit_hold')
parser.add_argument('--skip-stats', action='store_true')
args = parser.parse_args()
import django
django.setup()
from django.conf import settings
from django.db import connections
source_db = str(settings.DATABASES['default']['NAME'])
if not os.path.isabs(source_db):
source_db = os.path.join(BASE_DIR, source_db)
tmp_dir = tempfile.mkdtemp(prefix='hold_verify_')
settings.DATABASES['default']['NAME'] = _copy_db(source_db, tmp_dir)
connections.close_all()
print('دیتابیسِ کپیِ موقت: %s' % settings.DATABASES['default']['NAME'])
claim_1_silent_failure()
claim_2_no_success_message()
claim_3_stale_column_lock()
claim_4_escape_hatch()
claim_5_edit_to_visit()
if not args.skip_stats:
real_db_stats()
shutil.rmtree(tmp_dir, ignore_errors=True)
if __name__ == '__main__':
main()