feat: add contact form functionality with admin management #50

Merged
Saani merged 1 commits from feature/meetings into main 2025-11-28 15:53:17 +00:00
12 changed files with 422 additions and 12 deletions

View File

@ -139,9 +139,9 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('SMTP_HOST', 'smtp.hostinger.com') EMAIL_HOST = os.getenv('SMTP_HOST', 'smtp.hostinger.com')
EMAIL_PORT = int(os.getenv('SMTP_PORT', 465)) EMAIL_PORT = int(os.getenv('SMTP_PORT', 465))
EMAIL_USE_SSL = True 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') 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 = { REST_FRAMEWORK = {

View File

@ -8,6 +8,24 @@ def api_root(request, format=None):
base_url = request.build_absolute_uri('/api/') base_url = request.build_absolute_uri('/api/')
endpoints = { 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': { 'authentication': {
'description': 'User authentication and management endpoints', 'description': 'User authentication and management endpoints',
'base_path': '/api/auth/', 'base_path': '/api/auth/',

View File

@ -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),
),
]

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Contact Form Submission</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f3f4f6;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f3f4f6; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<tr>
<td style="background: linear-gradient(135deg, #f43f5e 0%, #ec4899 100%); padding: 40px; border-radius: 12px 12px 0 0; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">
🔔 New Contact Message
</h1>
<p style="margin: 10px 0 0 0; color: #fce7f3; font-size: 16px;">
Someone just reached out to you
</p>
</td>
</tr>
<tr>
<td style="padding: 40px;">
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom: 30px;">
<tr>
<td colspan="2" style="padding-bottom: 20px;">
<h2 style="margin: 0; color: #111827; font-size: 20px; font-weight: 600;">
Contact Information
</h2>
</td>
</tr>
<tr>
<td style="padding: 12px 0; width: 120px; color: #6b7280; font-size: 14px; font-weight: 600;">
Name:
</td>
<td style="padding: 12px 0; color: #111827; font-size: 14px;">
{{ contact_message.name }}
</td>
</tr>
<tr style="border-top: 1px solid #e5e7eb;">
<td style="padding: 12px 0; width: 120px; color: #6b7280; font-size: 14px; font-weight: 600;">
Email:
</td>
<td style="padding: 12px 0;">
<a href="mailto:{{ contact_message.email }}" style="color: #f43f5e; text-decoration: none; font-size: 14px;">
{{ contact_message.email }}
</a>
</td>
</tr>
{% if contact_message.phone %}
<tr style="border-top: 1px solid #e5e7eb;">
<td style="padding: 12px 0; width: 120px; color: #6b7280; font-size: 14px; font-weight: 600;">
Phone:
</td>
<td style="padding: 12px 0;">
<a href="tel:{{ contact_message.phone }}" style="color: #f43f5e; text-decoration: none; font-size: 14px;">
{{ contact_message.phone }}
</a>
</td>
</tr>
{% endif %}
<tr style="border-top: 1px solid #e5e7eb;">
<td style="padding: 12px 0; width: 120px; color: #6b7280; font-size: 14px; font-weight: 600;">
Received:
</td>
<td style="padding: 12px 0; color: #111827; font-size: 14px;">
{{ contact_message.created_at|date:"F d, Y" }} at {{ contact_message.created_at|time:"h:i A" }}
</td>
</tr>
</table>
<div style="margin-bottom: 30px;">
<h2 style="margin: 0 0 15px 0; color: #111827; font-size: 20px; font-weight: 600;">
Message
</h2>
<div style="background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px;">
<p style="margin: 0; color: #374151; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">{{ contact_message.message }}</p>
</div>
</div>
<div style="text-align: center; margin-top: 30px;">
<a href="mailto:{{ contact_message.email }}" style="display: inline-block; background: linear-gradient(135deg, #f43f5e 0%, #ec4899 100%); color: #ffffff; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-weight: 600; font-size: 15px; box-shadow: 0 4px 6px rgba(244, 63, 94, 0.3);">
Reply to {{ contact_message.name }}
</a>
</div>
</td>
</tr>
<tr>
<td style="background-color: #f9fafb; padding: 30px; border-radius: 0 0 12px 12px; text-align: center; border-top: 1px solid #e5e7eb;">
<p style="margin: 0; color: #6b7280; font-size: 13px; line-height: 1.6;">
This is an automated notification from your contact form.<br>
Please respond to the sender as soon as possible.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thank You for Contacting Us</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f3f4f6;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f3f4f6; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<tr>
<td style="background: linear-gradient(135deg, #f43f5e 0%, #ec4899 100%); padding: 40px; border-radius: 12px 12px 0 0; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">
✨ Thank You!
</h1>
<p style="margin: 10px 0 0 0; color: #fce7f3; font-size: 16px;">
We've received your message
</p>
</td>
</tr>
<tr>
<td style="padding: 40px;">
<div style="text-align: center; margin-bottom: 30px;">
<div style="display: inline-block; background-color: #fef2f2; border-radius: 50%; padding: 20px; margin-bottom: 20px;">
<span style="font-size: 48px;"></span>
</div>
</div>
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 22px; font-weight: 600; text-align: center;">
Hi {{ contact_message.name }},
</h2>
<p style="margin: 0 0 20px 0; color: #374151; font-size: 16px; line-height: 1.6; text-align: center;">
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.
</p>
<div style="background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 25px; margin: 30px 0;">
<h3 style="margin: 0 0 15px 0; color: #111827; font-size: 16px; font-weight: 600;">
Your Message:
</h3>
<p style="margin: 0; color: #6b7280; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">{{ contact_message.message }}</p>
</div>
<div style="background-color: #fef2f2; border-left: 4px solid #f43f5e; padding: 20px; border-radius: 6px; margin: 30px 0;">
<p style="margin: 0 0 10px 0; color: #991b1b; font-weight: 600; font-size: 14px;">
📧 We'll reply to:
</p>
<p style="margin: 0; color: #b91c1c; font-size: 14px;">
{{ contact_message.email }}
</p>
</div>
<p style="margin: 30px 0 0 0; color: #6b7280; font-size: 14px; line-height: 1.6; text-align: center;">
If you have any urgent questions in the meantime, please don't hesitate to reach out to us directly.
</p>
</td>
</tr>
<tr>
<td style="background-color: #f9fafb; padding: 30px; border-radius: 0 0 12px 12px; text-align: center; border-top: 1px solid #e5e7eb;">
<p style="margin: 0 0 15px 0; color: #111827; font-size: 14px; font-weight: 600;">
Need immediate assistance?
</p>
<p style="margin: 0; color: #6b7280; font-size: 13px; line-height: 1.6;">
Email us at <a href="mailto:{{support_email}}" style="color: #f43f5e; text-decoration: none;">{{support_email}}</a><br>
or call us at <a href="tel:+1754816-2311" style="color: #f43f5e; text-decoration: none;">+1 (754) 816-2311</a>
</p>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
© {{ current_year }} {{ company_name }}. All rights reserved.
</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,7 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import CustomUser, UserProfile from .models import CustomUser, UserProfile, ContactMessage
# Register your models here.
@admin.register(CustomUser) @admin.register(CustomUser)
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
@ -17,3 +15,41 @@ class UserProfileAdmin(admin.ModelAdmin):
ordering = ('user__email',) 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"

