Работа с Enum полями
This commit is contained in:
@@ -12,3 +12,5 @@ from .auth_reject_reson import AuthRejectReason
|
|||||||
from .http_method import HttpMethod
|
from .http_method import HttpMethod
|
||||||
from .ui_language import UILanguage
|
from .ui_language import UILanguage
|
||||||
from .view_set_action import ViewSetAction
|
from .view_set_action import ViewSetAction
|
||||||
|
from .workspace_kind import WorkspaceKind
|
||||||
|
from .workspace_role import WorkspaceRole
|
||||||
|
|||||||
26
net/xeaf/rack/enums/workspace_kind.py
Normal file
26
net/xeaf/rack/enums/workspace_kind.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса WorkspaceKind
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import unique
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreEnum
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class WorkspaceKind(CoreEnum):
|
||||||
|
"""
|
||||||
|
Перечисление Видов пространств
|
||||||
|
"""
|
||||||
|
|
||||||
|
PRIVATE = 'PRIVATE'
|
||||||
|
""" Частное (личное) пространство """
|
||||||
|
|
||||||
|
CORPORATE = 'CORPORATE'
|
||||||
|
""" Корпоративное пространство """
|
||||||
29
net/xeaf/rack/enums/workspace_role.py
Normal file
29
net/xeaf/rack/enums/workspace_role.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса WorkspaceRole
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import unique
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreEnum
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class WorkspaceRole(CoreEnum):
|
||||||
|
"""
|
||||||
|
Перечисление ролей Аккаунта в рабочем пространстве
|
||||||
|
"""
|
||||||
|
|
||||||
|
OWNER = 'OWNER'
|
||||||
|
""" Владелец """
|
||||||
|
|
||||||
|
ADMIN = 'ADMIN'
|
||||||
|
""" Администратор """
|
||||||
|
|
||||||
|
MEMBER = 'MEMBER'
|
||||||
|
""" Участник """
|
||||||
@@ -14,6 +14,7 @@ from django.core.management.base import no_translations
|
|||||||
from net.xeaf.rack.core import CoreCommand
|
from net.xeaf.rack.core import CoreCommand
|
||||||
from net.xeaf.rack.models.user_story import AccountTestData
|
from net.xeaf.rack.models.user_story import AccountTestData
|
||||||
from net.xeaf.rack.models.user_story import SessionTestData
|
from net.xeaf.rack.models.user_story import SessionTestData
|
||||||
|
from net.xeaf.rack.models.user_story import WorkspaceTestData
|
||||||
|
|
||||||
|
|
||||||
class Command(CoreCommand):
|
class Command(CoreCommand):
|
||||||
@@ -37,6 +38,7 @@ class Command(CoreCommand):
|
|||||||
call_command("migrate")
|
call_command("migrate")
|
||||||
|
|
||||||
# Инициализируем данные
|
# Инициализируем данные
|
||||||
|
WorkspaceTestData.init_workspaces()
|
||||||
AccountTestData.init_accounts()
|
AccountTestData.init_accounts()
|
||||||
SessionTestData.init_sessions()
|
SessionTestData.init_sessions()
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ from .crypto_manager import CryptoManager
|
|||||||
from .locale_manager import LocaleManager
|
from .locale_manager import LocaleManager
|
||||||
from .request_manager import RequestManager
|
from .request_manager import RequestManager
|
||||||
from .session_manager import SessionManager
|
from .session_manager import SessionManager
|
||||||
|
from .workspace_manager import WorkspaceManager
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class AccountManager:
|
|||||||
Проверяет возможность авторизации Аккаунта
|
Проверяет возможность авторизации Аккаунта
|
||||||
|
|
||||||
:param account: Аккаунт
|
:param account: Аккаунт
|
||||||
|
|
||||||
:return: Причина отказа в авторизации или None, если авторизация возможна
|
:return: Причина отказа в авторизации или None, если авторизация возможна
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class AccountManager:
|
|||||||
return AuthRejectReason.INVALID_USERNAME_OR_PASSWORD
|
return AuthRejectReason.INVALID_USERNAME_OR_PASSWORD
|
||||||
if not account.is_active:
|
if not account.is_active:
|
||||||
return AuthRejectReason.ACCOUNT_IS_NOT_ACTIVE
|
return AuthRejectReason.ACCOUNT_IS_NOT_ACTIVE
|
||||||
if account.joined_at is None:
|
if not account.joined_at:
|
||||||
return AuthRejectReason.REGISTRATION_IS_NOT_COMPLETE
|
return AuthRejectReason.REGISTRATION_IS_NOT_COMPLETE
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -56,7 +57,7 @@ class AccountManager:
|
|||||||
:return: Аккаунт или None, если Аккаунт не найден
|
:return: Аккаунт или None, если Аккаунт не найден
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AccountModel.objects.filter(username=username).first()
|
return AccountModel.objects.filter(username=username).select_related("workspace").first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def restore_account(cls, account: AccountModel) -> None:
|
def restore_account(cls, account: AccountModel) -> None:
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class SessionManager[T:SessionModel]:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
model = cls._get_session_model_class()
|
model = cls._get_session_model_class()
|
||||||
session = model.objects.filter(token__exact=token).first()
|
session = model.objects.filter(token__exact=token).select_related("account__workspace").first()
|
||||||
if session is not None and not session.is_expired():
|
if session is not None and not session.is_expired():
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|||||||
29
net/xeaf/rack/managers/workspace_manager.py
Normal file
29
net/xeaf/rack/managers/workspace_manager.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса AccountManager
|
||||||
|
"""
|
||||||
|
|
||||||
|
from net.xeaf.rack.models import WorkspaceModel
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceManager:
|
||||||
|
"""
|
||||||
|
Реализует методы работы с Рабочими пространствами
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_by_slug(cls, slug: str) -> WorkspaceModel | None:
|
||||||
|
"""
|
||||||
|
Поиск Рабочего пространства по его идентификатору
|
||||||
|
|
||||||
|
:param slug: Идентификатор Рабочего пространства
|
||||||
|
|
||||||
|
:return: Рабочее пространство или None
|
||||||
|
"""
|
||||||
|
|
||||||
|
return WorkspaceModel.objects.filter(slug=slug).first()
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
# Generated by Django 6.0.7 on 2026-07-14 09:26
|
# Generated by Django 6.0.7 on 2026-07-24 08:25
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import net.xeaf.rack.enums.workspace_role
|
||||||
import uuid
|
import uuid
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -16,6 +17,23 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WorkspaceModel',
|
||||||
|
fields=[
|
||||||
|
('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')),
|
||||||
|
('slug', models.SlugField(max_length=36, verbose_name='rack.mixin.slug')),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'rack.models.workspace',
|
||||||
|
'verbose_name_plural': 'rack.models.workspaces',
|
||||||
|
'db_table': 'rack_workspaces',
|
||||||
|
'indexes': [models.Index(fields=['slug'], name='rack_worksp_slug_3aa25b_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AccountModel',
|
name='AccountModel',
|
||||||
fields=[
|
fields=[
|
||||||
@@ -34,9 +52,11 @@ class Migration(migrations.Migration):
|
|||||||
('full_name', models.CharField(blank=True, max_length=127, null=True, verbose_name='rack.models.account.full_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')),
|
('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')),
|
('last_login', models.DateTimeField(blank=True, default=None, null=True, verbose_name='rack.models.account.last_login')),
|
||||||
|
('role', models.CharField(choices=net.xeaf.rack.enums.workspace_role.WorkspaceRole.choices, default=net.xeaf.rack.enums.workspace_role.WorkspaceRole['OWNER'], max_length=12, verbose_name='rack.models.account.role')),
|
||||||
('extra', models.JSONField(default=dict, verbose_name='rack.models.account.extra')),
|
('extra', models.JSONField(default=dict, 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')),
|
('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')),
|
('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')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='net_xeaf_rack.workspacemodel', verbose_name='rack.models.workspace')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'rack.models.account',
|
'verbose_name': 'rack.models.account',
|
||||||
@@ -56,29 +76,19 @@ class Migration(migrations.Migration):
|
|||||||
('token', models.CharField(max_length=255, verbose_name='rack.models.session_model.token')),
|
('token', models.CharField(max_length=255, 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')),
|
('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')),
|
('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')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='rack.models.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')),
|
('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={
|
options={
|
||||||
'verbose_name': 'rack.models.session',
|
'verbose_name': 'rack.models.session',
|
||||||
'verbose_name_plural': 'rack.models.session_plural',
|
'verbose_name_plural': 'rack.models.session_plural',
|
||||||
'db_table': 'rack_sessions',
|
'db_table': 'rack_sessions',
|
||||||
|
'indexes': [models.Index(fields=['account'], name='rack_sessio_account_bd8396_idx'), models.Index(fields=['actual_account'], name='rack_sessio_actual__7e7637_idx')],
|
||||||
|
'constraints': [models.UniqueConstraint(fields=('token',), name='unique_session_token')],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='accountmodel',
|
model_name='accountmodel',
|
||||||
constraint=models.UniqueConstraint(fields=('username',), name='unique_account_username'),
|
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'),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# Generated by Django 6.0.7 on 2026-07-20 07:59
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('net_xeaf_rack', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='sessionmodel',
|
|
||||||
name='account',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='rack.models.account'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
19
net/xeaf/rack/migrations/0002_workspacemodel_kind.py
Normal file
19
net/xeaf/rack/migrations/0002_workspacemodel_kind.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-07-28 08:29
|
||||||
|
|
||||||
|
import net.xeaf.rack.enums.workspace_kind
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('net_xeaf_rack', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemodel',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(choices=net.xeaf.rack.enums.workspace_kind.WorkspaceKind.choices, default=net.xeaf.rack.enums.workspace_kind.WorkspaceKind['PRIVATE'], max_length=16, verbose_name='rack.models.workspace_model.kind'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
from .account_model import AccountModel
|
from .account_model import AccountModel
|
||||||
from .session_model import SessionModel
|
from .session_model import SessionModel
|
||||||
|
from .workspace_model import WorkspaceModel
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from net.xeaf.rack.enums import WorkspaceRole
|
||||||
from net.xeaf.rack.models.mixins import CreatedAtMixin
|
from net.xeaf.rack.models.mixins import CreatedAtMixin
|
||||||
from net.xeaf.rack.models.mixins import DeletedAtMixin
|
from net.xeaf.rack.models.mixins import DeletedAtMixin
|
||||||
from net.xeaf.rack.models.mixins import DescriptionMixin
|
from net.xeaf.rack.models.mixins import DescriptionMixin
|
||||||
from net.xeaf.rack.models.mixins import UpdatedAtMixin
|
from net.xeaf.rack.models.mixins import UpdatedAtMixin
|
||||||
from net.xeaf.rack.models.mixins import UuidIdMixin
|
from net.xeaf.rack.models.mixins import UuidIdMixin
|
||||||
|
from .workspace_model import WorkspaceModel
|
||||||
|
|
||||||
class AccountModel(AbstractUser, UuidIdMixin, CreatedAtMixin, UpdatedAtMixin, DeletedAtMixin, DescriptionMixin, ):
|
class AccountModel(AbstractUser, UuidIdMixin, CreatedAtMixin, UpdatedAtMixin, DeletedAtMixin, DescriptionMixin, ):
|
||||||
"""
|
"""
|
||||||
@@ -73,6 +74,25 @@ class AccountModel(AbstractUser, UuidIdMixin, CreatedAtMixin, UpdatedAtMixin, De
|
|||||||
)
|
)
|
||||||
""" Дата и время последнего входа """
|
""" Дата и время последнего входа """
|
||||||
|
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
WorkspaceModel,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("rack.models.workspace"),
|
||||||
|
)
|
||||||
|
""" Рабочее пространство пользователя """
|
||||||
|
|
||||||
|
role = models.CharField(
|
||||||
|
choices=WorkspaceRole.choices,
|
||||||
|
default=WorkspaceRole.OWNER,
|
||||||
|
null=False,
|
||||||
|
max_length=12,
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("rack.models.account.role"),
|
||||||
|
)
|
||||||
|
""" Роль пользователя """
|
||||||
|
|
||||||
extra = models.JSONField(
|
extra = models.JSONField(
|
||||||
null=False,
|
null=False,
|
||||||
default=dict,
|
default=dict,
|
||||||
|
|||||||
12
net/xeaf/rack/models/fields/__init__.py
Normal file
12
net/xeaf/rack/models/fields/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет описания классов полей моделей данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .enum_choice_field import EnumChoiceField
|
||||||
|
from .extensible_enum_choice_field import ExtensibleEnumChoiceField
|
||||||
146
net/xeaf/rack/models/fields/enum_choice_field.py
Normal file
146
net/xeaf/rack/models/fields/enum_choice_field.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса EnumChoiceField
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.core_enum import CoreEnum
|
||||||
|
|
||||||
|
|
||||||
|
class EnumChoiceField(models.CharField):
|
||||||
|
"""
|
||||||
|
Поле выбора перечисления со строгой рантайм-валидацией типов
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAX_DEFAULT_LENGTH = 64
|
||||||
|
""" Максимальный размер поля """
|
||||||
|
|
||||||
|
_enum_cls: Type[CoreEnum]
|
||||||
|
""" Класс перечисления """
|
||||||
|
|
||||||
|
def __init__(self, choices: Type[CoreEnum], *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param choices: Класс перечисления CoreEnum
|
||||||
|
:param args: Дополнительные аргументы
|
||||||
|
:param kwargs: Дополнительные параметры
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(choices, type) or not issubclass(choices, CoreEnum):
|
||||||
|
raise TypeError("The 'choices' parameter must be a subclass of CoreEnum.")
|
||||||
|
|
||||||
|
self._enum_cls = choices
|
||||||
|
|
||||||
|
kwargs.setdefault('max_length', self.MAX_DEFAULT_LENGTH)
|
||||||
|
kwargs['choices'] = choices.choices()
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# noinspection unused-parameter
|
||||||
|
def from_db_value[E: CoreEnum](self, value: Any, expression: Any, connection: Any) -> E | str | None:
|
||||||
|
"""
|
||||||
|
Преобразует сырое значение из базы данных в объект CoreEnum
|
||||||
|
|
||||||
|
:param value: Входящее сырое значение
|
||||||
|
:param expression: Выражение для преобразования значения
|
||||||
|
:param connection: Соединение с базой данных
|
||||||
|
|
||||||
|
:return: Представление объекта CoreEnum или строка, или None
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._normalize_and_validate(value, action_context="read")
|
||||||
|
|
||||||
|
def to_python[E: CoreEnum](self, value: Any) -> E | str | None:
|
||||||
|
"""
|
||||||
|
Строго преобразует входящее значение в объект CoreEnum при очистке форм или сериализации
|
||||||
|
|
||||||
|
:param value: Входящее значение
|
||||||
|
|
||||||
|
:return: Объект CoreEnum или строка, или None
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._normalize_and_validate(value, action_context="clean")
|
||||||
|
|
||||||
|
def validate(self, value: Any, model_instance: Any) -> None:
|
||||||
|
"""
|
||||||
|
Валидация значения поля
|
||||||
|
"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return super().validate(value, model_instance)
|
||||||
|
|
||||||
|
lookup_value = value.value if isinstance(value, self._enum_cls) else value
|
||||||
|
|
||||||
|
if lookup_value not in self._enum_cls.values():
|
||||||
|
raise ValidationError(
|
||||||
|
self.error_messages['invalid_choice'],
|
||||||
|
code='invalid_choice',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(models.CharField, self).validate(value, model_instance)
|
||||||
|
|
||||||
|
def get_prep_value(self, value: Any) -> str | None:
|
||||||
|
"""
|
||||||
|
Преобразует объект CoreEnum обратно в строку перед сохранением в БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Стандартная проверка
|
||||||
|
value = super().get_prep_value(value)
|
||||||
|
|
||||||
|
# Проверяем строгость типов и ограничений через наш метод
|
||||||
|
resolved = self._normalize_and_validate(value, action_context="save")
|
||||||
|
|
||||||
|
if isinstance(resolved, self._enum_cls):
|
||||||
|
return resolved.value
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
def deconstruct(self) -> tuple[str, str, Sequence[Any], dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Возвращает параметры для сериализации поля
|
||||||
|
|
||||||
|
:return: Кортеж с параметрами для сериализации поля
|
||||||
|
"""
|
||||||
|
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
kwargs['choices'] = self._enum_cls
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
def _normalize_and_validate(self, value: Any, action_context: str) -> CoreEnum | str | None:
|
||||||
|
"""
|
||||||
|
Внутренний метод для проверки null/blank ограничений и приведения значения к CoreEnum.
|
||||||
|
Реализует паттерн Fail-Fast для предотвращения работы с некорректными данными.
|
||||||
|
|
||||||
|
:param value: Входящее сырое значение
|
||||||
|
:param action_context: Контекст выполнения для текста ошибки (e.g., 'read', 'save')
|
||||||
|
|
||||||
|
:return: Объект CoreEnum, пустая строка или None
|
||||||
|
:raises ValueError: Если значение нарушает правила null/blank или отсутствует в перечислении
|
||||||
|
"""
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
if not self.null:
|
||||||
|
raise ValueError(f"Cannot {action_context} null; the field '{self.name}' is not nullable.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
if not self.blank:
|
||||||
|
raise ValueError(f"Cannot {action_context} blank string; the field '{self.name}' does not allow blanks.")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(value, self._enum_cls):
|
||||||
|
return value
|
||||||
|
|
||||||
|
return self._enum_cls.from_string(str(value))
|
||||||
156
net/xeaf/rack/models/fields/extensible_enum_choice_field.py
Normal file
156
net/xeaf/rack/models/fields/extensible_enum_choice_field.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ExtensibleEnumChoiceField
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.core_enum import CoreEnum
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensibleEnumChoiceField(models.CharField):
|
||||||
|
"""
|
||||||
|
Конфигурируемое поле выбора перечисления, класс которого
|
||||||
|
полностью настраивается пользователем через settings.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAX_DEFAULT_LENGTH = 64
|
||||||
|
""" Максимальный размер поля по умолчанию """
|
||||||
|
|
||||||
|
_settings_key: str
|
||||||
|
""" Ключ в settings.py для получения замещающего класса перечисления """
|
||||||
|
|
||||||
|
_active_enum_cls: Type[CoreEnum]
|
||||||
|
""" Активный в текущем рантайме класс перечисления """
|
||||||
|
|
||||||
|
def __init__(self, choices: str, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(choices, str):
|
||||||
|
raise TypeError(
|
||||||
|
f"The 'choices' parameter in {self.__class__.__name__} must be a string containing "
|
||||||
|
f"the settings key name, got {type(choices).__name__} instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._settings_key = choices
|
||||||
|
|
||||||
|
user_enum = getattr(settings, self._settings_key, None)
|
||||||
|
|
||||||
|
if user_enum is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"The required configuration setting '{self._settings_key}' "
|
||||||
|
f"is missing or not set in your settings.py!"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(user_enum, type) or not issubclass(user_enum, CoreEnum):
|
||||||
|
raise TypeError(
|
||||||
|
f"The '{self._settings_key}' setting in settings.py "
|
||||||
|
f"must be a subclass of CoreEnum to maintain strict typing."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._active_enum_cls = user_enum
|
||||||
|
|
||||||
|
kwargs.setdefault('max_length', self.MAX_DEFAULT_LENGTH)
|
||||||
|
kwargs['choices'] = self._active_enum_cls.choices()
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _normalize_and_validate(self, value: Any, action_context: str) -> CoreEnum | str | None:
|
||||||
|
"""
|
||||||
|
Внутренний метод для проверки ограничений и приведения значения к активному классу CoreEnum.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
if not self.null:
|
||||||
|
raise ValueError(f"Cannot {action_context} null; the field '{self.name}' is not nullable.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
if not self.blank:
|
||||||
|
raise ValueError(f"Cannot {action_context} blank string; the field '{self.name}' does not allow blanks.")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(value, CoreEnum):
|
||||||
|
if isinstance(value, self._active_enum_cls):
|
||||||
|
return value
|
||||||
|
str_val = value.value
|
||||||
|
else:
|
||||||
|
str_val = str(value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._active_enum_cls.from_string(str_val)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot {action_context} value '{value}'; it cannot be resolved by active "
|
||||||
|
f"enum class '{self._active_enum_cls.__name__}' configured via '{self._settings_key}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
# noinspection unused-parameter
|
||||||
|
def from_db_value[E: CoreEnum](self, value: Any, expression: Any, connection: Any) -> E | str | None:
|
||||||
|
"""
|
||||||
|
Преобразует сырое значение из базы данных в объект замещенного перечисления
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._normalize_and_validate(value, action_context="read")
|
||||||
|
|
||||||
|
def to_python[E: CoreEnum](self, value: Any) -> E | str | None:
|
||||||
|
"""
|
||||||
|
Строго преобразует входящее значение в объект замещенного перечисления
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._normalize_and_validate(value, action_context="clean")
|
||||||
|
|
||||||
|
def validate(self, value: Any, model_instance: Any) -> None:
|
||||||
|
"""
|
||||||
|
Валидация значения поля по замещенному списку
|
||||||
|
"""
|
||||||
|
|
||||||
|
if value is None or value == "":
|
||||||
|
return super().validate(value, model_instance)
|
||||||
|
|
||||||
|
lookup_value = value.value if isinstance(value, CoreEnum) else value
|
||||||
|
|
||||||
|
if lookup_value not in self._active_enum_cls.values():
|
||||||
|
raise ValidationError(
|
||||||
|
self.error_messages['invalid_choice'],
|
||||||
|
code='invalid_choice',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().validate(value, model_instance)
|
||||||
|
|
||||||
|
def get_prep_value(self, value: Any) -> str | None:
|
||||||
|
"""
|
||||||
|
Преобразует объект CoreEnum обратно в строку перед сохранением в БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
value = super().get_prep_value(value)
|
||||||
|
resolved = self._normalize_and_validate(value, action_context="save")
|
||||||
|
|
||||||
|
if isinstance(resolved, self._active_enum_cls):
|
||||||
|
return resolved.value
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
def deconstruct(self) -> tuple[str, str, Sequence[Any], dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Возвращает параметры для сериализации поля
|
||||||
|
|
||||||
|
:return: Кортеж с параметрами для сериализации поля
|
||||||
|
"""
|
||||||
|
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
kwargs['choices'] = self._settings_key
|
||||||
|
return name, path, args, kwargs
|
||||||
@@ -55,6 +55,7 @@ class JournalModel(CoreModel, CreatedAtMixin):
|
|||||||
"""
|
"""
|
||||||
Параметры конфигурации класса
|
Параметры конфигурации класса
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Индексы для производительности
|
# Индексы для производительности
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["account"]),
|
models.Index(fields=["account"]),
|
||||||
|
|||||||
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
from .account_test_data import AccountTestData
|
from .account_test_data import AccountTestData
|
||||||
from .session_test_data import SessionTestData
|
from .session_test_data import SessionTestData
|
||||||
|
from .workspace_test_data import WorkspaceTestData
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from net.xeaf.rack.enums import WorkspaceRole
|
||||||
from net.xeaf.rack.models import AccountModel
|
from net.xeaf.rack.models import AccountModel
|
||||||
from net.xeaf.rack.utils import DateUtils
|
from net.xeaf.rack.utils import DateUtils
|
||||||
|
from .workspace_test_data import WorkspaceTestData
|
||||||
|
|
||||||
|
|
||||||
class AccountTestData:
|
class AccountTestData:
|
||||||
@@ -56,6 +58,8 @@ class AccountTestData:
|
|||||||
account.full_name = "Иван И. Иванов"
|
account.full_name = "Иван И. Иванов"
|
||||||
account.email = "i.ivanov@xeaf.net"
|
account.email = "i.ivanov@xeaf.net"
|
||||||
account.set_password(cls.DEFAULT_PASSWORD)
|
account.set_password(cls.DEFAULT_PASSWORD)
|
||||||
|
account.workspace_id = WorkspaceTestData.IVANOV_WORKSPACE_ID
|
||||||
|
account.role = WorkspaceRole.OWNER
|
||||||
account.joined_at = DateUtils.now()
|
account.joined_at = DateUtils.now()
|
||||||
account.save()
|
account.save()
|
||||||
|
|
||||||
@@ -72,6 +76,8 @@ class AccountTestData:
|
|||||||
account.full_name = "Петр П. Петров"
|
account.full_name = "Петр П. Петров"
|
||||||
account.email = "p.petrov@xeaf.net"
|
account.email = "p.petrov@xeaf.net"
|
||||||
account.set_password(cls.DEFAULT_PASSWORD)
|
account.set_password(cls.DEFAULT_PASSWORD)
|
||||||
|
account.workspace_id = WorkspaceTestData.PETROV_WORKSPACE_ID
|
||||||
|
account.role = WorkspaceRole.OWNER
|
||||||
account.joined_at = DateUtils.now()
|
account.joined_at = DateUtils.now()
|
||||||
account.save()
|
account.save()
|
||||||
|
|
||||||
|
|||||||
64
net/xeaf/rack/models/user_story/workspace_test_data.py
Normal file
64
net/xeaf/rack/models/user_story/workspace_test_data.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса WorkspaceTestData
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from net.xeaf.rack.models import WorkspaceModel
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceTestData:
|
||||||
|
"""
|
||||||
|
Инициализирует модели данных тестовых Рабочих пространств
|
||||||
|
"""
|
||||||
|
|
||||||
|
IVANOV_WORKSPACE_ID = uuid.UUID("377cbf24-6658-4b51-a7b2-7ca1ca0f331d")
|
||||||
|
""" Уникальный идентификатор пространства пользователя Иван Иванов """
|
||||||
|
|
||||||
|
IVANOV_WORKSPACE = 'i-ivanov'
|
||||||
|
""" Имя пространства пользователя Иван Иванов """
|
||||||
|
|
||||||
|
PETROV_WORKSPACE_ID = uuid.UUID("22711511-a3c2-4891-88c3-68c14272f949")
|
||||||
|
""" Уникальный идентификатор пространства пользователя Петр Петров """
|
||||||
|
|
||||||
|
PETROV_WORKSPACE = 'p-petrov'
|
||||||
|
""" Имя пространства пользователя Петр Петров """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_workspaces(cls):
|
||||||
|
"""
|
||||||
|
Инициализирует модели данных тестовых Рабочих пространств
|
||||||
|
"""
|
||||||
|
|
||||||
|
cls.init_ivanov_workspace()
|
||||||
|
cls.init_petrov_workspace()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_ivanov_workspace(cls):
|
||||||
|
"""
|
||||||
|
Инициализирует модель данных тестового пространства пользователя Иван Иванов
|
||||||
|
"""
|
||||||
|
|
||||||
|
workspace = WorkspaceModel()
|
||||||
|
workspace.id = cls.IVANOV_WORKSPACE_ID
|
||||||
|
workspace.slug = cls.IVANOV_WORKSPACE
|
||||||
|
workspace.description = "Пространство пользователя Иван Иванов"
|
||||||
|
workspace.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_petrov_workspace(cls):
|
||||||
|
"""
|
||||||
|
Инициализирует модель данных тестового пространства пользователя Петр Петров
|
||||||
|
"""
|
||||||
|
|
||||||
|
workspace = WorkspaceModel()
|
||||||
|
workspace.id = cls.PETROV_WORKSPACE_ID
|
||||||
|
workspace.slug = cls.PETROV_WORKSPACE
|
||||||
|
workspace.description = "Пространство пользователя Петр Петров"
|
||||||
|
workspace.save()
|
||||||
47
net/xeaf/rack/models/workspace_model.py
Normal file
47
net/xeaf/rack/models/workspace_model.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса Session
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CorePersistentModel
|
||||||
|
from net.xeaf.rack.enums import WorkspaceKind
|
||||||
|
from net.xeaf.rack.models.mixins import DescriptionMixin
|
||||||
|
from net.xeaf.rack.models.mixins import SlugMixin
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceModel(CorePersistentModel, SlugMixin, DescriptionMixin):
|
||||||
|
"""
|
||||||
|
Модель рабочего пространства
|
||||||
|
"""
|
||||||
|
|
||||||
|
kind = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=WorkspaceKind.choices,
|
||||||
|
default=WorkspaceKind.PRIVATE,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("rack.models.workspace_model.kind")
|
||||||
|
)
|
||||||
|
""" Вид пространства """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Параметры конфигурации класса
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Индексы для производительности
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["slug"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
db_table = "rack_workspaces"
|
||||||
|
verbose_name = _("rack.models.workspace")
|
||||||
|
verbose_name_plural = _("rack.models.workspaces")
|
||||||
10
net/xeaf/rack/serializers/auth/__init__.py
Normal file
10
net/xeaf/rack/serializers/auth/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет описания классов сериализаторов авторизации
|
||||||
|
"""
|
||||||
|
|
||||||
Reference in New Issue
Block a user