kwork-poizonstore/account/models/user.py
phzhik 00686e9dc4 + BonusProgramConfig
* Moved GlobalSettings to core app
* Moved bonus program logic from User to BonusProgram class
* Worked on error handling a bit
2024-05-24 02:19:00 +04:00

205 lines
7.3 KiB
Python

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 account.models.bonus import BonusProgram
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) -> bool:
# 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(BonusProgram.add_signup_bonus)(user)
# 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}")
return freshly_created
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")