GNXSOFT.COM

This commit is contained in:
Iliyan Angelov
2025-09-26 00:15:37 +03:00
commit fe26b7cca4
16323 changed files with 2011881 additions and 0 deletions

3
gnx-react/.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

37
gnx-react/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,26 @@
"use client";
import Header from "@/components/shared/layout/header/Header";
import AboutBanner from "@/components/pages/about/AboutBanner";
import AboutServiceComponent from "@/components/pages/about/AboutService";
import Footer from "@/components/shared/layout/footer/Footer";
import AboutScrollProgressButton from "@/components/pages/about/AboutScrollProgressButton";
import AboutInitAnimations from "@/components/pages/about/AboutInitAnimations";
import AboutStarter from "@/components/pages/about/AboutStarter";
const page = () => {
return (
<div className="enterprise-about-page">
<Header />
<main>
<AboutBanner />
<AboutServiceComponent />
<AboutStarter />
</main>
<Footer />
<AboutScrollProgressButton />
<AboutInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,23 @@
import Header from "@/components/shared/layout/header/Header";
import BlogSingle from "@/components/pages/blog/BlogSingle";
import LatestPost from "@/components/pages/blog/LatestPost";
import Footer from "@/components/shared/layout/footer/Footer";
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
import BlogInitAnimations from "@/components/pages/blog/BlogInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<BlogSingle />
<LatestPost />
</main>
<Footer />
<BlogScrollProgressButton />
<BlogInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,21 @@
import Header from "@/components/shared/layout/header/Header";
import BlogItems from "@/components/pages/blog/BlogItems";
import Footer from "@/components/shared/layout/footer/Footer";
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
import BlogInitAnimations from "@/components/pages/blog/BlogInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<BlogItems />
</main>
<Footer />
<BlogScrollProgressButton />
<BlogInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,21 @@
import Header from "@/components/shared/layout/header/Header";
import JobSingle from "@/components/pages/career/JobSingle";
import Footer from "@/components/shared/layout/footer/Footer";
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<JobSingle />
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,27 @@
import Header from "@/components/shared/layout/header/Header";
import CareerBanner from "@/components/pages/career/CareerBanner";
import OpenPosition from "@/components/pages/career/OpenPosition";
import Thrive from "@/components/pages/career/Thrive";
import MasonryGallery from "@/components/pages/career/MasonryGallery";
import Footer from "@/components/shared/layout/footer/Footer";
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<CareerBanner />
<OpenPosition />
<Thrive />
<MasonryGallery />
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,25 @@
import Header from "@/components/shared/layout/header/Header";
import CaseSingle from "@/components/pages/case-study/CaseSingle";
import Process from "@/components/pages/case-study/Process";
import RelatedCase from "@/components/pages/case-study/RelatedCase";
import Footer from "@/components/shared/layout/footer/Footer";
import CaseStudyScrollProgressButton from "@/components/pages/case-study/CaseStudyScrollProgressButton";
import CaseStudyInitAnimations from "@/components/pages/case-study/CaseStudyInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<CaseSingle />
<Process />
<RelatedCase />
</main>
<Footer />
<CaseStudyScrollProgressButton />
<CaseStudyInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,21 @@
import Header from "@/components/shared/layout/header/Header";
import CaseItems from "@/components/pages/case-study/CaseItems";
import Footer from "@/components/shared/layout/footer/Footer";
import CaseStudyScrollProgressButton from "@/components/pages/case-study/CaseStudyScrollProgressButton";
import CaseStudyInitAnimations from "@/components/pages/case-study/CaseStudyInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<CaseItems />
</main>
<Footer />
<CaseStudyScrollProgressButton />
<CaseStudyInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,21 @@
import Header from "@/components/shared/layout/header/Header";
import ContactSection from "@/components/pages/contact/ContactSection";
import Footer from "@/components/shared/layout/footer/Footer";
import ContactScrollProgressButton from "@/components/pages/contact/ContactScrollProgressButton";
import ContactInitAnimations from "@/components/pages/contact/ContactInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<ContactSection />
</main>
<Footer />
<ContactScrollProgressButton />
<ContactInitAnimations />
</div>
);
};
export default page;

86
gnx-react/app/layout.tsx Normal file
View File

