@@ -74,6 +74,8 @@ class BusinessAccountService: self.reject() else: raise Exception(_('Invited account can either accept or reject an invitation')) + self.account.user.is_confirmed = True + self.account.user.save() return self.account def accept(self): @@ -105,6 +107,9 @@ class BusinessAccountService: if self.account.user.account_type not in ('business_account', 'business_security'): raise Exception(_("You do not have sufficient rights to perform this action")) + if self.account.acceptance_status != InvitationStatus.ACCEPTED: + raise Exception(_('You cannot change the password of an unconfirmed e-mail user.')) + self.account.user.set_password(serializer.validated_data['password_1']) self.account.user.save() @@ -68,7 +68,6 @@ class BusinessHostService: 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.is_confirmed = True user.save() PaymentPlanService(user).subscribe_user_to_plan(PaymentPlan.objects.get(price=0, is_corporate=False)) @@ -76,7 +75,6 @@ class BusinessHostService: account_service = BusinessAccountService.create( user, self.user.host_account, - status=InvitationStatus.ACCEPTED, account_privileges=account_privileges, ) if token_limit is not None: @@ -123,24 +123,25 @@ class EmailService: def send_corporate_greeting_email(self, account: BusinessAccount, password: str | None = None): if self.user.host_account is None: raise Exception(_('Regular users cannot send introductory letters')) - message = f""" - Добро пожаловать на платформу AIR! - Этот аккаунт был создан для вас {self.user.host_account.company_name} в рабочих целях. - Вы можете использовать все ресурсы корпоративного аккаунта - оплачивать - подписку нет необходимости. - - Ваши данные для входа: - - email: {account.user.email} - password: {password} - """ - mail = EmailMessage( + token = EmailTokenService(account.user).generate_user_token() + try: + template = get_template('authentication/corporate_greeting_email.html') + html_message = template.render(context={ + 'company_name': self.user.host_account.company_name, + 'invitation_url': settings.INVITATION_RESPONSE_URL, + 'token': token.key, + 'email': account.user.email, + 'password': password + }) + except TemplateDoesNotExist: + raise LetterNotFound + except Exception as exc: + raise LetterUnknownException from exc + self.send_email( subject='Ваш аккаунт на платформе AIR', - body=message, - from_email=settings.EMAIL_HOST_USER, - to=(account.user.email,), + message=html_message, + user_email=account.user.email ) - mail.send(fail_silently=True) def send_reinvited_email(self, account: BusinessAccount, password: str | None = None) -> None: try: @@ -0,0 +1,25 @@ + + + + + Ваш аккаунт на платформе AIR + + +

Добро пожаловать на платформу AIR!

+

+ Этот аккаунт был создан для вас {{ company_name }} в рабочих целях.
+ Вы можете использовать все ресурсы корпоративного аккаунта — оплачивать + подписку нет необходимости. +

+

+ Чтобы ответить на приглашение — перейдите по ссылке ниже:
+ [Нажмите для подтверждения]
+ Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку:
+ {{ invitation_url }}?token={{ token }} +

+

Ваши данные для входа:
+ E-mail: {{ email }}
+ Пароль: {{ password }}

+

Всегда с вами, команда AIR

+ + @@ -1136,4 +1136,10 @@ msgid "Letter template not found" msgstr "Шаблон письма не найден" msgid "There was an unknown error while sending an email" -msgstr "При отправке письма произошла неизвестная ошибка" \ No newline at end of file +msgstr "При отправке письма произошла неизвестная ошибка" + +msgid "You cannot change the password of an unconfirmed e-mail user." +msgstr "Вы не можете изменить пароль неподтвержденного по e-mail пользователя." + +msgid "Model version not found or does not exist. Available versions: %(available_versions)s" +msgstr "Версия модели не найдена или не существует. Доступные версии: %(available_versions)s" @@ -62,6 +62,7 @@ class NeuronModelSelector: models = models.filter(title__in=allowed_models) if category: models = models.filter(category__slug=category) + models = models.distinct() if serialize: return NeuronModelsSerializer(models, many=True) return models @@ -51,6 +51,25 @@ class Fluxlorafast(SimpleService): return messages def make(self, input_message: Message, save: bool = True) -> list[Message]: + sizes = { + '1:1': 'square', + '1:1 HD': 'square_hd', + 'Портрет 4:3': 'portrait_4_3', + 'Портрет 16:9': 'portrait_16_9', + '4:3': 'landscape_4_3', + '16:9': 'landscape_16_9', + } + loras = { + 'Tarot': { + 'path': 'https://huggingface.co/multimodalart/flux-tarot-v1/resolve/main/flux_tarot_v1_lora.safetensors' + }, + 'Realistic': { + 'path': 'https://civitai.com/api/download/models/724060?type=Model&format=SafeTensor&a=1' + }, + 'Logo': { + 'path': 'https://huggingface.co/Shakker-Labs/FLUX.1-dev-LoRA-Logo-Design/resolve/main/FLUX-dev-lora-Logo-Design.safetensors' + }, + } start_time = time.time() version = input_message.info.get('version') translated_prompt = self.translate_prompt(input_message.content) @@ -58,6 +77,8 @@ class Fluxlorafast(SimpleService): { 'prompt': translated_prompt, 'model_version': 'fb90c17a-d410-41e7-9961-dc7c687bc627', + 'image_size': sizes.get(input_message.info.pop('image_size', None), '4:3'), + 'loras': [loras.get(input_message.info.pop('loras', None), 'Realistic')], **input_message.info, } ) @@ -9,6 +9,7 @@ import requests from django.core.files import File from messages.models import Message +from ml_model.exceptions import InvalidVersionException from ml_model.models import ( ModelInput, NeuronModel, @@ -76,7 +77,9 @@ class Fluxproultra(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() - version = input_message.info.get('version') + version = input_message.info.pop('version', 'flux-1.1-pro-ultra') + if version not in self.TOKENS_COST.keys(): + raise InvalidVersionException(available_versions=', '.join(self.TOKENS_COST.keys())) callback_data = dict( { 'prompt': self.translate_prompt(input_message.content), @@ -15,3 +15,13 @@ class LargeResourceConsumptionException(Exception): ... class DeploymentDisabled(Exception): def __str__(self): return _('The model is currently disabled. Please try again later.') + + +class InvalidVersionException(Exception): + def __init__(self, available_versions: str) -> None: + self.available_versions = available_versions + + def __str__(self) -> str: + return _( + 'Model version not found or does not exist. Available versions: %(available_versions)s' + ) % {'available_versions': self.available_versions} @@ -127,7 +127,7 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name ) as client: resp = client.post( 'chat/completions', - json={'model': version, 'messages': messages, **callback_data}, + json={'model': version, 'messages': messages, 'transforms': ['middle-out'],**callback_data}, ) if ( (data := resp.json()) @@ -15,16 +15,28 @@ services: python manage.py compilemessages python -m uvicorn --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level info backend.asgi:application labels: - - traefik.enable=true - - traefik.docker.network=infrastructure - - traefik.http.routers.backend.rule=Host(`$DOMAIN`) - - traefik.http.routers.backend.entrypoints=web,websecure - - traefik.http.routers.backend.tls=true - - traefik.http.routers.backend.tls.certresolver=defaultresolver - - traefik.http.routers.backend.service=backend - - traefik.http.services.backend.loadbalancer.server.port=8000 - - traefik.http.middlewares.backend.redirectscheme.scheme=https - - traefik.http.middlewares.backend.redirectscheme.permanent=true + - traefik.enable=true + - traefik.docker.network=infrastructure + + - traefik.http.routers.backend.rule=Host(`$DOMAIN`) + - traefik.http.routers.backend.entrypoints=web,websecure + - traefik.http.routers.backend.tls=true + - traefik.http.routers.backend.tls.certresolver=defaultresolver + - traefik.http.routers.backend.service=backend + + - traefik.http.routers.backend-admin-http.rule=Host(`$DOMAIN`) && PathPrefix(`/djangoadmin`) + - traefik.http.routers.backend-admin-http.entrypoints=web + - traefik.http.routers.backend-admin-http.tls=false + - traefik.http.routers.backend-admin-http.service=backend + - traefik.http.routers.backend-admin-http.middlewares=sts-header@file,https-redirect@file + + - traefik.http.routers.backend-admin-https.rule=Host(`$DOMAIN`) && PathPrefix(`/djangoadmin`) + - traefik.http.routers.backend-admin-https.entrypoints=websecure + - traefik.http.routers.backend-admin-https.service=backend + - traefik.http.routers.backend-admin-https.tls=true + - traefik.http.routers.backend-admin-https.tls.certresolver=defaultresolver + + - traefik.http.services.backend.loadbalancer.server.port=8000 env_file: - $ENV depends_on: @@ -42,7 +42,7 @@ services: - node.role == worker labels: - traefik.enable=true - - traefik.docker.network=infrastructure + - traefik.swarm.network=infrastructure - traefik.http.routers.backend.rule=Host(`$DOMAIN`) - traefik.http.routers.backend.entrypoints=web,websecure @@ -54,7 +54,7 @@ services: - traefik.http.routers.backend-admin-http.entrypoints=web - traefik.http.routers.backend-admin-http.tls=false - traefik.http.routers.backend-admin-http.service=backend - - traefik.http.routers.backend-admin-http.middlewares=sts-header@swarm,https-redirect@swarm + - traefik.http.routers.backend-admin-http.middlewares=sts-header@file,https-redirect@file - traefik.http.routers.backend-admin-https.rule=Host(`$DOMAIN`) && PathPrefix(`/djangoadmin`) - traefik.http.routers.backend-admin-https.entrypoints=websecure