From cd5ad1d7532e0cf860db4c39553411589190594f Mon Sep 17 00:00:00 2001 From: saani Date: Fri, 28 Nov 2025 15:52:06 +0000 Subject: [PATCH] feat: add contact form functionality with admin management Add a complete contact form system with the following changes: - Create ContactMessage model to store form submissions with tracking fields (is_read, is_responded) - Implement ContactMessage admin interface with custom actions, filters, and bulk operations - Add contact endpoint documentation to API root view - Update email configuration to use admin@attunehearttherapy.com as sender address This enables users to submit contact inquiries and allows administrators to track and manage these messages efficiently through the Django admin panel. --- booking_system/settings.py | 4 +- booking_system/views.py | 18 +++ ..._alter_appointmentrequest_jitsi_room_id.py | 18 +++ .../emails/admin_contact_notification.html | 105 ++++++++++++++++++ .../emails/user_contact_confirmation.html | 80 +++++++++++++ users/admin.py | 42 ++++++- users/migrations/0002_contactmessage.py | 31 ++++++ users/models.py | 20 +++- users/serializers.py | 23 +++- users/urls.py | 2 + users/utils.py | 50 ++++++++- users/views.py | 41 ++++++- 12 files changed, 422 insertions(+), 12 deletions(-) create mode 100644 meetings/migrations/0002_alter_appointmentrequest_jitsi_room_id.py create mode 100644 templates/emails/admin_contact_notification.html create mode 100644 templates/emails/user_contact_confirmation.html create mode 100644 users/migrations/0002_contactmessage.py diff --git a/booking_system/settings.py b/booking_system/settings.py index c4a466d..d0c5741 100644 --- a/booking_system/settings.py +++ b/booking_system/settings.py @@ -139,9 +139,9 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = os.getenv('SMTP_HOST', 'smtp.hostinger.com') EMAIL_PORT = int(os.getenv('SMTP_PORT', 465)) EMAIL_USE_SSL = True -EMAIL_HOST_USER = os.getenv('SMTP_USERNAME', 'hello@attunehearttherapy.com') +EMAIL_HOST_USER = os.getenv('SMTP_USERNAME', 'admin@attunehearttherapy.com') EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASSWORD') -DEFAULT_FROM_EMAIL = os.getenv('SMTP_FROM', 'hello@attunehearttherapy.com') +DEFAULT_FROM_EMAIL = os.getenv('SMTP_FROM', 'admin@attunehearttherapy.com') REST_FRAMEWORK = { diff --git a/booking_system/views.py b/booking_system/views.py index 0da84f3..8ec9df0 100644 --- a/booking_system/views.py +++ b/booking_system/views.py @@ -8,6 +8,24 @@ def api_root(request, format=None): base_url = request.build_absolute_uri('/api/') endpoints = { + 'contact': { + 'description': 'Contact form submission endpoint', + 'base_path': '/api/auth/', + 'endpoints': { + 'contact': { + 'description': 'Submit a contact form', + 'url': request.build_absolute_uri('/api/auth/contact/'), + 'methods': ['POST'], + 'required_fields': ['name', 'email', 'phone', 'message'], + 'example_request': { + 'name': 'John Doe', + 'email': 'n8E5I@example.com', + 'phone': '+1234567890', + 'message': 'Hello, how can I help you?' + } + } + }, + }, 'authentication': { 'description': 'User authentication and management endpoints', 'base_path': '/api/auth/', diff --git a/meetings/migrations/0002_alter_appointmentrequest_jitsi_room_id.py b/meetings/migrations/0002_alter_appointmentrequest_jitsi_room_id.py new file mode 100644 index 0000000..3b5060a --- /dev/null +++ b/meetings/migrations/0002_alter_appointmentrequest_jitsi_room_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-11-28 15:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('meetings', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='appointmentrequest', + name='jitsi_room_id', + field=models.CharField(blank=True, default=None, help_text='Jitsi room ID', max_length=100, null=True, unique=True), + ), + ] diff --git a/templates/emails/admin_contact_notification.html b/templates/emails/admin_contact_notification.html new file mode 100644 index 0000000..f1c8c8e --- /dev/null +++ b/templates/emails/admin_contact_notification.html @@ -0,0 +1,105 @@ + + + + + + New Contact Form Submission + + + + + + +
+ + + + + + + + + + + + + +
+

+ 🔔 New Contact Message +

+

+ Someone just reached out to you +

+
+ + + + + + + + + + + + + {% if contact_message.phone %} + + + + + {% endif %} + + + + +
+

+ Contact Information +