@@ -0,0 +1,86 @@
import type { Metadata } from "next";
import { Inter, Montserrat } from "next/font/google";
import "@/public/styles/main.scss";
import { CookieConsentProvider } from "@/components/shared/layout/CookieConsentContext";
import { CookieConsent } from "@/components/shared/layout/CookieConsent";
const montserrat = Montserrat({
subsets: ["latin"],
display: "swap",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--mont",
fallback: [
"-apple-system",
"Segoe UI",
"Roboto",
"Ubuntu",
"Fira Sans",
"Arial",
"sans-serif",
],
});
const inter = Inter({
subsets: ["latin"],
display: "swap",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--inter",
fallback: [
"-apple-system",
"Segoe UI",
"Roboto",
"Ubuntu",
"Fira Sans",
"Arial",
"sans-serif",
],
});
export const metadata: Metadata = {
title: "EnterpriseSoft Solutions | Enterprise Software Development & IT Solutions",
description: "Leading enterprise software development company providing custom solutions, system integrations, and digital transformation services for Fortune 500 companies",
keywords: [
"Enterprise Software",
"Custom Development",
"System Integration",
"Digital Transformation",
"Enterprise Solutions",
"Software Consulting",
"API Development",
"Cloud Migration",
],
authors: [
{
name: "EnterpriseSoft Solutions",
url: "https://enterprisesoft.com",
},
],
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.variable} ${montserrat.variable}`}>
<CookieConsentProvider
config={{
companyName: "EnterpriseSoft Solutions",
privacyPolicyUrl: "/privacy-policy",
cookiePolicyUrl: "/cookie-policy",
dataControllerEmail: "privacy@enterprisesoft.com",
retentionPeriod: 365,
enableAuditLog: true,
enableDetailedSettings: true,
showPrivacyNotice: true,
}}
>
{children}
<CookieConsent />
</CookieConsentProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,31 @@
import Link from "next/link";
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
import HomeInitAnimations from "@/components/pages/home/HomeInitAnimations";
const page = () => {
return (
<div className="tp-app">
<div className="tp-error pt-120 pb-120 text-center">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-8">
<h1 className="fw-7 text-uppercase mt-8">404 ERROR</h1>
<p className="text-xl fw-5 mt-12">Page Not Found</p>
<div className="mt-40 d-flex justify-content-center">
<Link href="/" className="btn-anim btn-anim-light">
Go Home
<i className="fa-solid fa-arrow-trend-up"></i>
<span></span>
</Link>
</div>
</div>
</div>
</div>
</div>
<HomeScrollProgressButton />
<HomeInitAnimations />
</div>
);
};
export default page;

29
gnx-react/app/page.tsx Normal file
View File

@@ -0,0 +1,29 @@
import Header from "@/components/shared/layout/header/Header";
import HomeBanner from "@/components/pages/home/HomeBanner";
import Overview from "@/components/pages/home/Overview";
import Story from "@/components/pages/home/Story";
import ServiceIntro from "@/components/pages/home/ServiceIntro";
import HomeLatestPost from "@/components/pages/home/HomeLatestPost";
import Footer from "@/components/shared/layout/footer/Footer";
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
import HomeInitAnimations from "@/components/pages/home/HomeInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<HomeBanner />
<Overview />
<Story />
<ServiceIntro />
<HomeLatestPost />
</main>
<Footer />
<HomeScrollProgressButton />
<HomeInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,85 @@
import { notFound } from 'next/navigation';
import Header from "@/components/shared/layout/header/Header";
import ServiceDetailsBanner from "@/components/shared/banners/ServiceDetailsBanner";
import ServiceDetails from "@/components/pages/services/ServiceDetails";
import ServiceFeatures from "@/components/pages/services/ServiceFeatures";
import ServiceDeliverables from "@/components/pages/services/ServiceDeliverables";
import ServiceProcess from "@/components/pages/services/ServiceProcess";
import Transform from "@/components/pages/services/Transform";
import Footer from "@/components/shared/layout/footer/Footer";
import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton";
import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnimations";
import { serviceService, Service } from "@/lib/api/serviceService";
interface ServicePageProps {
params: Promise<{
slug: string;
}>;
}
// Generate static params for all services (optional - for better performance)
export async function generateStaticParams() {
try {
const services = await serviceService.getServices();
return services.results.map((service: Service) => ({
slug: service.slug,
}));
} catch (error) {
console.error('Error generating static params:', error);
return [];
}
}
// Generate metadata for each service page
export async function generateMetadata({ params }: ServicePageProps) {
try {
const { slug } = await params;
const service = await serviceService.getServiceBySlug(slug);
return {
title: `${service.title} - GNX Services`,
description: service.description,
openGraph: {
title: service.title,
description: service.description,
images: service.image_url ? [service.image_url] : [],
},
};
} catch (error) {
return {
title: 'Service Not Found - GNX',
description: 'The requested service could not be found.',
};
}
}
const ServicePage = async ({ params }: ServicePageProps) => {
let service: Service;
try {
const { slug } = await params;
service = await serviceService.getServiceBySlug(slug);
} catch (error) {
console.error('Error fetching service:', error);
notFound();
}
return (
<div className="enterprise-app">
<Header />
<main className="enterprise-main">
<ServiceDetailsBanner service={service} />
<ServiceDetails service={service} />
<ServiceFeatures service={service} />
<ServiceDeliverables service={service} />
<Transform service={service} />
<ServiceProcess service={service} />
</main>
<Footer />
<ServicesScrollProgressButton />
<ServicesInitAnimations />
</div>
);
};
export default ServicePage;

View File

@@ -0,0 +1,23 @@
import Header from "@/components/shared/layout/header/Header";
import ServicesBanner from "@/components/pages/services/ServicesBanner";
import ServiceMain from "@/components/pages/services/ServiceMain";
import Footer from "@/components/shared/layout/footer/Footer";
import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton";
import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnimations";
const page = () => {
return (
<div className="enterprise-app">
<Header />
<main className="enterprise-main">
<ServicesBanner />
<ServiceMain />
</main>
<Footer />
<ServicesScrollProgressButton />
<ServicesInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,88 @@
# About Us API
This Django app provides API endpoints for managing about us page content.
## Models
### AboutBanner
- Main banner section with title, description, badge, CTA button, and image
- Related models: AboutStat, AboutSocialLink
### AboutService
- Service section with company information and features
- Related models: AboutFeature
### AboutProcess
- Development process section with methodology and steps
- Related models: AboutProcessStep
### AboutJourney
- Company journey section with milestones
- Related models: AboutMilestone
## API Endpoints
### Combined Endpoint
- `GET /api/about/page/` - Get all about page data in one request
### Individual Endpoints
#### Banner
- `GET /api/about/banner/` - List all active banners
- `GET /api/about/banner/{id}/` - Get specific banner
#### Service
- `GET /api/about/service/` - List all active services
- `GET /api/about/service/{id}/` - Get specific service
#### Process
- `GET /api/about/process/` - List all active processes
- `GET /api/about/process/{id}/` - Get specific process
#### Journey
- `GET /api/about/journey/` - List all active journeys
- `GET /api/about/journey/{id}/` - Get specific journey
## Management Commands
### Populate Sample Data
```bash
python manage.py populate_about_data
```
This command creates sample data for all about us sections.
## Frontend Integration
The frontend uses the following files:
- `lib/api/aboutService.ts` - API service for fetching data
- `lib/hooks/useAbout.ts` - React hooks for data management
- Components in `components/pages/about/` - Updated to use API data
## Usage Example
```typescript
import { useAbout } from '@/lib/hooks/useAbout';
const AboutPage = () => {
const { data, loading, error } = useAbout();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>{data?.banner.title}</h1>
<p>{data?.banner.description}</p>
{/* Render other sections */}
</div>
);
};
```
## Admin Interface
All models are available in the Django admin interface for easy content management:
- Navigate to `/admin/` after creating a superuser
- Manage about us content through the admin interface
- Upload images and manage relationships between models

View File

View File

@@ -0,0 +1,109 @@
from django.contrib import admin
from .models import (
AboutBanner, AboutStat, AboutSocialLink,
AboutService, AboutFeature,
AboutProcess, AboutProcessStep,
AboutJourney, AboutMilestone
)
class AboutStatInline(admin.TabularInline):
model = AboutStat
extra = 0
ordering = ['order']
class AboutSocialLinkInline(admin.TabularInline):
model = AboutSocialLink
extra = 0
ordering = ['order']
@admin.register(AboutBanner)
class AboutBannerAdmin(admin.ModelAdmin):
list_display = ['title', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['title', 'description']
inlines = [AboutStatInline, AboutSocialLinkInline]
readonly_fields = ['created_at', 'updated_at']
class AboutFeatureInline(admin.TabularInline):
model = AboutFeature
extra = 0
ordering = ['order']
@admin.register(AboutService)
class AboutServiceAdmin(admin.ModelAdmin):
list_display = ['title', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['title', 'description']
inlines = [AboutFeatureInline]
readonly_fields = ['created_at', 'updated_at']
class AboutProcessStepInline(admin.TabularInline):
model = AboutProcessStep
extra = 0
ordering = ['order']
@admin.register(AboutProcess)
class AboutProcessAdmin(admin.ModelAdmin):
list_display = ['title', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['title', 'description']
inlines = [AboutProcessStepInline]
readonly_fields = ['created_at', 'updated_at']
class AboutMilestoneInline(admin.TabularInline):
model = AboutMilestone
extra = 0
ordering = ['order']
@admin.register(AboutJourney)
class AboutJourneyAdmin(admin.ModelAdmin):
list_display = ['title', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['title', 'description']
inlines = [AboutMilestoneInline]
readonly_fields = ['created_at', 'updated_at']
# Register individual models for direct access
@admin.register(AboutStat)
class AboutStatAdmin(admin.ModelAdmin):
list_display = ['banner', 'number', 'label', 'order']
list_filter = ['banner']
ordering = ['banner', 'order']
@admin.register(AboutSocialLink)
class AboutSocialLinkAdmin(admin.ModelAdmin):
list_display = ['banner', 'platform', 'url', 'order']
list_filter = ['banner', 'platform']
ordering = ['banner', 'order']
@admin.register(AboutFeature)
class AboutFeatureAdmin(admin.ModelAdmin):
list_display = ['service', 'title', 'order']
list_filter = ['service']
ordering = ['service', 'order']
@admin.register(AboutProcessStep)
class AboutProcessStepAdmin(admin.ModelAdmin):
list_display = ['process', 'step_number', 'title', 'order']
list_filter = ['process']
ordering = ['process', 'order']
@admin.register(AboutMilestone)
class AboutMilestoneAdmin(admin.ModelAdmin):
list_display = ['journey', 'year', 'title', 'order']
list_filter = ['journey']
ordering = ['journey', 'order']

View File

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

View File

@@ -0,0 +1,220 @@
from django.core.management.base import BaseCommand
from about.models import (
AboutBanner, AboutStat, AboutSocialLink,
AboutService, AboutFeature,
AboutProcess, AboutProcessStep,
AboutJourney, AboutMilestone
)
class Command(BaseCommand):
help = 'Populate the database with sample about us data'
def handle(self, *args, **options):
self.stdout.write('Creating sample about us data...')
# Create About Banner
banner, created = AboutBanner.objects.get_or_create(
title="Powering Enterprise Digital Transformation",
defaults={
'subtitle': "Leading Enterprise Software Solutions",
'description': "We are a leading enterprise software company that empowers Fortune 500 companies and growing businesses with cutting-edge technology solutions. Our mission is to accelerate digital transformation through innovative software platforms, cloud infrastructure, and data-driven insights.",
'badge_text': "Enterprise Software Solutions",
'badge_icon': "fa-solid fa-building",
'cta_text': "Discover Enterprise Solutions",
'cta_link': "services",
'cta_icon': "fa-solid fa-arrow-trend-up",
'is_active': True
}
)
if created:
self.stdout.write(f'Created banner: {banner.title}')
# Create Banner Stats
stats_data = [
{'number': '500+', 'label': 'Enterprise Clients', 'order': 1},
{'number': '99.9%', 'label': 'Uptime SLA', 'order': 2},
{'number': '24/7', 'label': 'Enterprise Support', 'order': 3},
{'number': '15+', 'label': 'Years Experience', 'order': 4},
]
for stat_data in stats_data:
AboutStat.objects.create(banner=banner, **stat_data)
# Create Social Links
social_links_data = [
{
'platform': 'LinkedIn',
'url': 'https://www.linkedin.com/company/enterprisesoft-solutions',
'icon': 'fa-brands fa-linkedin-in',
'aria_label': 'Connect with us on LinkedIn',
'order': 1
},
{
'platform': 'GitHub',
'url': 'https://github.com/enterprisesoft',
'icon': 'fa-brands fa-github',
'aria_label': 'Follow us on GitHub',
'order': 2
},
{
'platform': 'Twitter',
'url': 'https://www.twitter.com/enterprisesoft',
'icon': 'fa-brands fa-twitter',
'aria_label': 'Follow us on Twitter',
'order': 3
},
{
'platform': 'Stack Overflow',
'url': 'https://stackoverflow.com/teams/enterprisesoft',
'icon': 'fa-brands fa-stack-overflow',
'aria_label': 'Visit our Stack Overflow team',
'order': 4
},
]
for social_data in social_links_data:
AboutSocialLink.objects.create(banner=banner, **social_data)
# Create About Service
service, created = AboutService.objects.get_or_create(
title="Enterprise Technology Leaders",
defaults={
'subtitle': "About Our Company",
'description': "Founded in 2008, EnterpriseSoft Solutions has emerged as a premier enterprise software company, serving Fortune 500 companies and innovative startups worldwide. Our team of 200+ engineers, architects, and consultants specializes in delivering mission-critical software solutions that drive digital transformation and business growth.",
'badge_text': "About Our Company",
'badge_icon': "fa-solid fa-users",
'cta_text': "Explore Our Solutions",
'cta_link': "service-single",
'is_active': True
}
)
if created:
self.stdout.write(f'Created service: {service.title}')
# Create Service Features
features_data = [
{
'title': 'Enterprise Security',
'description': 'SOC 2 Type II Certified',
'icon': 'fa-solid fa-shield-halved',
'order': 1
},
{
'title': 'Cloud Native',
'description': 'AWS, Azure, GCP Partners',
'icon': 'fa-solid fa-cloud',
'order': 2
},
{
'title': 'Certified Experts',
'description': 'Microsoft, AWS, Google Certified',
'icon': 'fa-solid fa-certificate',
'order': 3
},
{
'title': 'Global Reach',
'description': 'Offices in 5 Countries',
'icon': 'fa-solid fa-globe',
'order': 4
},
]
for feature_data in features_data:
AboutFeature.objects.create(service=service, **feature_data)
# Create About Process
process, created = AboutProcess.objects.get_or_create(
title="Enterprise Development Process",
defaults={
'subtitle': "Our Methodology",
'description': "Our proven enterprise development methodology combines agile practices with enterprise-grade security, scalability, and compliance requirements. We follow industry best practices including DevOps, CI/CD, and microservices architecture to deliver robust, scalable solutions.",
'badge_text': "Our Methodology",
'badge_icon': "fa-solid fa-cogs",
'cta_text': "View Our Services",
'cta_link': "service-single",
'is_active': True
}
)
if created:
self.stdout.write(f'Created process: {process.title}')
# Create Process Steps
steps_data = [
{
'step_number': '01',
'title': 'Discovery & Planning',
'description': 'Comprehensive analysis and architecture design',
'order': 1
},
{
'step_number': '02',
'title': 'Development & Testing',
'description': 'Agile development with continuous testing',
'order': 2
},
{
'step_number': '03',
'title': 'Deployment & Integration',
'description': 'Seamless deployment and system integration',
'order': 3
},
{
'step_number': '04',
'title': 'Support & Maintenance',
'description': '24/7 enterprise support and maintenance',
'order': 4
},
]
for step_data in steps_data:
AboutProcessStep.objects.create(process=process, **step_data)
# Create About Journey
journey, created = AboutJourney.objects.get_or_create(
title="From Startup to Enterprise Leader",
defaults={
'subtitle': "Our Journey",
'description': "Founded in 2008 by three visionary engineers, Itify Technologies began as a small startup with a big dream: to revolutionize how enterprises approach software development. What started as a passion project has grown into a global enterprise software company serving Fortune 500 clients worldwide.",
'badge_text': "Our Journey",
'badge_icon': "fa-solid fa-rocket",
'cta_text': "Explore Solutions",
'cta_link': "services",
'is_active': True
}
)
if created:
self.stdout.write(f'Created journey: {journey.title}')
# Create Journey Milestones
milestones_data = [
{
'year': '2008',
'title': 'Company Founded',
'description': 'Started with 3 engineers',
'order': 1
},
{
'year': '2015',
'title': 'Enterprise Focus',
'description': 'Pivoted to enterprise solutions',
'order': 2
},
{
'year': '2020',
'title': 'Global Expansion',
'description': 'Opened offices in 5 countries',
'order': 3
},
]
for milestone_data in milestones_data:
AboutMilestone.objects.create(journey=journey, **milestone_data)
self.stdout.write(
self.style.SUCCESS('Successfully populated about us data!')
)

View File

@@ -0,0 +1,180 @@
# Generated by Django 4.2.7 on 2025-09-25 16:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AboutBanner',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('subtitle', models.CharField(blank=True, max_length=100)),
('description', models.TextField()),
('badge_text', models.CharField(default='Enterprise Software Solutions', max_length=100)),
('badge_icon', models.CharField(default='fa-solid fa-building', max_length=50)),
('cta_text', models.CharField(default='Discover Enterprise Solutions', max_length=100)),
('cta_link', models.CharField(default='services', max_length=100)),
('cta_icon', models.CharField(default='fa-solid fa-arrow-trend-up', max_length=50)),
('image', models.ImageField(blank=True, null=True, upload_to='about/banner/')),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'About Banner',
'verbose_name_plural': 'About Banners',
},
),
migrations.CreateModel(
name='AboutJourney',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('subtitle', models.CharField(blank=True, max_length=100)),
('description', models.TextField()),
('badge_text', models.CharField(default='Our Journey', max_length=100)),
('badge_icon', models.CharField(default='fa-solid fa-rocket', max_length=50)),
('image', models.ImageField(blank=True, null=True, upload_to='about/journey/')),
('cta_text', models.CharField(default='Explore Solutions', max_length=100)),
('cta_link', models.CharField(default='services', max_length=100)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'About Journey',
'verbose_name_plural': 'About Journeys',
},
),
migrations.CreateModel(
name='AboutProcess',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('subtitle', models.CharField(blank=True, max_length=100)),
('description', models.TextField()),
('badge_text', models.CharField(default='Our Methodology', max_length=100)),
('badge_icon', models.CharField(default='fa-solid fa-cogs', max_length=50)),
('image', models.ImageField(blank=True, null=True, upload_to='about/process/')),
('cta_text', models.CharField(default='View Our Services', max_length=100)),
('cta_link', models.CharField(default='service-single', max_length=100)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'About Process',
'verbose_name_plural': 'About Processes',
},
),
migrations.CreateModel(
name='AboutService',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('subtitle', models.CharField(blank=True, max_length=100)),
('description', models.TextField()),
('badge_text', models.CharField(default='About Our Company', max_length=100)),
('badge_icon', models.CharField(default='fa-solid fa-users', max_length=50)),
('image', models.ImageField(blank=True, null=True, upload_to='about/services/')),
('cta_text', models.CharField(default='Explore Our Solutions', max_length=100)),
('cta_link', models.CharField(default='service-single', max_length=100)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'About Service',
'verbose_name_plural': 'About Services',
},
),
migrations.CreateModel(
name='AboutStat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.CharField(max_length=20)),
('label', models.CharField(max_length=100)),
('order', models.PositiveIntegerField(default=0)),
('banner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='about.aboutbanner')),
],
options={
'verbose_name': 'About Statistic',
'verbose_name_plural': 'About Statistics',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='AboutSocialLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('platform', models.CharField(max_length=50)),
('url', models.URLField()),
('icon', models.CharField(max_length=50)),
('aria_label', models.CharField(max_length=100)),
('order', models.PositiveIntegerField(default=0)),
('banner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_links', to='about.aboutbanner')),
],
options={
'verbose_name': 'About Social Link',
'verbose_name_plural': 'About Social Links',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='AboutProcessStep',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('step_number', models.CharField(max_length=10)),
('title', models.CharField(max_length=100)),
('description', models.CharField(max_length=200)),
('order', models.PositiveIntegerField(default=0)),
('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='about.aboutprocess')),
],
options={
'verbose_name': 'About Process Step',
'verbose_name_plural': 'About Process Steps',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='AboutMilestone',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.CharField(max_length=10)),
('title', models.CharField(max_length=100)),
('description', models.CharField(max_length=200)),
('order', models.PositiveIntegerField(default=0)),
('journey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='milestones', to='about.aboutjourney')),
],
options={
'verbose_name': 'About Milestone',
'verbose_name_plural': 'About Milestones',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='AboutFeature',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('description', models.CharField(max_length=200)),
('icon', models.CharField(max_length=50)),
('order', models.PositiveIntegerField(default=0)),
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='about.aboutservice')),
],
options={
'verbose_name': 'About Feature',
'verbose_name_plural': 'About Features',
'ordering': ['order'],
},
),
]

View File

@@ -0,0 +1,176 @@
from django.db import models
from django.utils import timezone
class AboutBanner(models.Model):
"""Model for About Us banner section"""
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=100, blank=True)
description = models.TextField()
badge_text = models.CharField(max_length=100, default="Enterprise Software Solutions")
badge_icon = models.CharField(max_length=50, default="fa-solid fa-building")
cta_text = models.CharField(max_length=100, default="Discover Enterprise Solutions")
cta_link = models.CharField(max_length=100, default="services")
cta_icon = models.CharField(max_length=50, default="fa-solid fa-arrow-trend-up")
image = models.ImageField(upload_to='about/banner/', null=True, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "About Banner"
verbose_name_plural = "About Banners"
def __str__(self):
return self.title
class AboutStat(models.Model):
"""Model for About Us statistics"""
banner = models.ForeignKey(AboutBanner, on_delete=models.CASCADE, related_name='stats')
number = models.CharField(max_length=20)
label = models.CharField(max_length=100)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
verbose_name = "About Statistic"
verbose_name_plural = "About Statistics"
def __str__(self):
return f"{self.number} - {self.label}"
class AboutSocialLink(models.Model):
"""Model for About Us social links"""
banner = models.ForeignKey(AboutBanner, on_delete=models.CASCADE, related_name='social_links')
platform = models.CharField(max_length=50)
url = models.URLField()
icon = models.CharField(max_length=50)
aria_label = models.CharField(max_length=100)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
verbose_name = "About Social Link"
verbose_name_plural = "About Social Links"
def __str__(self):
return f"{self.platform} - {self.banner.title}"
class AboutService(models.Model):
"""Model for About Us service section"""
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=100, blank=True)
description = models.TextField()
badge_text = models.CharField(max_length=100, default="About Our Company")
badge_icon = models.CharField(max_length=50, default="fa-solid fa-users")
image = models.ImageField(upload_to='about/services/', null=True, blank=True)
cta_text = models.CharField(max_length=100, default="Explore Our Solutions")
cta_link = models.CharField(max_length=100, default="service-single")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "About Service"
verbose_name_plural = "About Services"
def __str__(self):
return self.title
class AboutFeature(models.Model):
"""Model for About Us features"""
service = models.ForeignKey(AboutService, on_delete=models.CASCADE, related_name='features')
title = models.CharField(max_length=100)
description = models.CharField(max_length=200)
icon = models.CharField(max_length=50)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
verbose_name = "About Feature"
verbose_name_plural = "About Features"
def __str__(self):
return f"{self.title} - {self.service.title}"
class AboutProcess(models.Model):
"""Model for About Us process section"""
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=100, blank=True)
description = models.TextField()
badge_text = models.CharField(max_length=100, default="Our Methodology")
badge_icon = models.CharField(max_length=50, default="fa-solid fa-cogs")
image = models.ImageField(upload_to='about/process/', null=True, blank=True)
cta_text = models.CharField(max_length=100, default="View Our Services")
cta_link = models.CharField(max_length=100, default="service-single")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "About Process"
verbose_name_plural = "About Processes"
def __str__(self):
return self.title
class AboutProcessStep(models.Model):
"""Model for About Us process steps"""
process = models.ForeignKey(AboutProcess, on_delete=models.CASCADE, related_name='steps')
step_number = models.CharField(max_length=10)
title = models.CharField(max_length=100)
description = models.CharField(max_length=200)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
verbose_name = "About Process Step"
verbose_name_plural = "About Process Steps"
def __str__(self):
return f"Step {self.step_number}: {self.title}"
class AboutJourney(models.Model):
"""Model for About Us journey section"""
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=100, blank=True)
description = models.TextField()
badge_text = models.CharField(max_length=100, default="Our Journey")
badge_icon = models.CharField(max_length=50, default="fa-solid fa-rocket")
image = models.ImageField(upload_to='about/journey/', null=True, blank=True)
cta_text = models.CharField(max_length=100, default="Explore Solutions")
cta_link = models.CharField(max_length=100, default="services")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "About Journey"
verbose_name_plural = "About Journeys"
def __str__(self):
return self.title
class AboutMilestone(models.Model):
"""Model for About Us milestones"""
journey = models.ForeignKey(AboutJourney, on_delete=models.CASCADE, related_name='milestones')
year = models.CharField(max_length=10)
title = models.CharField(max_length=100)
description = models.CharField(max_length=200)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
verbose_name = "About Milestone"
verbose_name_plural = "About Milestones"
def __str__(self):
return f"{self.year}: {self.title}"

View File

@@ -0,0 +1,130 @@
from rest_framework import serializers
from .models import (
AboutBanner, AboutStat, AboutSocialLink,
AboutService, AboutFeature,
AboutProcess, AboutProcessStep,
AboutJourney, AboutMilestone
)
class AboutStatSerializer(serializers.ModelSerializer):
class Meta:
model = AboutStat
fields = ['number', 'label', 'order']
class AboutSocialLinkSerializer(serializers.ModelSerializer):
class Meta:
model = AboutSocialLink
fields = ['platform', 'url', 'icon', 'aria_label', 'order']
class AboutBannerSerializer(serializers.ModelSerializer):
stats = AboutStatSerializer(many=True, read_only=True)
social_links = AboutSocialLinkSerializer(many=True, read_only=True)
image_url = serializers.SerializerMethodField()
class Meta:
model = AboutBanner
fields = [
'id', 'title', 'subtitle', 'description', 'badge_text', 'badge_icon',
'cta_text', 'cta_link', 'cta_icon', 'image_url', 'is_active',
'stats', 'social_links', 'created_at', 'updated_at'
]
def get_image_url(self, obj):
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
class AboutFeatureSerializer(serializers.ModelSerializer):
class Meta:
model = AboutFeature
fields = ['title', 'description', 'icon', 'order']
class AboutServiceSerializer(serializers.ModelSerializer):
features = AboutFeatureSerializer(many=True, read_only=True)
image_url = serializers.SerializerMethodField()
class Meta:
model = AboutService
fields = [
'id', 'title', 'subtitle', 'description', 'badge_text', 'badge_icon',
'image_url', 'cta_text', 'cta_link', 'is_active',
'features', 'created_at', 'updated_at'
]
def get_image_url(self, obj):
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
class AboutProcessStepSerializer(serializers.ModelSerializer):
class Meta:
model = AboutProcessStep
fields = ['step_number', 'title', 'description', 'order']
class AboutProcessSerializer(serializers.ModelSerializer):
steps = AboutProcessStepSerializer(many=True, read_only=True)
image_url = serializers.SerializerMethodField()
class Meta:
model = AboutProcess
fields = [
'id', 'title', 'subtitle', 'description', 'badge_text', 'badge_icon',
'image_url', 'cta_text', 'cta_link', 'is_active',
'steps', 'created_at', 'updated_at'
]
def get_image_url(self, obj):
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
class AboutMilestoneSerializer(serializers.ModelSerializer):
class Meta:
model = AboutMilestone
fields = ['year', 'title', 'description', 'order']
class AboutJourneySerializer(serializers.ModelSerializer):
milestones = AboutMilestoneSerializer(many=True, read_only=True)
image_url = serializers.SerializerMethodField()
class Meta:
model = AboutJourney
fields = [
'id', 'title', 'subtitle', 'description', 'badge_text', 'badge_icon',
'image_url', 'cta_text', 'cta_link', 'is_active',
'milestones', 'created_at', 'updated_at'
]
def get_image_url(self, obj):
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
class AboutPageSerializer(serializers.Serializer):
"""Combined serializer for the entire about page"""
banner = AboutBannerSerializer()
service = AboutServiceSerializer()
process = AboutProcessSerializer()
journey = AboutJourneySerializer()

View File

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

View File

@@ -0,0 +1,25 @@
from django.urls import path
from . import views
app_name = 'about'
urlpatterns = [
# Combined about page data
path('page/', views.about_page_data, name='about-page-data'),
# Banner endpoints
path('banner/', views.AboutBannerListAPIView.as_view(), name='about-banner-list'),
path('banner/<int:pk>/', views.AboutBannerDetailAPIView.as_view(), name='about-banner-detail'),
# Service endpoints
path('service/', views.AboutServiceListAPIView.as_view(), name='about-service-list'),
path('service/<int:pk>/', views.AboutServiceDetailAPIView.as_view(), name='about-service-detail'),
# Process endpoints
path('process/', views.AboutProcessListAPIView.as_view(), name='about-process-list'),
path('process/<int:pk>/', views.AboutProcessDetailAPIView.as_view(), name='about-process-detail'),
# Journey endpoints
path('journey/', views.AboutJourneyListAPIView.as_view(), name='about-journey-list'),
path('journey/<int:pk>/', views.AboutJourneyDetailAPIView.as_view(), name='about-journey-detail'),
]

View File

@@ -0,0 +1,151 @@
from rest_framework import generics, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from .models import (
AboutBanner, AboutService, AboutProcess, AboutJourney
)
from .serializers import (
AboutBannerSerializer, AboutServiceSerializer,
AboutProcessSerializer, AboutJourneySerializer,
AboutPageSerializer
)
class AboutBannerListAPIView(generics.ListAPIView):
"""API view to get all active about banners"""
queryset = AboutBanner.objects.filter(is_active=True)
serializer_class = AboutBannerSerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutBannerDetailAPIView(generics.RetrieveAPIView):
"""API view to get a specific about banner"""
queryset = AboutBanner.objects.filter(is_active=True)
serializer_class = AboutBannerSerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutServiceListAPIView(generics.ListAPIView):
"""API view to get all active about services"""
queryset = AboutService.objects.filter(is_active=True)
serializer_class = AboutServiceSerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutServiceDetailAPIView(generics.RetrieveAPIView):
"""API view to get a specific about service"""
queryset = AboutService.objects.filter(is_active=True)
serializer_class = AboutServiceSerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutProcessListAPIView(generics.ListAPIView):
"""API view to get all active about processes"""
queryset = AboutProcess.objects.filter(is_active=True)
serializer_class = AboutProcessSerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutProcessDetailAPIView(generics.RetrieveAPIView):
"""API view to get a specific about process"""
queryset = AboutProcess.objects.filter(is_active=True)
serializer_class = AboutProcessSerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutJourneyListAPIView(generics.ListAPIView):
"""API view to get all active about journeys"""
queryset = AboutJourney.objects.filter(is_active=True)
serializer_class = AboutJourneySerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class AboutJourneyDetailAPIView(generics.RetrieveAPIView):
"""API view to get a specific about journey"""
queryset = AboutJourney.objects.filter(is_active=True)
serializer_class = AboutJourneySerializer
permission_classes = [AllowAny]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
@api_view(['GET'])
@permission_classes([AllowAny])
def about_page_data(request):
"""
API endpoint to get all about page data in one request
Returns banner, service, process, and journey data
"""
try:
# Get the first active instance of each section
banner = AboutBanner.objects.filter(is_active=True).first()
service = AboutService.objects.filter(is_active=True).first()
process = AboutProcess.objects.filter(is_active=True).first()
journey = AboutJourney.objects.filter(is_active=True).first()
if not all([banner, service, process, journey]):
return Response(
{'error': 'Some about page sections are not configured'},
status=status.HTTP_404_NOT_FOUND
)
# Serialize each section
banner_serializer = AboutBannerSerializer(banner, context={'request': request})
service_serializer = AboutServiceSerializer(service, context={'request': request})
process_serializer = AboutProcessSerializer(process, context={'request': request})
journey_serializer = AboutJourneySerializer(journey, context={'request': request})
data = {
'banner': banner_serializer.data,
'service': service_serializer.data,
'process': process_serializer.data,
'journey': journey_serializer.data
}
return Response(data, status=status.HTTP_200_OK)
except Exception as e:
return Response(
{'error': f'An error occurred: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

View 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

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

@@ -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.

View File

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

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

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)

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,16 @@
"""
ASGI config for gnx project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gnx.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,247 @@
"""
Django settings for gnx project.
Generated by 'django-admin startproject' using Django 4.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
import os
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=True, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'rest_framework',
'corsheaders',
'django_filters',
'drf_yasg',
# Local apps
'contact',
'services',
'about',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'gnx.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'gnx.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Media files (User uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# File upload settings
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
FILE_UPLOAD_PERMISSIONS = 0o644
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Django REST Framework Configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
}
# CORS Configuration
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # React development server
"http://127.0.0.1:3000",
"http://localhost:3001",
"http://127.0.0.1:3001",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only allow all origins in development
# Email Configuration
# Production email settings - use SMTP backend
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = config('EMAIL_HOST', default='mail.gnxsoft.com')
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=False, cast=bool)
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL')
# Email timeout settings for production
EMAIL_TIMEOUT = config('EMAIL_TIMEOUT', default=30, cast=int)
# Company email for contact form notifications
COMPANY_EMAIL = config('COMPANY_EMAIL')
# Email connection settings for production reliability
EMAIL_CONNECTION_TIMEOUT = config('EMAIL_CONNECTION_TIMEOUT', default=10, cast=int)
EMAIL_READ_TIMEOUT = config('EMAIL_READ_TIMEOUT', default=10, cast=int)
# Logging Configuration
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': BASE_DIR / 'logs' / 'django.log',
'formatter': 'verbose',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
'loggers': {
'django': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propagate': False,
},
'contact': {
'handlers': ['file', 'console'],
'level': 'DEBUG',
'propagate': False,
},
},
}

View File

@@ -0,0 +1,57 @@
"""
URL configuration for gnx project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
# API Documentation Schema
schema_view = get_schema_view(
openapi.Info(
title="GNX API",
default_version='v1',
description="API for GNX Software Solutions - Contact form submissions and Services management",
terms_of_service="https://www.gnxsoft.com/terms/",
contact=openapi.Contact(email="info@gnxsoft.com"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [
path('admin/', admin.site.urls),
# API Documentation
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'),
# API Root - All API endpoints under /api/
path('api/', include([
path('contact/', include('contact.urls')),
path('services/', include('services.urls')),
path('about/', include('about.urls')),
])),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -0,0 +1,16 @@
"""
WSGI config for gnx project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gnx.settings')
application = get_wsgi_application()

File diff suppressed because it is too large Load Diff

22
gnx-react/backend/manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gnx.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -0,0 +1,48 @@
# Production Environment Configuration for GNX Contact Form
# Copy this file to .env and update with your actual values
# Django Settings
SECRET_KEY=your-super-secret-production-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com,your-server-ip
# Database (Production)
DATABASE_URL=postgresql://username:password@localhost:5432/gnx_production
# Email Configuration (Production)
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_USE_SSL=False
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=noreply@gnxsoft.com
# Company email for contact form notifications
COMPANY_EMAIL=contact@gnxsoft.com
# Email timeout settings for production reliability
EMAIL_TIMEOUT=30
EMAIL_CONNECTION_TIMEOUT=10
EMAIL_READ_TIMEOUT=10
# Security Settings
SECURE_SSL_REDIRECT=True
SECURE_HSTS_SECONDS=31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS=True
SECURE_HSTS_PRELOAD=True
SECURE_CONTENT_TYPE_NOSNIFF=True
SECURE_BROWSER_XSS_FILTER=True
X_FRAME_OPTIONS=DENY
# CORS Settings (Production)
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
CORS_ALLOW_CREDENTIALS=True
# Static Files
STATIC_ROOT=/var/www/gnx/staticfiles/
MEDIA_ROOT=/var/www/gnx/media/
# Logging
LOG_LEVEL=INFO

View File

View File

@@ -0,0 +1,114 @@
from django.contrib import admin
from django.utils.html import format_html
from .models import Service, ServiceFeature, ServiceExpertise, ServiceCategory
class ServiceFeatureInline(admin.TabularInline):
model = ServiceFeature
extra = 0
fields = ['title', 'description', 'icon', 'display_order']
ordering = ['display_order']
class ServiceExpertiseInline(admin.TabularInline):
model = ServiceExpertise
extra = 0
fields = ['title', 'description', 'icon', 'display_order']
ordering = ['display_order']
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
list_display = ['title', 'slug', 'category', 'price', 'duration', 'deliverables_preview', 'featured', 'display_order', 'is_active', 'created_at']
list_filter = ['featured', 'is_active', 'category', 'created_at']
search_fields = ['title', 'description', 'slug', 'short_description', 'technologies', 'deliverables']
prepopulated_fields = {'slug': ('title',)}
ordering = ['display_order', 'title']
inlines = [ServiceFeatureInline, ServiceExpertiseInline]
actions = ['mark_as_featured', 'mark_as_not_featured']
fieldsets = (
('Basic Information', {
'fields': ('title', 'slug', 'description', 'short_description', 'icon', 'image', 'image_url')
}),
('Category & Classification', {
'fields': ('category', 'featured', 'display_order', 'is_active')
}),
('Project Details', {
'fields': ('duration', 'deliverables', 'technologies', 'process_steps'),
'description': 'Define what the client will receive and the project timeline'
}),
('Section Descriptions', {
'fields': ('features_description', 'deliverables_description', 'process_description', 'why_choose_description', 'expertise_description'),
'description': 'Customize descriptions for each section on the service detail page',
'classes': ('collapse',)
}),
('Pricing', {
'fields': ('price',)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# Make deliverables field more prominent
if 'deliverables' in form.base_fields:
form.base_fields['deliverables'].widget.attrs.update({
'rows': 4,
'cols': 80,
'placeholder': 'Enter what the client will receive (e.g., Complete web application, Admin dashboard, Documentation, Testing, Deployment)'
})
return form
def deliverables_preview(self, obj):
"""Display a preview of deliverables in the list view"""
if obj.deliverables:
# Show first 50 characters of deliverables
preview = obj.deliverables[:50]
if len(obj.deliverables) > 50:
preview += "..."
return format_html('<span title="{}">{}</span>', obj.deliverables, preview)
return format_html('<span style="color: #999;">No deliverables defined</span>')
deliverables_preview.short_description = 'Deliverables Preview'
def mark_as_featured(self, request, queryset):
"""Admin action to mark services as featured"""
updated = queryset.update(featured=True)
self.message_user(request, f'{updated} service(s) marked as featured.')
mark_as_featured.short_description = "Mark selected services as featured"
def mark_as_not_featured(self, request, queryset):
"""Admin action to mark services as not featured"""
updated = queryset.update(featured=False)
self.message_user(request, f'{updated} service(s) marked as not featured.')
mark_as_not_featured.short_description = "Mark selected services as not featured"
@admin.register(ServiceFeature)
class ServiceFeatureAdmin(admin.ModelAdmin):
list_display = ['title', 'service', 'display_order']
list_filter = ['service', 'display_order']
search_fields = ['title', 'description', 'service__title']
ordering = ['service', 'display_order']
@admin.register(ServiceExpertise)
class ServiceExpertiseAdmin(admin.ModelAdmin):
list_display = ['title', 'service', 'display_order']
list_filter = ['service', 'display_order']
search_fields = ['title', 'description', 'service__title']
ordering = ['service', 'display_order']
@admin.register(ServiceCategory)
class ServiceCategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'display_order', 'is_active']
list_filter = ['is_active', 'display_order']
search_fields = ['name', 'description', 'slug']
prepopulated_fields = {'slug': ('name',)}
ordering = ['display_order', 'name']

View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ServicesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'services'
verbose_name = 'Services'

Some files were not shown because too many files have changed in this diff Show More