کاربر ایرانی «۰۹۱۲» تایپ میکند
بیشتر کاربران گوشی با کیبورد فارسی عدد وارد میکنند. رقم «۵» فارسی (U+06F5) با «5» لاتین فرق دارد و RegexValidator با \d در پایتون آن را قبول میکند اما پایگاه داده آن را همانطور ذخیره میکند؛ نتیجه: دو کاربر با یک موبایل، جستوجوهایی که پیدا نمیکنند و پیامکی که نمیرسد. راه درست این است که ورودی را قبل از هر اعتبارسنجی نرمال کنیم؛ یعنی در to_python یک فیلد فرم اختصاصی.
# apps/core/forms_fa.py
import re
from django import forms
from django.core.exceptions import ValidationError
DIGITS = str.maketrans("۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩", "01234567890123456789")
ARABIC_LETTERS = str.maketrans({"ي": "ی", "ك": "ک", "ى": "ی"})
def normalize_digits(value):
return (value or "").translate(DIGITS).strip()
def normalize_text(value):
return re.sub(r"\s+", " ", (value or "").translate(ARABIC_LETTERS)).strip()
class MobileField(forms.CharField):
default_error_messages = {"invalid": "شمارهی موبایل معتبر نیست؛ مثال: 09121234567"}
def to_python(self, value):
value = re.sub(r"[\s\-()]", "", normalize_digits(super().to_python(value)))
if value.startswith("+98"):
value = "0" + value[3:]
elif value.startswith("0098"):
value = "0" + value[4:]
elif value.startswith("9") and len(value) == 10:
value = "0" + value
return value
def validate(self, value):
super().validate(value)
if value and not re.fullmatch(r"09\d{9}", value):
raise ValidationError(self.error_messages["invalid"], code="invalid")
def validate_national_code(value):
code = normalize_digits(value)
if not re.fullmatch(r"\d{10}", code) or len(set(code)) == 1:
raise ValidationError("کد ملی باید ۱۰ رقم معتبر باشد.", code="invalid")
check = int(code[9])
s = sum(int(code[i]) * (10 - i) for i in range(9)) % 11
if (s < 2 and check != s) or (s >= 2 and check != 11 - s):
raise ValidationError("کد ملی نامعتبر است.", code="checksum")
استفاده در ModelForm با clean_ و clean
class CustomerForm(forms.ModelForm):
mobile = MobileField(label="موبایل")
national_code = forms.CharField(label="کد ملی", required=False)
class Meta:
model = Customer
fields = ["full_name", "mobile", "national_code", "city"]
def clean_full_name(self):
return normalize_text(self.cleaned_data["full_name"])
def clean_national_code(self):
code = normalize_digits(self.cleaned_data.get("national_code"))
if code:
validate_national_code(code)
return code
def clean(self):
cleaned = super().clean()
if cleaned.get("city") == "خارج از کشور" and not cleaned.get("national_code"):
self.add_error("national_code", "برای ارسال خارجی کد ملی الزامی است.")
return cleaned
الگوریتم کد ملی: ۹ رقم اول را بهترتیب در ۱۰ تا ۲ ضرب و جمع کنید، باقیمانده بر ۱۱ را حساب کنید؛ اگر کمتر از ۲ بود رقم کنترل باید خودش باشد، وگرنه ۱۱ منهای آن. کدهایی مثل «۱۱۱۱۱۱۱۱۱۱» از نظر ریاضی درستاند اما نامعتبرند؛ برای همین len(set(code)) == 1 را رد میکنیم.
نکتههایی که کمتر کسی میداند
- اگر نرمالسازی را در
clean_mobileبگذارید دیر است:max_length=11قبل از آن اجرا شده و «+989121234567» را با خطای طول رد کرده است. جای نرمالسازیto_pythonاست. \dدر regex پایتون ارقام فارسی و عربی را هم تطبیق میدهد (یونیکد)؛ اگر فقط لاتین میخواهید[0-9]بنویسید یا پرچمre.ASCIIبدهید.clean()حتی وقتی فیلدها خطا دارند اجرا میشود؛ پس همیشه باcleaned.get()بخوانید نهcleaned[...]، وگرنه KeyError میگیرید.add_error(None, "...")خطای کلی فرم (non_field_errors) میسازد و در قالب با{{ form.non_field_errors }}نمایش داده میشود.- نرمالسازی «ي» و «ك» عربی را هم در ذخیره و هم در عبارت جستوجو انجام دهید؛ اگر فقط یکی را درست کنید، جستوجو همچنان رکوردهای قدیمی را پیدا نمیکند.