+
+ Name: + + {{ contact_message.name }} +
+ Email: + + + {{ contact_message.email }} + +
+ Phone: + + + {{ contact_message.phone }} + +
+ Received: + + {{ contact_message.created_at|date:"F d, Y" }} at {{ contact_message.created_at|time:"h:i A" }} +
+ +
+

+ Message +

+
+

{{ contact_message.message }}

+
+
+ + +
+

+ This is an automated notification from your contact form.
+ Please respond to the sender as soon as possible. +

+
+
+ + \ No newline at end of file diff --git a/templates/emails/user_contact_confirmation.html b/templates/emails/user_contact_confirmation.html new file mode 100644 index 0000000..244c077 --- /dev/null +++ b/templates/emails/user_contact_confirmation.html @@ -0,0 +1,80 @@ + + + + + + Thank You for Contacting Us + + + + + + +
+ + + + + + + + + + +
+

+ ✨ Thank You! +

+

+ We've received your message +

+
+
+
+ ✅ +
+
+ +

+ Hi {{ contact_message.name }}, +

+ +

+ Thank you for reaching out to us. We've received your message and our team will review it shortly. We typically respond within 24-48 hours. +

+
+

+ Your Message: +

+

{{ contact_message.message }}

+
+
+

+ 📧 We'll reply to: +

+

+ {{ contact_message.email }} +

+
+ +

+ If you have any urgent questions in the meantime, please don't hesitate to reach out to us directly. +

+
+

+ Need immediate assistance? +

+

+ Email us at {{support_email}}
+ or call us at +1 (754) 816-2311 +

+ +
+

+ © {{ current_year }} {{ company_name }}. All rights reserved. +

