Добавлены классы моделей данных Account, Session

This commit is contained in:
2026-07-12 12:59:06 +03:00
parent 164598294a
commit b2e15511c1
11 changed files with 512 additions and 0 deletions

View File

@@ -10,4 +10,8 @@
from .core_enum import CoreEnum
from .core_exception import CoreException
from .core_model import CoreModel
from .core_model import CoreModel
from .core_persistent_model import CorePersistentModel
from .core_persistent_model import CorePersistentModel
from .core_response import CoreResponse

View File

@@ -0,0 +1,26 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreModel
"""
from django.db import models
from net.xeaf.rack.models.mixins import UuidIdMixin
class CoreModel(UuidIdMixin, models.Model):
"""
Базовый класс для всех классов моделей данных проекта
"""
class Meta:
"""
Параметры конфигурации класса
"""
abstract = True

View File

@@ -0,0 +1,27 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CorePersistentModel
"""
from net.xeaf.rack.models.mixins import CreatedAtMixin
from net.xeaf.rack.models.mixins import DeletedAtMixin
from net.xeaf.rack.models.mixins import UpdatedAtMixin
from .core_model import CoreModel
class CorePersistentModel(CoreModel, CreatedAtMixin, DeletedAtMixin, UpdatedAtMixin):
"""
Базовый класс для всех классов моделей данных с постоянным хранением
"""
class Meta:
"""
Параметры конфигурации класса
"""
abstract = True

View File

@@ -0,0 +1,84 @@
# Generated by Django 6.0.7 on 2026-07-12 09:19
import django.contrib.auth.models
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='AccountModel',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='rack.mixin.created_at')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='rack.mixin.deleted_at')),
('description', models.TextField(blank=True, max_length=2048, null=True, verbose_name='rack.mixin.description')),
('updated_at', models.DateTimeField(blank=True, null=True, verbose_name='rack.mixin.updated_at')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='rack.mixin.id')),
('username', models.EmailField(default=None, max_length=254, verbose_name='rack.models.account.username')),
('nick_name', models.CharField(blank=True, max_length=32, null=True, verbose_name='rack.models.account.nick_name')),
('full_name', models.CharField(blank=True, max_length=127, null=True, verbose_name='rack.models.account.full_name')),
('joined_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='rack.models.account.joined_at')),
('last_login', models.DateTimeField(blank=True, default=None, null=True, verbose_name='rack.models.account.last_login')),
('extra', models.JSONField(default={}, verbose_name='rack.models.account.extra')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'rack.models.account',
'verbose_name_plural': 'rack.models.account_plural',
'db_table': 'rack_accounts',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='SessionModel',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='rack.mixin.created_at')),
('expires_at', models.DateTimeField(verbose_name='rack.mixin.expires_at')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='rack.mixin.id')),
('token', models.CharField(max_length=32, verbose_name='rack.models.session_model.token')),
('device_id', models.CharField(blank=True, default='WEB', max_length=36, null=True, verbose_name='rack.models.session_model.device_id')),
('fcm_token', models.CharField(blank=True, default=None, max_length=256, null=True, verbose_name='rack.models.session_model.fcm_token')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='rack.models.session_model.account')),
('actual_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fk_session_model_actual_account', to=settings.AUTH_USER_MODEL, verbose_name='rack.models.session_model.actual_account')),
],
options={
'verbose_name': 'rack.models.session',
'verbose_name_plural': 'rack.models.session_plural',
'db_table': 'rack_sessions',
},
),
migrations.AddConstraint(
model_name='accountmodel',
constraint=models.UniqueConstraint(fields=('username',), name='unique_account_username'),
),
migrations.AddIndex(
model_name='sessionmodel',
index=models.Index(fields=['account'], name='rack_sessio_account_bd8396_idx'),
),
migrations.AddIndex(
model_name='sessionmodel',
index=models.Index(fields=['actual_account'], name='rack_sessio_actual__7e7637_idx'),
),
migrations.AddConstraint(
model_name='sessionmodel',
constraint=models.UniqueConstraint(fields=('token',), name='unique_session_token'),
),
]

