This commit is contained in:
2026-08-24 15:54:08 +03:30
commit 236002a87c
133 changed files with 5946 additions and 0 deletions
View File
+79
View File
@@ -0,0 +1,79 @@
from rest_framework import serializers
from apps.banker.models import (
Contact,
ContactField,
ContactGroup,
MessageTemplate,
StructuredTemplate,
StructuredTemplateField,
)
# سریالایزرهای CRUD واقعی (create/update/delete تکی) — برخلاف سریالایزرهای
# backup در serializers.py، این‌ها اعتبارسنجی یکتابودن id فعال دارند چون هر
# رکورد فقط یک‌بار create می‌شود، نه هر بار جایگزین کامل.
class ContactGroupCrudSerializer(serializers.ModelSerializer):
is_general = serializers.BooleanField(read_only=True)
class Meta:
model = ContactGroup
fields = ["id", "name", "color", "is_general", "member_ids"]
class ContactFieldCrudSerializer(serializers.ModelSerializer):
class Meta:
model = ContactField
fields = ["id", "group", "label", "sort_order"]
def validate_group(self, group: ContactGroup) -> ContactGroup:
# قبلا گروه عمومی فیلد سفارشی نداشت (برای کاربرانی که نیاز به
# پیچیدگی نمی‌خواستند)؛ چون الان کل مفهوم «گروه» از UX برداشته شده
# و همه‌چیز روی همان یک گروه عمومی کار می‌کند، این محدودیت دیگر
# معنی ندارد و مانع تعریف هر فیلدی می‌شد.
request = self.context["request"]
if group.user_id != request.user.id:
raise serializers.ValidationError("این گروه متعلق به شما نیست.")
return group
class ContactCrudSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ["id", "name", "phone", "device_contact_id", "field_values", "channel_ids"]
class MessageTemplateCrudSerializer(serializers.ModelSerializer):
class Meta:
model = MessageTemplate
fields = ["id", "group", "name", "body", "mode"]
def validate_group(self, group: ContactGroup | None) -> ContactGroup | None:
if group is None:
return group
request = self.context["request"]
if group.user_id != request.user.id:
raise serializers.ValidationError("این گروه متعلق به شما نیست.")
return group
class StructuredTemplateFieldCrudSerializer(serializers.ModelSerializer):
class Meta:
model = StructuredTemplateField
fields = ["id", "template", "label", "sort_order"]
def validate_template(self, template: StructuredTemplate) -> StructuredTemplate:
request = self.context["request"]
if template.user_id != request.user.id:
raise serializers.ValidationError("این قالب متعلق به شما نیست.")
return template
class StructuredTemplateCrudSerializer(serializers.ModelSerializer):
fields = StructuredTemplateFieldCrudSerializer(many=True, read_only=True)
class Meta:
model = StructuredTemplate
fields = ["id", "name", "fields", "updated_at"]
read_only_fields = ["updated_at"]
+47
View File
@@ -0,0 +1,47 @@
from rest_framework import serializers
from apps.banker.models import Contact, ContactField, ContactGroup, MessageTemplate
# نکته: id هر رکورد را خودِ اپ (UUID) تولید می‌کند و روی سرور هم همان مقدار
# استفاده می‌شود. چون هر بار /backup/ کل رکوردهای کاربر جایگزین می‌شوند (نه
# create تکی)، اعتبارسنج یکتابودنِ خودکار DRF روی id باید خاموش شود؛ وگرنه
# از دومین بکاپ به بعد چون همان idها از قبل در دیتابیس هست، validation رد
# می‌شود.
_ID_NO_UNIQUE_CHECK = {"id": {"validators": []}}
class ContactGroupSerializer(serializers.ModelSerializer):
class Meta:
model = ContactGroup
fields = ["id", "name", "color", "is_general", "member_ids"]
extra_kwargs = _ID_NO_UNIQUE_CHECK
class ContactFieldSerializer(serializers.ModelSerializer):
class Meta:
model = ContactField
fields = ["id", "group_id", "label", "sort_order"]
extra_kwargs = _ID_NO_UNIQUE_CHECK
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ["id", "name", "phone", "device_contact_id", "field_values", "channel_ids"]
extra_kwargs = _ID_NO_UNIQUE_CHECK
class MessageTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = MessageTemplate
fields = ["id", "group_id", "name", "body", "mode"]
extra_kwargs = _ID_NO_UNIQUE_CHECK
class BackupSerializer(serializers.Serializer):
"""عکس کامل وضعیت اپ روی یک کاربر — برای پشتیبان‌گیری/بازیابی"""
contact_fields = ContactFieldSerializer(many=True, required=False, default=list)
contacts = ContactSerializer(many=True, required=False, default=list)
groups = ContactGroupSerializer(many=True, required=False, default=list)
templates = MessageTemplateSerializer(many=True, required=False, default=list)
+74
View File
@@ -0,0 +1,74 @@
from django.db import transaction
from apps.account.models import User
from apps.banker.models import Contact, ContactField, ContactGroup, MessageTemplate
class GroupService:
@staticmethod
def get_or_create_general(user: User) -> ContactGroup:
"""گروه عمومی کاربر را برمی‌گرداند؛ اگر هنوز ندارد، می‌سازد.
هر کاربر دقیقاً یک گروه با is_general=True دارد — بدون فیلد سفارشی،
برای کاربرانی که نیاز به پیچیدگی ندارند.
"""
import uuid
general = ContactGroup.objects.filter(user=user, is_general=True).first()
if general:
return general
return ContactGroup.objects.create(
id=str(uuid.uuid4()),
user=user,
name="عمومی",
color=0xFF667EEA,
is_general=True,
)
class BackupService:
@staticmethod
def build_snapshot(user: User) -> dict:
return {
"contact_fields": list(
ContactField.objects.filter(user=user).values("id", "group_id", "label", "sort_order")
),
"contacts": list(
Contact.objects.filter(user=user).values(
"id", "name", "phone", "device_contact_id", "field_values", "channel_ids"
)
),
"groups": list(
ContactGroup.objects.filter(user=user).values(
"id", "name", "color", "is_general", "member_ids"
)
),
"templates": list(
MessageTemplate.objects.filter(user=user).values("id", "group_id", "name", "body", "mode")
),
}
@staticmethod
@transaction.atomic
def replace_snapshot(user: User, data: dict) -> dict:
"""جایگزینی کامل دیتای کاربر روی سرور با آنچه از اپ فرستاده شده (منبع حقیقت، خودِ اپ است)"""
ContactField.objects.filter(user=user).delete()
Contact.objects.filter(user=user).delete()
MessageTemplate.objects.filter(user=user).delete()
ContactGroup.objects.filter(user=user).delete()
groups = list(data.get("groups", []))
if groups:
ContactGroup.objects.bulk_create(ContactGroup(user=user, **item) for item in groups)
if not any(g.get("is_general") for g in groups):
GroupService.get_or_create_general(user)
ContactField.objects.bulk_create(
ContactField(user=user, **item) for item in data.get("contact_fields", [])
)
Contact.objects.bulk_create(Contact(user=user, **item) for item in data.get("contacts", []))
MessageTemplate.objects.bulk_create(
MessageTemplate(user=user, **item) for item in data.get("templates", [])
)
return BackupService.build_snapshot(user)
+202
View File
@@ -0,0 +1,202 @@
from rest_framework import generics, status
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.banker.models import (
Contact,
ContactField,
ContactGroup,
MessageTemplate,
StructuredTemplate,
StructuredTemplateField,
)
from apps.billing.permissions import HasActiveFeature
from .crud_serializers import (
ContactCrudSerializer,
ContactFieldCrudSerializer,
ContactGroupCrudSerializer,
MessageTemplateCrudSerializer,
StructuredTemplateCrudSerializer,
StructuredTemplateFieldCrudSerializer,
)
from .serializers import BackupSerializer
from .services import BackupService, GroupService
class BackupView(APIView):
"""
پشتیبان‌گیری/بازیابی کامل مخاطبین، فیلدهای دلخواه، گروه‌ها و قالب‌های کاربر.
GET -> آخرین نسخه‌ی ذخیره‌شده روی سرور را برمی‌گرداند (برای بازیابی روی گوشی جدید)
POST -> دیتای فعلی اپ را می‌فرستد و کامل جایگزین نسخه‌ی قبلی روی سرور می‌شود
(منبع حقیقت خود اپ روی گوشی است، نه سرور)
"""
# عمداً HasActiveFeature ندارد — بک‌آپ صادرات/وارد کردن خامِ داده‌ی خود
# کاربر است (مثلا موقع عوض‌کردن گوشی)، نه استفاده از ریزاپ؛ گروگان‌گرفتن
# دیتای کاربر پشت پولی‌بودن اشتراک درست نیست.
permission_classes = [IsAuthenticated]
throttle_scope = "backup"
def get(self, request):
snapshot = BackupService.build_snapshot(request.user)
return Response({"success": True, "data": snapshot})
def post(self, request):
serializer = BackupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
snapshot = BackupService.replace_snapshot(request.user, serializer.validated_data)
return Response(
{"success": True, "message": "پشتیبان‌گیری با موفقیت انجام شد.", "data": snapshot},
status=status.HTTP_200_OK,
)
# -------------------- CRUD واقعی (تکی، نه replace کامل) --------------------
# همه‌ی ویوهای زیر روی request.user فیلتر می‌شوند و مالکیت رکورد را
# مستقل از ورودی کاربر تضمین می‌کنند.
class ContactGroupListCreateView(generics.ListCreateAPIView):
"""GET: لیست گروه‌های کاربر (گروه عمومی هم اگر نبود ساخته می‌شود). POST: گروه جدید."""
serializer_class = ContactGroupCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
GroupService.get_or_create_general(self.request.user)
return ContactGroup.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class ContactGroupDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ContactGroupCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return ContactGroup.objects.filter(user=self.request.user)
def perform_destroy(self, instance):
if instance.is_general:
raise ValidationError({"detail": "گروه عمومی قابل حذف نیست."})
instance.delete()
class GroupFieldListCreateView(generics.ListCreateAPIView):
"""فیلدهای سفارشی یک گروه مشخص — /groups/<group_id>/fields/"""
serializer_class = ContactFieldCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return ContactField.objects.filter(
user=self.request.user, group_id=self.kwargs["group_id"]
)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class ContactFieldDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ContactFieldCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return ContactField.objects.filter(user=self.request.user)
class ContactListCreateView(generics.ListCreateAPIView):
serializer_class = ContactCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return Contact.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class ContactDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ContactCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return Contact.objects.filter(user=self.request.user)
class MessageTemplateListCreateView(generics.ListCreateAPIView):
serializer_class = MessageTemplateCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return MessageTemplate.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class MessageTemplateDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = MessageTemplateCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return MessageTemplate.objects.filter(user=self.request.user)
class StructuredTemplateListCreateView(generics.ListCreateAPIView):
serializer_class = StructuredTemplateCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return StructuredTemplate.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class StructuredTemplateDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StructuredTemplateCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return StructuredTemplate.objects.filter(user=self.request.user)
class StructuredTemplateFieldListCreateView(generics.ListCreateAPIView):
"""فیلدهای یک قالب کلید-مقدار مشخص — /structured-templates/<template_id>/fields/"""
serializer_class = StructuredTemplateFieldCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return StructuredTemplateField.objects.filter(
template__user=self.request.user, template_id=self.kwargs["template_id"]
)
def perform_create(self, serializer):
serializer.save()
class StructuredTemplateFieldDetailView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StructuredTemplateFieldCrudSerializer
permission_classes = [IsAuthenticated, HasActiveFeature]
feature_key = "sms_bank"
def get_queryset(self):
return StructuredTemplateField.objects.filter(template__user=self.request.user)