Классы миксинов перенесены в проект

This commit is contained in:
2026-07-11 15:12:30 +03:00
parent 5d30e8cbaf
commit 760480536c
9 changed files with 485 additions and 2 deletions

View File

@@ -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

View File

@@ -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(

View 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

View 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

View 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

View File

@@ -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

View 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'))

View 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)

View 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)