@@ -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') @@ -14,7 +14,6 @@ from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AllowedModelsStatus, BusinessAccountDataSerializer, - BusinessAccountStatisticsSerializer, BusinessHostSerializer, NeuronModelStatisticsSerialiser, ) @@ -87,17 +86,6 @@ class BusinessHostSelector: host, context={'worker_amount': host.accounts.count(), 'token_cap_enabled': host.token_cap_enabled} ) - def get_all_account_statistics( - self, - ) -> BusinessAccountStatisticsSerializer: - accounts = self.list_business_accounts() - context = dict() - for account in accounts: - spending_amount = ModelPaymentSelector(account.user).calculate_self_spending() - context[account] = spending_amount - - return BusinessAccountStatisticsSerializer(accounts, many=True, context={'amounts': context}) - def get_per_model_statistics(self): accounts = self.list_business_accounts() context = dict() @@ -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} @@ -118,6 +118,7 @@ class HasInviteListFilter(admin.SimpleListFilter): class CustomUserModelAdmin(UserAdmin, ExportActionModelAdmin): list_display = ['email', '_balance', 'created_at'] date_hierarchy = 'created_at' + ordering = ('email',) search_fields = ['email', 'utm__utm_source'] search_help_text = 'Можно искать по email пользователя, UTM-источнику' @@ -126,7 +127,6 @@ class CustomUserModelAdmin(UserAdmin, ExportActionModelAdmin): resource_classes = (DefaultUserResource, ReferralUserResource) fields = [ 'uid', - 'username', 'email', 'password', 'first_name', @@ -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() @@ -204,14 +201,6 @@ class DeletedAccountDataSerializer(serializers.Serializer): return self.context.get('account_type') -class BusinessAccountStatisticsSerializer(serializers.Serializer): - username = serializers.CharField() - spent_amount = serializers.SerializerMethodField() - - def get_spent_amount(self, obj): - return self.context['amounts'][obj] - - class NeuronModelStatisticsSerialiser(serializers.Serializer): title = serializers.CharField() spent_amount = serializers.SerializerMethodField() @@ -105,11 +105,6 @@ urlpatterns = [ views.HostInvitationAPIView.as_view(), name='host-invitation-management', ), - path( - 'business-host/stats/accounts', - views.HostAccountStatisticsAPIView.as_view(), - name='add-judicial-to-host', - ), path( 'business-host/stats/models', views.HostModelStatisticsAPIView.as_view(), @@ -60,7 +60,6 @@ from authentication.serializers import ( AddModelsSerializer, AllowedModelsStatus, BusinessAccountDataSerializer, - BusinessAccountStatisticsSerializer, BusinessGroupSerializer, BusinessGroupsSerializer, BusinessGroupUpdateSerializer, @@ -424,19 +423,6 @@ class HostModelStatisticsAPIView(APIView): return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) -class HostAccountStatisticsAPIView(APIView): - permission_classes = (HasBusinessAdminPermissions,) - - @extend_schema(responses={200: BusinessAccountStatisticsSerializer}) - def get(self, request, *args, **kwargs): - """List company stats by company accounts.""" - try: - result = BusinessHostSelector(self.request.user).get_all_account_statistics() - return Response(result.data, status=status.HTTP_200_OK) - except Exception as err: - return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) - - class DeleteUserAPIView(APIView): permission_classes = (IsAuthenticated,) @@ -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) @@ -23,7 +23,7 @@ class ReferralSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ('username', 'joined_at', 'profile_picture_link') + fields = ('email', '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(