View File

@@ -7,3 +7,6 @@
"""
Пакет описания классов моделей данных
"""
from .account_model import AccountModel
from .session_model import SessionModel

View File

@@ -0,0 +1,116 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса Account
"""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from net.xeaf.rack.models.mixins import CreatedAtMixin
from net.xeaf.rack.models.mixins import DeletedAtMixin
from net.xeaf.rack.models.mixins import DescriptionMixin
from net.xeaf.rack.models.mixins import UpdatedAtMixin
from net.xeaf.rack.models.mixins import UuidIdMixin
class AccountModel(AbstractUser, UuidIdMixin, CreatedAtMixin, UpdatedAtMixin, DeletedAtMixin, DescriptionMixin, ):
"""
Модель Аккаунта пользователя
"""
first_name = None
""" Удаляем поле Имя """
last_name = None
""" Удаляем поле Фамилия """
date_joined = None
""" Удаляем поле Дата регистрации """
username = models.EmailField(
null=False,
default=None,
blank=False,
verbose_name=_("rack.models.account.username"),
)
""" Имя пользователя """
nick_name = models.CharField(
null=True,
max_length=32,
blank=True,
verbose_name=_("rack.models.account.nick_name"),
)
""" Псевдоним """
full_name = models.CharField(
null=True,
max_length=127,
blank=True,
verbose_name=_("rack.models.account.full_name"),
)
""" Полное имя пользователя """
joined_at = models.DateTimeField(
null=True,
default=None,
blank=True,
verbose_name=_("rack.models.account.joined_at"),
)
""" Дата и время завершения регистрации """
last_login = models.DateTimeField(
null=True,
default=None,
blank=True,
verbose_name=_("rack.models.account.last_login"),
)
""" Дата и время последнего входа """
extra = models.JSONField(
null=False,
default={},
blank=False,
verbose_name=_("rack.models.account.extra"),
)
""" Дополнительная информация """
def get_full_name(self):
"""
Возвращает полное имя пользователя,
требуется для совместимости
"""
return self.full_name
def get_short_name(self):
"""
Возвращает краткое имя пользователя
требуется для совместимости
"""
return self.nick_name
class Meta:
"""
Параметры конфигурации класса
"""
# Уникальность
constraints = [
models.UniqueConstraint(
fields=["username"],
name="unique_account_username"
)
]
# Данные таблицы
db_table = "rack_accounts"
verbose_name = _("rack.models.account")
verbose_name_plural = _("rack.models.account_plural")

View File

@@ -0,0 +1,93 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса Session
"""
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from net.xeaf.rack.core import CoreModel
from net.xeaf.rack.managers import CryptoManager
from net.xeaf.rack.models.mixins import CreatedAtMixin
from net.xeaf.rack.models.mixins import ExpiresAtMixin
class SessionModel(CoreModel, CreatedAtMixin, ExpiresAtMixin):
"""
Модель сессии пользователя
"""
token = models.CharField(
max_length=CryptoManager.DEFAULT_TOKEN_LENGTH,
null=False,
blank=False,
verbose_name=_("rack.models.session_model.token"),
)
""" Токен сессии """
account = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=False,
blank=False,
verbose_name=_("rack.models.session_model.account"),
)
""" Аккаунт пользователя """
actual_account = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="fk_session_model_actual_account",
verbose_name=_("rack.models.session_model.actual_account"),
)
""" Актуальный аккаунт пользователя """
device_id = models.CharField(
max_length=36,
default='WEB',
null=True,
blank=True,
verbose_name=_("rack.models.session_model.device_id"),
)
""" Идентификатор устройства """
fcm_token = models.CharField(
max_length=256,
default=None,
null=True,
blank=True,
verbose_name=_("rack.models.session_model.fcm_token"),
)
""" Токен для подключения к FCM """
class Meta:
"""
Параметры конфигурации класса
"""
# Уникальность
constraints = [
models.UniqueConstraint(
fields=["token"],
name="unique_session_token"
)
]
# Индексы для производительности
indexes = [
models.Index(fields=["account"]),
models.Index(fields=["actual_account"]),
]
# Данные таблицы
db_table = "rack_sessions"
verbose_name = _("rack.models.session")
verbose_name_plural = _("rack.models.session_plural")

