Добавлены классы моделей данных Account, Session
This commit is contained in:
@@ -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
|
||||
|
||||
26
net/xeaf/rack/core/core_model.py
Normal file
26
net/xeaf/rack/core/core_model.py
Normal 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
|
||||
27
net/xeaf/rack/core/core_persistent_model.py
Normal file
27
net/xeaf/rack/core/core_persistent_model.py
Normal 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
|
||||
84
net/xeaf/rack/migrations/0001_initial.py
Normal file
84
net/xeaf/rack/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -7,3 +7,6 @@
|
||||
"""
|
||||
Пакет описания классов моделей данных
|
||||
"""
|
||||
|
||||
from .account_model import AccountModel
|
||||
from .session_model import SessionModel
|
||||
|
||||
116
net/xeaf/rack/models/account_model.py
Normal file
116
net/xeaf/rack/models/account_model.py
Normal 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")
|
||||
93
net/xeaf/rack/models/session_model.py
Normal file
93
net/xeaf/rack/models/session_model.py
Normal 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")
|
||||
@@ -7,3 +7,6 @@
|
||||
"""
|
||||
Пакет описания тестов классов моделей данных
|
||||
"""
|
||||
|
||||
from .account_model_tests import AccountModelTests
|
||||
from .session_model_tests import SessionModelTests
|
||||
|
||||
72
net/xeaf/rack/tests/models/account_model_tests.py
Normal file
72
net/xeaf/rack/tests/models/account_model_tests.py
Normal 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()
|
||||
|
||||
83
net/xeaf/rack/tests/models/session_model_tests.py
Normal file
83
net/xeaf/rack/tests/models/session_model_tests.py
Normal 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()
|
||||
@@ -99,6 +99,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
},
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'net_xeaf_rack.AccountModel'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
Reference in New Issue
Block a user