@@ -0,0 +1,31 @@ +# Generated by Django 5.0.9 on 2025-01-08 14:19 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0014_alter_businessaccount_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='UserSetting', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('device', models.CharField(choices=[('mobile', 'Телефон'), ('desktop', 'ПК')], verbose_name='Устройство')), + ('type', models.CharField(choices=[('sidebar', 'Сайдбар')], verbose_name='Тип')), + ('value', models.JSONField(verbose_name='Значение')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Настройка пользователя', + 'verbose_name_plural': 'Настройки пользователя', + 'unique_together': {('user', 'type', 'device')}, + }, + ), + ] @@ -1,5 +1,6 @@ import random from typing import TYPE_CHECKING, Optional +from uuid import uuid4 from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from django.core.exceptions import ObjectDoesNotExist @@ -236,3 +237,32 @@ def on_user_creation_signal(sender, instance, created, **kwargs): instance.last_name, None ) instance.save() + + +class UserSetting(models.Model): + class DeviceChoices(models.TextChoices): + MOBILE = 'mobile', 'Телефон' + DESKTOP = 'desktop', 'ПК' + + class TypeChoices(models.TextChoices): + SIDEBAR = 'sidebar', 'Сайдбар' + + id = models.UUIDField( + primary_key=True, editable=False, default=uuid4, verbose_name='ID' + ) + + user = models.ForeignKey( + CustomUserModel, + on_delete=models.CASCADE, + related_name='user_settings', + verbose_name='Пользователь', + ) + + device = models.CharField(choices=DeviceChoices.choices, verbose_name='Устройство') + type = models.CharField(choices=TypeChoices.choices, verbose_name='Тип') + value = models.JSONField(verbose_name='Значение') + + class Meta: + verbose_name = 'Настройка пользователя' + verbose_name_plural = 'Настройки пользователя' + unique_together = ['user', 'type', 'device'] @@ -0,0 +1,30 @@ +from typing import List +from uuid import UUID + +from ninja import Query, Router + +from authentication.schemas import ( + CreateUserSettingSchema, + UpdateUserSettingSchema, + UserSettingFilterSchema, + UserSettingSchema, +) +from authentication.security import SyncAuthBearer +from authentication.services.user_services import UserService + +router = Router(auth=SyncAuthBearer(), tags=['users']) + + +@router.get('settings/', tags=['users/settings'], response=List[UserSettingSchema]) +def get_user_settings(request, filters: UserSettingFilterSchema = Query(...)): + return UserService.list_settings(filters=filters.get_filter_expression()) + + +@router.post('settings/', tags=['users/settings'], response=UserSettingSchema) +def add_setting(request, data: CreateUserSettingSchema): + return UserService.add_setting(user_id=request.auth.uid, **data.model_dump()) + + +@router.put('settings/{id}', tags=['users/settings'], response={204: None}) +def update_setting(request, id: UUID, data: UpdateUserSettingSchema): + UserService.update_setting(setting_id=id, **data.model_dump()) @@ -1,6 +1,8 @@ -from typing import Tuple +from typing import Any, Tuple +from uuid import UUID from django.contrib.auth import authenticate, login, logout +from django.db.models import Q, QuerySet from django.db.transaction import atomic from django.utils.translation import gettext_lazy as _ from rest_framework.request import Request @@ -13,6 +15,7 @@ from authentication.models import ( TelegramUser, VKUser, ) +from authentication.models.user import UserSetting from authentication.selectors.email_token_selector import EmailTokenSelector from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( @@ -280,6 +283,22 @@ class UserService: emails__contains=[request.query_params['email']] ).exists() + @classmethod + def list_settings(cls, filters: Q = Q()) -> QuerySet[UserSetting]: + return UserSetting.objects.filter(filters) + + @classmethod + def add_setting( + cls, user_id: UUID, device: str, type: str, value: Any + ) -> UserSetting: + return UserSetting.objects.create( + user_id=user_id, device=device, type=type, value=value + ) + + @classmethod + def update_setting(cls, setting_id: UUID, value: Any) -> None: + UserSetting.objects.filter(id=setting_id).update(value=value) + # For Social Auth pipeline def update_profile_picture_social(*args, **kwargs): @@ -0,0 +1,28 @@ +from typing import List, Optional + +from ninja import Field, FilterSchema, ModelSchema + +from authentication.models.user import UserSetting + + +class UserSettingFilterSchema(FilterSchema): + device: Optional[str] = None + types: List[str] = Field(None, q='type__in') + + +class UserSettingSchema(ModelSchema): + class Meta: + model = UserSetting + exclude = ('user',) + + +class CreateUserSettingSchema(ModelSchema): + class Meta: + model = UserSetting + fields = ('device', 'type', 'value') + + +class UpdateUserSettingSchema(ModelSchema): + class Meta: + model = UserSetting + fields = ('value',) @@ -1,6 +1,7 @@ import re from datetime import date, datetime, timedelta from itertools import chain +from logging import getLogger from uuid import UUID from django.conf import settings @@ -74,6 +75,8 @@ from tools.copywrite.models import Copywrite from tools.media.models import Image from tools.public_api.models import APIKey +logger = getLogger(__name__) + class UserLoginAPIView(APIView): permission_classes = (IsAnonymous,) @@ -85,6 +88,7 @@ class UserLoginAPIView(APIView): response = UserService.login_user(request) return Response(response.data, status=status.HTTP_200_OK) except Exception as err: + logger.exception(err) return Response( {'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST @@ -32,7 +32,6 @@ MIDDLEWARE = [ 'authentication.middleware.CompanyIPMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'reports.middleware.RequestResponseMiddleware', ] CORE_APP = 'core.apps.CoreConfig' @@ -12,6 +12,7 @@ from backend.public import urlpatterns as public_urlpatterns api = NinjaAPI(title='AIR API', version='1.0.0') api.add_router('copywrite/', 'tools.copywrite.routes.v1.router') +api.add_router('users/', 'authentication.routes.v1.router') @api.exception_handler(ObjectDoesNotExist) @@ -251,7 +251,7 @@ class Chatgpt(SimpleService): def calculate_price(self, usage: int, model: str, *args, **kwargs) -> Decimal: price = usage * self.TOKENS_COST[model] - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def count_image_tokens( self, image: InMemoryUploadedFile, high_resolution: bool = True @@ -38,7 +38,7 @@ class Claude(SimpleService): usage['input_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] + usage['output_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] ) - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, r: str, t: timedelta, save: bool = True) -> list[Message]: msgs = [ @@ -32,7 +32,7 @@ class Codellama(SimpleService): sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) * self.price ) - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, r: Iterator[Any], t: timedelta, save: bool = True @@ -85,7 +85,7 @@ class Dalle(SimpleService): price = self.TOKEN_PAYMENT_RULES[input_message.info.get('version', 'dall-e-2')][ input_message.info.get('size', '1024x1024') ] * input_message.info.get('n', 1) - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, input_prompt: str, r: list[str], t: timedelta, save: bool = True @@ -55,7 +55,7 @@ class Deepl(SimpleService): else: symbols += len(message.content) price = symbols * self.price - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, @@ -90,9 +90,8 @@ class Djourney(SimpleService): ) def calculate_price(self, process_time: timedelta) -> Decimal: - total_seconds = Decimal(process_time.total_seconds()) - return self.PRICE * total_seconds - + price = self.PRICE * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, prompt: str, images: list, time: timedelta, save: bool = True ) -> list[Message]: @@ -51,8 +51,8 @@ class Epicphotogasm(SimpleService): super().__init__(store) def calculate_price(self, process_time: timedelta) -> Decimal: - price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('.01')) + price = self.price * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, input_prompt: str, r: list[str], t: timedelta, save: bool = True @@ -80,12 +80,13 @@ class Granite(SimpleService): return result.json() def calculate_price(self, result: str, input_message: Message) -> Decimal: - return Decimal( + price = Decimal( sum( [self.TOKEN_PAYMENT_RULES['granite-output'] / 1_000_000 * len(result.split(' '))] + [self.TOKEN_PAYMENT_RULES['granite-input'] / 1_000_000 * len(input_message.content.split(' '))] ) ) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, result: str, time: timedelta, save: bool = True @@ -108,8 +108,8 @@ class Iconic(SimpleService): ) def calculate_price(self, process_time: timedelta) -> Decimal: - total_seconds = Decimal(process_time.total_seconds()) - return self.PRICE * total_seconds + price = self.PRICE * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, prompt: str, images: list, time: timedelta, save: bool = True @@ -61,7 +61,7 @@ class Kandinsky(SimpleService): def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, input_prompt: str, r: list[str], t: timedelta, save: bool = True @@ -91,8 +91,8 @@ class Lightning(SimpleService): ) def calculate_price(self, process_time: timedelta) -> Decimal: - total_seconds = Decimal(process_time.total_seconds()) - return self.PRICE * total_seconds + price = self.PRICE * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, prompt: str, images: list, time: timedelta, save: bool = True @@ -46,7 +46,7 @@ class Llama(SimpleService): def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, r: Iterator[Any], t: timedelta, save: bool = True @@ -91,8 +91,8 @@ class Logoai(SimpleService): ) def calculate_price(self, process_time: timedelta) -> Decimal: - total_seconds = Decimal(process_time.total_seconds()) - return self.PRICE * total_seconds + price = self.PRICE * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, prompt: str, images: list, time: timedelta, save: bool = True @@ -67,7 +67,7 @@ class Mistral(SimpleService): ] ) ) - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def make_messages(self, r: Iterator[Any], t: timedelta): msgs = [] @@ -57,7 +57,7 @@ class Musicgen(SimpleService): def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, r: str, t: timedelta, save: bool = True) -> list[Message]: out: list[Message] = [ @@ -54,7 +54,7 @@ class Openjourney(SimpleService): def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, input_prompt: str, r: list[str], t: timedelta, save: bool = True @@ -97,7 +97,8 @@ class Pulid(SimpleService): ) def calculate_price(self, process_time: timedelta) -> Decimal: - return self.PRICE * Decimal(process_time.total_seconds()) + price = self.PRICE * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, prompt: str, images: list, time: timedelta, save: bool = True @@ -90,7 +90,8 @@ class Sdxlemoji(SimpleService): ) def calculate_price(self, process_time: timedelta) -> Decimal: - return self.PRICE * Decimal(process_time.total_seconds()) + price = self.PRICE * Decimal(process_time.total_seconds()) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, prompt: str, images: list, time: timedelta, save: bool = True @@ -76,7 +76,7 @@ class Upscaleai(SimpleService): def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('.01')) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( self, input_prompt: str, results: list[str], t: timedelta, save: bool = True @@ -3,7 +3,7 @@ FROM python:3.12-slim as build WORKDIR /code COPY pyproject.toml poetry.lock /code/ -RUN --mount=type=cache,target=/root/.cache/pip pip install poetry +RUN --mount=type=cache,target=/root/.cache/pip pip install poetry && poetry self add poetry-plugin-export RUN poetry export --only main --output=requirements.txt FROM python:3.12-slim @@ -3,7 +3,7 @@ FROM python:3.12-slim as build WORKDIR /code COPY pyproject.toml poetry.lock /code/ -RUN --mount=type=cache,target=/root/.cache/pip pip install poetry +RUN --mount=type=cache,target=/root/.cache/pip pip install poetry && poetry self add poetry-plugin-export RUN poetry export --with test --with debug --output=requirements.txt