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