adding
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
Contact,
|
||||
ContactField,
|
||||
ContactGroup,
|
||||
MessageTemplate,
|
||||
StructuredTemplate,
|
||||
StructuredTemplateField,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(ContactGroup)
|
||||
class ContactGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "user", "is_general", "updated_at")
|
||||
list_filter = ("is_general",)
|
||||
search_fields = ("name", "user__phone")
|
||||
|
||||
|
||||
@admin.register(ContactField)
|
||||
class ContactFieldAdmin(admin.ModelAdmin):
|
||||
list_display = ("label", "group", "user", "sort_order", "updated_at")
|
||||
list_filter = ("group",)
|
||||
search_fields = ("label", "user__phone", "group__name")
|
||||
|
||||
|
||||
@admin.register(Contact)
|
||||
class ContactAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "phone", "user", "updated_at")
|
||||
search_fields = ("name", "phone", "user__phone")
|
||||
|
||||
|
||||
@admin.register(MessageTemplate)
|
||||
class MessageTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "mode", "group", "user", "updated_at")
|
||||
list_filter = ("mode", "group")
|
||||
search_fields = ("name", "user__phone")
|
||||
|
||||
|
||||
class StructuredTemplateFieldInline(admin.TabularInline):
|
||||
model = StructuredTemplateField
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(StructuredTemplate)
|
||||
class StructuredTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "user", "updated_at")
|
||||
search_fields = ("name", "user__phone")
|
||||
inlines = [StructuredTemplateFieldInline]
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BankerConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.banker"
|
||||
verbose_name = "دادههای پیامکبان"
|
||||
@@ -0,0 +1,89 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-22 15:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Contact',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام')),
|
||||
('phone', models.CharField(max_length=20, verbose_name='شماره موبایل')),
|
||||
('device_contact_id', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('field_values', models.JSONField(blank=True, default=dict, verbose_name='مقادیر فیلدهای دلخواه')),
|
||||
('channel_ids', models.JSONField(blank=True, default=dict, verbose_name='شناسه\u200cی کانال\u200cها')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'مخاطب',
|
||||
'verbose_name_plural': 'مخاطبین',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContactGroup',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام گروه')),
|
||||
('color', models.BigIntegerField(verbose_name='رنگ (ARGB)')),
|
||||
('is_general', models.BooleanField(default=False, help_text='گروه پیش\u200cفرض بدون فیلد سفارشی؛ هر کاربر دقیقاً یکی از این\u200cها دارد.', verbose_name='گروه عمومی')),
|
||||
('member_ids', models.JSONField(blank=True, default=list, verbose_name='اعضا')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_groups', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'گروه',
|
||||
'verbose_name_plural': 'گروه\u200cها',
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContactField',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('label', models.CharField(max_length=100, verbose_name='عنوان فیلد')),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_fields', to=settings.AUTH_USER_MODEL)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='banker.contactgroup')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'فیلد دلخواه مخاطب',
|
||||
'verbose_name_plural': 'فیلدهای دلخواه مخاطبین',
|
||||
'ordering': ['group', 'sort_order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageTemplate',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام قالب')),
|
||||
('body', models.TextField(verbose_name='متن قالب')),
|
||||
('mode', models.CharField(choices=[('merge', 'قالب\u200cبندی'), ('quick', 'تمپلیت آماده')], max_length=10)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='banker.contactgroup')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='templates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'قالب پیام',
|
||||
'verbose_name_plural': 'قالب\u200cهای پیام',
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='contactgroup',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_general', True)), fields=('user',), name='one_general_group_per_user'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-23 21:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('banker', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StructuredTemplate',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام قالب')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structured_templates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'قالب کلید-مقدار',
|
||||
'verbose_name_plural': 'قالب\u200cهای کلید-مقدار',
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StructuredTemplateField',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('label', models.CharField(max_length=100, verbose_name='عنوان')),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='banker.structuredtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'فیلد قالب کلید-مقدار',
|
||||
'verbose_name_plural': 'فیلدهای قالب کلید-مقدار',
|
||||
'ordering': ['template', 'sort_order'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,148 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# این مدلها معادل دقیق دیتابیس محلی (SQLite) اپ فلاتر هستند تا اطلاعات
|
||||
# مخاطبین/گروهها/قالبهای کاربر روی سرور هم پشتیبانگیری شود و با تعویض
|
||||
# گوشی از دست نرود. آیدیها همان UUID تولیدشده در خود اپ هستند (نه
|
||||
# auto-increment) تا هماهنگی هنگام بکاپ/ریستور ساده باشد.
|
||||
#
|
||||
# فیلدهای دلخواه (ContactField) و قالبها (MessageTemplate) به یک گروه
|
||||
# (ContactGroup) تعلق دارند — نه سراسری. مثال: گروه «تور» فیلد «شماره
|
||||
# گذرنامه» دارد، گروه دیگری «کد ملی». هر کاربر دقیقاً یک گروه با
|
||||
# is_general=True دارد که فیلد سفارشی ندارد و قالبهای آن فقط از فیلدهای
|
||||
# پایه (نام/شماره) استفاده میکنند — برای کاربرانی که نیاز به پیچیدگی ندارند.
|
||||
|
||||
|
||||
class ContactGroup(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contact_groups")
|
||||
name = models.CharField(max_length=150, verbose_name=_("نام گروه"))
|
||||
color = models.BigIntegerField(verbose_name=_("رنگ (ARGB)"))
|
||||
is_general = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("گروه عمومی"),
|
||||
help_text=_("گروه پیشفرض بدون فیلد سفارشی؛ هر کاربر دقیقاً یکی از اینها دارد."),
|
||||
)
|
||||
# لیستی از contact.id های عضو این گروه
|
||||
member_ids = models.JSONField(default=list, blank=True, verbose_name=_("اعضا"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("گروه")
|
||||
verbose_name_plural = _("گروهها")
|
||||
ordering = ["-updated_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["user"],
|
||||
condition=models.Q(is_general=True),
|
||||
name="one_general_group_per_user",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ContactField(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contact_fields"
|
||||
)
|
||||
group = models.ForeignKey(ContactGroup, on_delete=models.CASCADE, related_name="fields")
|
||||
label = models.CharField(max_length=100, verbose_name=_("عنوان فیلد"))
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("فیلد دلخواه مخاطب")
|
||||
verbose_name_plural = _("فیلدهای دلخواه مخاطبین")
|
||||
ordering = ["group", "sort_order"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label} ({self.group.name})"
|
||||
|
||||
|
||||
class Contact(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contacts")
|
||||
name = models.CharField(max_length=150, verbose_name=_("نام"))
|
||||
phone = models.CharField(max_length=20, verbose_name=_("شماره موبایل"))
|
||||
device_contact_id = models.CharField(max_length=100, blank=True, null=True)
|
||||
# {field_id: value} — معادل جدول contact_field_values در اپ. فیلدهای
|
||||
# مربوط به گروههایی هستند که این مخاطب عضوشان است (member_ids گروه)
|
||||
field_values = models.JSONField(default=dict, blank=True, verbose_name=_("مقادیر فیلدهای دلخواه"))
|
||||
# {channel: external_id} — مثلا {"telegram": "123456789"} — برای ارسال
|
||||
# خودکار از طریق Bot API فقط وقتی گیرنده قبلا چت با بات را شروع کرده
|
||||
channel_ids = models.JSONField(default=dict, blank=True, verbose_name=_("شناسهی کانالها"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("مخاطب")
|
||||
verbose_name_plural = _("مخاطبین")
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.phone})"
|
||||
|
||||
|
||||
class MessageTemplate(models.Model):
|
||||
class Mode(models.TextChoices):
|
||||
MERGE = "merge", _("قالببندی")
|
||||
QUICK = "quick", _("تمپلیت آماده")
|
||||
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="templates")
|
||||
# null = قالب عمومی (فقط فیلدهای پایهی نام/شماره)
|
||||
group = models.ForeignKey(
|
||||
ContactGroup, on_delete=models.CASCADE, related_name="templates", null=True, blank=True
|
||||
)
|
||||
name = models.CharField(max_length=150, verbose_name=_("نام قالب"))
|
||||
body = models.TextField(verbose_name=_("متن قالب"))
|
||||
mode = models.CharField(max_length=10, choices=Mode.choices)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("قالب پیام")
|
||||
verbose_name_plural = _("قالبهای پیام")
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class StructuredTemplate(models.Model):
|
||||
"""قالب کلید-مقدار — برای گزارشهای تکرارشونده مثل «امروز چقدر از هر
|
||||
کالا موجود دارم». برخلاف MessageTemplate (متن آزاد با {فیلد})، اینجا
|
||||
فقط یک لیست از عنوانهاست (مثلا «کلم»، «کاهو») که اپ یکییکی مقدارشان
|
||||
را از کاربر میپرسد و در نهایت خطبهخط بهشکل «عنوان: مقدار» میفرستد."""
|
||||
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="structured_templates"
|
||||
)
|
||||
name = models.CharField(max_length=150, verbose_name=_("نام قالب"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("قالب کلید-مقدار")
|
||||
verbose_name_plural = _("قالبهای کلید-مقدار")
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class StructuredTemplateField(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
template = models.ForeignKey(StructuredTemplate, on_delete=models.CASCADE, related_name="fields")
|
||||
label = models.CharField(max_length=100, verbose_name=_("عنوان"))
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("فیلد قالب کلید-مقدار")
|
||||
verbose_name_plural = _("فیلدهای قالب کلید-مقدار")
|
||||
ordering = ["template", "sort_order"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label} ({self.template.name})"
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.urls import path
|
||||
|
||||
from .api.views import (
|
||||
BackupView,
|
||||
ContactDetailView,
|
||||
ContactFieldDetailView,
|
||||
ContactGroupDetailView,
|
||||
ContactGroupListCreateView,
|
||||
ContactListCreateView,
|
||||
GroupFieldListCreateView,
|
||||
MessageTemplateDetailView,
|
||||
MessageTemplateListCreateView,
|
||||
StructuredTemplateDetailView,
|
||||
StructuredTemplateFieldDetailView,
|
||||
StructuredTemplateFieldListCreateView,
|
||||
StructuredTemplateListCreateView,
|
||||
)
|
||||
|
||||
app_name = "banker"
|
||||
urlpatterns = [
|
||||
path("backup/", BackupView.as_view(), name="backup"),
|
||||
path("groups/", ContactGroupListCreateView.as_view(), name="group-list"),
|
||||
path("groups/<str:pk>/", ContactGroupDetailView.as_view(), name="group-detail"),
|
||||
path("groups/<str:group_id>/fields/", GroupFieldListCreateView.as_view(), name="group-field-list"),
|
||||
path("fields/<str:pk>/", ContactFieldDetailView.as_view(), name="field-detail"),
|
||||
path("contacts/", ContactListCreateView.as_view(), name="contact-list"),
|
||||
path("contacts/<str:pk>/", ContactDetailView.as_view(), name="contact-detail"),
|
||||
path("templates/", MessageTemplateListCreateView.as_view(), name="template-list"),
|
||||
path("templates/<str:pk>/", MessageTemplateDetailView.as_view(), name="template-detail"),
|
||||
path(
|
||||
"structured-templates/", StructuredTemplateListCreateView.as_view(), name="structured-template-list"
|
||||
),
|
||||
path(
|
||||
"structured-templates/<str:pk>/",
|
||||
StructuredTemplateDetailView.as_view(),
|
||||
name="structured-template-detail",
|
||||
),
|
||||
path(
|
||||
"structured-templates/<str:template_id>/fields/",
|
||||
StructuredTemplateFieldListCreateView.as_view(),
|
||||
name="structured-template-field-list",
|
||||
),
|
||||
path(
|
||||
"structured-template-fields/<str:pk>/",
|
||||
StructuredTemplateFieldDetailView.as_view(),
|
||||
name="structured-template-field-detail",
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user