Классы миксинов перенесены в проект
This commit is contained in:
@@ -11,3 +11,6 @@
|
|||||||
from .created_at_mixin import CreatedAtMixin
|
from .created_at_mixin import CreatedAtMixin
|
||||||
from .deleted_at_mixin import DeletedAtMixin
|
from .deleted_at_mixin import DeletedAtMixin
|
||||||
from .expired_at_mixin import ExpiresAtMixin
|
from .expired_at_mixin import ExpiresAtMixin
|
||||||
|
from .slug_mixin import SlugMixin
|
||||||
|
from .updated_at_mixin import UpdatedAtMixin
|
||||||
|
from .uuid_id_mixin import UuidIdMixin
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
class ExpiresAtMixin(models.Model):
|
class ExpiresAtMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
Реализует свойство даты и времени истечения
|
Добавляет к модели свойство даты и времени истечения
|
||||||
"""
|
"""
|
||||||
|
|
||||||
expires_at = models.DateTimeField(
|
expires_at = models.DateTimeField(
|
||||||
|
|||||||
34
net/xeaf/rack/models/mixins/slug_mixin.py
Normal file
34
net/xeaf/rack/models/mixins/slug_mixin.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса SlugMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class SlugMixin(models.Model):
|
||||||
|
"""
|
||||||
|
Добавляет к модели свойство Идентификатор
|
||||||
|
"""
|
||||||
|
|
||||||
|
slug = models.SlugField(
|
||||||
|
max_length=36,
|
||||||
|
editable=True,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
verbose_name=_("rack.mixin.slug"),
|
||||||
|
)
|
||||||
|
""" Текстовый идентификатор """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Конфигурация класс
|
||||||
|
"""
|
||||||
|
|
||||||
|
abstract = True
|
||||||
69
net/xeaf/rack/models/mixins/updated_at_mixin.py
Normal file
69
net/xeaf/rack/models/mixins/updated_at_mixin.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса UpdatedAtMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatedAtMixin(models.Model):
|
||||||
|
"""
|
||||||
|
Добавляет к модели свойство даты и времени последнего изменения
|
||||||
|
"""
|
||||||
|
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
editable=True,
|
||||||
|
verbose_name=_("rack.mixin.updated_at"),
|
||||||
|
)
|
||||||
|
""" Дата и время последнего изменения """
|
||||||
|
|
||||||
|
def is_updated(self) -> bool:
|
||||||
|
"""
|
||||||
|
Возвращает признак измененной записи
|
||||||
|
|
||||||
|
:return: True, если запись была изменена после создания
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.updated_at is not None
|
||||||
|
|
||||||
|
def is_updated_since(self, dt: datetime) -> bool:
|
||||||
|
"""
|
||||||
|
Возвращает признак изменения записи после указанной даты и времени
|
||||||
|
|
||||||
|
:param dt: Дата и время для сравнения
|
||||||
|
:return: True, если запись была изменена после указанной даты и времени
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.updated_at is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.updated_at > dt
|
||||||
|
|
||||||
|
def update(self, save: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает дату и время последнего изменения на текущее время
|
||||||
|
|
||||||
|
:param save: Признак необходимости сохранения
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.updated_at = timezone.now()
|
||||||
|
if save:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Конфигурация класса
|
||||||
|
"""
|
||||||
|
|
||||||
|
abstract = True
|
||||||
34
net/xeaf/rack/models/mixins/uuid_id_mixin.py
Normal file
34
net/xeaf/rack/models/mixins/uuid_id_mixin.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса UuidIdMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class UuidIdMixin(models.Model):
|
||||||
|
"""
|
||||||
|
Добавляет к модели свойство уникального идентификатора типа UUID
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = models.UUIDField(
|
||||||
|
primary_key=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
verbose_name=_("rack.mixin.id"))
|
||||||
|
""" Уникальный идентификатор """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Параметры конфигурации класса
|
||||||
|
"""
|
||||||
|
|
||||||
|
abstract = True
|
||||||
@@ -11,4 +11,6 @@
|
|||||||
from .created_at_mixin_tests import CreatedAtMixinTests
|
from .created_at_mixin_tests import CreatedAtMixinTests
|
||||||
from .deleted_at_mixin_tests import DeletedAtMixinTests
|
from .deleted_at_mixin_tests import DeletedAtMixinTests
|
||||||
from .expired_at_mixin_tests import ExpiresAtMixinTests
|
from .expired_at_mixin_tests import ExpiresAtMixinTests
|
||||||
|
from .slug_mixin_tests import SlugMixinTests
|
||||||
|
from .updated_at_mixin_tests import UpdatedAtMixinTests
|
||||||
|
from .uuid_id_mixin_tests import UuidIdMixinTests
|
||||||
|
|||||||
59
net/xeaf/rack/tests/models/mixins/slug_mixin_tests.py
Normal file
59
net/xeaf/rack/tests/models/mixins/slug_mixin_tests.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса SlugMixinTests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.db import models
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from net.xeaf.rack.models.mixins import SlugMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SlugMixinTests(TransactionTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для миксина SlugMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Создаем тестовую модель и таблицу для каждого теста
|
||||||
|
"""
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||||
|
|
||||||
|
class TestSlugModel(SlugMixin):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'test'
|
||||||
|
db_table = 'test_slug_mixin'
|
||||||
|
|
||||||
|
self.TestModel = TestSlugModel
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(TestSlugModel)
|
||||||
|
|
||||||
|
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_slug_field_exists(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что поле slug добавлено в модель
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(self.TestModel, 'slug'))
|
||||||
182
net/xeaf/rack/tests/models/mixins/updated_at_mixin_tests.py
Normal file
182
net/xeaf/rack/tests/models/mixins/updated_at_mixin_tests.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса UpdatedAtMixinTests
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 UpdatedAtMixin
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatedAtMixinTests(TransactionTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для миксина UpdatedAtMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Создаем тестовую модель и таблицу для каждого теста
|
||||||
|
"""
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||||
|
|
||||||
|
class TestUpdatedAtModel(UpdatedAtMixin):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'test'
|
||||||
|
db_table = 'test_updated_at_mixin'
|
||||||
|
|
||||||
|
self.TestModel = TestUpdatedAtModel
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(TestUpdatedAtModel)
|
||||||
|
|
||||||
|
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_updated_at_null_by_default(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что при создании updated_at = None
|
||||||
|
"""
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
self.assertIsNone(instance.updated_at)
|
||||||
|
|
||||||
|
def test_update_sets_current_time(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что update() устанавливает текущее время
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
before = timezone.now()
|
||||||
|
|
||||||
|
instance.update(save=False)
|
||||||
|
|
||||||
|
self.assertIsNotNone(instance.updated_at)
|
||||||
|
self.assertGreaterEqual(instance.updated_at, before)
|
||||||
|
self.assertLessEqual(instance.updated_at, timezone.now())
|
||||||
|
|
||||||
|
def test_update_saves_to_db_when_save_true(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что update(save=True) сохраняет в БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
instance.update(save=True)
|
||||||
|
instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsNotNone(instance.updated_at)
|
||||||
|
|
||||||
|
def test_update_does_not_save_when_save_false(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что update(save=False) НЕ сохраняет в БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
instance.update(save=False)
|
||||||
|
instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsNone(instance.updated_at)
|
||||||
|
|
||||||
|
def test_update_saves_by_default(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что по умолчанию update() сохраняет в БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
instance.update()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsNotNone(instance.updated_at)
|
||||||
|
|
||||||
|
def test_is_updated_returns_false_when_not_updated(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что is_updated() возвращает False для новой записи
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
self.assertFalse(instance.is_updated())
|
||||||
|
|
||||||
|
def test_is_updated_returns_true_when_updated(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что is_updated() возвращает True после update()
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
instance.update(save=False)
|
||||||
|
self.assertTrue(instance.is_updated())
|
||||||
|
|
||||||
|
def test_is_updated_since_returns_false_when_not_updated(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что is_updated_since() возвращает False для новой записи
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
dt = timezone.now() - timedelta(minutes=5)
|
||||||
|
|
||||||
|
self.assertFalse(instance.is_updated_since(dt))
|
||||||
|
|
||||||
|
def test_is_updated_since_returns_true_after_update(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что is_updated_since() возвращает True после update()
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
dt = timezone.now()
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
instance.update(save=False)
|
||||||
|
|
||||||
|
self.assertTrue(instance.is_updated_since(dt))
|
||||||
|
|
||||||
|
def test_is_updated_since_returns_false_if_updated_before_dt(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что is_updated_since() возвращает False,
|
||||||
|
если обновление было раньше указанного времени
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
instance.update(save=False)
|
||||||
|
old_updated_at = instance.updated_at
|
||||||
|
|
||||||
|
# Проверяем, что updated_at был раньше, чем через секунду
|
||||||
|
dt = old_updated_at + timedelta(seconds=1)
|
||||||
|
self.assertFalse(instance.is_updated_since(dt))
|
||||||
|
|
||||||
|
def test_multiple_updates(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что повторный вызов update() обновляет время
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
|
||||||
|
instance.update(save=False)
|
||||||
|
first_time = instance.updated_at
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
instance.update(save=False)
|
||||||
|
second_time = instance.updated_at
|
||||||
|
|
||||||
|
self.assertNotEqual(first_time, second_time)
|
||||||
|
self.assertGreater(second_time, first_time)
|
||||||
100
net/xeaf/rack/tests/models/mixins/uuid_id_mixin_tests.py
Normal file
100
net/xeaf/rack/tests/models/mixins/uuid_id_mixin_tests.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса UuidIdMixinTests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.db import models
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from net.xeaf.rack.models.mixins import UuidIdMixin
|
||||||
|
|
||||||
|
|
||||||
|
class UuidIdMixinTests(TransactionTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для миксина UuidIdMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Создаем тестовую модель и таблицу для каждого теста
|
||||||
|
"""
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||||
|
|
||||||
|
class TestUuidModel(UuidIdMixin):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'test'
|
||||||
|
db_table = 'test_uuid_mixin'
|
||||||
|
|
||||||
|
self.TestModel = TestUuidModel
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(TestUuidModel)
|
||||||
|
|
||||||
|
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_id_is_uuid_by_default(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что при создании id автоматически генерируется как UUID
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
|
||||||
|
self.assertIsNotNone(instance.id)
|
||||||
|
self.assertIsInstance(instance.id, uuid.UUID)
|
||||||
|
|
||||||
|
def test_id_is_unique(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что каждый экземпляр получает уникальный UUID
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance1 = self.TestModel.objects.create(name='First')
|
||||||
|
instance2 = self.TestModel.objects.create(name='Second')
|
||||||
|
|
||||||
|
self.assertNotEqual(instance1.id, instance2.id)
|
||||||
|
|
||||||
|
def test_id_is_primary_key(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что id является первичным ключом
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = self.TestModel._meta.get_field('id')
|
||||||
|
self.assertTrue(field.primary_key)
|
||||||
|
|
||||||
|
def test_id_is_editable_false(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что id нельзя редактировать
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = self.TestModel._meta.get_field('id')
|
||||||
|
self.assertFalse(field.editable)
|
||||||
|
|
||||||
|
def test_id_can_be_used_in_queries(self):
|
||||||
|
"""
|
||||||
|
Проверяем, что можно искать по UUID
|
||||||
|
"""
|
||||||
|
|
||||||
|
instance = self.TestModel.objects.create(name='Test Instance')
|
||||||
|
|
||||||
|
found = self.TestModel.objects.get(id=instance.id)
|
||||||
|
self.assertEqual(found.id, instance.id)
|
||||||
Reference in New Issue
Block a user