import logging from asgiref.sync import sync_to_async from django.contrib.admin import display from django.contrib.auth.hashers import make_password from django.contrib.auth.models import UserManager as _UserManager, AbstractUser from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.phonenumber import PhoneNumber from account.models import BonusProgramMixin from store.utils import concat_not_null_values from tg_bot.tasks import send_tg_message logger = logging.getLogger(__name__) class UserQuerySet(models.QuerySet): # TODO: optimize queries def with_base_related(self): return self class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager): def _create_user(self, email, password, **extra_fields): if email: email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.password = make_password(password) user.save(using=self._db) return user def create_user(self, email=None, password=None, **extra_fields): extra_fields.setdefault("is_staff", False) return self._create_user(email, password, **extra_fields) def create_draft_user(self, **extra_fields): extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_draft_user", True) return self._create_user(email=None, password=None, **extra_fields) def create_superuser(self, email=None, password=None, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("role", User.ADMIN) if extra_fields.get("is_staff") is not True: raise ValueError("Superuser must have is_staff=True.") return self._create_user(email, password, **extra_fields) def invite_user(self, referral_code, user_id): inviter = User.objects.filter(referral_code=referral_code).first() user_to_invite = User.objects.filter(id=user_id).first() if inviter is None or user_to_invite is None: return if inviter.id == user_to_invite.id: logger.warning(f"User #{inviter.id} tried to invite himself via referral code {referral_code}") return obj, created = ReferralRelationship.objects.get_or_create(inviter_id=inviter.id, invited_id=user_to_invite.id) if not created: logger.warning(f"User #{inviter.id} already invited user #{user_id}") return async def bind_tg_user(self, tg_user_id, phone, referral_code=None): # Normalize phone: 79111234567 -> +79111234567 phone = PhoneNumber.from_string(phone).as_e164 """ 1) No user with given phone or tg_user_id -> create draft user, add tg_user_id & phone 2) User exists with given phone, but no tg_user_id -> add tg_user_id to User 3) User exists with tg_user_id, but no phone -> add phone to User 4) User exists with given tg_user_id & phone -> just authorize """ user = await User.objects.filter( Q(phone=phone) | (Q(tg_user_id=tg_user_id)) ).afirst() freshly_created = False # Sign up through Telegram bot if user is None: user = await sync_to_async(self.create_draft_user)(phone=phone, tg_user_id=tg_user_id) logger.info(f"tgbot: Created draft user #{user.id} for phone [{phone}]") freshly_created = True # First-time binding Telegram <-> User ? if freshly_created or user.tg_user_id is None: # Add bonus for Telegram login await sync_to_async(user.add_signup_bonus)() # Create referral relationship # Only for fresh registration if freshly_created and referral_code is not None: await sync_to_async(User.objects.invite_user)(referral_code, user.id) # Bind Telegram chat to user if not freshly_created: user.phone = phone user.tg_user_id = tg_user_id await user.asave() logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}") class User(BonusProgramMixin, AbstractUser): ADMIN = "admin" ORDER_MANAGER = "ordermanager" PRODUCT_MANAGER = "productmanager" CLIENT = "client" ROLE_CHOICES = ( (ADMIN, 'Администратор'), (ORDER_MANAGER, 'Менеджер по заказам'), (PRODUCT_MANAGER, 'Менеджер по закупкам'), (CLIENT, 'Клиент'), ) # Login by email USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['phone'] username = None email = models.EmailField("Эл. почта", blank=True, null=True, unique=True) # Base info first_name = models.CharField(_("first name"), max_length=150, blank=True, null=True) last_name = models.CharField(_("last name"), max_length=150, blank=True, null=True) middle_name = models.CharField("Отчество", max_length=150, blank=True, null=True) role = models.CharField("Роль", max_length=30, choices=ROLE_CHOICES, default=CLIENT, editable=False) # Contacts phone = PhoneNumberField('Телефон', null=True, blank=True, unique=True) telegram = models.CharField('Telegram', max_length=100, null=True, blank=True) # Bot-related # User is created via Telegram bot and has no password yet. # User can set initial password via /users/set_initial_password/ is_draft_user = models.BooleanField("Черновик пользователя", default=False) tg_user_id = models.BigIntegerField("id пользователя в Telegram", null=True, blank=True, unique=True) objects = UserManager() def __str__(self): value = self.email or self.phone or self.id return str(value) @property def is_superuser(self): return self.role == self.ADMIN @property def is_manager(self): return self.role in (self.ADMIN, self.ORDER_MANAGER, self.PRODUCT_MANAGER) @display(description='ФИО') def full_name(self): return concat_not_null_values(self.last_name, self.first_name, self.middle_name) @property def invited_users(self): return User.objects.filter(user_inviter__inviter=self.id) @property def inviter(self): return User.objects.filter(user_invited__invited=self.id).first() def notify_tg_bot(self, message, **kwargs): if self.tg_user_id is None: return send_tg_message.delay(self.tg_user_id, message, **kwargs) def save(self, *args, **kwargs): # If password changed, it is no longer a draft User if self._password is not None: self.is_draft_user = False super().save(*args, **kwargs) class ReferralRelationship(models.Model): invited_at = models.DateTimeField(auto_now_add=True) inviter = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_invited") invited = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_inviter") class Meta: unique_together = (('inviter', 'invited'),) def clean(self): if self.inviter_id == self.invited_id: raise ValidationError("User can't invite himself")