@@ -0,0 +1,19 @@ +from django.db.models import Model +from django.utils.translation import gettext as _ + + +class UnknownError(Exception): + def __str__(self) -> str: + return _('An unknown error has occurred. Please contact support') + + +class DuplicateError(Exception): + def __init__(self, model: type[Model], attrs: tuple[str, ...]) -> None: + self.model = model + self.attrs = attrs + + def __str__(self) -> str: + return _('A %(model)s with fields %(fields)s already exists') % { + 'model': self.model._meta.verbose_name, + 'fields': ', '.join(self.attrs), + } @@ -2,13 +2,13 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-19 18:36+0300\n" +"POT-Creation-Date: 2025-12-25 13:42+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -97,7 +97,7 @@ msgid "Domain not found" msgstr "Домен не найден" #: authentication/models/business_account.py:16 payments/models/promocode.py:72 -#: tools/public_api/models.py:33 +#: tools/public_api/models.py:34 msgid "Owner" msgstr "Владелец" @@ -120,7 +120,7 @@ msgid "Acceptance" msgstr "Подтверждение" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:43 +#: authentication/models/business_group.py:21 tools/public_api/models.py:44 msgid "Token limit" msgstr "Лимит токенов" @@ -162,8 +162,8 @@ msgstr "Бизнес Группы" #: authentication/models/business_host.py:22 #: authentication/models/email_token.py:13 authentication/models/user.py:222 #: authentication/models/user.py:223 authentication/models/user_telegram.py:22 -#: authentication/models/user_vk.py:12 payments/admin.py:36 -#: payments/admin.py:97 payments/models/invoice.py:15 +#: authentication/models/user_vk.py:12 payments/admin.py:37 +#: payments/admin.py:98 payments/models/invoice.py:15 #: payments/models/payment.py:26 payments/models/payment_plan.py:61 msgid "User" msgstr "Пользователь" @@ -211,7 +211,7 @@ msgid "PSRN" msgstr "ОГРН" #: authentication/models/business_host.py:69 ml_model/models.py:180 -#: payments/models/payment_plan_feature.py:12 tools/public_api/models.py:30 +#: payments/models/payment_plan_feature.py:18 tools/public_api/models.py:31 msgid "Name" msgstr "Наименование" @@ -360,7 +360,7 @@ msgid "Picture name" msgstr "Имя аватара" #: authentication/models/user.py:144 tools/chats/models.py:13 -#: tools/public_api/models.py:45 +#: tools/public_api/models.py:46 msgid "Is deleted" msgstr "Удален" @@ -505,7 +505,7 @@ msgstr "Пароли не совпадают" msgid "You cannot change the password of an unconfirmed e-mail user." msgstr "Вы не можете изменить пароль неподтвержденного по e-mail пользователя." -#: authentication/services/business_host_service.py:154 +#: authentication/services/business_host_service.py:155 msgid "No user_email is provided" msgstr "" @@ -574,6 +574,15 @@ msgstr "" msgid "Token is invalid" msgstr "" +#: core/exceptions.py:5 +msgid "An unknown error has occurred. Please contact support" +msgstr "" +"Произошла неизвестная ошибка. Пожалуйста, обратитесь в службу поддержки" + +#: lib/exceptions.py:15 +msgid "A %(model)s with fields %(fields)s already exists" +msgstr "Уже существует %(model)s с полями %(fields)s" + #: messages/serializers.py:44 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" @@ -661,7 +670,7 @@ msgid "Slug" msgstr "Ярлык" #: ml_model/models.py:28 ml_model/models.py:80 -#: payments/models/payment_plan_feature.py:18 +#: payments/models/payment_plan_feature.py:24 msgid "Category" msgstr "Категория" @@ -710,7 +719,7 @@ msgstr "Теги" msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:156 ml_model/models.py:402 payments/admin.py:103 +#: ml_model/models.py:156 ml_model/models.py:402 payments/admin.py:104 msgid "Model" msgstr "Модель" @@ -956,13 +965,13 @@ msgstr "Нет изображения для улучшения" msgid "Model data cannot be retrieved" msgstr "Невозможно получить данные модели" -#: payments/admin.py:34 payments/admin.py:75 payments/admin.py:95 +#: payments/admin.py:35 payments/admin.py:76 payments/admin.py:96 msgid "You can search by user email, exacted company name" msgstr "" "Вы можете осуществлять поиск по e-mail пользователя, точному названию " "компании" -#: payments/admin.py:39 payments/admin.py:100 +#: payments/admin.py:40 payments/admin.py:101 msgid "Missing" msgstr "Отсутствующий" @@ -983,18 +992,6 @@ msgstr "" msgid "The payer does not exist" msgstr "Плательщик не существует" -#: payments/models/choices.py:6 -msgid "Text Page" -msgstr "Страница текста" - -#: payments/models/choices.py:7 -msgid "File (pcs)" -msgstr "Файл (шт.)" - -#: payments/models/choices.py:8 -msgid "Time (mins)" -msgstr "Время (мин.)" - #: payments/models/invoice.py:23 msgid "Generative Model" msgstr "Генеративная модель" @@ -1048,7 +1045,7 @@ msgid "Is visible" msgstr "Видимый" #: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 -#: payments/models/payment_plan_feature.py:11 +#: payments/models/payment_plan_feature.py:16 msgid "Payment Plan" msgstr "Платежный План" @@ -1076,19 +1073,31 @@ msgstr "Рекуррентная задача на платеж" msgid "User Balance" msgstr "Баланс пользователя" +#: payments/models/payment_plan_feature.py:11 +msgid "Text Page" +msgstr "Страница текста" + +#: payments/models/payment_plan_feature.py:12 +msgid "File (pcs)" +msgstr "Файл (шт.)" + #: payments/models/payment_plan_feature.py:13 +msgid "Time (mins)" +msgstr "Время (мин.)" + +#: payments/models/payment_plan_feature.py:19 msgid "Quantity" msgstr "Количество" -#: payments/models/payment_plan_feature.py:24 +#: payments/models/payment_plan_feature.py:30 msgid "Measurement unit" msgstr "Единица измерения" -#: payments/models/payment_plan_feature.py:31 +#: payments/models/payment_plan_feature.py:37 msgid "Payment plan feature" msgstr "Преимущество платежного плана" -#: payments/models/payment_plan_feature.py:32 +#: payments/models/payment_plan_feature.py:38 msgid "Payment plan features" msgstr "Преимущества платежного плана" @@ -1140,11 +1149,11 @@ msgstr "Платежный метод" msgid "Payment Methods" msgstr "Платежные методы" -#: payments/routes/v1.py:70 +#: payments/routes/v1.py:75 msgid "Expenses" msgstr "Затраты" -#: payments/routes/v1.py:71 +#: payments/routes/v1.py:76 msgid "Refills" msgstr "Пополнения" @@ -1152,14 +1161,6 @@ msgstr "Пополнения" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:32 -msgid "Business accounts are not allowed to make purchases" -msgstr "Сотрудники не могут производить покупки" - -#: payments/selectors/payment_plan_selector.py:57 -msgid "No plan by this uid" -msgstr "Не найдено подписки по этому ID" - #: payments/services/model_billing_service.py:31 msgid "Unknown account type" msgstr "Неизвестный тип аккаунта" @@ -1246,15 +1247,15 @@ msgstr "Необходимо повысить лимит токенов у API- msgid "API Key not found" msgstr "API-ключ не найден" -#: tools/public_api/models.py:46 +#: tools/public_api/models.py:47 msgid "Expires at" msgstr "Когда заканчивается" -#: tools/public_api/models.py:50 +#: tools/public_api/models.py:57 msgid "API Key" msgstr "API Ключ" -#: tools/public_api/models.py:51 +#: tools/public_api/models.py:58 msgid "API Keys" msgstr "API Ключи" @@ -1284,6 +1285,12 @@ msgstr "Отсутствует обязательный параметр: 'messa msgid "Model not found" msgstr "Модель не найдена" +#~ msgid "Business accounts are not allowed to make purchases" +#~ msgstr "Сотрудники не могут производить покупки" + +#~ msgid "No plan by this uid" +#~ msgstr "Не найдено подписки по этому ID" + #~ msgid "Author" #~ msgstr "Автор" @@ -0,0 +1,23 @@ +# Generated by Django 5.0.11 on 2025-12-24 15:07 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('public_api', '0009_alter_apikey_unique_together'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='apikey', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='apikey', + constraint=models.UniqueConstraint(condition=models.Q(('is_deleted', False)), fields=('user', 'name'), name='unique_active_apikey_per_user'), + ), + ] @@ -3,11 +3,11 @@ from decimal import Decimal from django.contrib.admin.models import ADDITION, DELETION, LogEntry from django.contrib.contenttypes.models import ContentType +from django.db import IntegrityError +from django.utils.translation import gettext as _ -from authentication.selectors.account_status_selector import ( - AccountStatusSelector, -) from core.service import BaseService +from lib.exceptions import DuplicateError, UnknownError from tools.public_api.models import APIKey from tools.public_api.selectors.api_key import APIKeySelector from tools.public_api.serializers import APIKeyResultSerializer @@ -15,11 +15,14 @@ from tools.public_api.serializers import APIKeyResultSerializer class APIKeyService(BaseService): def create(self, payload: dict, serialize: bool = False) -> APIKey | APIKeyResultSerializer: - if not ( - self.user.account_type in ('business_host', 'regular', 'business_admin') - ): + if not (self.user.account_type in ('business_host', 'regular', 'business_admin')): raise Exception('Can not create API key from business sub-account.') - api_key = APIKey.objects.create(user=self.user, **payload) + try: + api_key = APIKey.objects.create(user=self.user, **payload) + except IntegrityError as exc: + if 'unique' in str(exc): + raise DuplicateError(model=APIKey, attrs=(_('Name'), _('Owner'))) + raise UnknownError from exc LogEntry.objects.log_action( self.user.pk, ContentType.objects.get_for_model(api_key).pk, @@ -4,6 +4,7 @@ from datetime import date, timedelta from django.contrib.auth import get_user_model from django.db import models +from django.db.models import UniqueConstraint, Q from django.utils.translation import gettext_lazy as _ from core.models import BaseModel @@ -46,7 +47,13 @@ class APIKey(BaseModel): expires_at = models.DateField(verbose_name=_('Expires at'), null=True, blank=True) class Meta: - unique_together = ['user', 'name', 'is_deleted'] + constraints = [ + UniqueConstraint( + fields=['user', 'name'], + condition=Q(is_deleted=False), + name='unique_active_apikey_per_user', + ) + ] verbose_name = _('API Key') verbose_name_plural = _('API Keys')