adding
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import random
|
||||
import string
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
TRIAL_DAYS = 7
|
||||
_REFERRAL_CODE_ALPHABET = string.ascii_uppercase + string.digits
|
||||
|
||||
|
||||
# -------------------- Manager --------------------
|
||||
class UserManager(BaseUserManager):
|
||||
def create_user(self, phone, password=None, **extra_fields):
|
||||
if not phone:
|
||||
raise ValueError(_("شماره موبایل الزامی است"))
|
||||
|
||||
phone = self.normalize_phone(phone)
|
||||
|
||||
user = self.model(phone=phone, **extra_fields)
|
||||
if password:
|
||||
user.set_password(password)
|
||||
else:
|
||||
user.set_unusable_password()
|
||||
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, phone, password, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", True)
|
||||
extra_fields.setdefault("is_superuser", True)
|
||||
extra_fields.setdefault("is_active", True)
|
||||
extra_fields.setdefault("state", User.State.ACTIVE)
|
||||
|
||||
if not password:
|
||||
raise ValueError(_("سوپریوزر باید رمز عبور داشته باشد"))
|
||||
|
||||
return self.create_user(phone, password, **extra_fields)
|
||||
|
||||
@staticmethod
|
||||
def normalize_phone(phone: str) -> str:
|
||||
phone = phone.strip().replace(" ", "").replace("-", "")
|
||||
if phone.startswith("+98"):
|
||||
phone = "0" + phone[3:]
|
||||
elif phone.startswith("98") and len(phone) == 12:
|
||||
phone = "0" + phone[2:]
|
||||
return phone
|
||||
|
||||
|
||||
# -------------------- Validators --------------------
|
||||
def phone_validator(phone: str):
|
||||
"""شماره موبایل: فقط عدد، شروع با 09، طول ۱۱ رقم"""
|
||||
if not phone.isdigit():
|
||||
raise ValidationError(_("شماره موبایل باید فقط شامل اعداد باشد"))
|
||||
if not phone.startswith("09"):
|
||||
raise ValidationError(_("شماره موبایل باید با 09 شروع شود"))
|
||||
if len(phone) != 11:
|
||||
raise ValidationError(_("شماره موبایل باید ۱۱ رقم باشد"))
|
||||
return phone
|
||||
|
||||
|
||||
# -------------------- User Model --------------------
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
class State(models.TextChoices):
|
||||
INACTIVE = "inactive", _("غیرفعال")
|
||||
ACTIVE = "active", _("فعال")
|
||||
BLOCKED = "blocked", _("مسدود شده")
|
||||
|
||||
phone = models.CharField(
|
||||
max_length=11,
|
||||
unique=True,
|
||||
validators=[phone_validator],
|
||||
verbose_name=_("شماره موبایل"),
|
||||
)
|
||||
name = models.CharField(max_length=100, blank=True, verbose_name=_("نام"))
|
||||
|
||||
state = models.CharField(
|
||||
max_length=10,
|
||||
choices=State.choices,
|
||||
default=State.INACTIVE,
|
||||
verbose_name=_("وضعیت کاربر"),
|
||||
)
|
||||
|
||||
# OTP — کد بهصورت هششده ذخیره میشود، هرگز متن ساده
|
||||
otp_code = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("کد OTP (هششده)"))
|
||||
otp_expiry = models.DateTimeField(blank=True, null=True, verbose_name=_("انقضای OTP"))
|
||||
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("فعال"))
|
||||
is_staff = models.BooleanField(default=False, verbose_name=_("کارمند"))
|
||||
created_at = models.DateTimeField(default=timezone.now, verbose_name=_("تاریخ ایجاد"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("تاریخ بروزرسانی"))
|
||||
|
||||
# -------------------- جم / اشتراک / معرفی --------------------
|
||||
gem_balance = models.PositiveIntegerField(default=0, verbose_name=_("موجودی جم"))
|
||||
trial_expires_at = models.DateTimeField(
|
||||
default=None, null=True, blank=True, verbose_name=_("پایان دورهی رایگان")
|
||||
)
|
||||
referral_code = models.CharField(
|
||||
max_length=8, unique=True, blank=True, verbose_name=_("کد معرف")
|
||||
)
|
||||
referred_by = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="referrals",
|
||||
verbose_name=_("معرف"),
|
||||
)
|
||||
# جلوگیری از دوبار پاداشدادن وقتی همان کاربر چندبار verify میشود —
|
||||
# پاداشِ معرف فقط یکبار، دقیقا سر اولین verify موفق کاربر جدید.
|
||||
referral_bonus_granted = models.BooleanField(default=False, verbose_name=_("پاداش معرفی دادهشد"))
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = "phone"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("کاربر")
|
||||
verbose_name_plural = _("کاربران")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.phone} - {self.name}".strip(" -")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.referral_code:
|
||||
self.referral_code = self._generate_referral_code()
|
||||
if self.trial_expires_at is None and self._state.adding:
|
||||
self.trial_expires_at = timezone.now() + timedelta(days=TRIAL_DAYS)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _generate_referral_code() -> str:
|
||||
for _attempt in range(20):
|
||||
code = "".join(random.choices(_REFERRAL_CODE_ALPHABET, k=6))
|
||||
if not User.objects.filter(referral_code=code).exists():
|
||||
return code
|
||||
# عملا در این مقیاس هرگز رخ نمیدهد؛ فقط یک fallback امن
|
||||
return "".join(random.choices(_REFERRAL_CODE_ALPHABET, k=8))
|
||||
|
||||
@property
|
||||
def is_in_trial(self) -> bool:
|
||||
return bool(self.trial_expires_at and timezone.now() < self.trial_expires_at)
|
||||
|
||||
def activate(self):
|
||||
self.state = self.State.ACTIVE
|
||||
self.is_active = True
|
||||
self.otp_code = None
|
||||
self.otp_expiry = None
|
||||
self.save(update_fields=["state", "is_active", "otp_code", "otp_expiry"])
|
||||
|
||||
def block(self):
|
||||
self.state = self.State.BLOCKED
|
||||
self.is_active = False
|
||||
self.save(update_fields=["state", "is_active"])
|
||||
|
||||
|
||||
class TermsAndConditions(models.Model):
|
||||
"""متن قوانین و مقررات — تکردیفی (singleton)، از پنل ادمین ویرایش
|
||||
میشود و صفحهی ورود قبل از لاگین آن را میخواند (بدون نیاز به توکن)."""
|
||||
|
||||
content = models.TextField(
|
||||
blank=True, verbose_name=_("متن قوانین و مقررات"), help_text=_("این متن در دیالوگ «قوانین و مقررات» صفحهی ورود نشان داده میشود.")
|
||||
)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("قوانین و مقررات")
|
||||
verbose_name_plural = _("قوانین و مقررات")
|
||||
|
||||
def __str__(self):
|
||||
return "قوانین و مقررات"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_solo(cls) -> "TermsAndConditions":
|
||||
obj, _created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
Reference in New Issue
Block a user