Начало переноса классов миксинов
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Работа с криптографией
|
||||
Описание класса CryptoManager
|
||||
"""
|
||||
|
||||
import secrets
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания моделей данных
|
||||
Пакет описания классов моделей данных
|
||||
"""
|
||||
|
||||
13
net/xeaf/rack/models/mixins/__init__.py
Normal file
13
net/xeaf/rack/models/mixins/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания классов расширений моделей данных
|
||||
"""
|
||||
|
||||
from .created_at_mixin import CreatedAtMixin
|
||||
from .deleted_at_mixin import DeletedAtMixin
|
||||
from .expired_at_mixin import ExpiresAtMixin
|
||||
34
net/xeaf/rack/models/mixins/created_at_mixin.py
Normal file
34
net/xeaf/rack/models/mixins/created_at_mixin.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CreatedAtMixin
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CreatedAtMixin(models.Model):
|
||||
"""
|
||||
Добавляет к модели свойство даты и времени создания
|
||||
"""
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
null=False,
|
||||
blank=False,
|
||||
editable=False,
|
||||
verbose_name=_("rack.mixin.created_at"),
|
||||
)
|
||||
""" Дата и время создания """
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Конфигурация класса
|
||||
"""
|
||||
|
||||
abstract = True
|
||||
65
net/xeaf/rack/models/mixins/deleted_at_mixin.py
Normal file
65
net/xeaf/rack/models/mixins/deleted_at_mixin.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DeletedAtMixin
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class DeletedAtMixin(models.Model):
|
||||
"""
|
||||
Добавляет к модели свойство даты и времени удаления
|
||||
"""
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
editable=True,
|
||||
verbose_name=_("rack.mixin.deleted_at"),
|
||||
)
|
||||
""" Дата и время удаления """
|
||||
|
||||
def soft_delete(self, save: bool = True) -> None:
|
||||
"""
|
||||
Помечает запись на удаление
|
||||
|
||||
:param save: Признак необходимости сохранения
|
||||
"""
|
||||
|
||||
self.deleted_at = timezone.now()
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def undelete(self, save: bool = True) -> None:
|
||||
"""
|
||||
Снимает пометку удаленной записи
|
||||
|
||||
:param save: Признак необходимости сохранения
|
||||
"""
|
||||
|
||||
self.deleted_at = None
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def is_deleted(self) -> bool:
|
||||
"""
|
||||
Возвращает признак удаленной записи
|
||||
|
||||
:return: True, если запись была удалена
|
||||
"""
|
||||
|
||||
return self.deleted_at is not None
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Конфигурация класса
|
||||
"""
|
||||
|
||||
abstract = True
|
||||
72
net/xeaf/rack/models/mixins/expired_at_mixin.py
Normal file
72
net/xeaf/rack/models/mixins/expired_at_mixin.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExpiredAtMixin
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ExpiresAtMixin(models.Model):
|
||||
"""
|
||||
Реализует свойство даты и времени истечения
|
||||
"""
|
||||
|
||||
expires_at = models.DateTimeField(
|
||||
null=False,
|
||||
blank=False,
|
||||
editable=True,
|
||||
verbose_name=_("rack.mixin.expires_at"),
|
||||
)
|
||||
""" Дата и время истечения """
|
||||
|
||||
def renew(self, seconds: int, save: bool = False) -> None:
|
||||
"""
|
||||
Устанавливает время истечения на заданное количество секунд
|
||||
|
||||
:param seconds: Количество секунд до истечения
|
||||
:param save: Признак необходимости сохранения
|
||||
"""
|
||||
|
||||
# Проверка значений
|
||||
if seconds <=0:
|
||||
raise ValueError(f"The seconds parameter must be greater than 0")
|
||||
|
||||
self.expires_at = timezone.now() + timedelta(seconds=seconds)
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def expire(self, save: bool = False) -> None:
|
||||
"""
|
||||
Устанавливает время истечения на сейчас
|
||||
|
||||
:param save: Признак необходимости сохранения
|
||||
"""
|
||||
|
||||
self.expires_at = timezone.now()
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""
|
||||
Возвращает признак просроченного значения
|
||||
|
||||
:return: Признак просроченного значения
|
||||
"""
|
||||
|
||||
return self.expires_at < timezone.now()
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Параметры класса
|
||||
"""
|
||||
|
||||
abstract = True
|
||||
@@ -7,3 +7,13 @@
|
||||
"""
|
||||
Пакет описания классов тестов
|
||||
"""
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
if 'test' in sys.argv:
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message="Model .* was already registered",
|
||||
category=RuntimeWarning
|
||||
)
|
||||
|
||||
9
net/xeaf/rack/tests/models/__init__.py
Normal file
9
net/xeaf/rack/tests/models/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов моделей данных
|
||||
"""
|
||||
14
net/xeaf/rack/tests/models/mixins/__init__.py
Normal file
14
net/xeaf/rack/tests/models/mixins/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов расширений моделей данных
|
||||
"""
|
||||
|
||||
from .created_at_mixin_tests import CreatedAtMixinTests
|
||||
from .deleted_at_mixin_tests import DeletedAtMixinTests
|
||||
from .expired_at_mixin_tests import ExpiresAtMixinTests
|
||||
|
||||
129
net/xeaf/rack/tests/models/mixins/created_at_mixin_tests.py
Normal file
129
net/xeaf/rack/tests/models/mixins/created_at_mixin_tests.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CreatedAtMixinTests
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from net.xeaf.rack.models.mixins import CreatedAtMixin
|
||||
|
||||
|
||||
class CreatedAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина CreatedAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
# Отключаем foreign keys
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
# Создаем тестовую модель
|
||||
class TestCreatedAtModel(CreatedAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_created_at_mixin'
|
||||
|
||||
self.TestModel = TestCreatedAtModel
|
||||
|
||||
# Создаем таблицу
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestCreatedAtModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
# Удаляем таблицу
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
# Включаем foreign keys обратно
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_created_at_is_auto_now_add(self):
|
||||
"""
|
||||
Проверяем, что created_at автоматически заполняется при создании
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertIsNotNone(instance.created_at)
|
||||
self.assertIsInstance(instance.created_at, datetime)
|
||||
|
||||
def test_created_at_is_auto_now_add_true(self):
|
||||
"""
|
||||
Проверяем, что auto_now_add=True
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertIsInstance(field, models.DateTimeField)
|
||||
self.assertTrue(field.auto_now_add)
|
||||
|
||||
def test_created_at_editable_false(self):
|
||||
"""
|
||||
Проверяем, что editable=False
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertFalse(field.editable)
|
||||
|
||||
def test_different_instances_have_different_created_at(self):
|
||||
"""
|
||||
Проверяем, что у разных экземпляров разное время создания
|
||||
"""
|
||||
|
||||
instance1 = self.TestModel.objects.create(name='First')
|
||||
time.sleep(0.1)
|
||||
instance2 = self.TestModel.objects.create(name='Second')
|
||||
|
||||
self.assertNotEqual(instance1.created_at, instance2.created_at)
|
||||
self.assertLess(instance1.created_at, instance2.created_at)
|
||||
|
||||
def test_created_at_preserved_on_update(self):
|
||||
"""
|
||||
Проверяем, что created_at не меняется при обновлении
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
old_created_at = instance.created_at
|
||||
|
||||
instance.name = 'Updated Name'
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.created_at, old_created_at)
|
||||
|
||||
def test_created_at_not_null(self):
|
||||
"""
|
||||
Проверяем, что created_at не может быть NULL
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertFalse(field.null)
|
||||
|
||||
def test_verbose_name(self):
|
||||
"""
|
||||
Проверяем verbose_name поля
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertIsInstance(field, models.Field)
|
||||
self.assertEqual(field.verbose_name, "rack.mixin.created_at")
|
||||
251
net/xeaf/rack/tests/models/mixins/deleted_at_mixin_tests.py
Normal file
251
net/xeaf/rack/tests/models/mixins/deleted_at_mixin_tests.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CreatedAtMixinTests
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.models.mixins import DeletedAtMixin
|
||||
|
||||
|
||||
class DeletedAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина DeletedAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestDeletedAtModelModel(DeletedAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_deleted_at_mixin'
|
||||
|
||||
self.TestModel = TestDeletedAtModelModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestDeletedAtModelModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_deleted_at_null_by_default(self):
|
||||
"""
|
||||
Проверяем, что при создании deleted_at = None
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_sets_current_time(self):
|
||||
"""
|
||||
Проверяем, что soft_delete() устанавливает текущее время
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
before = timezone.now()
|
||||
|
||||
instance.soft_delete(save=False)
|
||||
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
self.assertGreaterEqual(instance.deleted_at, before)
|
||||
self.assertLessEqual(instance.deleted_at, timezone.now())
|
||||
|
||||
def test_soft_delete_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что при soft_delete(save=True) значение сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
|
||||
# Обновляем из БД
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что при soft_delete(save=False) значение НЕ сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=False)
|
||||
|
||||
# Обновляем из БД
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_saves_by_default(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию soft_delete() сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete()
|
||||
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
|
||||
def test_is_deleted_returns_false_when_not_deleted(self):
|
||||
"""
|
||||
Проверяем, что is_deleted() возвращает False для активной записи
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertFalse(instance.is_deleted())
|
||||
|
||||
def test_is_deleted_returns_true_when_deleted(self):
|
||||
"""
|
||||
Проверяем, что is_deleted() возвращает True для удаленной записи
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=False)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
def test_undelete_restores_record(self):
|
||||
"""
|
||||
Проверяем, что undelete() восстанавливает запись
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=False)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=False)
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_undelete_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что при undelete(save=True) значение сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_undelete_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что при undelete(save=False) значение НЕ сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
# В БД все еще deleted_at не None
|
||||
self.assertTrue(instance.is_deleted())
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
|
||||
def test_undelete_saves_by_default(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию undelete() сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
|
||||
instance.undelete()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_can_set_deleted_at_manually(self):
|
||||
"""
|
||||
Проверяем, что можно установить deleted_at вручную
|
||||
"""
|
||||
|
||||
custom_time = timezone.now() - timedelta(days=1)
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.deleted_at = custom_time
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.deleted_at, custom_time)
|
||||
|
||||
def test_multiple_soft_deletes_update_time(self):
|
||||
"""
|
||||
Проверяем, что повторный вызов soft_delete() обновляет время
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
instance.soft_delete(save=False)
|
||||
first_time = instance.deleted_at
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
instance.soft_delete(save=False)
|
||||
second_time = instance.deleted_at
|
||||
|
||||
self.assertNotEqual(first_time, second_time)
|
||||
self.assertGreater(second_time, first_time)
|
||||
|
||||
def test_undelete_after_manual_delete(self):
|
||||
"""
|
||||
Проверяем, что undelete() работает после ручной установки deleted_at
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.deleted_at = timezone.now() - timedelta(days=1)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=False)
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_twice_with_save(self):
|
||||
"""
|
||||
Проверяем, что повторный soft_delete с save=True корректно работает
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
instance.soft_delete(save=True)
|
||||
first_time = instance.deleted_at
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
instance.soft_delete(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertNotEqual(instance.deleted_at, first_time)
|
||||
self.assertGreater(instance.deleted_at, first_time)
|
||||
320
net/xeaf/rack/tests/models/mixins/expired_at_mixin_tests.py
Normal file
320
net/xeaf/rack/tests/models/mixins/expired_at_mixin_tests.py
Normal file
@@ -0,0 +1,320 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExpiredAtMixinTests
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.db.models.fields import DateTimeField
|
||||
from django.test import TransactionTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.models.mixins import ExpiresAtMixin
|
||||
|
||||
|
||||
class ExpiresAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина ExpiresAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestExpiredAtModel(ExpiresAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_expires_at_mixin'
|
||||
|
||||
self.TestModel = TestExpiredAtModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestExpiredAtModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_expires_at_required(self):
|
||||
"""
|
||||
Проверяем, что expires_at обязательное поле
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('expires_at')
|
||||
self.assertFalse(field.null)
|
||||
self.assertIsInstance(field, DateTimeField)
|
||||
self.assertFalse(field.blank)
|
||||
|
||||
def test_renew_sets_expiration_time(self):
|
||||
"""
|
||||
Проверяем, что renew() устанавливает время истечения
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
before = timezone.now()
|
||||
instance.renew(seconds=3600, save=False)
|
||||
|
||||
self.assertIsNotNone(instance.expires_at)
|
||||
self.assertGreaterEqual(instance.expires_at, before + timedelta(seconds=3600))
|
||||
self.assertLessEqual(instance.expires_at, timezone.now() + timedelta(seconds=3600))
|
||||
|
||||
def test_renew_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что renew(save=True) сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
instance.renew(seconds=3600, save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertGreaterEqual(instance.expires_at, timezone.now())
|
||||
|
||||
def test_renew_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что renew(save=False) НЕ сохраняет в БД
|
||||
"""
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.renew(seconds=3600, save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_renew_raises_error_for_negative_seconds(self):
|
||||
"""
|
||||
Проверяем, что renew() выбрасывает ошибку для отрицательного значения
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
instance.renew(seconds=-1)
|
||||
|
||||
self.assertIn('greater than 0', str(context.exception))
|
||||
|
||||
def test_renew_raises_error_for_zero_seconds(self):
|
||||
"""
|
||||
Проверяем, что renew() выбрасывает ошибку для нулевого значения
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
instance.renew(seconds=0)
|
||||
|
||||
self.assertIn('greater than 0', str(context.exception))
|
||||
|
||||
def test_expire_sets_time_to_now(self):
|
||||
"""
|
||||
Проверяем, что expire() устанавливает время истечения на сейчас
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
before = timezone.now()
|
||||
instance.expire(save=False)
|
||||
|
||||
self.assertIsNotNone(instance.expires_at)
|
||||
self.assertGreaterEqual(instance.expires_at, before)
|
||||
self.assertLessEqual(instance.expires_at, timezone.now())
|
||||
|
||||
def test_expire_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что expire(save=True) сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
instance.expire(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertLessEqual(instance.expires_at, timezone.now())
|
||||
|
||||
def test_expire_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что expire(save=False) НЕ сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.expire(save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_is_expired_returns_false_when_not_expired(self):
|
||||
"""
|
||||
Проверяем, что is_expired() возвращает False для активной записи
|
||||
"""
|
||||
|
||||
future_time = timezone.now() + timedelta(days=1)
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=future_time
|
||||
)
|
||||
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
def test_is_expired_returns_true_when_expired(self):
|
||||
"""
|
||||
Проверяем, что is_expired() возвращает True для просроченной записи
|
||||
"""
|
||||
|
||||
past_time = timezone.now() - timedelta(days=1)
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=past_time
|
||||
)
|
||||
|
||||
self.assertTrue(instance.is_expired())
|
||||
|
||||
def test_is_expired_after_renew(self):
|
||||
"""
|
||||
Проверяем, что is_expired() корректно работает после renew()
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() - timedelta(days=1)
|
||||
)
|
||||
self.assertTrue(instance.is_expired())
|
||||
|
||||
instance.renew(seconds=3600, save=False)
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
def test_is_expired_after_expire(self):
|
||||
"""
|
||||
Проверяем, что is_expired() корректно работает после expire()
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
instance.expire(save=False)
|
||||
self.assertTrue(instance.is_expired())
|
||||
|
||||
def test_renew_with_large_seconds(self):
|
||||
"""
|
||||
Проверяем, что renew() работает с большими значениями
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
seconds = 86400 * 365 # 1 год
|
||||
instance.renew(seconds=seconds, save=False)
|
||||
|
||||
expected = timezone.now() + timedelta(seconds=seconds)
|
||||
self.assertLessEqual(instance.expires_at, expected + timedelta(seconds=1))
|
||||
self.assertGreaterEqual(instance.expires_at, expected - timedelta(seconds=1))
|
||||
|
||||
def test_renew_saves_by_default_false(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию renew() НЕ сохраняет в БД
|
||||
(save=False по умолчанию, в отличие от soft_delete)
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.renew(seconds=3600)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_expire_saves_by_default_false(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию expire() НЕ сохраняет в БД
|
||||
(save=False по умолчанию, в отличие от soft_delete)
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.expire()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_expires_at_editable_true(self):
|
||||
"""
|
||||
Проверяем, что поле expires_at редактируемое
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('expires_at')
|
||||
self.assertTrue(field.editable)
|
||||
|
||||
def test_can_set_expires_at_manually(self):
|
||||
"""
|
||||
Проверяем, что можно установить expires_at вручную
|
||||
"""
|
||||
|
||||
custom_time = timezone.now() + timedelta(days=5)
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=custom_time
|
||||
)
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, custom_time)
|
||||
Reference in New Issue
Block a user