update
This commit is contained in:
0
backEnd/contact/__init__.py
Normal file
0
backEnd/contact/__init__.py
Normal file
BIN
backEnd/contact/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/email_service.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/email_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/models.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/contact/__pycache__/views.cpython-312.pyc
Normal file
BIN
backEnd/contact/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
283
backEnd/contact/admin.py
Normal file
283
backEnd/contact/admin.py
Normal file
@@ -0,0 +1,283 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from .models import ContactSubmission
|
||||
|
||||
|
||||
@admin.register(ContactSubmission)
|
||||
class ContactSubmissionAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for ContactSubmission model.
|
||||
Provides comprehensive management of contact form submissions.
|
||||
"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'full_name_display',
|
||||
'company',
|
||||
'email',
|
||||
'project_type_display',
|
||||
'status_badge',
|
||||
'priority_badge',
|
||||
'is_enterprise_client_display',
|
||||
'created_at',
|
||||
'assigned_to',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'status',
|
||||
'priority',
|
||||
'industry',
|
||||
'company_size',
|
||||
'project_type',
|
||||
'timeline',
|
||||
'budget',
|
||||
'newsletter_subscription',
|
||||
'privacy_consent',
|
||||
'created_at',
|
||||
'assigned_to',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'company',
|
||||
'job_title',
|
||||
'message',
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'full_name_display',
|
||||
'is_enterprise_client_display',
|
||||
'is_high_priority_display',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Personal Information', {
|
||||
'fields': (
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'full_name_display',
|
||||
'email',
|
||||
'phone',
|
||||
)
|
||||
}),
|
||||
('Company Information', {
|
||||
'fields': (
|
||||
'company',
|
||||
'job_title',
|
||||
'industry',
|
||||
'company_size',
|
||||
'is_enterprise_client_display',
|
||||
)
|
||||
}),
|
||||
('Project Details', {
|
||||
'fields': (
|
||||
'project_type',
|
||||
'timeline',
|
||||
'budget',
|
||||
'message',
|
||||
)
|
||||
}),
|
||||
('Communication Preferences', {
|
||||
'fields': (
|
||||
'newsletter_subscription',
|
||||
'privacy_consent',
|
||||
)
|
||||
}),
|
||||
('Management', {
|
||||
'fields': (
|
||||
'status',
|
||||
'priority',
|
||||
'is_high_priority_display',
|
||||
'assigned_to',
|
||||
'admin_notes',
|
||||
)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': (
|
||||
'created_at',
|
||||
'updated_at',
|
||||
),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
ordering = ['-created_at']
|
||||
list_per_page = 25
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
actions = [
|
||||
'mark_as_contacted',
|
||||
'mark_as_qualified',
|
||||
'mark_as_closed',
|
||||
'set_high_priority',
|
||||
'set_medium_priority',
|
||||
'set_low_priority',
|
||||
]
|
||||
|
||||
def full_name_display(self, obj):
|
||||
"""Display full name with link to detail view."""
|
||||
return format_html(
|
||||
'<strong>{}</strong>',
|
||||
obj.full_name
|
||||
)
|
||||
full_name_display.short_description = 'Full Name'
|
||||
full_name_display.admin_order_field = 'first_name'
|
||||
|
||||
def project_type_display(self, obj):
|
||||
"""Display project type with color coding."""
|
||||
if not obj.project_type:
|
||||
return '-'
|
||||
|
||||
colors = {
|
||||
'software-development': '#007bff',
|
||||
'cloud-migration': '#28a745',
|
||||
'digital-transformation': '#ffc107',
|
||||
'data-analytics': '#17a2b8',
|
||||
'security-compliance': '#dc3545',
|
||||
'integration': '#6f42c1',
|
||||
'consulting': '#fd7e14',
|
||||
}
|
||||
|
||||
color = colors.get(obj.project_type, '#6c757d')
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{}</span>',
|
||||
color,
|
||||
obj.get_project_type_display()
|
||||
)
|
||||
project_type_display.short_description = 'Project Type'
|
||||
project_type_display.admin_order_field = 'project_type'
|
||||
|
||||
def status_badge(self, obj):
|
||||
"""Display status as a colored badge."""
|
||||
colors = {
|
||||
'new': '#007bff',
|
||||
'in_progress': '#ffc107',
|
||||
'contacted': '#17a2b8',
|
||||
'qualified': '#28a745',
|
||||
'closed': '#6c757d',
|
||||
}
|
||||
|
||||
color = colors.get(obj.status, '#6c757d')
|
||||
return format_html(
|
||||
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;">{}</span>',
|
||||
color,
|
||||
obj.get_status_display().upper()
|
||||
)
|
||||
status_badge.short_description = 'Status'
|
||||
status_badge.admin_order_field = 'status'
|
||||
|
||||
def priority_badge(self, obj):
|
||||
"""Display priority as a colored badge."""
|
||||
colors = {
|
||||
'urgent': '#dc3545',
|
||||
'high': '#fd7e14',
|
||||
'medium': '#ffc107',
|
||||
'low': '#28a745',
|
||||
}
|
||||
|
||||
color = colors.get(obj.priority, '#6c757d')
|
||||
return format_html(
|
||||
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;">{}</span>',
|
||||
color,
|
||||
obj.get_priority_display().upper()
|
||||
)
|
||||
priority_badge.short_description = 'Priority'
|
||||
priority_badge.admin_order_field = 'priority'
|
||||
|
||||
def is_enterprise_client_display(self, obj):
|
||||
"""Display enterprise client status."""
|
||||
if obj.is_enterprise_client:
|
||||
return format_html(
|
||||
'<span style="color: #28a745; font-weight: bold;">✓ Enterprise</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: #6c757d;">SMB</span>'
|
||||
)
|
||||
is_enterprise_client_display.short_description = 'Client Type'
|
||||
is_enterprise_client_display.admin_order_field = 'company_size'
|
||||
|
||||
def is_high_priority_display(self, obj):
|
||||
"""Display high priority status."""
|
||||
if obj.is_high_priority:
|
||||
return format_html(
|
||||
'<span style="color: #dc3545; font-weight: bold;">⚠ High Priority</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: #6c757d;">Normal</span>'
|
||||
)
|
||||
is_high_priority_display.short_description = 'Priority Level'
|
||||
|
||||
# Admin Actions
|
||||
def mark_as_contacted(self, request, queryset):
|
||||
"""Mark selected submissions as contacted."""
|
||||
updated = queryset.update(status='contacted')
|
||||
self.message_user(
|
||||
request,
|
||||
f'{updated} submission(s) marked as contacted.'
|
||||
)
|
||||
mark_as_contacted.short_description = "Mark selected submissions as contacted"
|
||||
|
||||
def mark_as_qualified(self, request, queryset):
|
||||
"""Mark selected submissions as qualified."""
|
||||
updated = queryset.update(status='qualified')
|
||||
self.message_user(
|
||||
request,
|
||||
f'{updated} submission(s) marked as qualified.'
|
||||
)
|
||||
mark_as_qualified.short_description = "Mark selected submissions as qualified"
|
||||
|
||||
def mark_as_closed(self, request, queryset):
|
||||
"""Mark selected submissions as closed."""
|
||||
updated = queryset.update(status='closed')
|
||||
self.message_user(
|
||||
request,
|
||||
f'{updated} submission(s) marked as closed.'
|
||||
)
|
||||
mark_as_closed.short_description = "Mark selected submissions as closed"
|
||||
|
||||
def set_high_priority(self, request, queryset):
|
||||
"""Set selected submissions to high priority."""
|
||||
updated = queryset.update(priority='high')
|
||||
self.message_user(
|
||||
request,
|
||||
f'{updated} submission(s) set to high priority.'
|
||||
)
|
||||
set_high_priority.short_description = "Set selected submissions to high priority"
|
||||
|
||||
def set_medium_priority(self, request, queryset):
|
||||
"""Set selected submissions to medium priority."""
|
||||
updated = queryset.update(priority='medium')
|
||||
self.message_user(
|
||||
request,
|
||||
f'{updated} submission(s) set to medium priority.'
|
||||
)
|
||||
set_medium_priority.short_description = "Set selected submissions to medium priority"
|
||||
|
||||
def set_low_priority(self, request, queryset):
|
||||
"""Set selected submissions to low priority."""
|
||||
updated = queryset.update(priority='low')
|
||||
self.message_user(
|
||||
request,
|
||||
f'{updated} submission(s) set to low priority.'
|
||||
)
|
||||
set_low_priority.short_description = "Set selected submissions to low priority"
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queryset for admin list view."""
|
||||
return super().get_queryset(request).select_related()
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable adding new submissions through admin."""
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Allow deletion only for superusers."""
|
||||
return request.user.is_superuser
|
||||
6
backEnd/contact/apps.py
Normal file
6
backEnd/contact/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ContactConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'contact'
|
||||
313
backEnd/contact/email_service.py
Normal file
313
backEnd/contact/email_service.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
Email service for contact form notifications.
|
||||
Production-ready with retry logic and comprehensive error handling.
|
||||
"""
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
from django.utils.html import strip_tags
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _send_email_with_retry(email_message, max_retries: int = 3, delay: float = 1.0) -> bool:
|
||||
"""
|
||||
Send email with retry logic for production reliability.
|
||||
|
||||
Args:
|
||||
email_message: EmailMultiAlternatives instance
|
||||
max_retries: Maximum number of retry attempts
|
||||
delay: Delay between retries in seconds
|
||||
|
||||
Returns:
|
||||
bool: True if email was sent successfully, False otherwise
|
||||
"""
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
# Test connection before sending
|
||||
connection = get_connection()
|
||||
connection.open()
|
||||
connection.close()
|
||||
|
||||
# Send the email
|
||||
email_message.send()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Email send attempt {attempt + 1} failed: {str(e)}")
|
||||
|
||||
if attempt < max_retries:
|
||||
time.sleep(delay * (2 ** attempt)) # Exponential backoff
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Failed to send email after {max_retries + 1} attempts: {str(e)}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _create_email_connection() -> Optional[EmailBackend]:
|
||||
"""
|
||||
Create a robust email connection with production settings.
|
||||
|
||||
Returns:
|
||||
EmailBackend instance or None if connection fails
|
||||
"""
|
||||
try:
|
||||
connection = get_connection(
|
||||
host=settings.EMAIL_HOST,
|
||||
port=settings.EMAIL_PORT,
|
||||
username=settings.EMAIL_HOST_USER,
|
||||
password=settings.EMAIL_HOST_PASSWORD,
|
||||
use_tls=settings.EMAIL_USE_TLS,
|
||||
use_ssl=settings.EMAIL_USE_SSL,
|
||||
timeout=getattr(settings, 'EMAIL_TIMEOUT', 30),
|
||||
connection_timeout=getattr(settings, 'EMAIL_CONNECTION_TIMEOUT', 10),
|
||||
read_timeout=getattr(settings, 'EMAIL_READ_TIMEOUT', 10),
|
||||
)
|
||||
|
||||
# Test the connection
|
||||
connection.open()
|
||||
connection.close()
|
||||
|
||||
return connection
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create email connection: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def send_contact_submission_notification(submission):
|
||||
"""
|
||||
Send email notification for new contact form submission.
|
||||
|
||||
Args:
|
||||
submission: ContactSubmission instance
|
||||
|
||||
Returns:
|
||||
bool: True if email was sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Get company email from settings
|
||||
company_email = getattr(settings, 'COMPANY_EMAIL', None)
|
||||
if not company_email:
|
||||
logger.warning("COMPANY_EMAIL not configured in settings")
|
||||
return False
|
||||
|
||||
# Prepare email context
|
||||
context = {
|
||||
'submission': submission,
|
||||
}
|
||||
|
||||
# Render email templates
|
||||
html_content = render_to_string(
|
||||
'contact/contact_submission_email.html',
|
||||
context
|
||||
)
|
||||
text_content = render_to_string(
|
||||
'contact/contact_submission_email.txt',
|
||||
context
|
||||
)
|
||||
|
||||
# Create email subject with priority indicator
|
||||
priority_emoji = {
|
||||
'urgent': '🚨',
|
||||
'high': '⚠️',
|
||||
'medium': '📋',
|
||||
'low': '📝'
|
||||
}.get(submission.priority, '📋')
|
||||
|
||||
subject = f"{priority_emoji} New Contact Form Submission - {submission.company} (#{submission.id})"
|
||||
|
||||
# Create email message
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[company_email],
|
||||
reply_to=[submission.email] # Allow direct reply to customer
|
||||
)
|
||||
|
||||
# Add headers for better email handling
|
||||
email.extra_headers = {
|
||||
'X-Priority': '1' if submission.priority in ['urgent', 'high'] else '3',
|
||||
'X-MSMail-Priority': 'High' if submission.priority in ['urgent', 'high'] else 'Normal',
|
||||
}
|
||||
|
||||
# Attach HTML version
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
# Send email with retry logic
|
||||
success = _send_email_with_retry(email)
|
||||
|
||||
if success:
|
||||
logger.info(f"Contact submission notification sent for submission #{submission.id}")
|
||||
else:
|
||||
logger.error(f"Failed to send contact submission notification for submission #{submission.id} after retries")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send contact submission notification for submission #{submission.id}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def send_contact_submission_confirmation(submission):
|
||||
"""
|
||||
Send confirmation email to the customer who submitted the form.
|
||||
|
||||
Args:
|
||||
submission: ContactSubmission instance
|
||||
|
||||
Returns:
|
||||
bool: True if email was sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Prepare email context
|
||||
context = {
|
||||
'submission': submission,
|
||||
}
|
||||
|
||||
# Create simple confirmation email
|
||||
subject = "Thank you for contacting GNX Software Solutions"
|
||||
|
||||
# Simple text email for confirmation
|
||||
message = f"""
|
||||
Dear {submission.full_name},
|
||||
|
||||
Thank you for reaching out to GNX Software Solutions!
|
||||
|
||||
We have received your inquiry about {submission.get_project_type_display() if submission.project_type else 'your project'} and will review it carefully.
|
||||
|
||||
Here are the details of your submission:
|
||||
- Submission ID: #{submission.id}
|
||||
- Company: {submission.company}
|
||||
- Project Type: {submission.get_project_type_display() if submission.project_type else 'Not specified'}
|
||||
- Timeline: {submission.get_timeline_display() if submission.timeline else 'Not specified'}
|
||||
|
||||
Our team will contact you within 24 hours to discuss your project requirements and how we can help you achieve your goals.
|
||||
|
||||
If you have any urgent questions, please don't hesitate to contact us directly.
|
||||
|
||||
Best regards,
|
||||
The GNX Team
|
||||
|
||||
---
|
||||
GNX Software Solutions
|
||||
Email: {settings.DEFAULT_FROM_EMAIL}
|
||||
"""
|
||||
|
||||
# Create email message
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[submission.email]
|
||||
)
|
||||
|
||||
# Send email with retry logic
|
||||
success = _send_email_with_retry(email)
|
||||
|
||||
if success:
|
||||
logger.info(f"Contact submission confirmation sent to {submission.email} for submission #{submission.id}")
|
||||
else:
|
||||
logger.error(f"Failed to send contact submission confirmation for submission #{submission.id} after retries")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send contact submission confirmation for submission #{submission.id}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def check_email_health() -> dict:
|
||||
"""
|
||||
Check email service health for production monitoring.
|
||||
|
||||
Returns:
|
||||
dict: Health status information
|
||||
"""
|
||||
health_status = {
|
||||
'email_service': 'unknown',
|
||||
'connection_test': False,
|
||||
'configuration_valid': False,
|
||||
'error_message': None
|
||||
}
|
||||
|
||||
try:
|
||||
# Check configuration
|
||||
required_settings = [
|
||||
'EMAIL_HOST', 'EMAIL_PORT', 'EMAIL_HOST_USER',
|
||||
'EMAIL_HOST_PASSWORD', 'DEFAULT_FROM_EMAIL', 'COMPANY_EMAIL'
|
||||
]
|
||||
|
||||
missing_settings = []
|
||||
for setting in required_settings:
|
||||
if not getattr(settings, setting, None):
|
||||
missing_settings.append(setting)
|
||||
|
||||
if missing_settings:
|
||||
health_status['error_message'] = f"Missing email settings: {', '.join(missing_settings)}"
|
||||
return health_status
|
||||
|
||||
health_status['configuration_valid'] = True
|
||||
|
||||
# Test connection
|
||||
connection = _create_email_connection()
|
||||
if connection:
|
||||
health_status['connection_test'] = True
|
||||
health_status['email_service'] = 'healthy'
|
||||
else:
|
||||
health_status['email_service'] = 'unhealthy'
|
||||
health_status['error_message'] = 'Failed to establish email connection'
|
||||
|
||||
except Exception as e:
|
||||
health_status['email_service'] = 'error'
|
||||
health_status['error_message'] = str(e)
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
def send_test_email(to_email: str) -> bool:
|
||||
"""
|
||||
Send a test email to verify email configuration.
|
||||
|
||||
Args:
|
||||
to_email: Email address to send test email to
|
||||
|
||||
Returns:
|
||||
bool: True if test email was sent successfully
|
||||
"""
|
||||
try:
|
||||
subject = "GNX Email Service Test"
|
||||
message = f"""
|
||||
This is a test email from the GNX contact form system.
|
||||
|
||||
If you receive this email, the email service is working correctly.
|
||||
|
||||
Timestamp: {time.strftime("%Y-%m-%d %H:%M:%S UTC")}
|
||||
"""
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[to_email]
|
||||
)
|
||||
|
||||
success = _send_email_with_retry(email)
|
||||
|
||||
if success:
|
||||
logger.info(f"Test email sent successfully to {to_email}")
|
||||
else:
|
||||
logger.error(f"Failed to send test email to {to_email}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending test email to {to_email}: {str(e)}")
|
||||
return False
|
||||
47
backEnd/contact/migrations/0001_initial.py
Normal file
47
backEnd/contact/migrations/0001_initial.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 4.2.7 on 2025-09-25 07:22
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContactSubmission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=100, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=100, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, validators=[django.core.validators.EmailValidator()], verbose_name='Business Email')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone Number')),
|
||||
('company', models.CharField(max_length=200, verbose_name='Company Name')),
|
||||
('job_title', models.CharField(max_length=100, verbose_name='Job Title')),
|
||||
('industry', models.CharField(blank=True, choices=[('technology', 'Technology'), ('finance', 'Finance'), ('healthcare', 'Healthcare'), ('manufacturing', 'Manufacturing'), ('retail', 'Retail'), ('education', 'Education'), ('government', 'Government'), ('other', 'Other')], max_length=50, null=True, verbose_name='Industry')),
|
||||
('company_size', models.CharField(blank=True, choices=[('1-10', '1-10 employees'), ('11-50', '11-50 employees'), ('51-200', '51-200 employees'), ('201-1000', '201-1000 employees'), ('1000+', '1000+ employees')], max_length=20, null=True, verbose_name='Company Size')),
|
||||
('project_type', models.CharField(blank=True, choices=[('software-development', 'Software Development'), ('cloud-migration', 'Cloud Migration'), ('digital-transformation', 'Digital Transformation'), ('data-analytics', 'Data Analytics'), ('security-compliance', 'Security & Compliance'), ('integration', 'System Integration'), ('consulting', 'Consulting Services')], max_length=50, null=True, verbose_name='Project Type')),
|
||||
('timeline', models.CharField(blank=True, choices=[('immediate', 'Immediate (0-3 months)'), ('short', 'Short-term (3-6 months)'), ('medium', 'Medium-term (6-12 months)'), ('long', 'Long-term (12+ months)'), ('planning', 'Still planning')], max_length=20, null=True, verbose_name='Project Timeline')),
|
||||
('budget', models.CharField(blank=True, choices=[('under-50k', 'Under €50,000'), ('50k-100k', '€50,000 - €100,000'), ('100k-250k', '€100,000 - €250,000'), ('250k-500k', '€250,000 - €500,000'), ('500k-1m', '€500,000 - €1,000,000'), ('over-1m', 'Over €1,000,000'), ('discuss', 'Prefer to discuss')], max_length=20, null=True, verbose_name='Project Budget Range')),
|
||||
('message', models.TextField(verbose_name='Project Description')),
|
||||
('newsletter_subscription', models.BooleanField(default=False, verbose_name='Newsletter Subscription')),
|
||||
('privacy_consent', models.BooleanField(default=False, verbose_name='Privacy Policy Consent')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('closed', 'Closed')], default='new', max_length=20, verbose_name='Status')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent')], default='medium', max_length=10, verbose_name='Priority')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('admin_notes', models.TextField(blank=True, null=True, verbose_name='Admin Notes')),
|
||||
('assigned_to', models.CharField(blank=True, max_length=100, null=True, verbose_name='Assigned To')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Contact Submission',
|
||||
'verbose_name_plural': 'Contact Submissions',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['email'], name='contact_con_email_394734_idx'), models.Index(fields=['company'], name='contact_con_company_80f428_idx'), models.Index(fields=['status'], name='contact_con_status_b337da_idx'), models.Index(fields=['created_at'], name='contact_con_created_0e637d_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backEnd/contact/migrations/__init__.py
Normal file
0
backEnd/contact/migrations/__init__.py
Normal file
Binary file not shown.
BIN
backEnd/contact/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/contact/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
190
backEnd/contact/models.py
Normal file
190
backEnd/contact/models.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from django.db import models
|
||||
from django.core.validators import EmailValidator
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class ContactSubmission(models.Model):
|
||||
"""
|
||||
Model to store contact form submissions from the GNX website.
|
||||
Based on the comprehensive contact form structure from ContactSection.tsx
|
||||
"""
|
||||
|
||||
# Personal Information
|
||||
first_name = models.CharField(max_length=100, verbose_name="First Name")
|
||||
last_name = models.CharField(max_length=100, verbose_name="Last Name")
|
||||
email = models.EmailField(validators=[EmailValidator()], verbose_name="Business Email")
|
||||
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name="Phone Number")
|
||||
|
||||
# Company Information
|
||||
company = models.CharField(max_length=200, verbose_name="Company Name")
|
||||
job_title = models.CharField(max_length=100, verbose_name="Job Title")
|
||||
|
||||
# Industry and Company Size
|
||||
INDUSTRY_CHOICES = [
|
||||
('technology', 'Technology'),
|
||||
('finance', 'Finance'),
|
||||
('healthcare', 'Healthcare'),
|
||||
('manufacturing', 'Manufacturing'),
|
||||
('retail', 'Retail'),
|
||||
('education', 'Education'),
|
||||
('government', 'Government'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
industry = models.CharField(
|
||||
max_length=50,
|
||||
choices=INDUSTRY_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Industry"
|
||||
)
|
||||
|
||||
COMPANY_SIZE_CHOICES = [
|
||||
('1-10', '1-10 employees'),
|
||||
('11-50', '11-50 employees'),
|
||||
('51-200', '51-200 employees'),
|
||||
('201-1000', '201-1000 employees'),
|
||||
('1000+', '1000+ employees'),
|
||||
]
|
||||
company_size = models.CharField(
|
||||
max_length=20,
|
||||
choices=COMPANY_SIZE_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Company Size"
|
||||
)
|
||||
|
||||
# Project Details
|
||||
PROJECT_TYPE_CHOICES = [
|
||||
('software-development', 'Software Development'),
|
||||
('cloud-migration', 'Cloud Migration'),
|
||||
('digital-transformation', 'Digital Transformation'),
|
||||
('data-analytics', 'Data Analytics'),
|
||||
('security-compliance', 'Security & Compliance'),
|
||||
('integration', 'System Integration'),
|
||||
('consulting', 'Consulting Services'),
|
||||
]
|
||||
project_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PROJECT_TYPE_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Project Type"
|
||||
)
|
||||
|
||||
TIMELINE_CHOICES = [
|
||||
('immediate', 'Immediate (0-3 months)'),
|
||||
('short', 'Short-term (3-6 months)'),
|
||||
('medium', 'Medium-term (6-12 months)'),
|
||||
('long', 'Long-term (12+ months)'),
|
||||
('planning', 'Still planning'),
|
||||
]
|
||||
timeline = models.CharField(
|
||||
max_length=20,
|
||||
choices=TIMELINE_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Project Timeline"
|
||||
)
|
||||
|
||||
BUDGET_CHOICES = [
|
||||
('under-50k', 'Under €50,000'),
|
||||
('50k-100k', '€50,000 - €100,000'),
|
||||
('100k-250k', '€100,000 - €250,000'),
|
||||
('250k-500k', '€250,000 - €500,000'),
|
||||
('500k-1m', '€500,000 - €1,000,000'),
|
||||
('over-1m', 'Over €1,000,000'),
|
||||
('discuss', 'Prefer to discuss'),
|
||||
]
|
||||
budget = models.CharField(
|
||||
max_length=20,
|
||||
choices=BUDGET_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Project Budget Range"
|
||||
)
|
||||
|
||||
message = models.TextField(verbose_name="Project Description")
|
||||
|
||||
# Privacy & Communication
|
||||
newsletter_subscription = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Newsletter Subscription"
|
||||
)
|
||||
privacy_consent = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Privacy Policy Consent"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
STATUS_CHOICES = [
|
||||
('new', 'New'),
|
||||
('in_progress', 'In Progress'),
|
||||
('contacted', 'Contacted'),
|
||||
('qualified', 'Qualified'),
|
||||
('closed', 'Closed'),
|
||||
]
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='new',
|
||||
verbose_name="Status"
|
||||
)
|
||||
|
||||
priority = models.CharField(
|
||||
max_length=10,
|
||||
choices=[
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High'),
|
||||
('urgent', 'Urgent'),
|
||||
],
|
||||
default='medium',
|
||||
verbose_name="Priority"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated At")
|
||||
|
||||
# Admin notes for internal use
|
||||
admin_notes = models.TextField(blank=True, null=True, verbose_name="Admin Notes")
|
||||
assigned_to = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Assigned To"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Contact Submission"
|
||||
verbose_name_plural = "Contact Submissions"
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['email']),
|
||||
models.Index(fields=['company']),
|
||||
models.Index(fields=['status']),
|
||||
models.Index(fields=['created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name} - {self.company} ({self.created_at.strftime('%Y-%m-%d')})"
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
@property
|
||||
def is_high_priority(self):
|
||||
return self.priority in ['high', 'urgent']
|
||||
|
||||
@property
|
||||
def is_enterprise_client(self):
|
||||
return self.company_size in ['201-1000', '1000+']
|
||||
|
||||
def get_industry_display(self):
|
||||
return dict(self.INDUSTRY_CHOICES).get(self.industry, self.industry)
|
||||
|
||||
def get_project_type_display(self):
|
||||
return dict(self.PROJECT_TYPE_CHOICES).get(self.project_type, self.project_type)
|
||||
|
||||
def get_budget_display(self):
|
||||
return dict(self.BUDGET_CHOICES).get(self.budget, self.budget)
|
||||
196
backEnd/contact/serializers.py
Normal file
196
backEnd/contact/serializers.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ContactSubmission
|
||||
|
||||
|
||||
class ContactSubmissionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for ContactSubmission model.
|
||||
Handles both creation and retrieval of contact form submissions.
|
||||
"""
|
||||
|
||||
# Computed fields
|
||||
full_name = serializers.ReadOnlyField()
|
||||
is_high_priority = serializers.ReadOnlyField()
|
||||
is_enterprise_client = serializers.ReadOnlyField()
|
||||
industry_display = serializers.SerializerMethodField()
|
||||
project_type_display = serializers.SerializerMethodField()
|
||||
budget_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ContactSubmission
|
||||
fields = [
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'full_name',
|
||||
'email',
|
||||
'phone',
|
||||
'company',
|
||||
'job_title',
|
||||
'industry',
|
||||
'industry_display',
|
||||
'company_size',
|
||||
'project_type',
|
||||
'project_type_display',
|
||||
'timeline',
|
||||
'budget',
|
||||
'budget_display',
|
||||
'message',
|
||||
'newsletter_subscription',
|
||||
'privacy_consent',
|
||||
'status',
|
||||
'priority',
|
||||
'is_high_priority',
|
||||
'is_enterprise_client',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'admin_notes',
|
||||
'assigned_to',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'status',
|
||||
'priority',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'admin_notes',
|
||||
'assigned_to',
|
||||
]
|
||||
|
||||
def get_industry_display(self, obj):
|
||||
return obj.get_industry_display()
|
||||
|
||||
def get_project_type_display(self, obj):
|
||||
return obj.get_project_type_display()
|
||||
|
||||
def get_budget_display(self, obj):
|
||||
return obj.get_budget_display()
|
||||
|
||||
def validate_email(self, value):
|
||||
"""
|
||||
Custom email validation to ensure it's a business email.
|
||||
"""
|
||||
if value and not any(domain in value.lower() for domain in ['@gmail.com', '@yahoo.com', '@hotmail.com']):
|
||||
return value
|
||||
# Allow personal emails but log them
|
||||
return value
|
||||
|
||||
def validate_privacy_consent(self, value):
|
||||
"""
|
||||
Ensure privacy consent is given.
|
||||
"""
|
||||
if not value:
|
||||
raise serializers.ValidationError("Privacy consent is required to submit the form.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Cross-field validation.
|
||||
"""
|
||||
# Ensure required fields are present
|
||||
required_fields = ['first_name', 'last_name', 'email', 'company', 'job_title', 'message']
|
||||
for field in required_fields:
|
||||
if not attrs.get(field):
|
||||
raise serializers.ValidationError(f"{field.replace('_', ' ').title()} is required.")
|
||||
|
||||
# Validate enterprise client indicators
|
||||
if attrs.get('company_size') in ['201-1000', '1000+'] and attrs.get('budget') in ['under-50k', '50k-100k']:
|
||||
# This might be a mismatch, but we'll allow it and flag for review
|
||||
pass
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class ContactSubmissionCreateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Simplified serializer for creating contact submissions.
|
||||
Only includes fields that should be provided by the frontend.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ContactSubmission
|
||||
fields = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'phone',
|
||||
'company',
|
||||
'job_title',
|
||||
'industry',
|
||||
'company_size',
|
||||
'project_type',
|
||||
'timeline',
|
||||
'budget',
|
||||
'message',
|
||||
'newsletter_subscription',
|
||||
'privacy_consent',
|
||||
]
|
||||
|
||||
def validate_privacy_consent(self, value):
|
||||
"""
|
||||
Ensure privacy consent is given.
|
||||
"""
|
||||
if not value:
|
||||
raise serializers.ValidationError("Privacy consent is required to submit the form.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Cross-field validation for creation.
|
||||
"""
|
||||
# Ensure required fields are present
|
||||
required_fields = ['first_name', 'last_name', 'email', 'company', 'job_title', 'message']
|
||||
for field in required_fields:
|
||||
if not attrs.get(field):
|
||||
raise serializers.ValidationError(f"{field.replace('_', ' ').title()} is required.")
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class ContactSubmissionListSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Simplified serializer for listing contact submissions.
|
||||
Used in admin views and API listings.
|
||||
"""
|
||||
|
||||
full_name = serializers.ReadOnlyField()
|
||||
is_high_priority = serializers.ReadOnlyField()
|
||||
is_enterprise_client = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = ContactSubmission
|
||||
fields = [
|
||||
'id',
|
||||
'full_name',
|
||||
'email',
|
||||
'company',
|
||||
'job_title',
|
||||
'project_type',
|
||||
'status',
|
||||
'priority',
|
||||
'is_high_priority',
|
||||
'is_enterprise_client',
|
||||
'created_at',
|
||||
]
|
||||
|
||||
|
||||
class ContactSubmissionUpdateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for updating contact submissions (admin use).
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ContactSubmission
|
||||
fields = [
|
||||
'status',
|
||||
'priority',
|
||||
'admin_notes',
|
||||
'assigned_to',
|
||||
]
|
||||
|
||||
def validate_status(self, value):
|
||||
"""
|
||||
Validate status transitions.
|
||||
"""
|
||||
# Add business logic for status transitions if needed
|
||||
return value
|
||||
437
backEnd/contact/templates/contact/contact_submission_email.html
Normal file
437
backEnd/contact/templates/contact/contact_submission_email.html
Normal file
@@ -0,0 +1,437 @@
|
||||
<!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 - GNX</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2c3e50;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.email-container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at 20% 80%, rgba(255,255,255,0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255,255,255,0.1) 0%, transparent 50%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.priority-urgent {
|
||||
background-color: #ff4757;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 71, 87, 0.3);
|
||||
}
|
||||
|
||||
.priority-high {
|
||||
background-color: #ff6b35;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.priority-medium {
|
||||
background-color: #ffa726;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 167, 38, 0.3);
|
||||
}
|
||||
|
||||
.priority-low {
|
||||
background-color: #66bb6a;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(102, 187, 106, 0.3);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section:hover {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: linear-gradient(90deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 12px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
min-width: 140px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
flex: 1;
|
||||
color: #2c3e50;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 10px;
|
||||
font-style: italic;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.footer p:last-child {
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.company-info {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.company-info strong {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.email-container {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.field {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
min-width: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<h1>📧 New Contact Form Submission</h1>
|
||||
<p>GNX Software Solutions</p>
|
||||
<div class="priority-badge priority-{{ submission.priority }}">
|
||||
{{ submission.get_priority_display }} Priority
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">📋</span>
|
||||
Submission Details
|
||||
</h3>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="field">
|
||||
<span class="field-label">Submission ID:</span>
|
||||
<span class="field-value"><strong>#{{ submission.id }}</strong></span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Priority:</span>
|
||||
<span class="field-value">
|
||||
<span class="priority-badge priority-{{ submission.priority }}" style="font-size: 11px; padding: 4px 8px;">
|
||||
{{ submission.get_priority_display }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Status:</span>
|
||||
<span class="field-value">{{ submission.get_status_display }}</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Submitted:</span>
|
||||
<span class="field-value">{{ submission.created_at|date:"F d, Y \a\t g:i A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">👤</span>
|
||||
Contact Information
|
||||
</h3>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="field">
|
||||
<span class="field-label">Name:</span>
|
||||
<span class="field-value"><strong>{{ submission.full_name }}</strong></span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Email:</span>
|
||||
<span class="field-value">
|
||||
<a href="mailto:{{ submission.email }}" style="color: #667eea; text-decoration: none;">{{ submission.email }}</a>
|
||||
</span>
|
||||
</div>
|
||||
{% if submission.phone %}
|
||||
<div class="field">
|
||||
<span class="field-label">Phone:</span>
|
||||
<span class="field-value">
|
||||
<a href="tel:{{ submission.phone }}" style="color: #667eea; text-decoration: none;">{{ submission.phone }}</a>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">🏢</span>
|
||||
Company Information
|
||||
</h3>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="field">
|
||||
<span class="field-label">Company:</span>
|
||||
<span class="field-value"><strong>{{ submission.company }}</strong></span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Job Title:</span>
|
||||
<span class="field-value">{{ submission.job_title }}</span>
|
||||
</div>
|
||||
{% if submission.industry %}
|
||||
<div class="field">
|
||||
<span class="field-label">Industry:</span>
|
||||
<span class="field-value">{{ submission.get_industry_display }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.company_size %}
|
||||
<div class="field">
|
||||
<span class="field-label">Company Size:</span>
|
||||
<span class="field-value">{{ submission.get_company_size_display }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">🎯</span>
|
||||
Project Details
|
||||
</h3>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
{% if submission.project_type %}
|
||||
<div class="field">
|
||||
<span class="field-label">Project Type:</span>
|
||||
<span class="field-value">{{ submission.get_project_type_display }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.timeline %}
|
||||
<div class="field">
|
||||
<span class="field-label">Timeline:</span>
|
||||
<span class="field-value">{{ submission.get_timeline_display }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.budget %}
|
||||
<div class="field">
|
||||
<span class="field-label">Budget:</span>
|
||||
<span class="field-value"><strong>{{ submission.get_budget_display }}</strong></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="field">
|
||||
<span class="field-label">Message:</span>
|
||||
<div class="message-content">
|
||||
{{ submission.message|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if submission.newsletter_subscription or submission.privacy_consent %}
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">✅</span>
|
||||
Preferences
|
||||
</h3>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
{% if submission.newsletter_subscription %}
|
||||
<div class="field">
|
||||
<span class="field-label">Newsletter Subscription:</span>
|
||||
<span class="field-value">✅ Yes</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.privacy_consent %}
|
||||
<div class="field">
|
||||
<span class="field-label">Privacy Policy Consent:</span>
|
||||
<span class="field-value">✅ Yes</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>📧 This email was automatically generated from the GNX website contact form.</p>
|
||||
<p>⏰ Please respond to the customer within 24 hours as per our service commitment.</p>
|
||||
|
||||
<div class="company-info">
|
||||
<p><strong>GNX Software Solutions</strong></p>
|
||||
<p>Email: support@gnxsoft.com | Web: gnxsoft.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
NEW CONTACT FORM SUBMISSION - GNX SOFTWARE SOLUTIONS
|
||||
====================================================
|
||||
|
||||
Submission Details:
|
||||
------------------
|
||||
Submission ID: #{{ submission.id }}
|
||||
Priority: {{ submission.get_priority_display }}
|
||||
Status: {{ submission.get_status_display }}
|
||||
Submitted: {{ submission.created_at|date:"F d, Y \a\t g:i A" }}
|
||||
|
||||
Contact Information:
|
||||
-------------------
|
||||
Name: {{ submission.full_name }}
|
||||
Email: {{ submission.email }}
|
||||
{% if submission.phone %}Phone: {{ submission.phone }}{% endif %}
|
||||
|
||||
Company Information:
|
||||
-------------------
|
||||
Company: {{ submission.company }}
|
||||
Job Title: {{ submission.job_title }}
|
||||
{% if submission.industry %}Industry: {{ submission.get_industry_display }}{% endif %}
|
||||
{% if submission.company_size %}Company Size: {{ submission.get_company_size_display }}{% endif %}
|
||||
|
||||
Project Details:
|
||||
---------------
|
||||
{% if submission.project_type %}Project Type: {{ submission.get_project_type_display }}{% endif %}
|
||||
{% if submission.timeline %}Timeline: {{ submission.get_timeline_display }}{% endif %}
|
||||
{% if submission.budget %}Budget: {{ submission.get_budget_display }}{% endif %}
|
||||
|
||||
Message:
|
||||
{{ submission.message }}
|
||||
|
||||
{% if submission.newsletter_subscription or submission.privacy_consent %}
|
||||
Preferences:
|
||||
-----------
|
||||
{% if submission.newsletter_subscription %}Newsletter Subscription: Yes{% endif %}
|
||||
{% if submission.privacy_consent %}Privacy Policy Consent: Yes{% endif %}
|
||||
{% endif %}
|
||||
|
||||
---
|
||||
This email was automatically generated from the GNX website contact form.
|
||||
Please respond to the customer within 24 hours as per our service commitment.
|
||||
3
backEnd/contact/tests.py
Normal file
3
backEnd/contact/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
12
backEnd/contact/urls.py
Normal file
12
backEnd/contact/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ContactSubmissionViewSet
|
||||
|
||||
# Create a router and register our viewsets
|
||||
router = DefaultRouter()
|
||||
router.register(r'submissions', ContactSubmissionViewSet, basename='contact-submission')
|
||||
|
||||
# The API URLs are now determined automatically by the router
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
262
backEnd/contact/views.py
Normal file
262
backEnd/contact/views.py
Normal file
@@ -0,0 +1,262 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from django.db.models import Q
|
||||
from .models import ContactSubmission
|
||||
from .serializers import (
|
||||
ContactSubmissionSerializer,
|
||||
ContactSubmissionCreateSerializer,
|
||||
ContactSubmissionListSerializer,
|
||||
ContactSubmissionUpdateSerializer
|
||||
)
|
||||
from .email_service import (
|
||||
send_contact_submission_notification,
|
||||
send_contact_submission_confirmation,
|
||||
check_email_health,
|
||||
send_test_email
|
||||
)
|
||||
|
||||
|
||||
class ContactSubmissionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing contact form submissions.
|
||||
|
||||
Provides endpoints for:
|
||||
- Creating new contact submissions (public)
|
||||
- Listing submissions (admin only)
|
||||
- Retrieving individual submissions (admin only)
|
||||
- Updating submission status (admin only)
|
||||
"""
|
||||
|
||||
queryset = ContactSubmission.objects.all()
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status', 'priority', 'industry', 'company_size', 'project_type']
|
||||
search_fields = ['first_name', 'last_name', 'email', 'company', 'job_title']
|
||||
ordering_fields = ['created_at', 'updated_at', 'priority']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Return appropriate serializer class based on action.
|
||||
"""
|
||||
if self.action == 'create':
|
||||
return ContactSubmissionCreateSerializer
|
||||
elif self.action == 'list':
|
||||
return ContactSubmissionListSerializer
|
||||
elif self.action in ['update', 'partial_update']:
|
||||
return ContactSubmissionUpdateSerializer
|
||||
return ContactSubmissionSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
"""
|
||||
Set permissions based on action.
|
||||
"""
|
||||
if self.action == 'create':
|
||||
# Allow anyone to create contact submissions
|
||||
permission_classes = [AllowAny]
|
||||
else:
|
||||
# Require authentication for all other actions
|
||||
permission_classes = [IsAuthenticated]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create a new contact submission.
|
||||
Public endpoint for form submissions.
|
||||
"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Set initial priority based on company size and budget
|
||||
instance = serializer.save()
|
||||
self._set_initial_priority(instance)
|
||||
|
||||
# Send email notifications
|
||||
try:
|
||||
# Send notification to company email
|
||||
send_contact_submission_notification(instance)
|
||||
|
||||
# Send confirmation email to customer
|
||||
send_contact_submission_confirmation(instance)
|
||||
|
||||
except Exception as e:
|
||||
# Log the error but don't fail the submission
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Failed to send email notifications for submission #{instance.id}: {str(e)}")
|
||||
|
||||
# Return success response
|
||||
return Response({
|
||||
'message': 'Thank you for your submission! We\'ll contact you within 24 hours.',
|
||||
'submission_id': instance.id,
|
||||
'status': 'success'
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
def _set_initial_priority(self, instance):
|
||||
"""
|
||||
Set initial priority based on submission data.
|
||||
"""
|
||||
priority = 'medium' # default
|
||||
|
||||
# High priority for enterprise clients with large budgets
|
||||
if (instance.company_size in ['201-1000', '1000+'] and
|
||||
instance.budget in ['250k-500k', '500k-1m', 'over-1m']):
|
||||
priority = 'high'
|
||||
|
||||
# Urgent for immediate timeline with high budget
|
||||
elif (instance.timeline == 'immediate' and
|
||||
instance.budget in ['100k-250k', '250k-500k', '500k-1m', 'over-1m']):
|
||||
priority = 'urgent'
|
||||
|
||||
# Low priority for small companies with small budgets
|
||||
elif (instance.company_size in ['1-10', '11-50'] and
|
||||
instance.budget in ['under-50k', '50k-100k']):
|
||||
priority = 'low'
|
||||
|
||||
instance.priority = priority
|
||||
instance.save(update_fields=['priority'])
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def mark_contacted(self, request, pk=None):
|
||||
"""
|
||||
Mark a submission as contacted.
|
||||
"""
|
||||
submission = self.get_object()
|
||||
submission.status = 'contacted'
|
||||
submission.save(update_fields=['status'])
|
||||
|
||||
return Response({
|
||||
'message': 'Submission marked as contacted',
|
||||
'status': submission.status
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def mark_qualified(self, request, pk=None):
|
||||
"""
|
||||
Mark a submission as qualified.
|
||||
"""
|
||||
submission = self.get_object()
|
||||
submission.status = 'qualified'
|
||||
submission.save(update_fields=['status'])
|
||||
|
||||
return Response({
|
||||
'message': 'Submission marked as qualified',
|
||||
'status': submission.status
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def close_submission(self, request, pk=None):
|
||||
"""
|
||||
Close a submission.
|
||||
"""
|
||||
submission = self.get_object()
|
||||
submission.status = 'closed'
|
||||
submission.save(update_fields=['status'])
|
||||
|
||||
return Response({
|
||||
'message': 'Submission closed',
|
||||
'status': submission.status
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def stats(self, request):
|
||||
"""
|
||||
Get statistics about contact submissions.
|
||||
"""
|
||||
total = self.get_queryset().count()
|
||||
new = self.get_queryset().filter(status='new').count()
|
||||
in_progress = self.get_queryset().filter(status='in_progress').count()
|
||||
contacted = self.get_queryset().filter(status='contacted').count()
|
||||
qualified = self.get_queryset().filter(status='qualified').count()
|
||||
closed = self.get_queryset().filter(status='closed').count()
|
||||
|
||||
# Priority breakdown
|
||||
urgent = self.get_queryset().filter(priority='urgent').count()
|
||||
high = self.get_queryset().filter(priority='high').count()
|
||||
medium = self.get_queryset().filter(priority='medium').count()
|
||||
low = self.get_queryset().filter(priority='low').count()
|
||||
|
||||
# Enterprise clients
|
||||
enterprise = self.get_queryset().filter(
|
||||
company_size__in=['201-1000', '1000+']
|
||||
).count()
|
||||
|
||||
return Response({
|
||||
'total_submissions': total,
|
||||
'status_breakdown': {
|
||||
'new': new,
|
||||
'in_progress': in_progress,
|
||||
'contacted': contacted,
|
||||
'qualified': qualified,
|
||||
'closed': closed,
|
||||
},
|
||||
'priority_breakdown': {
|
||||
'urgent': urgent,
|
||||
'high': high,
|
||||
'medium': medium,
|
||||
'low': low,
|
||||
},
|
||||
'enterprise_clients': enterprise,
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def recent(self, request):
|
||||
"""
|
||||
Get recent submissions (last 7 days).
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
recent_date = datetime.now() - timedelta(days=7)
|
||||
recent_submissions = self.get_queryset().filter(
|
||||
created_at__gte=recent_date
|
||||
).order_by('-created_at')[:10]
|
||||
|
||||
serializer = ContactSubmissionListSerializer(recent_submissions, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def high_priority(self, request):
|
||||
"""
|
||||
Get high priority submissions.
|
||||
"""
|
||||
high_priority_submissions = self.get_queryset().filter(
|
||||
priority__in=['urgent', 'high']
|
||||
).order_by('-created_at')
|
||||
|
||||
serializer = ContactSubmissionListSerializer(high_priority_submissions, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def email_health(self, request):
|
||||
"""
|
||||
Check email service health for production monitoring.
|
||||
"""
|
||||
health_status = check_email_health()
|
||||
return Response(health_status)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def send_test_email(self, request):
|
||||
"""
|
||||
Send a test email to verify email configuration.
|
||||
"""
|
||||
email = request.data.get('email')
|
||||
if not email:
|
||||
return Response({
|
||||
'error': 'Email address is required'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
success = send_test_email(email)
|
||||
|
||||
if success:
|
||||
return Response({
|
||||
'message': f'Test email sent successfully to {email}',
|
||||
'status': 'success'
|
||||
})
|
||||
else:
|
||||
return Response({
|
||||
'error': 'Failed to send test email',
|
||||
'status': 'error'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
Reference in New Issue
Block a user