From b73f33fd8c74de47a2527c7b9f0f079c757d015b Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Wed, 17 Jun 2026 14:39:35 +0330 Subject: [PATCH] contact us api added --- apps/api/admin.py | 17 ++- apps/api/migrations/0002_supportmessage.py | 78 +++++++++++ apps/api/models.py | 42 ++++++ apps/api/permissions.py | 142 ++++++++++----------- apps/api/serializers.py | 35 ++++- apps/api/urls.py | 3 +- apps/api/views/api_views.py | 60 ++++++++- 7 files changed, 301 insertions(+), 76 deletions(-) create mode 100644 apps/api/migrations/0002_supportmessage.py diff --git a/apps/api/admin.py b/apps/api/admin.py index da59e97..6b17560 100644 --- a/apps/api/admin.py +++ b/apps/api/admin.py @@ -9,7 +9,7 @@ from dj_language.models import Language from filer.models.thumbnailoptionmodels import ThumbnailOption # from filer.admin.thumbnailoptionmodels import ThumbnailOptionAdmin as OriginalThumbnailOptionAdmin -from .models import Comment, AppVersion +from .models import Comment, AppVersion, SupportMessage admin.site.unregister(ThumbnailOption) @@ -116,8 +116,23 @@ class AppVersionAdmin(ModelAdmin): ordering = ['-created_at'] +class SupportMessageAdmin(ModelAdmin): + list_display = [ + 'sender_name', + 'sender_email', + 'subject', + 'sender_phone', + 'created_at', + ] + search_fields = ['sender_name', 'sender_email', 'subject', 'sender_phone', 'message'] + list_filter = ['created_at'] + ordering = ['-created_at'] + + # Register models with both admin sites # project_admin_site.register(Comment, CommentAdmin) project_admin_site.register(AppVersion, AppVersionAdmin) +project_admin_site.register(SupportMessage, SupportMessageAdmin) # dovoodi_admin_site.register(Comment, CommentAdmin) dovoodi_admin_site.register(AppVersion, AppVersionAdmin) +dovoodi_admin_site.register(SupportMessage, SupportMessageAdmin) diff --git a/apps/api/migrations/0002_supportmessage.py b/apps/api/migrations/0002_supportmessage.py new file mode 100644 index 0000000..8a7adfc --- /dev/null +++ b/apps/api/migrations/0002_supportmessage.py @@ -0,0 +1,78 @@ +from django.db import migrations, models +import phonenumber_field.modelfields +import utils.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="SupportMessage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sender_name", + models.CharField( + help_text="Full name of the sender", + max_length=255, + verbose_name="Sender Name", + ), + ), + ( + "sender_email", + models.EmailField( + help_text="Email address of the sender", + max_length=254, + verbose_name="Sender Email", + ), + ), + ( + "subject", + models.CharField( + help_text="Message subject", + max_length=255, + verbose_name="Subject", + ), + ), + ( + "sender_phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, + help_text="e.g., +49 151 12345678", + max_length=128, + null=True, + region=None, + validators=[utils.validators.validate_possible_number], + verbose_name="Sender Phone", + ), + ), + ( + "message", + models.TextField( + help_text="Support message body", + verbose_name="Message", + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ], + options={ + "verbose_name": "Support Message", + "verbose_name_plural": "Support Messages", + "ordering": ["-created_at"], + }, + ), + ] diff --git a/apps/api/models.py b/apps/api/models.py index af8dfea..6e03149 100644 --- a/apps/api/models.py +++ b/apps/api/models.py @@ -2,6 +2,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from dj_language.field import LanguageField from django.core.validators import RegexValidator +from phonenumber_field.modelfields import PhoneNumberField +from utils.validators import validate_possible_number class Comment(models.Model): @@ -136,3 +138,43 @@ class AppVersion(models.Model): Get the latest active version """ return cls.objects.filter(is_active=True).order_by('-created_at').first() + + +class SupportMessage(models.Model): + sender_name = models.CharField( + max_length=255, + verbose_name=_('Sender Name'), + help_text=_('Full name of the sender'), + ) + sender_email = models.EmailField( + verbose_name=_('Sender Email'), + help_text=_('Email address of the sender'), + ) + subject = models.CharField( + max_length=255, + verbose_name=_('Subject'), + help_text=_('Message subject'), + ) + sender_phone = PhoneNumberField( + validators=[validate_possible_number], + blank=True, + null=True, + verbose_name=_('Sender Phone'), + help_text=_("e.g., +49 151 12345678"), + ) + message = models.TextField( + verbose_name=_('Message'), + help_text=_('Support message body'), + ) + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name=_('Created At'), + ) + + class Meta: + verbose_name = _('Support Message') + verbose_name_plural = _('Support Messages') + ordering = ['-created_at'] + + def __str__(self): + return f'{self.sender_name} - {self.subject}' diff --git a/apps/api/permissions.py b/apps/api/permissions.py index c4d8baa..722543e 100644 --- a/apps/api/permissions.py +++ b/apps/api/permissions.py @@ -1,71 +1,71 @@ -from rest_framework import permissions -from rest_framework.authtoken.models import Token -from django.contrib.auth.models import AnonymousUser - - -class SwaggerTokenPermission(permissions.BasePermission): - """ - Custom permission for Swagger that allows access to authenticated users via token - or admin users via session authentication - """ - - def has_permission(self, request, view): - # Check if user is admin (for session-based access) - if request.user and request.user.is_authenticated and request.user.is_staff: - return True - - # Check for token in session (from our custom auth system) - swagger_token = request.session.get('swagger_token') - if swagger_token: - try: - token_obj = Token.objects.get(key=swagger_token) - if token_obj.user.is_active: - return True - except Token.DoesNotExist: - pass - - # Check for Authorization header - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if auth_header.startswith('Token '): - token = auth_header.split(' ')[1] - try: - token_obj = Token.objects.get(key=token) - if token_obj.user.is_active: - return True - except Token.DoesNotExist: - pass - - return False - - -class IsAdminOrSwaggerToken(permissions.BasePermission): - """ - Permission that allows access to admin users or users with valid swagger token - """ - - def has_permission(self, request, view): - # Allow admin users - if request.user and request.user.is_authenticated and request.user.is_staff: - return True - - # Check swagger token in session - swagger_token = request.session.get('swagger_token') - if swagger_token: - try: - token_obj = Token.objects.get(key=swagger_token) - return token_obj.user.is_active - except Token.DoesNotExist: - pass - - return False - - -class IsProfessorUser(permissions.BasePermission): - """ - Permission that allows access only to professors or admin users. - """ - - def has_permission(self, request, view): - if not (request.user and request.user.is_authenticated): - return False - return request.user.user_type == 'professor' or request.user.is_staff \ No newline at end of file +from rest_framework import permissions +from rest_framework.authtoken.models import Token +from django.contrib.auth.models import AnonymousUser + + +class SwaggerTokenPermission(permissions.BasePermission): + """ + Custom permission for Swagger that allows access to authenticated users via token + or admin users via session authentication + """ + + def has_permission(self, request, view): + # Check if user is admin (for session-based access) + if request.user and request.user.is_authenticated and request.user.is_staff: + return True + + # Check for token in session (from our custom auth system) + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + if token_obj.user.is_active: + return True + except Token.DoesNotExist: + pass + + # Check for Authorization header + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if auth_header.startswith('Token '): + token = auth_header.split(' ')[1] + try: + token_obj = Token.objects.get(key=token) + if token_obj.user.is_active: + return True + except Token.DoesNotExist: + pass + + return False + + +class IsAdminOrSwaggerToken(permissions.BasePermission): + """ + Permission that allows access to admin users or users with valid swagger token + """ + + def has_permission(self, request, view): + # Allow admin users + if request.user and request.user.is_authenticated and request.user.is_staff: + return True + + # Check swagger token in session + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + return token_obj.user.is_active + except Token.DoesNotExist: + pass + + return False + + +class IsProfessorUser(permissions.BasePermission): + """ + Permission that allows access only to professors or admin users. + """ + + def has_permission(self, request, view): + if not (request.user and request.user.is_authenticated): + return False + return request.user.user_type == 'professor' or request.user.is_staff diff --git a/apps/api/serializers.py b/apps/api/serializers.py index cdb4707..bfde6b5 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from utils import FileFieldSerializer -from .models import Comment, AppVersion +from .models import Comment, AppVersion, SupportMessage class CommentSerializer(serializers.ModelSerializer): @@ -52,3 +52,36 @@ class AppVersionSerializer(serializers.ModelSerializer): 'updated_at', ] read_only_fields = ['id', 'created_at', 'updated_at'] + + +class SupportMessageCreateSerializer(serializers.ModelSerializer): + class Meta: + model = SupportMessage + fields = [ + 'id', + 'sender_name', + 'sender_email', + 'subject', + 'sender_phone', + 'message', + 'created_at', + ] + read_only_fields = ['id', 'created_at'] + + def validate_sender_name(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Sender name is required.") + return value + + def validate_subject(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Subject is required.") + return value + + def validate_message(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Message is required.") + return value diff --git a/apps/api/urls.py b/apps/api/urls.py index 530cf26..b76ec15 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -1,7 +1,7 @@ from django.urls import path,include from .views import HomeView, CountryView, CommentListAPIView, CityListView -from .views.api_views import AppVersionListAPIView +from .views.api_views import AppVersionListAPIView, SupportMessageCreateAPIView @@ -11,4 +11,5 @@ urlpatterns = [ path('cities/', CityListView.as_view(), name='city-list'), path('comments/', CommentListAPIView.as_view(), name='comment-list'), path('app-versions/', AppVersionListAPIView.as_view(), name='appversion-list'), + path('contact-us/', SupportMessageCreateAPIView.as_view(), name='contact-us-create-message'), ] diff --git a/apps/api/views/api_views.py b/apps/api/views/api_views.py index d136b8c..3de6873 100644 --- a/apps/api/views/api_views.py +++ b/apps/api/views/api_views.py @@ -1,5 +1,6 @@ import random -from rest_framework.generics import GenericAPIView, ListAPIView +from rest_framework import status +from rest_framework.generics import CreateAPIView, GenericAPIView, ListAPIView from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import serializers @@ -10,7 +11,11 @@ from drf_yasg import openapi from rest_framework.authtoken.models import Token from apps.account.models import User from apps.api.models import Comment, AppVersion -from apps.api.serializers import CommentSerializer, AppVersionSerializer +from apps.api.serializers import ( + AppVersionSerializer, + CommentSerializer, + SupportMessageCreateSerializer, +) from utils.pagination import StandardResultsSetPagination class HomeSerializer(serializers.Serializer): @@ -239,3 +244,54 @@ class AppVersionListAPIView(ListAPIView): ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) + + +class SupportMessageCreateAPIView(CreateAPIView): + serializer_class = SupportMessageCreateSerializer + permission_classes = [AllowAny] + + @swagger_auto_schema( + tags=["Support"], + operation_summary="Create support message", + operation_description=( + "Create a new support message from the website or mobile contact form.\n\n" + "This endpoint is public and does not require authentication.\n\n" + "Required fields:\n" + "- `sender_name`: full name of the sender\n" + "- `sender_email`: valid email address\n" + "- `subject`: message subject\n" + "- `message`: message body\n\n" + "Optional fields:\n" + "- `sender_phone`: sender phone number in international format\n\n" + "Phone number validation follows the same rules used in the account module." + ), + request_body=SupportMessageCreateSerializer, + responses={ + status.HTTP_201_CREATED: openapi.Response( + description="Support message created successfully.", + schema=SupportMessageCreateSerializer, + examples={ + "application/json": { + "id": 12, + "sender_name": "Ali Ahmadi", + "sender_email": "ali.ahmadi@example.com", + "subject": "Problem accessing my purchased course", + "sender_phone": "+989121234567", + "message": "Hello, I purchased a course today but the lessons are not available in my account. Please check my access.", + "created_at": "2026-06-17T09:30:12.421000Z", + } + }, + ), + status.HTTP_400_BAD_REQUEST: openapi.Response( + description="Validation error.", + examples={ + "application/json": { + "sender_email": ["Enter a valid email address."], + "sender_phone": ["The phone number entered is not valid."], + } + }, + ), + }, + ) + def post(self, request, *args, **kwargs): + return super().post(request, *args, **kwargs)