This commit is contained in:
Iliyan Angelov
2025-11-24 03:52:08 +02:00
parent dfcaebaf8c
commit 366f28677a
18241 changed files with 865352 additions and 567 deletions

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

283
backEnd/contact/admin.py Normal file
View 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
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ContactConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'contact'

View 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

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

View File

190
backEnd/contact/models.py Normal file
View 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)

View 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

View 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>

View File

@@ -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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
backEnd/contact/urls.py Normal file
View 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
View 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)