+
+
+
+ + \ No newline at end of file diff --git a/users/admin.py b/users/admin.py index 0c3609f..6abbec7 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,7 +1,5 @@ from django.contrib import admin -from .models import CustomUser, UserProfile - -# Register your models here. +from .models import CustomUser, UserProfile, ContactMessage @admin.register(CustomUser) class UserAdmin(admin.ModelAdmin): @@ -17,3 +15,41 @@ class UserProfileAdmin(admin.ModelAdmin): ordering = ('user__email',) + +@admin.register(ContactMessage) +class ContactMessageAdmin(admin.ModelAdmin): + list_display = ['name', 'email', 'phone', 'created_at', 'is_read', 'is_responded'] + list_filter = ['is_read', 'is_responded', 'created_at'] + search_fields = ['name', 'email', 'phone', 'message'] + readonly_fields = ['created_at'] + date_hierarchy = 'created_at' + + fieldsets = ( + ('Contact Information', { + 'fields': ('name', 'email', 'phone') + }), + ('Message', { + 'fields': ('message',) + }), + ('Status', { + 'fields': ('is_read', 'is_responded', 'created_at') + }), + ) + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related() + + actions = ['mark_as_read', 'mark_as_responded'] + + def mark_as_read(self, request, queryset): + updated = queryset.update(is_read=True) + self.message_user(request, f'{updated} message(s) marked as read.') + mark_as_read.short_description = "Mark selected as read" + + def mark_as_responded(self, request, queryset): + updated = queryset.update(is_responded=True) + self.message_user(request, f'{updated} message(s) marked as responded.') + mark_as_responded.short_description = "Mark selected as responded" + + diff --git a/users/migrations/0002_contactmessage.py b/users/migrations/0002_contactmessage.py new file mode 100644 index 0000000..8525ae5 --- /dev/null +++ b/users/migrations/0002_contactmessage.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.8 on 2025-11-28 15:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ContactMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(blank=True, max_length=20)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_read', models.BooleanField(default=False)), + ('is_responded', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'Contact Message', + 'verbose_name_plural': 'Contact Messages', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/users/models.py b/users/models.py index ac1f77f..93fe49b 100644 --- a/users/models.py +++ b/users/models.py @@ -37,4 +37,22 @@ class UserProfile(models.Model): updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return f"{self.user.email} Profile" \ No newline at end of file + return f"{self.user.email} Profile" + + +class ContactMessage(models.Model): + name = models.CharField(max_length=255) + email = models.EmailField() + phone = models.CharField(max_length=20, blank=True) + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + is_read = models.BooleanField(default=False) + is_responded = models.BooleanField(default=False) + + class Meta: + ordering = ['-created_at'] + verbose_name = 'Contact Message' + verbose_name_plural = 'Contact Messages' + + def __str__(self): + return f"{self.name} - {self.email} - {self.created_at.strftime('%Y-%m-%d')}" diff --git a/users/serializers.py b/users/serializers.py index 55a2108..ce01757 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from django.contrib.auth.password_validation import validate_password -from .models import CustomUser, UserProfile +from .models import CustomUser, UserProfile, ContactMessage class UserProfileSerializer(serializers.ModelSerializer): class Meta: @@ -53,4 +53,23 @@ class ResetPasswordSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): class Meta: model = CustomUser - fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined', 'last_login', 'is_staff', 'is_superuser', 'is_active') \ No newline at end of file + fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined', 'last_login', 'is_staff', 'is_superuser', 'is_active') + +from rest_framework import serializers +from .models import ContactMessage + +class ContactMessageSerializer(serializers.ModelSerializer): + class Meta: + model = ContactMessage + fields = ['id', 'name', 'email', 'phone', 'message', 'created_at'] + read_only_fields = ['id', 'created_at'] + + def validate_name(self, value): + if len(value.strip()) < 2: + raise serializers.ValidationError("Name must be at least 2 characters long.") + return value.strip() + + def validate_message(self, value): + if len(value.strip()) < 10: + raise serializers.ValidationError("Message must be at least 10 characters long.") + return value.strip() diff --git a/users/urls.py b/users/urls.py index 42984ec..34d68f6 100644 --- a/users/urls.py +++ b/users/urls.py @@ -3,6 +3,8 @@ from rest_framework_simplejwt.views import TokenRefreshView from . import views urlpatterns = [ + path('contact/', views.ContactMessageView.as_view(), name='contact-message'), + path('register/', views.register_user, name='register'), path('login/', views.login_user, name='login'), diff --git a/users/utils.py b/users/utils.py index 44d8137..e068ce0 100644 --- a/users/utils.py +++ b/users/utils.py @@ -6,6 +6,9 @@ from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags +import logging + +logger = logging.getLogger(__name__) def generate_otp(): return str(random.randint(100000, 999999)) @@ -54,4 +57,49 @@ def send_otp_via_email(email, otp, user_name=None, context='registration'): def is_otp_expired(otp_expiry): if otp_expiry and timezone.now() < otp_expiry: return False - return True \ No newline at end of file + return True + +def send_email_notifications(contact_message): + try: + send_admin_notification(contact_message) + + + send_user_confirmation(contact_message) + + except Exception as e: + logger.error(f"Error sending email notifications: {str(e)}") + +def send_admin_notification(contact_message): + subject = f"New Contact Form Submission from {contact_message.name}" + + html_content = render_to_string('emails/admin_contact_notification.html', { + 'contact_message': contact_message + }) + + email = EmailMultiAlternatives( + subject=subject, + body=html_content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[settings.DEFAULT_FROM_EMAIL] + ) + email.content_subtype = 'html' + email.send() + +def send_user_confirmation(self, contact_message): + subject = "Thank you for contacting us" + + html_content = render_to_string('emails/user_contact_confirmation.html', { + 'contact_message': contact_message, + 'company_name': 'Attune Heart Therapy', + 'support_email': 'admin@attunehearttherapy.com', + 'current_year': timezone.now().year, + }) + + email = EmailMultiAlternatives( + subject=subject, + body=html_content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[contact_message.email] + ) + email.content_subtype = 'html' + email.send() diff --git a/users/views.py b/users/views.py index 5f8cea2..c088f75 100644 --- a/users/views.py +++ b/users/views.py @@ -1,16 +1,51 @@ from rest_framework import status, generics from rest_framework.decorators import api_view, permission_classes +from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework_simplejwt.tokens import RefreshToken from django.contrib.auth import authenticate -from .models import CustomUser, UserProfile -from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ForgotPasswordSerializer, VerifyPasswordResetOTPSerializer -from .utils import send_otp_via_email, is_otp_expired, generate_otp +from .models import CustomUser, UserProfile, ContactMessage +from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ForgotPasswordSerializer, VerifyPasswordResetOTPSerializer, ContactMessageSerializer +from .utils import send_otp_via_email, is_otp_expired, generate_otp,send_email_notifications from django.utils import timezone from datetime import timedelta from rest_framework.reverse import reverse from meetings.models import AppointmentRequest +import logging + +logger = logging.getLogger(__name__) + +class ContactMessageView(APIView): + def post(self, request): + serializer = ContactMessageSerializer(data=request.data) + + if serializer.is_valid(): + try: + contact_message = serializer.save() + + send_email_notifications(contact_message) + + return Response({ + 'success': True, + 'message': 'Thank you for your message. We will get back to you soon!', + 'data': serializer.data + }, status=status.HTTP_201_CREATED) + + except Exception as e: + logger.error(f"Error processing contact form: {str(e)}") + return Response({ + 'success': False, + 'message': 'There was an error processing your request. Please try again later.' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + 'success': False, + 'message': 'Please check your input and try again.', + 'errors': serializer.errors + }, status=status.HTTP_400_BAD_REQUEST) + + @api_view(['POST']) -- 2.39.5