improve bazdid 4

This commit is contained in:
javad committed 2026-10-21 16:34:13 +03:30
1 parent 8fd881e0ea
commit 825f2a5973
13 files changed
+585 -15

No files matched your search

+2
View File
@@ -36,6 +36,7 @@ EXPECTED_URL_NAMES = {
'customerNazar',
'customerNewAction',
'customerEditAction',
'customerActionDelete',
'customerDel',
'customerFactors',
'customerFactor',
@@ -99,6 +100,7 @@ PARAMETERIZED = {
'customerNazar': {'c_id': 1},
'customerNewAction': {'c_id': 1},
'customerEditAction': {'c_id': 1, 'action_id': 1},
'customerActionDelete': {'c_id': 1, 'action_id': 1},
'customerDel': {'c_id': 1},
'customerFactors': {'c_id': 1},
'customerFactor': {'c_id': 1, 'f_id': 1},
+260
View File
@@ -2106,3 +2106,263 @@ class TodayFollowupTests(TestCase):
self._coordination(customer, -4)
html = self.client.get(self.URL + '?view=uncoordinated').content.decode()
self.assertNotIn('class="ez-overdue-alert"', html)
class ActionSoftDeleteTests(TestCase):
"""حذف (soft-delete) یک اقدام: status='cancell' + حسابرسی.
قواعد قفل‌شده:
* حذف ⇒ اقدام از `entezar`، `ActionList` و داشبورد خارج می‌شود.
* رکورد فیزیکی حذف **نمی‌شود** (سابقه برای حسابرسی می‌ماند).
* اقدام دارای نظرسنجی قابل حذف نیست.
* پیامک ارسال‌نشدهٔ مرتبط لغو می‌شود.
* `last_bazdid_dt` بازمحاسبه می‌شود.
* GET بی‌اثر است؛ URL فقط POST می‌پذیرد.
"""
def setUp(self):
self.user = make_user('deleter', superuser=True)
self.client.force_login(self.user)
self.customer = make_customer('مشتری حذف')
Customer.objects.filter(pk=self.customer.pk).update(
installDate=timezone.localdate() - timedelta(days=500))
self.customer.refresh_from_db()
def _action(self, **kwargs):
defaults = dict(customer=self.customer, actionType='hamahangi',
status='finish', actionDateTime=timezone.now(),
finishDateTime=timezone.now())
defaults.update(kwargs)
return Action.objects.create(**defaults)
def _delete(self, action):
return self.client.post(
reverse('admin_panel:customerActionDelete',
args=[self.customer.pk, action.pk]))
def _followup_ids(self):
response = self.client.get('/panel/entezar/?view=today')
return {o.id for o in response.context['customers']}
# ---------- رفتار اصلی ----------
def test_delete_marks_cancelled_with_audit(self):
action = self._action()
self._delete(action)
action.refresh_from_db()
self.assertEqual(action.status, 'cancell')
self.assertIsNotNone(action.deleted_at)
self.assertEqual(action.deleted_by_id, self.user.pk)
self.assertTrue(action.is_deleted)
def test_record_is_not_physically_deleted(self):
action = self._action()
pk = action.pk
self._delete(action)
self.assertTrue(Action.objects.filter(pk=pk).exists(),
'رکورد باید برای حسابرسی باقی بماند')
def test_deleted_action_leaves_followup_list(self):
action = self._action()
self.assertIn(self.customer.id, self._followup_ids())
self._delete(action)
self.assertNotIn(self.customer.id, self._followup_ids())
def test_get_request_does_not_delete(self):
action = self._action()
response = self.client.get(reverse(
'admin_panel:customerActionDelete',
args=[self.customer.pk, action.pk]))
self.assertEqual(response.status_code, 302)
action.refresh_from_db()
self.assertIsNone(action.deleted_at)
def test_cannot_delete_twice(self):
action = self._action()
self._delete(action)
# بار دوم باید خطا بدهد ولی کرش نکند
response = self._delete(action)
self.assertEqual(response.status_code, 302)
action.refresh_from_db()
self.assertEqual(action.status, 'cancell')
# ---------- ممنوعیت نظرسنجی ----------
def test_action_with_survey_cannot_be_deleted(self):
action = self._action(nazarDT=timezone.now(), nazar='خوب', grade=20)
self._delete(action)
action.refresh_from_db()
self.assertEqual(action.status, 'finish', 'نباید وضعیت عوض شود')
self.assertIsNone(action.deleted_at)
def test_delete_blocked_message_shown(self):
action = self._action(nazarDT=timezone.now(), nazar='خوب', grade=20)
response = self._delete(action)
messages_list = list(response.wsgi_request._messages)
self.assertTrue(any('نظرسنجی' in str(m) for m in messages_list))
# ---------- پیامک ----------
def test_pending_visit_sms_is_cancelled(self):
from admin_panel.models import SMSQueue, SMSTemplate
SMSTemplate.objects.update_or_create(
sms_type='visit_created',
defaults={'text': 'بازدید {name}', 'is_enabled': True})
PhoneNumber.objects.create(customer=self.customer, title='موبایل',
phone_number='09120000000')
Customer.objects.filter(pk=self.customer.pk).update(phone1='09120000000')
self.customer.refresh_from_db()
from admin_panel.sms.triggers import notify_visit_scheduled
action = self._action(actionType='bazdid', status='waite')
notify_visit_scheduled(self.customer, action, created_by=self.user)
self.assertEqual(
SMSQueue.objects.filter(customer=self.customer,
sms_type='visit_created').count(), 1)
self._delete(action)
self.assertEqual(
SMSQueue.objects.filter(customer=self.customer,
sms_type='visit_created',
status=SMSQueue.Status.CANCELLED).count(), 1)
# ---------- بازمحاسبهٔ ستون ----------
def test_last_bazdid_recalculated_after_delete(self):
old_visit = self._action(actionType='bazdid',
actionDateTime=at(-400), finishDateTime=at(-400))
recent = self._action(actionType='bazdid',
actionDateTime=at(-2), finishDateTime=at(-2))
refresh_last_bazdid(self.customer, reason='create', user=self.user)
self.customer.refresh_from_db()
self.assertEqual(self.customer.last_bazdid_dt.date(), at(-2).date())
self._delete(recent)
self.customer.refresh_from_db()
self.assertEqual(self.customer.last_bazdid_dt.date(), at(-400).date())
self.assertTrue(Action.objects.filter(pk=old_visit.pk).exists())
# ---------- سایر نماها ----------
def test_deleted_action_hidden_from_customer_action_list(self):
action = self._action()
self._delete(action)
html = self.client.get(
'/panel/customer/%d/action' % self.customer.pk).content.decode()
self.assertNotIn('action_id=\'%d\'' % action.pk, html)
def test_deleted_action_visible_with_explicit_filter(self):
"""با فیلتر صریح status=cancell باید دیده شود (حسابرسی)."""
action = self._action()
self._delete(action)
html = self.client.get(
'/panel/customer/%d/action?status=cancell' % self.customer.pk
).content.decode()
self.assertIn('action_id=\'%d\'' % action.pk, html)
def test_deleted_action_hidden_from_actionlist_all(self):
action = self._action(status='waite', forUser=self.user)
self._delete(action)
response = self.client.get(reverse('admin_panel:ActionListAll'))
ids = {a.pk for a in response.context['actions']}
self.assertNotIn(action.pk, ids)
def test_deleted_action_hidden_from_nazar_page(self):
action = self._action(nazarDT=None)
self._delete(action)
response = self.client.get(
'/panel/customer/%d/nazar/' % self.customer.pk)
ids = {a.pk for a in response.context['actions']}
self.assertNotIn(action.pk, ids)
class ActionDeleteUITests(TestCase):
"""دکمهٔ حذف روی صفحهٔ اقدام‌ها: حضور، قفل نظرسنجی و اسکوپ."""
def setUp(self):
self.user = make_user('deleteUi', superuser=True)
self.client.force_login(self.user)
self.customer = make_customer('UI حذف')
Customer.objects.filter(pk=self.customer.pk).update(
installDate=timezone.localdate() - timedelta(days=500))
self.customer.refresh_from_db()
def _action(self, **kwargs):
defaults = dict(customer=self.customer, actionType='hamahangi',
status='waite', actionDateTime=timezone.now())
defaults.update(kwargs)
return Action.objects.create(**defaults)
def test_delete_button_rendered_on_customer_action_page(self):
action = self._action()
html = self.client.get(
'/panel/customer/%d/action' % self.customer.pk).content.decode()
self.assertIn('/action/%d/delete' % action.pk, html)
self.assertIn('ax-del is-danger', html)
self.assertIn('method="post"', html)
self.assertIn('csrfmiddlewaretoken', html)
# تأیید پیش از حذف
self.assertIn('confirm(', html)
def test_delete_button_absent_on_actionlist(self):
action = self._action(forUser=self.user)
html = self.client.get(
'/panel/ActionList/%d/today' % self.user.pk).content.decode()
self.assertNotIn('ax-del is-danger', html,
'دکمهٔ حذف نباید در فهرست وظایف ظاهر شود')
self.assertNotIn('/action/%d/delete' % action.pk, html)
def test_survey_action_shows_locked_state(self):
self._action(nazarDT=timezone.now(), nazar='خوب', grade=20)
html = self.client.get(
'/panel/customer/%d/action' % self.customer.pk).content.decode()
self.assertIn('ax-del is-locked', html)
self.assertNotIn('ax-del is-danger', html)
def test_deleted_action_hidden_by_default(self):
kept = self._action()
gone = self._action()
self.client.post(reverse('admin_panel:customerActionDelete',
args=[self.customer.pk, gone.pk]))
html = self.client.get(
'/panel/customer/%d/action' % self.customer.pk).content.decode()
self.assertIn("/action/%d/delete" % kept.pk, html)
self.assertNotIn("/action/%d/delete" % gone.pk, html)
class DeleteButtonDoesNotOpenModalTests(TestCase):
"""کلیک روی «حذف» نباید مودال ویرایش را باز کند.
دکمهٔ حذف داخل کارتی است که کلاس ``actionBoxOpenB`` دارد و هندلرِ بازکردنِ
مودال روی همان کارت بسته شده است؛ بدون محافظ، رویداد به والد می‌رسد و
مودالِ ویرایش هم باز می‌شد. دو لایه محافظ گذاشته شده است:
۱) گاردِ ``closest('.ax-del-form, .ax-del')`` در هندلرِ ``actionBoxOpenB``
۲) ``stopPropagation`` روی خودِ فرم/دکمهٔ حذف
"""
def setUp(self):
self.user = make_user('noModalOnDelete', superuser=True)
self.client.force_login(self.user)
self.customer = make_customer('حذف بدون مودال')
self.action = make_visit(self.customer, action_dt=at(-1),
finish_dt=at(-1), action_type='hamahangi')
self.html = self.client.get(
'/panel/customer/%d/action' % self.customer.pk).content.decode()
def test_modal_handler_guards_delete_controls(self):
self.assertIn("closest('.ax-del-form, .ax-del')", self.html)
def test_delete_controls_stop_propagation(self):
self.assertIn('stopPropagation', self.html)
self.assertRegex(
self.html,
r"\$\('\.ax-del-form, \.ax-del'\)"
r"\s*\n\s*\.off\('\.axdel'\)"
r"\s*\n\s*\.on\('click\.axdel mousedown\.axdel'",
)
def test_guard_precedes_modal_open_logic(self):
"""گارد باید پیش از ``isEditMode = true`` اجرا شود."""
guard = self.html.index("closest('.ax-del-form, .ax-del')")
opener = self.html.index('isEditMode = true;', guard - 900)
self.assertLess(guard, opener)
def test_delete_form_has_csrf_and_post(self):
self.assertIn('ax-del-form', self.html)
self.assertIn('csrfmiddlewaretoken', self.html)