View File

@@ -7,3 +7,6 @@
"""
Пакет описания тестов классов моделей данных
"""
from .account_model_tests import AccountModelTests
from .session_model_tests import SessionModelTests

View File

@@ -0,0 +1,72 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса AccountModelTests
"""
from django.db.utils import IntegrityError
from django.test import TestCase
from net.xeaf.rack.models import AccountModel
class AccountModelTests(TestCase):
"""
Тесты для класса AccountModel
"""
def test_create_account_with_defaults(self):
"""
Проверяет корректное создание аккаунта с дефолтными полями
"""
email = "test_user@example.com"
# Создаем через чистый ORM, как ты любишь, построчно
account = AccountModel()
account.username = email
account.save()
# Проверяем контракт полей
self.assertEqual(account.username, email)
self.assertIsNone(account.full_name)
self.assertIsNone(account.nick_name)
# Если ты исправил дефолт на dict, тест гарантирует безопасность:
self.assertEqual(account.extra, {})
def test_compatibility_methods(self):
"""
Проверяет методы совместимости get_full_name и get_short_name
"""
account = AccountModel()
account.username = "names@example.com"
account.full_name = "Иван Иванов"
account.nick_name = "vanya"
account.save()
self.assertEqual(account.get_full_name(), "Иван Иванов")
self.assertEqual(account.get_short_name(), "vanya")
def test_username_uniqueness_constraint(self):
"""
Проверяет жесткое ограничение уникальности на уровне UniqueConstraint
"""
duplicate_email = "duplicate@example.com"
# Создаем первого
account1 = AccountModel(username=duplicate_email)
account1.save()
# Попытка создать второго с тем же username должна намертво упасть в БД
account2 = AccountModel(username=duplicate_email)
with self.assertRaises(IntegrityError):
account2.save()

View File

@@ -0,0 +1,83 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса SessionModelTests
"""
from django.db.utils import IntegrityError
from django.test import TestCase
from net.xeaf.rack.managers import CryptoManager
from net.xeaf.rack.models import AccountModel
from net.xeaf.rack.models import SessionModel
class SessionModelTests(TestCase):
"""
Тесты для класса SessionModel
"""
def setUp(self):
"""
Атомарно создаем тестовых пользователей для связей
"""
self.primary_user = AccountModel.objects.create_user(
username="primary@example.com",
password="secure_password_123"
)
self.actual_user = AccountModel.objects.create_user(
username="actual@example.com",
password="secure_password_456"
)
def test_create_session_with_defaults_and_relations(self):
"""
Проверяет успешное создание сессии, заполнение дефолтных полей и внешних ключей
"""
token_str = CryptoManager.gen_token()
session = SessionModel()
session.token = token_str
session.account = self.primary_user
session.actual_account = self.actual_user
session.renew(3600)
session.save()
# Проверяем контракт полей и связей
self.assertEqual(session.token, token_str)
self.assertEqual(session.account.id, self.primary_user.id)
self.assertEqual(session.actual_account.id, self.actual_user.id)
self.assertEqual(session.device_id, "WEB")
def test_token_uniqueness_constraint(self):
"""
Проверяет жесткое ограничение уникальности токена сессии в базе данных
"""
duplicate_token = "same_token_xyz"
# Создаем первую валидную сессию
session1 = SessionModel(
token=duplicate_token,
account=self.primary_user,
actual_account=self.actual_user
)
session1.renew(3600)
session1.save()
# Попытка создать вторую сессию с тем же токеном должна намертво упасть в БД
session2 = SessionModel(
token=duplicate_token,
account=self.primary_user,
actual_account=self.actual_user
)
with self.assertRaises(IntegrityError):
session2.renew(3600)
session2.save()

View File

@@ -99,6 +99,7 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
AUTH_USER_MODEL = 'net_xeaf_rack.AccountModel'
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/