Начало переноса классов миксинов
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
# Все права защищены. Лицензия: MIT
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Работа с криптографией
|
Описание класса CryptoManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
|||||||
@@ -5,5 +5,5 @@
|
|||||||
# Все права защищены. Лицензия: MIT
|
# Все права защищены. Лицензия: 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