diff --git a/net/xeaf/rack/enums/__init__.py b/net/xeaf/rack/enums/__init__.py index 6dde42e..7606bb4 100644 --- a/net/xeaf/rack/enums/__init__.py +++ b/net/xeaf/rack/enums/__init__.py @@ -12,3 +12,5 @@ from .auth_reject_reson import AuthRejectReason from .http_method import HttpMethod from .ui_language import UILanguage from .view_set_action import ViewSetAction +from .workspace_kind import WorkspaceKind +from .workspace_role import WorkspaceRole diff --git a/net/xeaf/rack/enums/workspace_kind.py b/net/xeaf/rack/enums/workspace_kind.py new file mode 100644 index 0000000..b32393f --- /dev/null +++ b/net/xeaf/rack/enums/workspace_kind.py @@ -0,0 +1,26 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Описание класса WorkspaceKind +""" + +from enum import unique + +from net.xeaf.rack.core import CoreEnum + + +@unique +class WorkspaceKind(CoreEnum): + """ + Перечисление Видов пространств + """ + + PRIVATE = 'PRIVATE' + """ Частное (личное) пространство """ + + CORPORATE = 'CORPORATE' + """ Корпоративное пространство """ diff --git a/net/xeaf/rack/enums/workspace_role.py b/net/xeaf/rack/enums/workspace_role.py new file mode 100644 index 0000000..0c1a779 --- /dev/null +++ b/net/xeaf/rack/enums/workspace_role.py @@ -0,0 +1,29 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Описание класса WorkspaceRole +""" + +from enum import unique + +from net.xeaf.rack.core import CoreEnum + + +@unique +class WorkspaceRole(CoreEnum): + """ + Перечисление ролей Аккаунта в рабочем пространстве + """ + + OWNER = 'OWNER' + """ Владелец """ + + ADMIN = 'ADMIN' + """ Администратор """ + + MEMBER = 'MEMBER' + """ Участник """ diff --git a/net/xeaf/rack/management/commands/rack_test_database.py b/net/xeaf/rack/management/commands/rack_test_database.py index 9f2fb10..698b577 100644 --- a/net/xeaf/rack/management/commands/rack_test_database.py +++ b/net/xeaf/rack/management/commands/rack_test_database.py @@ -14,6 +14,7 @@ from django.core.management.base import no_translations from net.xeaf.rack.core import CoreCommand 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 WorkspaceTestData class Command(CoreCommand): @@ -37,6 +38,7 @@ class Command(CoreCommand): call_command("migrate") # Инициализируем данные + WorkspaceTestData.init_workspaces() AccountTestData.init_accounts() SessionTestData.init_sessions() diff --git a/net/xeaf/rack/managers/__init__.py b/net/xeaf/rack/managers/__init__.py index 7957049..3026cc2 100644 --- a/net/xeaf/rack/managers/__init__.py +++ b/net/xeaf/rack/managers/__init__.py @@ -13,3 +13,4 @@ from .crypto_manager import CryptoManager from .locale_manager import LocaleManager from .request_manager import RequestManager from .session_manager import SessionManager +from .workspace_manager import WorkspaceManager diff --git a/net/xeaf/rack/managers/account_manager.py b/net/xeaf/rack/managers/account_manager.py index 875bac8..7e23f40 100644 --- a/net/xeaf/rack/managers/account_manager.py +++ b/net/xeaf/rack/managers/account_manager.py @@ -23,6 +23,7 @@ class AccountManager: Проверяет возможность авторизации Аккаунта :param account: Аккаунт + :return: Причина отказа в авторизации или None, если авторизация возможна """ @@ -30,7 +31,7 @@ class AccountManager: return AuthRejectReason.INVALID_USERNAME_OR_PASSWORD if not account.is_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 None @@ -56,7 +57,7 @@ class AccountManager: :return: Аккаунт или None, если Аккаунт не найден """ - return AccountModel.objects.filter(username=username).first() + return AccountModel.objects.filter(username=username).select_related("workspace").first() @classmethod def restore_account(cls, account: AccountModel) -> None: diff --git a/net/xeaf/rack/managers/session_manager.py b/net/xeaf/rack/managers/session_manager.py index b3010b6..d87ed41 100644 --- a/net/xeaf/rack/managers/session_manager.py +++ b/net/xeaf/rack/managers/session_manager.py @@ -75,7 +75,7 @@ class SessionManager[T:SessionModel]: """ 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(): return session diff --git a/net/xeaf/rack/managers/workspace_manager.py b/net/xeaf/rack/managers/workspace_manager.py new file mode 100644 index 0000000..8c902a4 --- /dev/null +++ b/net/xeaf/rack/managers/workspace_manager.py @@ -0,0 +1,29 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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() diff --git a/net/xeaf/rack/migrations/0001_initial.py b/net/xeaf/rack/migrations/0001_initial.py index 7e1b156..b9c5081 100644 --- a/net/xeaf/rack/migrations/0001_initial.py +++ b/net/xeaf/rack/migrations/0001_initial.py @@ -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.db.models.deletion +import net.xeaf.rack.enums.workspace_role import uuid from django.conf import settings from django.db import migrations, models @@ -16,6 +17,23 @@ class Migration(migrations.Migration): ] 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( name='AccountModel', 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')), ('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')), + ('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')), ('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')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='net_xeaf_rack.workspacemodel', verbose_name='rack.models.workspace')), ], options={ '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')), ('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')), + ('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')), ], options={ 'verbose_name': 'rack.models.session', 'verbose_name_plural': 'rack.models.session_plural', '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( 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'), - ), ] diff --git a/net/xeaf/rack/migrations/0002_alter_sessionmodel_account.py b/net/xeaf/rack/migrations/0002_alter_sessionmodel_account.py deleted file mode 100644 index 955ac73..0000000 --- a/net/xeaf/rack/migrations/0002_alter_sessionmodel_account.py +++ /dev/null @@ -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'), - ), - ] diff --git a/net/xeaf/rack/migrations/0002_workspacemodel_kind.py b/net/xeaf/rack/migrations/0002_workspacemodel_kind.py new file mode 100644 index 0000000..1618163 --- /dev/null +++ b/net/xeaf/rack/migrations/0002_workspacemodel_kind.py @@ -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'), + ), + ] diff --git a/net/xeaf/rack/models/__init__.py b/net/xeaf/rack/models/__init__.py index ebec81e..d24994f 100644 --- a/net/xeaf/rack/models/__init__.py +++ b/net/xeaf/rack/models/__init__.py @@ -10,3 +10,4 @@ from .account_model import AccountModel from .session_model import SessionModel +from .workspace_model import WorkspaceModel diff --git a/net/xeaf/rack/models/account_model.py b/net/xeaf/rack/models/account_model.py index 2839c2d..2645398 100644 --- a/net/xeaf/rack/models/account_model.py +++ b/net/xeaf/rack/models/account_model.py @@ -12,12 +12,13 @@ from django.contrib.auth.models import AbstractUser from django.db import models 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 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 - +from .workspace_model import WorkspaceModel 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( null=False, default=dict, diff --git a/net/xeaf/rack/models/fields/__init__.py b/net/xeaf/rack/models/fields/__init__.py new file mode 100644 index 0000000..235486d --- /dev/null +++ b/net/xeaf/rack/models/fields/__init__.py @@ -0,0 +1,12 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Пакет описания классов полей моделей данных +""" + +from .enum_choice_field import EnumChoiceField +from .extensible_enum_choice_field import ExtensibleEnumChoiceField diff --git a/net/xeaf/rack/models/fields/enum_choice_field.py b/net/xeaf/rack/models/fields/enum_choice_field.py new file mode 100644 index 0000000..e0ae74e --- /dev/null +++ b/net/xeaf/rack/models/fields/enum_choice_field.py @@ -0,0 +1,146 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/models/fields/extensible_enum_choice_field.py b/net/xeaf/rack/models/fields/extensible_enum_choice_field.py new file mode 100644 index 0000000..f8f9d8d --- /dev/null +++ b/net/xeaf/rack/models/fields/extensible_enum_choice_field.py @@ -0,0 +1,156 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/models/journal_model.py b/net/xeaf/rack/models/journal_model.py index 644fbca..b047fc6 100644 --- a/net/xeaf/rack/models/journal_model.py +++ b/net/xeaf/rack/models/journal_model.py @@ -55,6 +55,7 @@ class JournalModel(CoreModel, CreatedAtMixin): """ Параметры конфигурации класса """ + # Индексы для производительности indexes = [ models.Index(fields=["account"]), diff --git a/net/xeaf/rack/models/user_story/__init__.py b/net/xeaf/rack/models/user_story/__init__.py index 5b92118..bb0d64f 100644 --- a/net/xeaf/rack/models/user_story/__init__.py +++ b/net/xeaf/rack/models/user_story/__init__.py @@ -10,3 +10,4 @@ from .account_test_data import AccountTestData from .session_test_data import SessionTestData +from .workspace_test_data import WorkspaceTestData diff --git a/net/xeaf/rack/models/user_story/account_test_data.py b/net/xeaf/rack/models/user_story/account_test_data.py index e112374..4d901e2 100644 --- a/net/xeaf/rack/models/user_story/account_test_data.py +++ b/net/xeaf/rack/models/user_story/account_test_data.py @@ -10,8 +10,10 @@ import uuid +from net.xeaf.rack.enums import WorkspaceRole from net.xeaf.rack.models import AccountModel from net.xeaf.rack.utils import DateUtils +from .workspace_test_data import WorkspaceTestData class AccountTestData: @@ -56,6 +58,8 @@ class AccountTestData: account.full_name = "Иван И. Иванов" account.email = "i.ivanov@xeaf.net" account.set_password(cls.DEFAULT_PASSWORD) + account.workspace_id = WorkspaceTestData.IVANOV_WORKSPACE_ID + account.role = WorkspaceRole.OWNER account.joined_at = DateUtils.now() account.save() @@ -72,6 +76,8 @@ class AccountTestData: account.full_name = "Петр П. Петров" account.email = "p.petrov@xeaf.net" account.set_password(cls.DEFAULT_PASSWORD) + account.workspace_id = WorkspaceTestData.PETROV_WORKSPACE_ID + account.role = WorkspaceRole.OWNER account.joined_at = DateUtils.now() account.save() diff --git a/net/xeaf/rack/models/user_story/workspace_test_data.py b/net/xeaf/rack/models/user_story/workspace_test_data.py new file mode 100644 index 0000000..17e42a8 --- /dev/null +++ b/net/xeaf/rack/models/user_story/workspace_test_data.py @@ -0,0 +1,64 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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() diff --git a/net/xeaf/rack/models/workspace_model.py b/net/xeaf/rack/models/workspace_model.py new file mode 100644 index 0000000..9066204 --- /dev/null +++ b/net/xeaf/rack/models/workspace_model.py @@ -0,0 +1,47 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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") diff --git a/net/xeaf/rack/serializers/auth/__init__.py b/net/xeaf/rack/serializers/auth/__init__.py new file mode 100644 index 0000000..f931eb0 --- /dev/null +++ b/net/xeaf/rack/serializers/auth/__init__.py @@ -0,0 +1,10 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Пакет описания классов сериализаторов авторизации +""" + diff --git a/net/xeaf/rack/serializers/auth/account_brief_response.py b/net/xeaf/rack/serializers/auth/account_brief_response.py new file mode 100644 index 0000000..e69de29 diff --git a/net/xeaf/rack/serializers/auth/account_detail_response.py b/net/xeaf/rack/serializers/auth/account_detail_response.py new file mode 100644 index 0000000..e69de29 diff --git a/net/xeaf/rack/serializers/auth/session_login_request.py b/net/xeaf/rack/serializers/auth/session_login_request.py new file mode 100644 index 0000000..e69de29 diff --git a/net/xeaf/rack/serializers/auth/session_login_response.py b/net/xeaf/rack/serializers/auth/session_login_response.py new file mode 100644 index 0000000..e69de29