@@ -1,12 +1,17 @@ from authentication.exceptions.business_host_exceptions.base_already import ( BaseAlready, ) +from django.utils.translation import gettext_lazy as _ class AlreadyHasPlan(BaseAlready): def msg(self): return dict( - message='user has a paid plan', + message=_( + 'You already have an active tariff plan. You must request a ' + 'cancellation of your current tariff plan, after which ' + 'you will be able to create a Corporate Account.' + ), user_id=self.user.uid, plan_id=self.user.payment_plan.plan.uid, ) @@ -8,9 +8,6 @@ __all__ = ('BaseAlready',) class InvalidPassword(Exception): ... -class InvalidUsername(Exception): ... - - class InvalidToken(Exception): def __str__(self) -> str: return 'Invalid token received, please try relog' @@ -0,0 +1,27 @@ +# Generated by Django 5.0.11 on 2025-11-06 14:11 + +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('authentication', '0024_alter_customusermodel_email'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='customusermodel', + name='idx_users_email_upper', + ), + migrations.RemoveField( + model_name='customusermodel', + name='username', + ), + migrations.AddIndex( + model_name='customusermodel', + index=models.Index(django.db.models.functions.text.Upper('email'), name='idx_users_email_upper'), + ), + ] @@ -28,7 +28,6 @@ if TYPE_CHECKING: class CustomUserModelManager(BaseUserManager): def create_user( self, - username: str, email: str, password: Optional[str] = None, **kwargs, @@ -39,15 +38,15 @@ class CustomUserModelManager(BaseUserManager): email = email.lower() - user = self.model(username=username, email=email, **kwargs) + user = self.model(email=email, **kwargs) user.set_password(password) user.save(using=self._db) return user - def create_superuser(self, username, email, password): - user = self.create_user(username, email, password=password) + def create_superuser(self, email, password): + user = self.create_user(email, password=password) user.is_staff = True user.is_superuser = True @@ -64,8 +63,7 @@ class CustomUserModelManager(BaseUserManager): class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] + USERNAME_FIELD = 'email' FIRST_NAME_PLACEHOLDERS = [ 'Любопытный', @@ -132,7 +130,6 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): default=random_last_name, verbose_name=_('Last name'), ) - username = models.CharField(max_length=100, unique=True, verbose_name=_('Username')) email = models.EmailField(max_length=100, unique=True, verbose_name=_('Email')) active = models.BooleanField(default=True, verbose_name=_('Is active')) @@ -221,7 +218,7 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): class Meta: ordering = ('-created_at',) - indexes = (Index(Upper('username'), name='idx_users_email_upper'),) + indexes = (Index(Upper('email'), name='idx_users_email_upper'),) verbose_name = _('User') verbose_name_plural = _('User') @@ -66,9 +66,8 @@ class BusinessHostService: token_limit: Decimal | None = None, account_privileges: Tuple[str, Any] | None = None, ) -> BusinessAccountService: - username = f'{self.user.host_account.company_name}_{random_with_N_digits(6)}' password = generate_token(15) - user = CustomUserModel.objects.create_user(username=username, email=email, password=password) + user = CustomUserModel.objects.create_user(email=email, password=password) user.save() PaymentPlanService(user).subscribe_user_to_plan(PaymentPlan.objects.get(price=0, is_corporate=False)) @@ -79,7 +79,7 @@ class EmailService: def send_error_email(self, error: ErrorReport): message = f""" - Пользователь {error.author.username} сообщил об ошибке: + Пользователь {error.author.email} сообщил об ошибке: {error.report_text} Скриншоты ошибки (если пользователь их приложил) - во вложении """ @@ -100,7 +100,7 @@ class EmailService: def send_copr_purchase_email(self, host: BusinessUserHost): message = f""" - Пользователь {host.user.username} зарегистрировал корпоративный аккаунт: + Пользователь {host.user.email} зарегистрировал корпоративный аккаунт: Компания: {host.company_name} Сектор: {host.company_sector} @@ -169,7 +169,7 @@ class EmailService: Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку: {}?token={} """, - account.user.username, + account.user.email, self.user.host_account.company_name, settings.INVITATION_RESPONSE_URL, token.key, @@ -9,18 +9,18 @@ from django.contrib.auth.hashers import check_password from authentication.exceptions import ( InvalidPassword, InvalidToken, - InvalidUsername, ) +from authentication.exceptions.user import WrongEmail from authentication.models.user import CustomUserModel class TokenService: @classmethod - def issue(cls, *, username: str, password: str) -> dict[str, str]: + def issue(cls, *, email: str, password: str) -> dict[str, str]: try: - user = CustomUserModel.objects.get(username=username) + user = CustomUserModel.objects.get(email=email) except CustomUserModel.DoesNotExist: - raise InvalidUsername + raise WrongEmail if not check_password(password=password, encoded=user.password): raise InvalidPassword @@ -54,15 +54,15 @@ class UserService: serializer = NewUserSerializer(data=request.data) serializer.is_valid(raise_exception=True) user_data = serializer.validated_data - username = user_data['email'] - if user := CustomUserModel.objects.filter(email=username).first(): + email = user_data['email'] + if user := CustomUserModel.objects.filter(email=email).first(): if not user.is_deleted: raise UserAlreadyExists user.is_deleted = False user.set_password(user_data['password']) else: user = CustomUserModel.objects.create_user( - username=username, email=username, password=user_data['password'] + email=email, password=user_data['password'] ) UTMService(user).create_utm( user_data.get('utm_source'), @@ -72,10 +72,10 @@ class UserService: user_data.get('utm_content'), ) EmailService(user).send_reg_conf_email() - if referer_username := user_data.get('referer'): + if referer_email := user_data.get('referer'): try: referer = ( - CustomUserModel.objects.filter(email=referer_username) + CustomUserModel.objects.filter(email=referer_email) .prefetch_related('user_referral_account') .get() ) @@ -114,7 +114,7 @@ class UserService: raise WrongEmail user = authenticate( - username=probable_user.username, + username=probable_user.email, password=serializer.validated_data['password'], ) @@ -223,7 +223,7 @@ class UserService: serializer = UpdateUserDataSerializer(data=request.data) serializer.is_valid(raise_exception=True) - for field in ('username', 'email', 'first_name', 'last_name'): + for field in ('email', 'first_name', 'last_name'): setattr( self.user, field, @@ -234,7 +234,7 @@ class UserService: if new_profile_picture is not None: new_picture_name = MinIOService().put_object( new_profile_picture, - f'{self.user.username}.png', + f'{self.user.email}.png', 'air-profiles', ) self.user.profile_picture_name = new_picture_name @@ -246,7 +246,7 @@ class UserService: serializer.is_valid(raise_exception=True) user = authenticate( - username=self.user.username, + username=self.user.email, password=serializer.validated_data['current_password'], ) if user is None: @@ -263,7 +263,7 @@ class UserService: serializer.is_valid(raise_exception=True) img = serializer.validated_data['new_picture'] - img_name = MinIOService().put_object(img, f'{self.user.username}.png', 'air-profiles') + img_name = MinIOService().put_object(img, f'{self.user.email}.png', 'air-profiles') self.user.profile_picture_name = img_name self.user.save() @@ -322,5 +322,4 @@ def detect_email(backend, response, details, **kwargs): and (login := response.get('login')) ): details['email'] = f'{login.split("@")[0]}@yandex.ru' if '@' not in login else login - details['username'] = response['login'] return kwargs | {'backend': backend} | {'response': response} | {'details': details} @@ -117,12 +117,16 @@ class HasInviteListFilter(admin.SimpleListFilter): @admin.register(CustomUserModel) class CustomUserModelAdmin(UserAdmin, ExportActionModelAdmin): list_display = ['email', '_balance', 'created_at'] - search_fields = ['email', 'created_at', 'utm__utm_source'] + date_hierarchy = 'created_at' + ordering = ('email',) + search_fields = ['email', 'utm__utm_source'] + + search_help_text = 'Можно искать по email пользователя, UTM-источнику' + readonly_fields = ['uid'] resource_classes = (DefaultUserResource, ReferralUserResource) fields = [ 'uid', - 'username', 'email', 'password', 'first_name', @@ -145,11 +149,7 @@ class CustomUserModelAdmin(UserAdmin, ExportActionModelAdmin): HasInviteListFilter, AdditionalListFilter, ] - verbose_name = 'Пользователь' - verbose_name_plural = 'Пользователи' - actions = [ - 'download_users', - ] + actions = ['download_users'] def get_queryset(self, request): qs = super().get_queryset(request) @@ -24,7 +24,7 @@ PATH_PREFETCH_MAP = { 'social_auth' ), 'only': ( - 'uid', 'first_name', 'last_name', 'username', 'created_at', 'email', 'active', + 'uid', 'first_name', 'last_name', 'created_at', 'email', 'active', 'is_superuser', 'is_staff', 'is_confirmed', 'is_subscribed_to_emails', 'profile_picture_name', *_gen_only('business_account', 'uid', 'parent_company__uid', 'show_balance', 'account_privileges', 'parent_company__user__uid'), @@ -19,7 +19,6 @@ class UserSchema(Schema): uid: UUID4 first_name: str last_name: str - username: str created_at: datetime email: str is_active: bool @@ -93,7 +93,6 @@ class RequestPassChangeSerializer(serializers.Serializer): class UpdateUserDataSerializer(serializers.Serializer): - username = serializers.CharField(required=False) email = serializers.EmailField(required=False) profile_picture = serializers.ImageField(required=False, write_only=True) first_name = serializers.CharField(required=False) @@ -101,7 +100,6 @@ class UpdateUserDataSerializer(serializers.Serializer): class UserDataSerializer(serializers.Serializer): - username = serializers.CharField() email = serializers.EmailField() is_superuser = serializers.BooleanField() account_type = serializers.SerializerMethodField() @@ -129,7 +127,6 @@ class UserDetailSerializer(serializers.Serializer): uid = serializers.UUIDField() first_name = serializers.CharField() last_name = serializers.CharField() - username = serializers.CharField() created_at = serializers.DateTimeField() email = serializers.CharField() is_active = serializers.BooleanField() @@ -473,6 +473,7 @@ if (SENTRY_URL := env.str('SENTRY_URL', '')) and RELEASE and ENVIRONMENT: transaction_style='url', middleware_spans=True, signals_spans=True, cache_spans=False ), ], + ignore_errors=['InsufficientBalance'] ) CACHEOPS_REDIS = env.str('CACHEOPS_REDIS', CACHES['default']['LOCATION']) @@ -13,7 +13,6 @@ from ninja import NinjaAPI from authentication.exceptions import ( InvalidPassword, InvalidToken, - InvalidUsername, ) from backend.public import urlpatterns as public_urlpatterns @@ -46,11 +45,6 @@ def invalid_password_error_handler(request, exc: InvalidPassword): return api.create_response(request, {'message': _('Wrong password')}, status=401) -@api.exception_handler(InvalidUsername) -def invalid_username_error_handler(request, exc: InvalidUsername): - return api.create_response(request, {'message': _('Wrong username')}, status=401) - - def healthz_status(request): return JsonResponse({'status': 'ok'}, status=200) @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-07-08 16:09+0300\n" +"POT-Creation-Date: 2025-11-05 13:41+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,50 +20,6 @@ msgstr "" "n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " "(n%100>=11 && n%100<=14)? 2 : 3);\n" -#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:45 -#: stories/models.py:18 -msgid "Icon" -msgstr "Миниатюра" - -#: achievements/admin.py:22 achievements/models.py:29 achievements/models.py:37 -msgid "Achievement" -msgstr "Достижение" - -#: achievements/apps.py:8 achievements/models.py:30 -msgid "Achievements" -msgstr "Достижения" - -#: achievements/models.py:14 ml_model/models.py:18 ml_model/models.py:38 -#: ml_model/models.py:70 ml_model/models.py:182 -msgid "Slug" -msgstr "Ярлык" - -#: achievements/models.py:16 ml_model/models.py:68 ml_model/models.py:181 -#: ml_model/models.py:270 payments/models/payment.py:52 -msgid "Description" -msgstr "Описание" - -#: achievements/models.py:43 authentication/models/business_host.py:22 -#: authentication/models/email_token.py:12 authentication/models/user.py:246 -#: authentication/models/user.py:247 authentication/models/user_telegram.py:22 -#: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:61 -msgid "User" -msgstr "Пользователь" - -#: achievements/models.py:46 -msgid "Issued at" -msgstr "Когда выдано" - -#: achievements/models.py:49 -#, python-format -msgid "Achievement %(achievement_title)s пользователя %(username)s" -msgstr "Достижение %(achievement_title)s пользователя %(username)s" - -#: achievements/models.py:55 achievements/models.py:56 -msgid "Issued achievement" -msgstr "Выданное достижение" - #: authentication/exceptions/business_account.py:6 msgid "Business account not found" msgstr "Сотрудник не найден" @@ -74,6 +30,16 @@ msgstr "Сотрудник не найден" msgid "You do not have sufficient rights to perform this action" msgstr "У вас недостаточно прав для выполнения этого действия" +#: authentication/exceptions/business_host_exceptions/already_has_plan.py:11 +msgid "" +"You already have an active tariff plan. You must request a cancellation of " +"your current tariff plan, after which you will be able to create a Corporate " +"Account." +msgstr "" +"У вас уже активен тарифный план. Необходимо запросить аннулирование текущего " +"тарифного плана, после чего у вас появится возможность создать Корпоративный " +"Аккаунт" + #: authentication/exceptions/business_host_exceptions/not_allowed_ip.py:6 msgid "Current IP not allowed in this context" msgstr "Текущий IP не разрешен в данном контексте" @@ -86,6 +52,10 @@ msgstr "Шаблон письма не найден" msgid "There was an unknown error while sending an email" msgstr "При отправке письма произошла неизвестная ошибка" +#: authentication/exceptions/email_exceptions/token_not_found.py:6 +msgid "Email token not found. Please contact support" +msgstr "E-mail токен не найден. Пожалуйста, свяжитесь со службой поддержки" + #: authentication/exceptions/email_token.py:6 #, fuzzy #| msgid "No email token provided" @@ -96,7 +66,7 @@ msgstr "Токен не получен" msgid "Wrong email" msgstr "Неверный email" -#: authentication/exceptions/user.py:11 backend/urls.py:41 +#: authentication/exceptions/user.py:11 backend/urls.py:46 msgid "Wrong password" msgstr "Неверный пароль" @@ -166,8 +136,7 @@ msgstr "Дочерние Бизнес Аккаунты" #: authentication/models/business_group.py:8 ml_model/models.py:17 #: ml_model/models.py:37 ml_model/models.py:61 ml_model/models.py:269 -#: payments/models/payment_plan.py:27 stories/models.py:12 stories/models.py:35 -#: tools/chats/models.py:9 +#: payments/models/payment_plan.py:27 tools/chats/models.py:9 msgid "Title" msgstr "Название" @@ -179,11 +148,20 @@ msgstr "Бизнес Группа" msgid "Business Groups" msgstr "Бизнес Группы" +#: authentication/models/business_host.py:22 +#: authentication/models/email_token.py:13 authentication/models/user.py:225 +#: authentication/models/user.py:226 authentication/models/user_telegram.py:22 +#: authentication/models/user_vk.py:12 payments/admin.py:35 +#: payments/admin.py:89 payments/models/invoice.py:15 +#: payments/models/payment.py:26 payments/models/payment_plan.py:61 +msgid "User" +msgstr "Пользователь" + #: authentication/models/business_host.py:31 msgid "Affiliated by" msgstr "Кем привлечена" -#: authentication/models/business_host.py:36 authentication/models/user.py:152 +#: authentication/models/business_host.py:36 authentication/models/user.py:138 #: authentication/models/whitelist.py:16 ml_model/models.py:167 #: payments/models/promocode.py:85 msgid "Is active" @@ -326,55 +304,60 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:15 ml_model/models.py:271 +#: authentication/models/email_token.py:16 ml_model/models.py:271 msgid "Key" msgstr "Ключ" -#: authentication/models/email_token.py:18 +#: authentication/models/email_token.py:22 msgid "Email Token" msgstr "Email Токен" -#: authentication/models/email_token.py:19 +#: authentication/models/email_token.py:23 msgid "Email Tokens" msgstr "Email Токены" -#: authentication/models/user.py:125 authentication/models/user_telegram.py:9 +#: authentication/models/user.py:126 authentication/models/user_telegram.py:9 msgid "First name" msgstr "Имя" -#: authentication/models/user.py:132 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:133 authentication/models/user_telegram.py:10 msgid "Last name" msgstr "Фамилия" -#: authentication/models/user.py:139 authentication/models/user_telegram.py:11 +#: authentication/models/user.py:135 authentication/models/user_telegram.py:11 msgid "Username" msgstr "Имя пользователя" -#: authentication/models/user.py:146 +#: authentication/models/user.py:136 msgid "Email" msgstr "Email" -#: authentication/models/user.py:154 +#: authentication/models/user.py:140 msgid "Is staff" msgstr "Административный" -#: authentication/models/user.py:155 +#: authentication/models/user.py:141 msgid "Is superuser" msgstr "Суперюзер" -#: authentication/models/user.py:156 +#: authentication/models/user.py:142 msgid "Is email confirmed" msgstr "Email подтвержден" -#: authentication/models/user.py:157 +#: authentication/models/user.py:143 msgid "Is subscribed" msgstr "Подписан на уведомления" -#: authentication/models/user.py:163 +#: authentication/models/user.py:145 msgid "Picture name" msgstr "Имя аватара" -#: authentication/models/user.py:172 authentication/models/utm.py:21 +#: authentication/models/user.py:147 tools/chats/models.py:13 +#: tools/public_api/models.py:45 +msgid "Is deleted" +msgstr "Удален" + +#: authentication/models/user.py:152 authentication/models/utm.py:21 msgid "UTM" msgstr "UTM" @@ -404,7 +387,7 @@ msgstr "Номер телефона" #: authentication/models/user_telegram.py:27 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:10 +#: tools/chats/models.py:10 msgid "Created at" msgstr "Когда создан" @@ -461,6 +444,24 @@ msgstr "Вайтлист для отмены политик" msgid "Whitelists to cancel policies" msgstr "Вайтлисты для отмены политик" +#: authentication/security.py:35 +#, fuzzy +#| msgid "Hidden" +msgid "Forbidden" +msgstr "Скрытый" + +#: authentication/security.py:48 +msgid "Access token is expired" +msgstr "Срок действия токена доступа истек" + +#: authentication/security.py:61 +msgid "User not found" +msgstr "Пользователь не найден" + +#: authentication/security.py:99 authentication/security.py:130 +msgid "Access token expired or does not exist" +msgstr "Токен доступа просрочен или не существует" + #: authentication/selectors/business_host_selector.py:42 #: authentication/selectors/business_host_selector.py:85 msgid "User haven't rights to access host account information" @@ -483,10 +484,6 @@ msgstr "Бизнес-аккаунт для данного юзера не най msgid "Invited account can either accept or reject an invitation" msgstr "Приглашенный аккаунт может принять или отклонить приглашение" -#: authentication/services/business_account_service.py:83 -msgid "Account is already confirmed" -msgstr "Аккаунт уже подтвержден" - #: authentication/services/business_account_service.py:102 #, fuzzy #| msgid "Passwords do not match" @@ -497,11 +494,11 @@ msgstr "Пароли не совпадают" msgid "You cannot change the password of an unconfirmed e-mail user." msgstr "Вы не можете изменить пароль неподтвержденного по e-mail пользователя." -#: authentication/services/business_host_service.py:149 +#: authentication/services/business_host_service.py:159 msgid "No user_email is provided" msgstr "" -#: authentication/services/email_service.py:48 +#: authentication/services/email_service.py:47 msgid "Error occured when proceed email sending" msgstr "Случилась ошибка во время отправки email" @@ -509,68 +506,68 @@ msgstr "Случилась ошибка во время отправки email" msgid "Regular users cannot send introductory letters" msgstr "Обычные пользователи не могут отсылать письма" -#: authentication/services/email_service.py:162 +#: authentication/services/email_service.py:160 msgid "Regular users cannot send invitation letters" msgstr "Обычные пользователи не могут отправлять письма для приглашений" -#: authentication/services/user_services.py:166 +#: authentication/services/user_services.py:164 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:183 +#: authentication/services/user_services.py:181 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:207 +#: authentication/services/user_services.py:205 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:211 +#: authentication/services/user_services.py:209 msgid "No token like this in a database" msgstr "Не найдено такого токена" -#: authentication/services/user_services.py:217 +#: authentication/services/user_services.py:215 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:255 +#: authentication/services/user_services.py:253 msgid "Current password is wrong" msgstr "Текущий пароль неверен" -#: authentication/views.py:120 authentication/views.py:229 -#: authentication/views.py:325 authentication/views.py:356 +#: authentication/views.py:121 authentication/views.py:230 +#: authentication/views.py:326 authentication/views.py:357 msgid "Server error occured" msgstr "Случилась серверная ошибка" -#: authentication/views.py:225 +#: authentication/views.py:226 msgid "Email not found" msgstr "Email не найден" -#: authentication/views.py:321 +#: authentication/views.py:322 msgid "Business account has been deleted" msgstr "Сотрудник успешно удален" -#: authentication/views.py:345 +#: authentication/views.py:346 msgid "Business account has been reinvited" msgstr "Повторное приглашение сотруднику успешно отправлено" -#: authentication/views.py:467 +#: authentication/views.py:469 msgid "Could not confirm email, please try again." msgstr "Невозможно подтвердить email, попробуйте позже" -#: backend/urls.py:31 +#: backend/urls.py:36 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:36 +#: backend/urls.py:41 msgid "Token is invalid" msgstr "" -#: backend/urls.py:46 +#: backend/urls.py:51 msgid "Wrong username" msgstr "Неверное имя пользователя" -#: messages/serializers.py:42 +#: messages/serializers.py:50 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" @@ -585,10 +582,28 @@ msgstr "" "Модель в настоящее время неактивна. Пожалуйста, повторите попытку позже." #: ml_model/exceptions.py:22 +msgid "Your request was blocked by our moderation system" +msgstr "Ваш запрос был заблокирован нашей системой модерации" + +#: ml_model/exceptions.py:33 +msgid "" +"Image size %(cw)dx%(ch)d is not supported. Please rotate image to " +"%(rw)dx%(rh)d" +msgstr "" +"Размер изображения %(cw)dx%(ch)d не поддерживается. Пожалуйста, переверните " +"до %(rw)dx%(rh)d" + +#: ml_model/exceptions.py:37 +msgid "Image size %(cw)sx%(ch)s is not supported. Required size: %(rw)sx%(rh)s" +msgstr "" +"Размер изображения %(cw)sx%(ch)s не поддерживается. Требуемый размер: " +"%(rw)sx%(rh)s" + +#: ml_model/exceptions.py:42 msgid "The model is not responding" msgstr "Модель не отвечает" -#: ml_model/exceptions.py:31 +#: ml_model/exceptions.py:51 #, python-format msgid "" "The attached file format is not supported. Available formats: " @@ -597,18 +612,27 @@ msgstr "" "Формат вложенного файла не поддерживается. Доступные форматы: " "%(available_extensions)s." -#: ml_model/exceptions.py:37 +#: ml_model/exceptions.py:57 msgid "The length of the context has been exceeded." msgstr "Длина контекста превышена." -#: ml_model/exceptions.py:42 +#: ml_model/exceptions.py:62 msgid "Jinja template not found" msgstr "Jinja-шаблон не найден" -#: ml_model/exceptions.py:47 +#: ml_model/exceptions.py:67 msgid "There was an unknown error while rendering a template" msgstr "При рендеринге шаблона произошла неизвестная ошибка" +#: ml_model/exceptions.py:72 +msgid "The neuron model does not exist" +msgstr "Нейронная модель не существует" + +#: ml_model/models.py:18 ml_model/models.py:38 ml_model/models.py:70 +#: ml_model/models.py:182 +msgid "Slug" +msgstr "Ярлык" + #: ml_model/models.py:28 ml_model/models.py:80 msgid "Category" msgstr "Категория" @@ -621,6 +645,10 @@ msgstr "Категории" msgid "Not SVG-pictures not allowed" msgstr "Нельзя использовать не SVG-картинки" +#: ml_model/models.py:45 +msgid "Icon" +msgstr "Миниатюра" + #: ml_model/models.py:52 msgid "Model Tag" msgstr "Тег модели" @@ -633,6 +661,11 @@ msgstr "Теги модели" msgid "Alternative Titles" msgstr "Альтернативные названия" +#: ml_model/models.py:68 ml_model/models.py:181 ml_model/models.py:270 +#: payments/models/payment.py:52 +msgid "Description" +msgstr "Описание" + #: ml_model/models.py:72 msgid "Fill automatically, don't touch" msgstr "Заполняется автоматически, не трогать" @@ -649,7 +682,7 @@ msgstr "Теги" msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:156 ml_model/models.py:403 +#: ml_model/models.py:156 ml_model/models.py:403 payments/admin.py:95 msgid "Model" msgstr "Модель" @@ -687,7 +720,7 @@ msgstr "Привязка к версиям" msgid "Text" msgstr "Текст" -#: ml_model/models.py:222 stories/models.py:36 +#: ml_model/models.py:222 msgid "Image" msgstr "Картинка" @@ -874,16 +907,16 @@ msgstr "Инструкция Модели" msgid "Model Instructions" msgstr "Инструкции Моделей" -#: ml_model/selectors/ml_models_selector.py:81 +#: ml_model/selectors/ml_models_selector.py:106 msgid "no model by this id" msgstr "Не найдено моделей по этому ID" -#: ml_model/services/chatgpt.py:138 +#: ml_model/services/chatgpt.py:187 msgid "Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)" msgstr "" "Невозможно распознать изображение. (Поддерживаемые форматы: PNG, JPG, JPEG)" -#: ml_model/services/chatgpt.py:163 +#: ml_model/services/chatgpt.py:212 msgid "No matching version found" msgstr "Соответствующая версия не найдена" @@ -896,6 +929,18 @@ msgstr "Неизвестный бакет для загрузки" msgid "No image given for improving" msgstr "Нет изображения для улучшения" +#: ml_model/views.py:65 +msgid "Model data cannot be retrieved" +msgstr "Невозможно получить данные модели" + +#: payments/admin.py:33 payments/admin.py:67 payments/admin.py:87 +msgid "You can search by user email, exacted company name" +msgstr "Вы можете осуществлять поиск по e-mail пользователя, точному названию компании" + +#: payments/admin.py:38 payments/admin.py:92 +msgid "Missing" +msgstr "Отсутствующий" + #: payments/apps.py:9 payments/models/payment.py:60 msgid "Payments" msgstr "Платежи" @@ -909,6 +954,10 @@ msgstr "" "Баланс: %(balance).2f токенов; Нужно: %(required)s токенов; Нужно еще: " "%(needed)s токенов" +#: payments/exceptions/payer_not_found.py:6 +msgid "The payer does not exist" +msgstr "Плательщик не существует" + #: payments/models/invoice.py:23 msgid "Generative Model" msgstr "Генеративная модель" @@ -1093,42 +1142,6 @@ msgstr "Пользовательский репорт" msgid "User Reports" msgstr "Пользовательские репорты" -#: stories/apps.py:8 stories/models.py:26 -msgid "Stories" -msgstr "Истории" - -#: stories/models.py:13 -msgid "Is published" -msgstr "Опубликовано" - -#: stories/models.py:25 stories/models.py:40 -msgid "Story" -msgstr "История" - -#: stories/models.py:49 stories/models.py:59 -msgid "Page" -msgstr "Страница" - -#: stories/models.py:50 -msgid "Pages" -msgstr "Страницы" - -#: stories/models.py:54 -msgid "Label" -msgstr "Метка" - -#: stories/models.py:55 -msgid "Redirect URL" -msgstr "URL перехода" - -#: stories/models.py:64 -msgid "Widget" -msgstr "Виджет" - -#: stories/models.py:65 -msgid "Widgets" -msgstr "Виджеты" - #: tools/apps.py:9 msgid "Tools" msgstr "Инструменты" @@ -1149,11 +1162,8 @@ msgstr "Публичный API" msgid "Media" msgstr "Медиа" -#: tools/apps.py:44 -msgid "Feed" -msgstr "Шейр пользователей" - -#: tools/chats/apis.py:180 tools/public_api/views/base.py:90 +#: tools/chats/apis.py:178 tools/media/apis.py:161 +#: tools/public_api/views/base.py:103 msgid "" "Error occured when create generation. It may cause NSFW-content not allowed, " "retry again" @@ -1161,10 +1171,6 @@ msgstr "" "Случилась ошибка во время генерации. Она может возникать из-за того, что " "NSFW-контент запрещен. Попробуйте снова" -#: tools/chats/models.py:13 tools/public_api/models.py:45 -msgid "Is deleted" -msgstr "Удален" - #: tools/chats/models.py:17 #, python-format msgid "Chat %(id)s" @@ -1194,69 +1200,95 @@ msgstr "API Ключ" msgid "API Keys" msgstr "API Ключи" -#: tools/public_api/views/base.py:58 +#: tools/public_api/views/base.py:66 msgid "Key limit exceeded" msgstr "Превышен лимит по ключу" -#: tools/public_api/views/base.py:63 +#: tools/public_api/views/base.py:71 msgid "Model is blocked by outdating or temporary block, please retry later" msgstr "" "Модель заблокирована, т.к закончила обновляться или временно заблокирована, " "попробуйте позже" -#~ msgid "Points" -#~ msgstr "Поинты" +#: tools/public_api/views/base.py:78 +msgid "The request must not be empty" +msgstr "Запрос не должен быть пустым" -msgid "Access token is expired" -msgstr "Срок действия токена доступа истек" +#: tools/public_api/views/ml_service.py:65 +msgid "You must provide a model parameter" +msgstr "Необходимо указать параметр 'model'" + +#: tools/public_api/views/ml_service.py:80 +msgid "Missing required parameter: 'messages'" +msgstr "Отсутствует обязательный параметр: 'messages'" -msgid "Token prefix is missing" -msgstr "Отсутствует префикс токена" +#: tools/public_api/views/ml_service.py:130 +msgid "Model not found" +msgstr "Модель не найдена" -msgid "Access token expired or does not exist" -msgstr "Токен доступа просрочен или не существует" +#~ msgid "Achievement" +#~ msgstr "Достижение" -msgid "Model data cannot be retrieved" -msgstr "Невозможно получить данные модели" +#~ msgid "Achievements" +#~ msgstr "Достижения" -msgid "Message" -msgstr "Сообщение" +#~ msgid "Issued at" +#~ msgstr "Когда выдано" -msgid "Detail" -msgstr "Подробности" +#, python-format +#~ msgid "Achievement %(achievement_title)s пользователя %(username)s" +#~ msgstr "Достижение %(achievement_title)s пользователя %(username)s" -msgid "Message error" -msgstr "Ошибка сообщения" +#~ msgid "Issued achievement" +#~ msgstr "Выданное достижение" -msgid "Message errors" -msgstr "Ошибки сообщения" +#~ msgid "Account is already confirmed" +#~ msgstr "Аккаунт уже подтвержден" -msgid "The payer does not exist" -msgstr "Плательщик не существует" +#~ msgid "Stories" +#~ msgstr "Истории" -msgid "The request must not be empty" -msgstr "Запрос не должен быть пустым" +#~ msgid "Is published" +#~ msgstr "Опубликовано" -msgid "Your request was blocked by our moderation system" -msgstr "Ваш запрос был заблокирован нашей системой модерации" +#~ msgid "Story" +#~ msgstr "История" -msgid "Image size %dx%d is not supported. Please rotate image to %dx%d" -msgstr "Размер изображения %dx%d не поддерживается. Пожалуйста, переверните до %dx%d" +#~ msgid "Page" +#~ msgstr "Страница" -msgid "Image size %dx%d is not supported. Required size: %dx%d" -msgstr "Размер изображения %dx%d не поддерживается. Требуемый размер: %dx%d" +#~ msgid "Pages" +#~ msgstr "Страницы" -msgid "You must provide a model parameter" -msgstr "Необходимо указать параметр 'model'" +#~ msgid "Label" +#~ msgstr "Метка" -msgid "Missing required parameter: 'messages'" -msgstr "Отсутствует обязательный параметр: 'messages'" +#~ msgid "Redirect URL" +#~ msgstr "URL перехода" -msgid "The neuron model does not exist" -msgstr "Нейронная модель не существует" +#~ msgid "Widget" +#~ msgstr "Виджет" -msgid "Email token not found. Please contact support" -msgstr "E-mail токен не найден. Пожалуйста, свяжитесь со службой поддержки" +#~ msgid "Widgets" +#~ msgstr "Виджеты" -msgid "Model not found" -msgstr "Модель не найдена" +#~ msgid "Feed" +#~ msgstr "Шейр пользователей" + +#~ msgid "Points" +#~ msgstr "Поинты" + +#~ msgid "Token prefix is missing" +#~ msgstr "Отсутствует префикс токена" + +#~ msgid "Message" +#~ msgstr "Сообщение" + +#~ msgid "Detail" +#~ msgstr "Подробности" + +#~ msgid "Message error" +#~ msgstr "Ошибка сообщения" + +#~ msgid "Message errors" +#~ msgstr "Ошибки сообщения" @@ -277,7 +277,7 @@ class Chatgpt(SimpleService): reasoning_data = { 'Минимальный': 'minimal', 'Низкий': 'low', - 'Средний': 'medium ', + 'Средний': 'medium', 'Высокий': 'high' } json_data['reasoning']['effort'] = reasoning_data[info['reasoning']] @@ -10,6 +10,7 @@ from django.core.files import File from backend import settings from messages.models import BaseStore, Message +from ml_model.exceptions import RequestBlocked, GenerationException from ml_model.services.base import SimpleService from poller.models import Proxy @@ -83,7 +84,12 @@ class Gptimage(SimpleService): else: data = client.post('images/edits', data=json_data, files=files).json() process_time = timedelta(seconds=(time.time() - start_time)) - image = data['data'][0]['b64_json'] + try: + image = data['data'][0]['b64_json'] + except KeyError as exc: + if data['error']['code'] == 'moderation_blocked': + raise RequestBlocked + raise GenerationException from exc text_tokens = data['usage']['input_tokens_details']['text_tokens'] image_tokens = data['usage']['input_tokens_details']['image_tokens'] output_tokens = data['usage']['output_tokens'] @@ -24,19 +24,18 @@ class RequestBlocked(Exception): class UnsupportedSize(Exception): def __init__(self, current_size: tuple[int, int], required_size: tuple[int, int]): - self.current_size = current_size - self.required_size = required_size + self.current_size = dict(zip(('cw', 'ch'), current_size)) + self.required_size = dict(zip(('rw', 'rh'), required_size)) def __str__(self): - if self.current_size == self.required_size[::-1]: - return _('Image size %dx%d is not supported. Please rotate image to %dx%d') % ( - *self.current_size, *self.required_size - ) - else: - return _('Image size %dx%d is not supported. Required size: %dx%d') % ( - *self.current_size, *self.required_size - ) - + if tuple(self.current_size.values()) == tuple(reversed(self.required_size.values())): + return _( + 'Image size %(cw)dx%(ch)d is not supported. ' + 'Please rotate image to %(rw)dx%(rh)d' + ) % (self.current_size | self.required_size) + return _( + 'Image size %(cw)sx%(ch)s is not supported. Required size: %(rw)sx%(rh)s' + ) % (self.current_size | self.required_size) class ModelTimeoutError(Exception): def __str__(self): @@ -1,12 +1,10 @@ from decimal import Decimal from django.contrib import admin -from django.contrib.admin import DateFieldListFilter from django.db import models -from django.db.models import Count, QuerySet, Sum +from django.db.models import Count, Sum from django.db.models.functions import Coalesce -from django.http import HttpResponse -from openpyxl import Workbook +from django.utils.translation import gettext_lazy as _ from authentication.admin import CustomUserModelAdmin from payments.models import ( @@ -23,47 +21,22 @@ from payments.models.referral_account import ReferralAccount, ReferralInvite @admin.register(Payment) class PaymentAdmin(admin.ModelAdmin): + list_display = ['uid', '_user', 'created_at', 'status'] + raw_id_fields = ['user'] + date_hierarchy = 'created_at' + search_fields = [ 'uid', - 'amount', - 'created_at', - *(f'user__{field}' for field in CustomUserModelAdmin.search_fields), - ] - raw_id_fields = ['user'] - list_filter = [ - ('created_at', DateFieldListFilter), - ] - actions = [ - 'download_report', + 'user__host_account__company_name', + 'user__business_account__parent_company__company_name', ] + search_help_text = _('You can search by user email, exacted company name') - @admin.action(description='Скачать отчет') - def download_report(self, request, qs: QuerySet[Payment]): - wb = Workbook() - sheet = wb.active - sheet.append( - [ - 'Когда создан', - 'Email пользователя', - 'Статус', - 'Сумма', - 'Описание к платежу', - ] - ) - for s in qs: - sheet.append( - [ - str(s.created_at), - s.user.email, - s.status, - s.amount, - s.description, - ] - ) - response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=payments_info.xlsx' - wb.save(response) - return response + @admin.display(description=_('User')) + def _user(self, obj: Payment): + if obj.user is None: + return _('Missing') + return str(obj.user) @admin.register(PaymentPlan) @@ -84,12 +57,14 @@ class PaymentPlanAdmin(admin.ModelAdmin): @admin.register(PaymentPlanUserInfo) class PaymentPlanUserInfoAdmin(admin.ModelAdmin): list_display = ['user', 'current_token_balance', 'plan', 'updated_at'] + raw_id_fields = ['user'] + search_fields = [ - 'current_token_balance', - 'plan__price', - *(f'user__{field}' for field in CustomUserModelAdmin.search_fields), + 'uid', + 'user__host_account__company_name', + 'user__business_account__parent_company__company_name', ] - raw_id_fields = ['user'] + search_help_text = _('You can search by user email, exacted company name') @admin.register(UserPaymentMethod) @@ -104,54 +79,22 @@ class InvoiceAdmin(admin.ModelAdmin): raw_id_fields = ['user', 'message', 'model'] search_fields = [ - 'user__username', + 'user__email', 'user__host_account__company_name', 'user__business_account__parent_company__company_name', ] - search_help_text = 'Можно искать по юзернейму, названию компании' - - actions = [ - 'download_report', - ] - - list_filter = [] + search_help_text = _('You can search by user email, exacted company name') - @admin.display(description='Пользователь') + @admin.display(description=_('User')) def _user(self, obj): if obj.user is None: - return 'Ошибка' - return obj.user.email + return _('Missing') + return str(obj.user) - @admin.display(description='Модель') + @admin.display(description=_('Model')) def _model(self, obj): - return obj.model.title - - @admin.action(description='Скачать отчет') - def download_report(self, request, qs: QuerySet[Invoice]): - wb = Workbook() - sheet = wb.active - sheet.append( - [ - 'Дата списания', - 'Email пользователя', - 'Название модели', - 'Сумма', - ] - ) - for s in qs: - sheet.append( - [ - str(s.created_at), - s.user.email, - s.model.title, - s.cost, - ] - ) - response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=invoices_info.xlsx' - wb.save(response) - return response + return str(obj.model) @admin.register(PromoCode) @@ -23,7 +23,7 @@ class ReferralSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ('username', 'joined_at', 'profile_picture_link') + fields = ('joined_at', 'profile_picture_link') class ReferralAccountSerializer(serializers.ModelSerializer): @@ -21,7 +21,7 @@ class ErrorReportService: image_names = [] if images := serializer.validated_data.get('images', False): for img in images: - file, filename = create_original_image(img, self.user.username, datetime.now().timestamp()) + file, filename = create_original_image(img, self.user.email, datetime.now().timestamp()) res_name = MinIOService().put_object(file, filename, 'air-errors') image_names.append(res_name) report = ErrorReport.objects.create( @@ -20,7 +20,7 @@ from rest_framework.views import APIView from messages.models import Message from messages.serializers import MessageSerializer from ml_model.exceptions import DeploymentDisabled, TemplateNotFound, TemplateUnknownException, \ - FileExtensionNotSupported, ExceededContextLengthError + FileExtensionNotSupported, ExceededContextLengthError, RequestBlocked from ml_model.services.base import SimpleService from payments.exceptions.insufficient_balance import InsufficientBalance from tools.chats.models import Chat @@ -159,9 +159,7 @@ class MessagesAPIView(APIView): }, status=HTTP_503_SERVICE_UNAVAILABLE, ) - except FileExtensionNotSupported as exc: - return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) - except ExceededContextLengthError as exc: + except (FileExtensionNotSupported, ExceededContextLengthError, RequestBlocked) as exc: return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) except TemplateNotFound as exc: return Response({'detail': f'{exc}'}, status=HTTP_500_INTERNAL_SERVER_ERROR) @@ -14,8 +14,7 @@ from messages.serializers import MessageSerializer from ml_model.models import NeuronModel from ml_model.services.base import SimpleService from payments.exceptions.insufficient_balance import InsufficientBalance -from ml_model.exceptions import UnsupportedSize - +from ml_model.exceptions import UnsupportedSize, RequestBlocked from .models import Audio, Image, Video @@ -154,7 +153,7 @@ class MediaAPIView(APIView): input_message.save() if isinstance(exc, InsufficientBalance): return Response({'detail': f'{exc}'}, status=HTTP_402_PAYMENT_REQUIRED) - if isinstance(exc, UnsupportedSize): + if isinstance(exc, (UnsupportedSize, RequestBlocked)): return Response({'detail': f'{exc}'}, status=HTTP_400_BAD_REQUEST) return Response( {