94 lines
4.3 KiB
Python
94 lines
4.3 KiB
Python
import hashlib
|
|
import re
|
|
import warnings
|
|
from pathlib import Path
|
|
import jdatetime
|
|
import magic
|
|
from PIL import Image, UnidentifiedImageError
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from apps.attachments.models import AttachmentPolicy
|
|
from .models import MAX_AMOUNT
|
|
|
|
DIGIT_MAP = str.maketrans('۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩', '01234567890123456789')
|
|
|
|
|
|
def digits(value):
|
|
return str(value or '').translate(DIGIT_MAP).strip()
|
|
|
|
|
|
def amount(value):
|
|
value = digits(value)
|
|
if not re.fullmatch(r'\d+|\d{1,3}(?:[,٬ ]\d{3})+', value):
|
|
raise ValidationError('مبلغ را به ریال، فقط با عدد صحیح وارد کنید.')
|
|
value = int(re.sub(r'[,٬ ]', '', value))
|
|
if not 0 < value <= MAX_AMOUNT:
|
|
raise ValidationError('مبلغ باید بیشتر از صفر و در محدوده مجاز باشد.')
|
|
return value
|
|
|
|
|
|
def bank_number(value):
|
|
value = re.sub(r'[\s-]', '', digits(value))
|
|
if value and not value.isascii():
|
|
raise ValidationError('شماره را با ارقام معتبر وارد کنید.')
|
|
if value and not value.isdecimal():
|
|
raise ValidationError('شماره حساب یا کارت باید فقط عدد باشد.')
|
|
return value
|
|
|
|
|
|
def iban(value):
|
|
value = re.sub(r'\s', '', digits(value)).upper()
|
|
if not value:
|
|
return ''
|
|
if not re.fullmatch(r'IR\d{24}', value) or int(value[4:] + '1827' + value[2:4]) % 97 != 1:
|
|
raise ValidationError('شماره شبای معتبر ایرانی وارد کنید؛ IR و ۲۴ رقم.')
|
|
return value
|
|
|
|
|
|
def paid_date(day, month, year):
|
|
if not all((day, month, year)):
|
|
raise ValidationError('تاریخ را کامل کنید؛ روز، ماه و سال لازماند.')
|
|
try:
|
|
value = jdatetime.date(int(digits(year)), int(digits(month)), int(digits(day))).togregorian()
|
|
except (ValueError, TypeError, OverflowError):
|
|
raise ValidationError('تاریخ شمسی معتبر انتخاب کنید.')
|
|
today = timezone.localdate()
|
|
if value > today:
|
|
raise ValidationError('تاریخ واریز نمیتواند بعد از امروز باشد.')
|
|
if jdatetime.date.fromgregorian(date=value).year < jdatetime.date.fromgregorian(date=today).year - 10:
|
|
raise ValidationError('تاریخ خارج از بازه ده سال گذشته است.')
|
|
return value
|
|
|
|
|
|
def image(upload):
|
|
policy = AttachmentPolicy.objects.filter(attachment_type='IMAGE', is_enabled=True).first()
|
|
if not policy:
|
|
raise ValidationError('ارسال تصویر فعلاً فعال نیست؛ با کارشناس هماهنگ کنید.')
|
|
if not upload.size or upload.size > policy.max_file_size:
|
|
raise ValidationError('تصویر خالی یا بزرگتر از حجم مجاز است.')
|
|
ext = Path(upload.name).suffix.lower()
|
|
if ext not in (policy.allowed_extensions or ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
|
|
raise ValidationError('فقط تصویر مجاز فیش را انتخاب کنید.')
|
|
head = upload.read(8192)
|
|
upload.seek(0)
|
|
mime = magic.from_buffer(head, mime=True)
|
|
expected = {'.jpg':'image/jpeg', '.jpeg':'image/jpeg', '.png':'image/png', '.gif':'image/gif', '.webp':'image/webp'}
|
|
if expected.get(ext) != mime or (policy.allowed_mime_types and mime not in policy.allowed_mime_types):
|
|
raise ValidationError('محتوای واقعی فایل با نوع تصویر سازگار نیست.')
|
|
try:
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter('error', Image.DecompressionBombWarning)
|
|
with Image.open(upload) as picture:
|
|
if picture.width * picture.height > 25_000_000:
|
|
raise ValidationError('ابعاد تصویر بیش از حد مجاز است.')
|
|
picture.verify()
|
|
except (UnidentifiedImageError, OSError, SyntaxError, Image.DecompressionBombError, Image.DecompressionBombWarning):
|
|
raise ValidationError('تصویر خراب است یا باز نمیشود؛ تصویر دیگری انتخاب کنید.')
|
|
finally:
|
|
upload.seek(0)
|
|
digest = hashlib.sha256()
|
|
for chunk in upload.chunks():
|
|
digest.update(chunk)
|
|
upload.seek(0)
|
|
return {'mime_type': mime, 'sha256': digest.hexdigest(), 'original_name': Path(upload.name).name[:255], 'file_size': upload.size}
|