View File

@ -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'],
},
),
]

View File

@ -37,4 +37,22 @@ class UserProfile(models.Model):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return f"{self.user.email} Profile" 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')}"

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth.password_validation import validate_password 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 UserProfileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -53,4 +53,23 @@ class ResetPasswordSerializer(serializers.Serializer):
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined', 'last_login', 'is_staff', 'is_superuser', 'is_active') 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()

View File

@ -3,6 +3,8 @@ from rest_framework_simplejwt.views import TokenRefreshView
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('contact/', views.ContactMessageView.as_view(), name='contact-message'),
path('register/', views.register_user, name='register'), path('register/', views.register_user, name='register'),
path('login/', views.login_user, name='login'), path('login/', views.login_user, name='login'),

View File

@ -6,6 +6,9 @@ from django.conf import settings
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
import logging
logger = logging.getLogger(__name__)
def generate_otp(): def generate_otp():
return str(random.randint(100000, 999999)) 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): def is_otp_expired(otp_expiry):
if otp_expiry and timezone.now() < otp_expiry: if otp_expiry and timezone.now() < otp_expiry:
return False return False
return True 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()

View File

@ -1,16 +1,51 @@
from rest_framework import status, generics from rest_framework import status, generics
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from .models import CustomUser, UserProfile from .models import CustomUser, UserProfile, ContactMessage
from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ForgotPasswordSerializer, VerifyPasswordResetOTPSerializer from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ForgotPasswordSerializer, VerifyPasswordResetOTPSerializer, ContactMessageSerializer
from .utils import send_otp_via_email, is_otp_expired, generate_otp from .utils import send_otp_via_email, is_otp_expired, generate_otp,send_email_notifications
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from meetings.models import AppointmentRequest 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']) @api_view(['POST'])