update
This commit is contained in:
14
backEnd/.env
Normal file
14
backEnd/.env
Normal file
@@ -0,0 +1,14 @@
|
||||
# Development Environment Configuration
|
||||
# Django Settings
|
||||
SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
# Email Configuration (Development - uses console backend by default)
|
||||
USE_SMTP_IN_DEV=False
|
||||
DEFAULT_FROM_EMAIL=support@gnxsoft.com
|
||||
COMPANY_EMAIL=support@gnxsoft.com
|
||||
SUPPORT_EMAIL=support@gnxsoft.com
|
||||
|
||||
# Site URL
|
||||
SITE_URL=http://localhost:3000
|
||||
88
backEnd/about/README.md
Normal file
88
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
backEnd/about/__init__.py
Normal file
0
backEnd/about/__init__.py
Normal file
BIN
backEnd/about/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/about/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/about/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/about/__pycache__/models.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/about/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/about/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/about/__pycache__/views.cpython-312.pyc
Normal file
BIN
backEnd/about/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
109
backEnd/about/admin.py
Normal file
109
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
backEnd/about/apps.py
Normal file
6
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
backEnd/about/management/__init__.py
Normal file
0
backEnd/about/management/__init__.py
Normal file
BIN
backEnd/about/management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/about/management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
0
backEnd/about/management/commands/__init__.py
Normal file
0
backEnd/about/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,515 @@
|
||||
"""
|
||||
Management command to import enterprise-grade sample data for About Us
|
||||
with images downloaded from Unsplash.
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
from io import BytesIO
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from about.models import (
|
||||
AboutBanner, AboutStat, AboutSocialLink,
|
||||
AboutService, AboutFeature,
|
||||
AboutProcess, AboutProcessStep,
|
||||
AboutJourney, AboutMilestone
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Import enterprise-grade sample data for About Us with images from Unsplash'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--unsplash-key',
|
||||
type=str,
|
||||
help='Unsplash API access key (optional, uses Unsplash Source API if not provided)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--width',
|
||||
type=int,
|
||||
default=1200,
|
||||
help='Image width in pixels (default: 1200)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--height',
|
||||
type=int,
|
||||
default=800,
|
||||
help='Image height in pixels (default: 800)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--update-existing',
|
||||
action='store_true',
|
||||
help='Update existing about data instead of skipping it',
|
||||
)
|
||||
|
||||
def download_image_from_unsplash(self, keyword, width=1200, height=800, api_key=None):
|
||||
"""
|
||||
Download an image from Unsplash based on keyword.
|
||||
|
||||
Args:
|
||||
keyword: Search keyword for the image
|
||||
width: Image width in pixels
|
||||
height: Image height in pixels
|
||||
api_key: Optional Unsplash API access key
|
||||
|
||||
Returns:
|
||||
BytesIO object containing the image data, or None if download fails
|
||||
"""
|
||||
try:
|
||||
if api_key:
|
||||
# Use Unsplash API (requires API key)
|
||||
url = "https://api.unsplash.com/photos/random"
|
||||
params = {
|
||||
'query': keyword,
|
||||
'orientation': 'landscape',
|
||||
'w': width,
|
||||
'h': height,
|
||||
'client_id': api_key
|
||||
}
|
||||
query_string = urllib.parse.urlencode(params)
|
||||
full_url = f"{url}?{query_string}"
|
||||
|
||||
req = urllib.request.Request(full_url)
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
image_url = data['urls']['regular']
|
||||
else:
|
||||
# Use Unsplash's direct image service (no key required)
|
||||
# Using curated enterprise/business images
|
||||
image_url = f"https://images.unsplash.com/photo-1552664730-d307ca884978?w={width}&h={height}&fit=crop"
|
||||
|
||||
# Download the actual image
|
||||
req = urllib.request.Request(image_url)
|
||||
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
|
||||
with urllib.request.urlopen(req, timeout=30) as img_response:
|
||||
image_data = BytesIO(img_response.read())
|
||||
image_data.seek(0)
|
||||
|
||||
return image_data
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Failed to download image for "{keyword}": {str(e)}')
|
||||
)
|
||||
return None
|
||||
|
||||
def handle(self, *args, **options):
|
||||
unsplash_key = options.get('unsplash_key')
|
||||
width = options.get('width', 1200)
|
||||
height = options.get('height', 800)
|
||||
update_existing = options.get('update_existing', False)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Starting enterprise About Us data import...'))
|
||||
|
||||
with transaction.atomic():
|
||||
# Clear existing data if updating
|
||||
if update_existing:
|
||||
self.stdout.write('Clearing existing about data...')
|
||||
AboutBanner.objects.all().delete()
|
||||
AboutService.objects.all().delete()
|
||||
AboutProcess.objects.all().delete()
|
||||
AboutJourney.objects.all().delete()
|
||||
|
||||
# Create About Banner
|
||||
banner_data = {
|
||||
'title': 'Enterprise Software Solutions for Mission-Critical Industries',
|
||||
'subtitle': 'GNX Soft Ltd - Your Trusted Enterprise Technology Partner',
|
||||
'description': 'GNX Soft Ltd is a leading Bulgarian enterprise software company delivering cutting-edge technology solutions to mission-critical industries worldwide. We empower organizations in Defense & Aerospace, Healthcare & Medical, Telecommunication, Banking, Public Sector, E-commerce, Food & Beverages, and Oil & Energy sectors with innovative, secure, and scalable software platforms that drive digital transformation and operational excellence. Our commitment to EU-based infrastructure, privacy-by-design principles, and defense-grade security makes us the preferred technology partner for organizations that demand the highest levels of reliability, compliance, and data protection.',
|
||||
'badge_text': 'Enterprise Software Solutions',
|
||||
'badge_icon': 'fa-solid fa-building',
|
||||
'cta_text': 'Explore Enterprise Solutions',
|
||||
'cta_link': 'services',
|
||||
'cta_icon': 'fa-solid fa-arrow-trend-up',
|
||||
'is_active': True
|
||||
}
|
||||
|
||||
banner, created = AboutBanner.objects.get_or_create(
|
||||
title=banner_data['title'],
|
||||
defaults=banner_data
|
||||
)
|
||||
|
||||
if not created and update_existing:
|
||||
for key, value in banner_data.items():
|
||||
setattr(banner, key, value)
|
||||
banner.save()
|
||||
|
||||
if created or update_existing:
|
||||
self.stdout.write(f'{"Created" if created else "Updated"} banner: {banner.title}')
|
||||
|
||||
# Download banner image
|
||||
self.stdout.write('Downloading banner image...')
|
||||
image_data = self.download_image_from_unsplash(
|
||||
'enterprise business technology office',
|
||||
width=width,
|
||||
height=height,
|
||||
api_key=unsplash_key
|
||||
)
|
||||
if image_data:
|
||||
try:
|
||||
filename = 'about-banner.jpg'
|
||||
banner.image.save(
|
||||
filename,
|
||||
ContentFile(image_data.read()),
|
||||
save=True
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(' ✓ Banner image saved'))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f' ✗ Failed to save banner image: {str(e)}'))
|
||||
|
||||
# Clear and recreate stats
|
||||
if update_existing:
|
||||
AboutStat.objects.filter(banner=banner).delete()
|
||||
|
||||
# Create Banner Stats
|
||||
stats_data = [
|
||||
{'number': '8', 'label': 'Industry Verticals', 'order': 1},
|
||||
{'number': '99.9%', 'label': 'Uptime SLA', 'order': 2},
|
||||
{'number': '24/7', 'label': 'Enterprise Support', 'order': 3},
|
||||
{'number': '500+', 'label': 'Enterprise Clients', 'order': 4},
|
||||
{'number': '2020', 'label': 'Founded', 'order': 5},
|
||||
]
|
||||
|
||||
for stat_data in stats_data:
|
||||
AboutStat.objects.get_or_create(
|
||||
banner=banner,
|
||||
label=stat_data['label'],
|
||||
defaults=stat_data
|
||||
)
|
||||
|
||||
# Clear and recreate social links
|
||||
if update_existing:
|
||||
AboutSocialLink.objects.filter(banner=banner).delete()
|
||||
|
||||
# Create Social Links
|
||||
social_links_data = [
|
||||
{
|
||||
'platform': 'LinkedIn',
|
||||
'url': 'https://www.linkedin.com/company/gnxsoft',
|
||||
'icon': 'fa-brands fa-linkedin-in',
|
||||
'aria_label': 'Connect with GNX Soft on LinkedIn',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'platform': 'GitHub',
|
||||
'url': 'https://github.com/gnxsoft',
|
||||
'icon': 'fa-brands fa-github',
|
||||
'aria_label': 'Follow GNX Soft on GitHub',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'platform': 'Twitter',
|
||||
'url': 'https://twitter.com/gnxsoft',
|
||||
'icon': 'fa-brands fa-twitter',
|
||||
'aria_label': 'Follow GNX Soft on Twitter',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'platform': 'Email',
|
||||
'url': 'mailto:info@gnxsoft.com',
|
||||
'icon': 'fa-solid fa-envelope',
|
||||
'aria_label': 'Contact GNX Soft via email',
|
||||
'order': 4
|
||||
},
|
||||
]
|
||||
|
||||
for social_data in social_links_data:
|
||||
AboutSocialLink.objects.get_or_create(
|
||||
banner=banner,
|
||||
platform=social_data['platform'],
|
||||
defaults=social_data
|
||||
)
|
||||
|
||||
# Create About Service
|
||||
service_data = {
|
||||
'title': 'Enterprise Technology Excellence Across Critical Industries',
|
||||
'subtitle': 'About GNX Soft Ltd',
|
||||
'description': 'Founded in 2020 and headquartered in Burgas, Bulgaria, GNX Soft Ltd is a premier enterprise software development company specializing in mission-critical solutions for highly regulated industries. Our expert team of 50+ engineers, architects, and consultants delivers secure, scalable, and compliant software solutions to Defense & Aerospace, Healthcare & Medical, Telecommunication, Banking & Finance, Public Sector, E-commerce, Food & Beverages, and Oil & Energy sectors. With EU-based infrastructure spanning Bulgaria, Germany, and Netherlands, we provide enterprise-grade solutions that meet the highest security and regulatory standards including GDPR, HIPAA, PCI-DSS, and industry-specific compliance requirements. Our commitment to privacy-by-design, defense-grade security, and continuous innovation positions us as a trusted technology partner for Fortune 500 companies and government organizations worldwide.',
|
||||
'badge_text': 'About GNX Soft Ltd',
|
||||
'badge_icon': 'fa-solid fa-users',
|
||||
'cta_text': 'Explore Our Solutions',
|
||||
'cta_link': 'services',
|
||||
'is_active': True
|
||||
}
|
||||
|
||||
service, created = AboutService.objects.get_or_create(
|
||||
title=service_data['title'],
|
||||
defaults=service_data
|
||||
)
|
||||
|
||||
if not created and update_existing:
|
||||
for key, value in service_data.items():
|
||||
setattr(service, key, value)
|
||||
service.save()
|
||||
|
||||
if created or update_existing:
|
||||
self.stdout.write(f'{"Created" if created else "Updated"} service: {service.title}')
|
||||
|
||||
# Download service image
|
||||
self.stdout.write('Downloading service image...')
|
||||
image_data = self.download_image_from_unsplash(
|
||||
'enterprise team collaboration technology',
|
||||
width=width,
|
||||
height=height,
|
||||
api_key=unsplash_key
|
||||
)
|
||||
if image_data:
|
||||
try:
|
||||
filename = 'about-service.jpg'
|
||||
service.image.save(
|
||||
filename,
|
||||
ContentFile(image_data.read()),
|
||||
save=True
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(' ✓ Service image saved'))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f' ✗ Failed to save service image: {str(e)}'))
|
||||
|
||||
# Clear and recreate features
|
||||
if update_existing:
|
||||
AboutFeature.objects.filter(service=service).delete()
|
||||
|
||||
# Create Service Features
|
||||
features_data = [
|
||||
{
|
||||
'title': 'EU-Based Company',
|
||||
'description': 'Headquartered in Burgas, Bulgaria with EU-wide presence',
|
||||
'icon': 'fa-solid fa-building',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'title': 'EU Infrastructure',
|
||||
'description': 'Data centers in Bulgaria, Germany, and Netherlands',
|
||||
'icon': 'fa-solid fa-server',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'title': '8 Industry Verticals',
|
||||
'description': 'Specialized expertise across critical sectors',
|
||||
'icon': 'fa-solid fa-industry',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'title': '24/7 Enterprise Support',
|
||||
'description': 'Round-the-clock support with SLA guarantees',
|
||||
'icon': 'fa-solid fa-headset',
|
||||
'order': 4
|
||||
},
|
||||
{
|
||||
'title': '500+ Enterprise Clients',
|
||||
'description': 'Trusted by Fortune 500 and government organizations',
|
||||
'icon': 'fa-solid fa-users',
|
||||
'order': 5
|
||||
},
|
||||
{
|
||||
'title': 'Defense-Grade Security',
|
||||
'description': 'Bank-level security with compliance certifications',
|
||||
'icon': 'fa-solid fa-shield-halved',
|
||||
'order': 6
|
||||
},
|
||||
]
|
||||
|
||||
for feature_data in features_data:
|
||||
AboutFeature.objects.get_or_create(
|
||||
service=service,
|
||||
title=feature_data['title'],
|
||||
defaults=feature_data
|
||||
)
|
||||
|
||||
# Create About Process
|
||||
process_data = {
|
||||
'title': 'Enterprise-Grade Development Methodology',
|
||||
'subtitle': 'Our Methodology',
|
||||
'description': 'GNX Soft Ltd employs a proven enterprise development methodology that combines agile practices with defense-grade security, regulatory compliance, and enterprise scalability. We follow industry best practices including DevOps, CI/CD, microservices architecture, and privacy-by-design principles to deliver robust, secure, and compliant solutions for highly regulated industries. Every project undergoes rigorous security assessments, Data Protection Impact Assessments (DPIAs), penetration testing, and compliance verification to meet the strictest industry standards including GDPR, HIPAA, PCI-DSS, SOC 2, and ISO 27001. Our methodology emphasizes continuous integration, automated testing, infrastructure as code, and comprehensive documentation to ensure long-term maintainability and scalability.',
|
||||
'badge_text': 'Our Methodology',
|
||||
'badge_icon': 'fa-solid fa-cogs',
|
||||
'cta_text': 'View Our Services',
|
||||
'cta_link': 'services',
|
||||
'is_active': True
|
||||
}
|
||||
|
||||
process, created = AboutProcess.objects.get_or_create(
|
||||
title=process_data['title'],
|
||||
defaults=process_data
|
||||
)
|
||||
|
||||
if not created and update_existing:
|
||||
for key, value in process_data.items():
|
||||
setattr(process, key, value)
|
||||
process.save()
|
||||
|
||||
if created or update_existing:
|
||||
self.stdout.write(f'{"Created" if created else "Updated"} process: {process.title}')
|
||||
|
||||
# Download process image
|
||||
self.stdout.write('Downloading process image...')
|
||||
image_data = self.download_image_from_unsplash(
|
||||
'enterprise development methodology process',
|
||||
width=width,
|
||||
height=height,
|
||||
api_key=unsplash_key
|
||||
)
|
||||
if image_data:
|
||||
try:
|
||||
filename = 'about-process.jpg'
|
||||
process.image.save(
|
||||
filename,
|
||||
ContentFile(image_data.read()),
|
||||
save=True
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(' ✓ Process image saved'))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f' ✗ Failed to save process image: {str(e)}'))
|
||||
|
||||
# Clear and recreate process steps
|
||||
if update_existing:
|
||||
AboutProcessStep.objects.filter(process=process).delete()
|
||||
|
||||
# Create Process Steps
|
||||
steps_data = [
|
||||
{
|
||||
'step_number': '01',
|
||||
'title': 'Discovery & Compliance Assessment',
|
||||
'description': 'Requirements analysis, regulatory assessment, DPIA, and security planning',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'step_number': '02',
|
||||
'title': 'Secure Development & Architecture',
|
||||
'description': 'Privacy-by-design, secure coding practices, microservices architecture, and continuous testing',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'step_number': '03',
|
||||
'title': 'Deployment & Integration',
|
||||
'description': 'EU infrastructure deployment, system integration, and performance optimization',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'step_number': '04',
|
||||
'title': 'Security Audit & Compliance',
|
||||
'description': 'Penetration testing, security audits, compliance verification, and certification',
|
||||
'order': 4
|
||||
},
|
||||
{
|
||||
'step_number': '05',
|
||||
'title': 'Support & Continuous Monitoring',
|
||||
'description': '24/7 enterprise support, monitoring, breach response, and continuous improvement',
|
||||
'order': 5
|
||||
},
|
||||
]
|
||||
|
||||
for step_data in steps_data:
|
||||
AboutProcessStep.objects.get_or_create(
|
||||
process=process,
|
||||
step_number=step_data['step_number'],
|
||||
defaults=step_data
|
||||
)
|
||||
|
||||
# Create About Journey
|
||||
journey_data = {
|
||||
'title': 'Building Enterprise Excellence Since 2020',
|
||||
'subtitle': 'Our Journey',
|
||||
'description': 'Founded in 2020 in Burgas, Bulgaria, GNX Soft Ltd was established with a clear mission: to deliver world-class enterprise software solutions for mission-critical industries while maintaining the highest standards of security, compliance, and data protection. From our inception, we focused exclusively on enterprise clients in highly regulated sectors including Defense & Aerospace, Healthcare & Medical, Banking, Public Sector, Telecommunication, E-commerce, Food & Beverages, and Oil & Energy. Our commitment to EU-based infrastructure, privacy-by-design principles, and defense-grade security has positioned us as a trusted technology partner for organizations that demand the highest levels of security and regulatory adherence. Today, we serve 500+ enterprise clients across 8 industry verticals, with a team of 50+ experts and EU-wide infrastructure supporting mission-critical operations 24/7.',
|
||||
'badge_text': 'Our Journey',
|
||||
'badge_icon': 'fa-solid fa-rocket',
|
||||
'cta_text': 'Explore Solutions',
|
||||
'cta_link': 'services',
|
||||
'is_active': True
|
||||
}
|
||||
|
||||
journey, created = AboutJourney.objects.get_or_create(
|
||||
title=journey_data['title'],
|
||||
defaults=journey_data
|
||||
)
|
||||
|
||||
if not created and update_existing:
|
||||
for key, value in journey_data.items():
|
||||
setattr(journey, key, value)
|
||||
journey.save()
|
||||
|
||||
if created or update_existing:
|
||||
self.stdout.write(f'{"Created" if created else "Updated"} journey: {journey.title}')
|
||||
|
||||
# Download journey image
|
||||
self.stdout.write('Downloading journey image...')
|
||||
image_data = self.download_image_from_unsplash(
|
||||
'enterprise growth journey success',
|
||||
width=width,
|
||||
height=height,
|
||||
api_key=unsplash_key
|
||||
)
|
||||
if image_data:
|
||||
try:
|
||||
filename = 'about-journey.jpg'
|
||||
journey.image.save(
|
||||
filename,
|
||||
ContentFile(image_data.read()),
|
||||
save=True
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(' ✓ Journey image saved'))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f' ✗ Failed to save journey image: {str(e)}'))
|
||||
|
||||
# Clear and recreate milestones
|
||||
if update_existing:
|
||||
AboutMilestone.objects.filter(journey=journey).delete()
|
||||
|
||||
# Create Journey Milestones
|
||||
milestones_data = [
|
||||
{
|
||||
'year': '2020',
|
||||
'title': 'Company Founded',
|
||||
'description': 'GNX Soft Ltd established in Burgas, Bulgaria with focus on enterprise solutions',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'year': '2021',
|
||||
'title': 'Industry Specialization',
|
||||
'description': 'Specialized in 8 mission-critical industries with first enterprise clients',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'year': '2022',
|
||||
'title': 'EU Infrastructure Expansion',
|
||||
'description': 'Deployed EU-wide data centers across Bulgaria, Germany, and Netherlands',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'year': '2023',
|
||||
'title': 'Enterprise Growth',
|
||||
'description': 'Reached 300+ enterprise clients with 50+ expert team members',
|
||||
'order': 4
|
||||
},
|
||||
{
|
||||
'year': '2024',
|
||||
'title': 'Industry Leadership',
|
||||
'description': 'Recognized as leading Bulgarian enterprise software provider with 500+ clients',
|
||||
'order': 5
|
||||
},
|
||||
{
|
||||
'year': '2025',
|
||||
'title': 'Global Expansion',
|
||||
'description': 'Expanding services globally while maintaining EU-based infrastructure and compliance',
|
||||
'order': 6
|
||||
},
|
||||
]
|
||||
|
||||
for milestone_data in milestones_data:
|
||||
AboutMilestone.objects.get_or_create(
|
||||
journey=journey,
|
||||
year=milestone_data['year'],
|
||||
defaults=milestone_data
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('\n✓ Successfully imported enterprise About Us data!')
|
||||
)
|
||||
if not unsplash_key:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
'\nNote: Using Unsplash Source API (no key required). '
|
||||
'For better reliability, consider using --unsplash-key with an API key from https://unsplash.com/developers'
|
||||
)
|
||||
)
|
||||
|
||||
239
backEnd/about/management/commands/populate_about_data.py
Normal file
239
backEnd/about/management/commands/populate_about_data.py
Normal file
@@ -0,0 +1,239 @@
|
||||
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...')
|
||||
|
||||
# Clear existing data first
|
||||
self.stdout.write('Clearing existing about data...')
|
||||
AboutBanner.objects.all().delete()
|
||||
AboutService.objects.all().delete()
|
||||
AboutProcess.objects.all().delete()
|
||||
AboutJourney.objects.all().delete()
|
||||
|
||||
# Create About Banner
|
||||
banner, created = AboutBanner.objects.get_or_create(
|
||||
title="Enterprise Software Solutions for Mission-Critical Industries",
|
||||
defaults={
|
||||
'subtitle': "GNX Soft Ltd - Your Trusted Enterprise Technology Partner",
|
||||
'description': "GNX Soft Ltd is a leading Bulgarian enterprise software company delivering cutting-edge technology solutions to mission-critical industries worldwide. We empower organizations in Defense & Aerospace, Healthcare & Medical, Telecommunication, Banking, Public Sector, E-commerce, Food & Beverages, and Oil & Energy sectors with innovative, secure, and scalable software platforms that drive digital transformation and operational excellence.",
|
||||
'badge_text': "Enterprise Software Solutions",
|
||||
'badge_icon': "fa-solid fa-building",
|
||||
'cta_text': "Explore 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': '8', 'label': 'Industry Verticals', 'order': 1},
|
||||
{'number': '99.9%', 'label': 'Uptime SLA', 'order': 2},
|
||||
{'number': '24/7', 'label': 'Enterprise Support', 'order': 3},
|
||||
{'number': '2020', 'label': 'Founded', '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/gnxsoft',
|
||||
'icon': 'fa-brands fa-linkedin-in',
|
||||
'aria_label': 'Connect with GNX Soft on LinkedIn',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'platform': 'GitHub',
|
||||
'url': 'https://github.com/gnxsoft',
|
||||
'icon': 'fa-brands fa-github',
|
||||
'aria_label': 'Follow GNX Soft on GitHub',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'platform': 'Twitter',
|
||||
'url': 'https://twitter.com/gnxsoft',
|
||||
'icon': 'fa-brands fa-twitter',
|
||||
'aria_label': 'Follow GNX Soft on Twitter',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'platform': 'Email',
|
||||
'url': 'mailto:info@gnxsoft.com',
|
||||
'icon': 'fa-solid fa-envelope',
|
||||
'aria_label': 'Contact GNX Soft via email',
|
||||
'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 Excellence Across Critical Industries",
|
||||
defaults={
|
||||
'subtitle': "About GNX Soft Ltd",
|
||||
'description': "Founded in 2020 and headquartered in Burgas, Bulgaria, GNX Soft Ltd is a premier enterprise software development company specializing in mission-critical solutions for highly regulated industries. Our expert team delivers secure, scalable, and compliant software solutions to Defense & Aerospace, Healthcare & Medical, Telecommunication, Banking & Finance, Public Sector, E-commerce, Food & Beverages, and Oil & Energy sectors. With EU-based infrastructure, we provide enterprise-grade solutions that meet the highest security and regulatory standards.",
|
||||
'badge_text': "About GNX Soft Ltd",
|
||||
'badge_icon': "fa-solid fa-users",
|
||||
'cta_text': "Explore Our Solutions",
|
||||
'cta_link': "services",
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
self.stdout.write(f'Created service: {service.title}')
|
||||
|
||||
# Create Service Features
|
||||
features_data = [
|
||||
{
|
||||
'title': 'EU-Based Company',
|
||||
'description': 'Headquartered in Bulgaria',
|
||||
'icon': 'fa-solid fa-building',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'title': 'EU Infrastructure',
|
||||
'description': 'Bulgaria, Germany, Netherlands',
|
||||
'icon': 'fa-solid fa-server',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'title': '8 Industry Verticals',
|
||||
'description': 'Specialized Expertise',
|
||||
'icon': 'fa-solid fa-industry',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'title': '24/7 Support',
|
||||
'description': 'Enterprise Support Team',
|
||||
'icon': 'fa-solid fa-headset',
|
||||
'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-Grade Development Methodology",
|
||||
defaults={
|
||||
'subtitle': "Our Methodology",
|
||||
'description': "GNX Soft Ltd employs a proven enterprise development methodology that combines agile practices with defense-grade security, regulatory compliance, and enterprise scalability. We follow industry best practices including DevOps, CI/CD, microservices architecture, and privacy-by-design principles to deliver robust, secure, and compliant solutions for highly regulated industries. Every project undergoes rigorous security assessments, Data Protection Impact Assessments (DPIAs), and compliance verification to meet the strictest industry standards.",
|
||||
'badge_text': "Our Methodology",
|
||||
'badge_icon': "fa-solid fa-cogs",
|
||||
'cta_text': "View Our Services",
|
||||
'cta_link': "services",
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
self.stdout.write(f'Created process: {process.title}')
|
||||
|
||||
# Create Process Steps
|
||||
steps_data = [
|
||||
{
|
||||
'step_number': '01',
|
||||
'title': 'Discovery & Compliance',
|
||||
'description': 'Requirements analysis, regulatory assessment, and DPIA',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'step_number': '02',
|
||||
'title': 'Secure Development',
|
||||
'description': 'Privacy-by-design, secure coding, continuous testing',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'step_number': '03',
|
||||
'title': 'Deployment & Integration',
|
||||
'description': 'EU infrastructure deployment and system integration',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'step_number': '04',
|
||||
'title': 'Support & Monitoring',
|
||||
'description': '24/7 enterprise support with breach response',
|
||||
'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="Building Enterprise Excellence Since 2020",
|
||||
defaults={
|
||||
'subtitle': "Our Journey",
|
||||
'description': "Founded in 2020 in Burgas, Bulgaria, GNX Soft Ltd was established with a clear mission: to deliver world-class enterprise software solutions for mission-critical industries while maintaining the highest standards of security, compliance, and data protection. From our inception, we focused exclusively on enterprise clients in highly regulated sectors including Defense & Aerospace, Healthcare & Medical, Banking, Public Sector, Telecommunication, E-commerce, Food & Beverages, and Oil & Energy. Our commitment to EU-based infrastructure and privacy-by-design principles has positioned us as a trusted technology partner for organizations that demand the highest levels of security and regulatory adherence.",
|
||||
'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': '2020',
|
||||
'title': 'Company Founded',
|
||||
'description': 'GNX Soft Ltd established in Burgas, Bulgaria',
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'year': '2021',
|
||||
'title': 'Industry Focus',
|
||||
'description': 'Specialized in 8 mission-critical industries',
|
||||
'order': 2
|
||||
},
|
||||
{
|
||||
'year': '2022',
|
||||
'title': 'Industry Expansion',
|
||||
'description': 'Expanded to 8 specialized industry verticals',
|
||||
'order': 3
|
||||
},
|
||||
{
|
||||
'year': '2023',
|
||||
'title': 'EU Infrastructure',
|
||||
'description': 'Deployed EU-wide data centers across 3 countries',
|
||||
'order': 4
|
||||
},
|
||||
{
|
||||
'year': '2024',
|
||||
'title': 'Enterprise Leader',
|
||||
'description': 'Recognized as leading Bulgarian enterprise software provider',
|
||||
'order': 5
|
||||
},
|
||||
]
|
||||
|
||||
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
backEnd/about/migrations/0001_initial.py
Normal file
180
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
backEnd/about/migrations/__init__.py
Normal file
0
backEnd/about/migrations/__init__.py
Normal file
Binary file not shown.
BIN
backEnd/about/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/about/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
176
backEnd/about/models.py
Normal file
176
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
backEnd/about/serializers.py
Normal file
130
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
backEnd/about/tests.py
Normal file
3
backEnd/about/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
25
backEnd/about/urls.py
Normal file
25
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
backEnd/about/views.py
Normal file
151
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
|
||||
)
|
||||
213
backEnd/blog/README.md
Normal file
213
backEnd/blog/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Blog API Documentation
|
||||
|
||||
This document provides information about the Blog API implementation for the GNX Software Solutions website.
|
||||
|
||||
## Overview
|
||||
|
||||
The Blog API is a RESTful API built with Django REST Framework that provides endpoints for managing blog posts, categories, authors, tags, and comments.
|
||||
|
||||
## Backend Setup
|
||||
|
||||
### Models
|
||||
|
||||
The blog app includes the following models:
|
||||
|
||||
1. **BlogAuthor** - Stores information about blog post authors
|
||||
2. **BlogCategory** - Categories for organizing blog posts
|
||||
3. **BlogTag** - Tags for additional post categorization
|
||||
4. **BlogPost** - The main blog post model
|
||||
5. **BlogComment** - Comments on blog posts (with moderation support)
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Base URL: `/api/blog/`
|
||||
|
||||
#### Blog Posts
|
||||
|
||||
- `GET /api/blog/posts/` - List all blog posts (with pagination and filtering)
|
||||
- Query parameters:
|
||||
- `category` - Filter by category slug
|
||||
- `tag` - Filter by tag slug
|
||||
- `author` - Filter by author ID
|
||||
- `search` - Search in title, content, and excerpt
|
||||
- `featured` - Filter featured posts
|
||||
- `ordering` - Sort by fields (e.g., `-published_at`, `views_count`)
|
||||
- `page` - Page number
|
||||
- `page_size` - Items per page (default: 9)
|
||||
|
||||
- `GET /api/blog/posts/{slug}/` - Get a single blog post by slug
|
||||
- `GET /api/blog/posts/featured/` - Get featured blog posts
|
||||
- `GET /api/blog/posts/latest/?limit=5` - Get latest blog posts
|
||||
- `GET /api/blog/posts/popular/?limit=5` - Get popular posts by views
|
||||
- `GET /api/blog/posts/{id}/related/` - Get related posts
|
||||
|
||||
#### Categories
|
||||
|
||||
- `GET /api/blog/categories/` - List all categories
|
||||
- `GET /api/blog/categories/{slug}/` - Get a single category by slug
|
||||
- `GET /api/blog/categories/with_posts/` - Get categories that have published posts
|
||||
|
||||
#### Authors
|
||||
|
||||
- `GET /api/blog/authors/` - List all authors
|
||||
- `GET /api/blog/authors/{id}/` - Get a single author
|
||||
- `GET /api/blog/authors/{id}/posts/` - Get all posts by an author
|
||||
|
||||
#### Tags
|
||||
|
||||
- `GET /api/blog/tags/` - List all tags
|
||||
- `GET /api/blog/tags/{slug}/` - Get a single tag by slug
|
||||
- `GET /api/blog/tags/{slug}/posts/` - Get all posts with a specific tag
|
||||
|
||||
#### Comments
|
||||
|
||||
- `GET /api/blog/comments/?post={post_id}` - Get comments for a post
|
||||
- `POST /api/blog/comments/` - Create a new comment (requires moderation)
|
||||
|
||||
### Management Commands
|
||||
|
||||
#### Populate Blog Data
|
||||
|
||||
To populate the database with sample blog data:
|
||||
|
||||
```bash
|
||||
python manage.py populate_blog
|
||||
```
|
||||
|
||||
This command creates:
|
||||
- 4 sample authors
|
||||
- 6 blog categories
|
||||
- 16 tags
|
||||
- 8 sample blog posts with full content
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### API Service
|
||||
|
||||
Location: `/lib/api/blogService.ts`
|
||||
|
||||
The service provides functions for:
|
||||
- Fetching blog posts (with filtering)
|
||||
- Getting single posts by slug
|
||||
- Fetching categories, tags, and authors
|
||||
- Getting featured, latest, and popular posts
|
||||
- Managing comments
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
Location: `/lib/hooks/useBlog.ts`
|
||||
|
||||
Available hooks:
|
||||
- `useBlogPosts(params)` - Fetch paginated list of posts
|
||||
- `useBlogPost(slug)` - Fetch a single post
|
||||
- `useFeaturedPosts()` - Fetch featured posts
|
||||
- `useLatestPosts(limit)` - Fetch latest posts
|
||||
- `usePopularPosts(limit)` - Fetch popular posts
|
||||
- `useBlogCategories()` - Fetch all categories
|
||||
- `useBlogTags()` - Fetch all tags
|
||||
- `useBlogAuthors()` - Fetch all authors
|
||||
|
||||
### Components
|
||||
|
||||
The following components have been updated to use the API:
|
||||
|
||||
1. **PostFilterButtons** - Fetches categories from API
|
||||
2. **PostFilterItems** - Fetches and filters blog posts by category
|
||||
3. **BlogSingle** - Fetches individual post by slug from URL params
|
||||
4. **LatestPost** - Fetches latest posts for the slider
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Fetching All Posts
|
||||
|
||||
```typescript
|
||||
const { posts, loading, error, pagination } = useBlogPosts({
|
||||
category: 'enterprise-software',
|
||||
page_size: 9
|
||||
});
|
||||
```
|
||||
|
||||
#### Fetching a Single Post
|
||||
|
||||
```typescript
|
||||
const { post, loading, error } = useBlogPost('api-first-approach-to-system-integration');
|
||||
```
|
||||
|
||||
#### Fetching Latest Posts
|
||||
|
||||
```typescript
|
||||
const { posts, loading, error } = useLatestPosts(5);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Backend Features
|
||||
|
||||
1. **Pagination** - All list endpoints support pagination
|
||||
2. **Filtering** - Filter posts by category, tag, author, and search
|
||||
3. **Ordering** - Sort posts by date, views, etc.
|
||||
4. **View Tracking** - Automatically increment view count when a post is viewed
|
||||
5. **Related Posts** - Automatically suggest related posts from the same category
|
||||
6. **Comment Moderation** - Comments require approval before being visible
|
||||
7. **SEO Support** - Meta descriptions and keywords for each post
|
||||
8. **Reading Time** - Estimated reading time for posts
|
||||
9. **Tags System** - Flexible tagging for better categorization
|
||||
|
||||
### Frontend Features
|
||||
|
||||
1. **Real-time Data** - All data fetched from API
|
||||
2. **Loading States** - Proper loading indicators
|
||||
3. **Error Handling** - Graceful error handling with fallbacks
|
||||
4. **Image Optimization** - Uses Next.js Image component
|
||||
5. **Responsive Design** - Mobile-friendly layouts
|
||||
6. **Category Filtering** - Filter posts by category with smooth animations
|
||||
7. **Social Sharing** - Share posts on social media
|
||||
8. **Related Posts** - Automatically shows related posts
|
||||
9. **SEO Friendly** - Proper meta tags and structured data
|
||||
|
||||
## Sample Data
|
||||
|
||||
The populated sample data includes:
|
||||
|
||||
### Categories
|
||||
- Enterprise Software
|
||||
- Digital Transformation
|
||||
- System Integration
|
||||
- Cloud Solutions
|
||||
- Security
|
||||
- API Development
|
||||
|
||||
### Sample Post Topics
|
||||
1. The Future of Enterprise Software Architecture
|
||||
2. Digital Transformation Strategies for Large Enterprises
|
||||
3. API-First Approach to System Integration
|
||||
4. Cloud Migration Best Practices for Enterprise
|
||||
5. Enterprise Security in the Digital Age
|
||||
6. Building Scalable API Architectures
|
||||
7. Microservices Architecture for Enterprise Applications
|
||||
8. Data Analytics and Business Intelligence Solutions
|
||||
|
||||
## Notes
|
||||
|
||||
- All blog posts support HTML content
|
||||
- Images can be uploaded or linked via URL
|
||||
- Posts can be marked as featured
|
||||
- Comments require moderation before being visible
|
||||
- The API respects published status (unpublished posts are not returned)
|
||||
- View counts are automatically tracked
|
||||
- Related posts are determined by category
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
1. Add full-text search using Elasticsearch
|
||||
2. Implement post series/collections
|
||||
3. Add newsletter subscription functionality
|
||||
4. Implement post scheduling
|
||||
5. Add analytics dashboard
|
||||
6. Support for multiple authors per post
|
||||
7. Rich text editor in admin
|
||||
8. Image upload and management
|
||||
9. Post translations/i18n support
|
||||
10. RSS feed generation
|
||||
|
||||
0
backEnd/blog/__init__.py
Normal file
0
backEnd/blog/__init__.py
Normal file
BIN
backEnd/blog/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/__pycache__/models.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/__pycache__/views.cpython-312.pyc
Normal file
BIN
backEnd/blog/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
98
backEnd/blog/admin.py
Normal file
98
backEnd/blog/admin.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from django.contrib import admin
|
||||
from .models import BlogPost, BlogCategory, BlogAuthor, BlogTag, BlogComment
|
||||
|
||||
|
||||
@admin.register(BlogAuthor)
|
||||
class BlogAuthorAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'email', 'is_active', 'created_at']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
search_fields = ['name', 'email']
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
@admin.register(BlogCategory)
|
||||
class BlogCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'slug', 'display_order', 'is_active', 'posts_count']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['title', 'slug']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
ordering = ['display_order', 'title']
|
||||
|
||||
def posts_count(self, obj):
|
||||
return obj.posts.count()
|
||||
posts_count.short_description = 'Posts Count'
|
||||
|
||||
|
||||
@admin.register(BlogTag)
|
||||
class BlogTagAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'is_active', 'posts_count']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'slug']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['name']
|
||||
|
||||
def posts_count(self, obj):
|
||||
return obj.posts.count()
|
||||
posts_count.short_description = 'Posts Count'
|
||||
|
||||
|
||||
@admin.register(BlogPost)
|
||||
class BlogPostAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title', 'author', 'category', 'published',
|
||||
'featured', 'views_count', 'published_at'
|
||||
]
|
||||
list_filter = [
|
||||
'published', 'featured', 'category',
|
||||
'author', 'published_at', 'created_at'
|
||||
]
|
||||
search_fields = ['title', 'content', 'excerpt']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
filter_horizontal = ['tags']
|
||||
date_hierarchy = 'published_at'
|
||||
ordering = ['-published_at', '-created_at']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('title', 'slug', 'author', 'category')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('excerpt', 'content')
|
||||
}),
|
||||
('Images', {
|
||||
'fields': ('thumbnail', 'thumbnail_url', 'featured_image', 'featured_image_url')
|
||||
}),
|
||||
('Categorization', {
|
||||
'fields': ('tags',)
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('meta_description', 'meta_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status & Visibility', {
|
||||
'fields': ('published', 'featured', 'reading_time', 'published_at')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('views_count',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['views_count']
|
||||
|
||||
|
||||
@admin.register(BlogComment)
|
||||
class BlogCommentAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'post', 'is_approved', 'created_at']
|
||||
list_filter = ['is_approved', 'created_at']
|
||||
search_fields = ['name', 'email', 'content', 'post__title']
|
||||
ordering = ['-created_at']
|
||||
actions = ['approve_comments', 'disapprove_comments']
|
||||
|
||||
def approve_comments(self, request, queryset):
|
||||
queryset.update(is_approved=True)
|
||||
approve_comments.short_description = "Approve selected comments"
|
||||
|
||||
def disapprove_comments(self, request, queryset):
|
||||
queryset.update(is_approved=False)
|
||||
disapprove_comments.short_description = "Disapprove selected comments"
|
||||
7
backEnd/blog/apps.py
Normal file
7
backEnd/blog/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
||||
verbose_name = 'Blog Management'
|
||||
0
backEnd/blog/management/__init__.py
Normal file
0
backEnd/blog/management/__init__.py
Normal file
BIN
backEnd/blog/management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/blog/management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
0
backEnd/blog/management/commands/__init__.py
Normal file
0
backEnd/blog/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
414
backEnd/blog/management/commands/populate_blog.py
Normal file
414
backEnd/blog/management/commands/populate_blog.py
Normal file
@@ -0,0 +1,414 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from blog.models import BlogAuthor, BlogCategory, BlogTag, BlogPost
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populate database with sample blog data'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('Starting to populate blog data...'))
|
||||
|
||||
# Clear existing data
|
||||
BlogPost.objects.all().delete()
|
||||
BlogTag.objects.all().delete()
|
||||
BlogCategory.objects.all().delete()
|
||||
BlogAuthor.objects.all().delete()
|
||||
|
||||
# Create Authors
|
||||
authors_data = [
|
||||
{
|
||||
'name': 'Sarah Johnson',
|
||||
'email': 'sarah@gnxsoft.com',
|
||||
'bio': 'Senior Technology Consultant with 15+ years in enterprise solutions'
|
||||
},
|
||||
{
|
||||
'name': 'Michael Chen',
|
||||
'email': 'michael@gnxsoft.com',
|
||||
'bio': 'Cloud Architecture Specialist and DevOps Expert'
|
||||
},
|
||||
{
|
||||
'name': 'Emily Rodriguez',
|
||||
'email': 'emily@gnxsoft.com',
|
||||
'bio': 'API Integration and System Integration Lead'
|
||||
},
|
||||
{
|
||||
'name': 'David Thompson',
|
||||
'email': 'david@gnxsoft.com',
|
||||
'bio': 'Digital Transformation and Business Intelligence Consultant'
|
||||
}
|
||||
]
|
||||
|
||||
authors = {}
|
||||
for author_data in authors_data:
|
||||
author = BlogAuthor.objects.create(**author_data)
|
||||
authors[author.name] = author
|
||||
self.stdout.write(f'Created author: {author.name}')
|
||||
|
||||
# Create Categories
|
||||
categories_data = [
|
||||
{
|
||||
'title': 'Enterprise Software',
|
||||
'slug': 'enterprise-software',
|
||||
'description': 'Articles about enterprise software development and architecture',
|
||||
'display_order': 1
|
||||
},
|
||||
{
|
||||
'title': 'Digital Transformation',
|
||||
'slug': 'digital-transformation',
|
||||
'description': 'Insights on digital transformation strategies',
|
||||
'display_order': 2
|
||||
},
|
||||
{
|
||||
'title': 'System Integration',
|
||||
'slug': 'system-integration',
|
||||
'description': 'Best practices for system and API integration',
|
||||
'display_order': 3
|
||||
},
|
||||
{
|
||||
'title': 'Cloud Solutions',
|
||||
'slug': 'cloud-solutions',
|
||||
'description': 'Cloud computing and migration strategies',
|
||||
'display_order': 4
|
||||
},
|
||||
{
|
||||
'title': 'Security',
|
||||
'slug': 'security',
|
||||
'description': 'Enterprise security and cybersecurity topics',
|
||||
'display_order': 5
|
||||
},
|
||||
{
|
||||
'title': 'API Development',
|
||||
'slug': 'api-development',
|
||||
'description': 'API design, development, and management',
|
||||
'display_order': 6
|
||||
}
|
||||
]
|
||||
|
||||
categories = {}
|
||||
for cat_data in categories_data:
|
||||
category = BlogCategory.objects.create(**cat_data)
|
||||
categories[category.slug] = category
|
||||
self.stdout.write(f'Created category: {category.title}')
|
||||
|
||||
# Create Tags
|
||||
tags_data = [
|
||||
'API', 'REST', 'GraphQL', 'Microservices', 'Cloud', 'AWS', 'Azure',
|
||||
'Security', 'DevOps', 'CI/CD', 'Docker', 'Kubernetes',
|
||||
'Enterprise', 'Integration', 'Architecture', 'Best Practices'
|
||||
]
|
||||
|
||||
tags = {}
|
||||
for tag_name in tags_data:
|
||||
tag = BlogTag.objects.create(name=tag_name)
|
||||
tags[tag_name] = tag
|
||||
self.stdout.write(f'Created tag: {tag.name}')
|
||||
|
||||
# Create Blog Posts
|
||||
posts_data = [
|
||||
{
|
||||
'title': 'The Future of Enterprise Software Architecture',
|
||||
'content': '''
|
||||
<h2>Introduction</h2>
|
||||
<p>Enterprise software architecture is evolving rapidly. In this comprehensive guide, we explore the latest trends and best practices that are shaping the future of enterprise systems.</p>
|
||||
|
||||
<h3>Microservices and Modularity</h3>
|
||||
<p>The shift towards microservices architecture has revolutionized how we build enterprise applications. By breaking down monolithic applications into smaller, independent services, organizations can achieve greater flexibility, scalability, and maintainability.</p>
|
||||
|
||||
<h3>Cloud-Native Development</h3>
|
||||
<p>Cloud-native architecture is becoming the standard for modern enterprise applications. This approach leverages cloud computing capabilities to build and run scalable applications in dynamic environments.</p>
|
||||
|
||||
<h3>API-First Design</h3>
|
||||
<p>API-first development ensures that applications are built with integration in mind from the start. This approach facilitates better communication between services and makes it easier to integrate with third-party systems.</p>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
<ul>
|
||||
<li>Design for scalability from the ground up</li>
|
||||
<li>Implement robust security measures at every layer</li>
|
||||
<li>Use containerization for consistent deployment</li>
|
||||
<li>Adopt continuous integration and deployment practices</li>
|
||||
<li>Monitor and optimize performance continuously</li>
|
||||
</ul>
|
||||
|
||||
<h2>Conclusion</h2>
|
||||
<p>The future of enterprise software architecture lies in flexibility, scalability, and cloud-native approaches. Organizations that embrace these principles will be better positioned to adapt to changing business needs.</p>
|
||||
''',
|
||||
'excerpt': 'Exploring the latest trends in enterprise software architecture, from microservices to cloud-native development.',
|
||||
'author': authors['Sarah Johnson'],
|
||||
'category': categories['enterprise-software'],
|
||||
'tags': ['Enterprise', 'Architecture', 'Microservices', 'Cloud'],
|
||||
'thumbnail_url': '/images/blog/one.png',
|
||||
'featured': True,
|
||||
'reading_time': 8,
|
||||
'days_ago': 5
|
||||
},
|
||||
{
|
||||
'title': 'Digital Transformation Strategies for Large Enterprises',
|
||||
'content': '''
|
||||
<h2>Understanding Digital Transformation</h2>
|
||||
<p>Digital transformation is more than just adopting new technologies—it's about fundamentally changing how your organization operates and delivers value to customers.</p>
|
||||
|
||||
<h3>Key Components of Successful Transformation</h3>
|
||||
<p>A successful digital transformation strategy must address technology, processes, and culture simultaneously. Organizations that focus on only one aspect often struggle to achieve their goals.</p>
|
||||
|
||||
<h3>Technology Stack Modernization</h3>
|
||||
<p>Legacy systems can hold organizations back. Modernizing your technology stack is often the first step in digital transformation, enabling new capabilities and improving efficiency.</p>
|
||||
|
||||
<h2>Implementation Strategy</h2>
|
||||
<ol>
|
||||
<li>Assess current state and define vision</li>
|
||||
<li>Identify quick wins and long-term goals</li>
|
||||
<li>Build cross-functional transformation teams</li>
|
||||
<li>Implement in phases with measurable milestones</li>
|
||||
<li>Continuously gather feedback and adjust</li>
|
||||
</ol>
|
||||
|
||||
<h2>Overcoming Challenges</h2>
|
||||
<p>Common challenges include resistance to change, budget constraints, and technical debt. Success requires strong leadership commitment and clear communication throughout the organization.</p>
|
||||
''',
|
||||
'excerpt': 'A comprehensive guide to digital transformation strategies tailored for large enterprise organizations.',
|
||||
'author': authors['David Thompson'],
|
||||
'category': categories['digital-transformation'],
|
||||
'tags': ['Digital Transformation', 'Enterprise', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/two.png',
|
||||
'featured': True,
|
||||
'reading_time': 10,
|
||||
'days_ago': 8
|
||||
},
|
||||
{
|
||||
'title': 'API-First Approach to System Integration',
|
||||
'content': '''
|
||||
<h2>What is API-First Development?</h2>
|
||||
<p>API-first development is a strategy where APIs are treated as first-class citizens in the development process. Instead of being an afterthought, APIs are designed and built before any other code.</p>
|
||||
|
||||
<h3>Benefits of API-First Approach</h3>
|
||||
<p>This approach offers numerous advantages including better developer experience, faster time to market, and improved system integration capabilities.</p>
|
||||
|
||||
<h3>RESTful API Design Principles</h3>
|
||||
<p>RESTful APIs follow specific architectural principles that make them scalable, maintainable, and easy to understand. Key principles include statelessness, resource-based URLs, and standard HTTP methods.</p>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
<ul>
|
||||
<li>Design APIs with clear and consistent naming conventions</li>
|
||||
<li>Version your APIs from the start</li>
|
||||
<li>Implement proper authentication and authorization</li>
|
||||
<li>Provide comprehensive documentation</li>
|
||||
<li>Use appropriate HTTP status codes</li>
|
||||
<li>Implement rate limiting and throttling</li>
|
||||
</ul>
|
||||
|
||||
<h3>GraphQL vs REST</h3>
|
||||
<p>While REST has been the standard for many years, GraphQL offers an alternative approach that can be more efficient for certain use cases. Understanding when to use each is crucial for effective API design.</p>
|
||||
''',
|
||||
'excerpt': 'Learn how an API-first approach can streamline system integration and improve your software architecture.',
|
||||
'author': authors['Emily Rodriguez'],
|
||||
'category': categories['system-integration'],
|
||||
'tags': ['API', 'REST', 'GraphQL', 'Integration'],
|
||||
'thumbnail_url': '/images/blog/three.png',
|
||||
'reading_time': 7,
|
||||
'days_ago': 12
|
||||
},
|
||||
{
|
||||
'title': 'Cloud Migration Best Practices for Enterprise',
|
||||
'content': '''
|
||||
<h2>Planning Your Cloud Migration</h2>
|
||||
<p>Cloud migration is a complex process that requires careful planning and execution. A well-thought-out strategy can mean the difference between success and costly setbacks.</p>
|
||||
|
||||
<h3>Assessing Your Current Infrastructure</h3>
|
||||
<p>Before migrating to the cloud, it's essential to thoroughly understand your current infrastructure, dependencies, and workload requirements.</p>
|
||||
|
||||
<h3>Choosing the Right Cloud Provider</h3>
|
||||
<p>AWS, Azure, and Google Cloud each offer unique strengths. The right choice depends on your specific requirements, existing technology stack, and business objectives.</p>
|
||||
|
||||
<h2>Migration Strategies</h2>
|
||||
<ol>
|
||||
<li><strong>Lift and Shift:</strong> Move applications as-is to the cloud</li>
|
||||
<li><strong>Replatform:</strong> Make minor optimizations during migration</li>
|
||||
<li><strong>Refactor:</strong> Redesign applications to be cloud-native</li>
|
||||
<li><strong>Rebuild:</strong> Completely rewrite applications for the cloud</li>
|
||||
</ol>
|
||||
|
||||
<h3>Security Considerations</h3>
|
||||
<p>Security should be a top priority during cloud migration. Implement encryption, access controls, and monitoring from day one.</p>
|
||||
|
||||
<h2>Post-Migration Optimization</h2>
|
||||
<p>Migration is just the beginning. Continuous optimization of costs, performance, and security is essential for maximizing cloud benefits.</p>
|
||||
''',
|
||||
'excerpt': 'Essential best practices and strategies for successfully migrating enterprise applications to the cloud.',
|
||||
'author': authors['Michael Chen'],
|
||||
'category': categories['cloud-solutions'],
|
||||
'tags': ['Cloud', 'AWS', 'Azure', 'Migration'],
|
||||
'thumbnail_url': '/images/blog/four.png',
|
||||
'featured': False,
|
||||
'reading_time': 9,
|
||||
'days_ago': 15
|
||||
},
|
||||
{
|
||||
'title': 'Enterprise Security in the Digital Age',
|
||||
'content': '''
|
||||
<h2>The Evolving Security Landscape</h2>
|
||||
<p>As enterprises embrace digital transformation, security challenges have become more complex. Organizations must adopt a comprehensive approach to protect their assets and data.</p>
|
||||
|
||||
<h3>Zero Trust Architecture</h3>
|
||||
<p>Zero Trust is a security model that assumes no user or system should be trusted by default, even if they're inside the network perimeter.</p>
|
||||
|
||||
<h3>Multi-Factor Authentication</h3>
|
||||
<p>MFA is no longer optional for enterprise security. Implementing strong authentication mechanisms is critical for protecting sensitive data and systems.</p>
|
||||
|
||||
<h2>Common Security Threats</h2>
|
||||
<ul>
|
||||
<li>Phishing and social engineering attacks</li>
|
||||
<li>Ransomware and malware</li>
|
||||
<li>Insider threats</li>
|
||||
<li>DDoS attacks</li>
|
||||
<li>API vulnerabilities</li>
|
||||
</ul>
|
||||
|
||||
<h3>Implementing a Security-First Culture</h3>
|
||||
<p>Technology alone cannot protect an organization. Security awareness training and a culture of security consciousness are equally important.</p>
|
||||
|
||||
<h2>Compliance and Regulations</h2>
|
||||
<p>Understanding and adhering to regulations like GDPR, HIPAA, and SOC 2 is essential for enterprise organizations operating in regulated industries.</p>
|
||||
''',
|
||||
'excerpt': 'Understanding modern security challenges and implementing robust security measures for enterprise environments.',
|
||||
'author': authors['Sarah Johnson'],
|
||||
'category': categories['security'],
|
||||
'tags': ['Security', 'Enterprise', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/five.png',
|
||||
'reading_time': 6,
|
||||
'days_ago': 18
|
||||
},
|
||||
{
|
||||
'title': 'Building Scalable API Architectures',
|
||||
'content': '''
|
||||
<h2>Scalability Fundamentals</h2>
|
||||
<p>Building APIs that can scale to millions of requests requires careful architectural planning and implementation of proven patterns.</p>
|
||||
|
||||
<h3>Horizontal vs Vertical Scaling</h3>
|
||||
<p>Understanding the difference between horizontal and vertical scaling is crucial for building scalable systems. Most modern APIs benefit from horizontal scaling strategies.</p>
|
||||
|
||||
<h3>Caching Strategies</h3>
|
||||
<p>Implementing effective caching can dramatically improve API performance and reduce load on backend systems. Learn about different caching layers and when to use them.</p>
|
||||
|
||||
<h2>Performance Optimization</h2>
|
||||
<ul>
|
||||
<li>Database query optimization</li>
|
||||
<li>Connection pooling</li>
|
||||
<li>Asynchronous processing</li>
|
||||
<li>Load balancing</li>
|
||||
<li>CDN integration</li>
|
||||
</ul>
|
||||
|
||||
<h3>Monitoring and Observability</h3>
|
||||
<p>You can't optimize what you can't measure. Implementing comprehensive monitoring and logging is essential for maintaining scalable APIs.</p>
|
||||
|
||||
<h2>Rate Limiting and Throttling</h2>
|
||||
<p>Protecting your API from abuse and ensuring fair usage requires implementing rate limiting and throttling mechanisms.</p>
|
||||
''',
|
||||
'excerpt': 'Learn the principles and patterns for building API architectures that can scale to meet growing demands.',
|
||||
'author': authors['Emily Rodriguez'],
|
||||
'category': categories['api-development'],
|
||||
'tags': ['API', 'Architecture', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/six.png',
|
||||
'reading_time': 8,
|
||||
'days_ago': 22
|
||||
},
|
||||
{
|
||||
'title': 'Microservices Architecture for Enterprise Applications',
|
||||
'content': '''
|
||||
<h2>Understanding Microservices</h2>
|
||||
<p>Microservices architecture has become the de facto standard for building scalable, maintainable enterprise applications. This architectural style structures an application as a collection of loosely coupled services.</p>
|
||||
|
||||
<h3>Key Characteristics</h3>
|
||||
<p>Microservices are independently deployable, organized around business capabilities, and can be written in different programming languages.</p>
|
||||
|
||||
<h3>Service Communication Patterns</h3>
|
||||
<p>Understanding different communication patterns between microservices is crucial. Options include synchronous REST APIs, message queues, and event-driven architectures.</p>
|
||||
|
||||
<h2>Design Patterns</h2>
|
||||
<ol>
|
||||
<li>API Gateway Pattern</li>
|
||||
<li>Service Discovery</li>
|
||||
<li>Circuit Breaker</li>
|
||||
<li>Event Sourcing</li>
|
||||
<li>CQRS (Command Query Responsibility Segregation)</li>
|
||||
</ol>
|
||||
|
||||
<h3>Containerization with Docker</h3>
|
||||
<p>Docker containers provide the perfect deployment vehicle for microservices, ensuring consistency across development and production environments.</p>
|
||||
|
||||
<h2>Challenges and Solutions</h2>
|
||||
<p>While microservices offer many benefits, they also introduce complexity in areas like distributed transactions, data consistency, and service orchestration.</p>
|
||||
''',
|
||||
'excerpt': 'A deep dive into microservices architecture patterns and best practices for enterprise applications.',
|
||||
'author': authors['Michael Chen'],
|
||||
'category': categories['enterprise-software'],
|
||||
'tags': ['Microservices', 'Architecture', 'Docker', 'Kubernetes'],
|
||||
'thumbnail_url': '/images/blog/seven.png',
|
||||
'featured': True,
|
||||
'reading_time': 11,
|
||||
'days_ago': 25
|
||||
},
|
||||
{
|
||||
'title': 'Data Analytics and Business Intelligence Solutions',
|
||||
'content': '''
|
||||
<h2>The Power of Data-Driven Decisions</h2>
|
||||
<p>In today's business environment, data analytics and business intelligence are no longer optional—they're essential for staying competitive.</p>
|
||||
|
||||
<h3>Modern BI Tools and Platforms</h3>
|
||||
<p>Modern BI platforms offer self-service analytics, real-time dashboards, and AI-powered insights that democratize data access across the organization.</p>
|
||||
|
||||
<h3>Building a Data Warehouse</h3>
|
||||
<p>A well-designed data warehouse serves as the foundation for business intelligence initiatives, providing a single source of truth for organizational data.</p>
|
||||
|
||||
<h2>Analytics Maturity Model</h2>
|
||||
<ol>
|
||||
<li>Descriptive Analytics - What happened?</li>
|
||||
<li>Diagnostic Analytics - Why did it happen?</li>
|
||||
<li>Predictive Analytics - What will happen?</li>
|
||||
<li>Prescriptive Analytics - What should we do?</li>
|
||||
</ol>
|
||||
|
||||
<h3>Real-Time Analytics</h3>
|
||||
<p>The ability to analyze data in real-time enables organizations to respond quickly to changing conditions and make timely decisions.</p>
|
||||
|
||||
<h2>Data Governance and Quality</h2>
|
||||
<p>Without proper data governance and quality management, analytics initiatives can produce unreliable results. Establishing data quality standards is crucial.</p>
|
||||
|
||||
<h3>Machine Learning Integration</h3>
|
||||
<p>Integrating machine learning models into BI platforms can provide deeper insights and enable predictive capabilities.</p>
|
||||
''',
|
||||
'excerpt': 'Leveraging data analytics and business intelligence to drive informed decision-making in enterprise environments.',
|
||||
'author': authors['David Thompson'],
|
||||
'category': categories['digital-transformation'],
|
||||
'tags': ['Analytics', 'BI', 'Enterprise', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/eight.png',
|
||||
'reading_time': 9,
|
||||
'days_ago': 30
|
||||
}
|
||||
]
|
||||
|
||||
# Create posts
|
||||
for post_data in posts_data:
|
||||
tag_names = post_data.pop('tags')
|
||||
days_ago = post_data.pop('days_ago')
|
||||
|
||||
post = BlogPost.objects.create(
|
||||
**post_data,
|
||||
published_at=timezone.now() - timedelta(days=days_ago)
|
||||
)
|
||||
|
||||
# Add tags
|
||||
for tag_name in tag_names:
|
||||
if tag_name in tags:
|
||||
post.tags.add(tags[tag_name])
|
||||
|
||||
self.stdout.write(f'Created post: {post.title}')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\nSuccessfully populated blog data!'))
|
||||
self.stdout.write(f'Created {BlogAuthor.objects.count()} authors')
|
||||
self.stdout.write(f'Created {BlogCategory.objects.count()} categories')
|
||||
self.stdout.write(f'Created {BlogTag.objects.count()} tags')
|
||||
self.stdout.write(f'Created {BlogPost.objects.count()} blog posts')
|
||||
|
||||
129
backEnd/blog/migrations/0001_initial.py
Normal file
129
backEnd/blog/migrations/0001_initial.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-08 09:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogAuthor',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, unique=True)),
|
||||
('bio', models.TextField(blank=True)),
|
||||
('avatar', models.ImageField(blank=True, null=True, upload_to='blog/authors/')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Author',
|
||||
'verbose_name_plural': 'Blog Authors',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('display_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Category',
|
||||
'verbose_name_plural': 'Blog Categories',
|
||||
'ordering': ['display_order', 'title'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Tag',
|
||||
'verbose_name_plural': 'Blog Tags',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=300)),
|
||||
('slug', models.SlugField(max_length=300, unique=True)),
|
||||
('content', models.TextField()),
|
||||
('excerpt', models.TextField(blank=True, help_text='Short excerpt for preview')),
|
||||
('thumbnail', models.ImageField(blank=True, null=True, upload_to='blog/thumbnails/')),
|
||||
('thumbnail_url', models.CharField(blank=True, help_text='External thumbnail URL', max_length=500)),
|
||||
('featured_image', models.ImageField(blank=True, null=True, upload_to='blog/featured/')),
|
||||
('featured_image_url', models.CharField(blank=True, help_text='External featured image URL', max_length=500)),
|
||||
('meta_description', models.CharField(blank=True, max_length=160)),
|
||||
('meta_keywords', models.CharField(blank=True, max_length=255)),
|
||||
('published', models.BooleanField(default=True)),
|
||||
('featured', models.BooleanField(default=False, help_text='Featured post')),
|
||||
('views_count', models.PositiveIntegerField(default=0)),
|
||||
('reading_time', models.PositiveIntegerField(default=5, help_text='Estimated reading time in minutes')),
|
||||
('published_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='blog.blogauthor')),
|
||||
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='blog.blogcategory')),
|
||||
('tags', models.ManyToManyField(blank=True, related_name='posts', to='blog.blogtag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Post',
|
||||
'verbose_name_plural': 'Blog Posts',
|
||||
'ordering': ['-published_at', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogComment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('content', models.TextField()),
|
||||
('is_approved', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='blog.blogcomment')),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='blog.blogpost')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Comment',
|
||||
'verbose_name_plural': 'Blog Comments',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blogpost',
|
||||
index=models.Index(fields=['-published_at'], name='blog_blogpo_publish_e75c11_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blogpost',
|
||||
index=models.Index(fields=['slug'], name='blog_blogpo_slug_361555_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blogpost',
|
||||
index=models.Index(fields=['published'], name='blog_blogpo_publish_059755_idx'),
|
||||
),
|
||||
]
|
||||
0
backEnd/blog/migrations/__init__.py
Normal file
0
backEnd/blog/migrations/__init__.py
Normal file
BIN
backEnd/blog/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
backEnd/blog/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/blog/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/blog/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
178
backEnd/blog/models.py
Normal file
178
backEnd/blog/models.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
class BlogAuthor(models.Model):
|
||||
"""Model for blog post authors"""
|
||||
name = models.CharField(max_length=200)
|
||||
email = models.EmailField(unique=True, blank=True, null=True)
|
||||
bio = models.TextField(blank=True)
|
||||
avatar = models.ImageField(upload_to='blog/authors/', blank=True, null=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 = "Blog Author"
|
||||
verbose_name_plural = "Blog Authors"
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class BlogCategory(models.Model):
|
||||
"""Model for blog post categories"""
|
||||
title = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
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 = "Blog Category"
|
||||
verbose_name_plural = "Blog Categories"
|
||||
ordering = ['display_order', 'title']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogTag(models.Model):
|
||||
"""Model for blog post tags"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(max_length=50, unique=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Tag"
|
||||
verbose_name_plural = "Blog Tags"
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
"""Model for blog posts"""
|
||||
title = models.CharField(max_length=300)
|
||||
slug = models.SlugField(max_length=300, unique=True)
|
||||
content = models.TextField()
|
||||
excerpt = models.TextField(blank=True, help_text="Short excerpt for preview")
|
||||
thumbnail = models.ImageField(upload_to='blog/thumbnails/', blank=True, null=True)
|
||||
thumbnail_url = models.CharField(max_length=500, blank=True, help_text="External thumbnail URL")
|
||||
featured_image = models.ImageField(upload_to='blog/featured/', blank=True, null=True)
|
||||
featured_image_url = models.CharField(max_length=500, blank=True, help_text="External featured image URL")
|
||||
|
||||
author = models.ForeignKey(
|
||||
BlogAuthor,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='posts'
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
BlogCategory,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='posts'
|
||||
)
|
||||
tags = models.ManyToManyField(BlogTag, related_name='posts', blank=True)
|
||||
|
||||
# SEO and metadata
|
||||
meta_description = models.CharField(max_length=160, blank=True)
|
||||
meta_keywords = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# Status and visibility
|
||||
published = models.BooleanField(default=True)
|
||||
featured = models.BooleanField(default=False, help_text="Featured post")
|
||||
views_count = models.PositiveIntegerField(default=0)
|
||||
reading_time = models.PositiveIntegerField(default=5, help_text="Estimated reading time in minutes")
|
||||
|
||||
# Timestamps
|
||||
published_at = models.DateTimeField(default=timezone.now)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Post"
|
||||
verbose_name_plural = "Blog Posts"
|
||||
ordering = ['-published_at', '-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['-published_at']),
|
||||
models.Index(fields=['slug']),
|
||||
models.Index(fields=['published']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
if not self.excerpt and self.content:
|
||||
# Generate excerpt from content (first 200 characters)
|
||||
self.excerpt = self.content[:200] + '...' if len(self.content) > 200 else self.content
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def get_thumbnail_url(self):
|
||||
"""Return the thumbnail URL (uploaded image or external URL)"""
|
||||
if self.thumbnail and hasattr(self.thumbnail, 'url'):
|
||||
return self.thumbnail.url
|
||||
elif self.thumbnail_url:
|
||||
return self.thumbnail_url
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_featured_image_url(self):
|
||||
"""Return the featured image URL (uploaded image or external URL)"""
|
||||
if self.featured_image and hasattr(self.featured_image, 'url'):
|
||||
return self.featured_image.url
|
||||
elif self.featured_image_url:
|
||||
return self.featured_image_url
|
||||
return None
|
||||
|
||||
def increment_views(self):
|
||||
"""Increment the view count"""
|
||||
self.views_count += 1
|
||||
self.save(update_fields=['views_count'])
|
||||
|
||||
|
||||
class BlogComment(models.Model):
|
||||
"""Model for blog post comments"""
|
||||
post = models.ForeignKey(BlogPost, on_delete=models.CASCADE, related_name='comments')
|
||||
name = models.CharField(max_length=100)
|
||||
email = models.EmailField()
|
||||
content = models.TextField()
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='replies'
|
||||
)
|
||||
is_approved = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Comment"
|
||||
verbose_name_plural = "Blog Comments"
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"Comment by {self.name} on {self.post.title}"
|
||||
113
backEnd/blog/serializers.py
Normal file
113
backEnd/blog/serializers.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from rest_framework import serializers
|
||||
from .models import BlogPost, BlogCategory, BlogAuthor, BlogTag, BlogComment
|
||||
|
||||
|
||||
class BlogAuthorSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog authors"""
|
||||
class Meta:
|
||||
model = BlogAuthor
|
||||
fields = ['id', 'name', 'email', 'bio', 'avatar']
|
||||
|
||||
|
||||
class BlogCategorySerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog categories"""
|
||||
posts_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = BlogCategory
|
||||
fields = ['id', 'title', 'slug', 'description', 'display_order', 'posts_count']
|
||||
|
||||
def get_posts_count(self, obj):
|
||||
return obj.posts.filter(published=True).count()
|
||||
|
||||
|
||||
class BlogTagSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog tags"""
|
||||
class Meta:
|
||||
model = BlogTag
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class BlogPostListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog post list view"""
|
||||
author_name = serializers.CharField(source='author.name', read_only=True)
|
||||
category_title = serializers.CharField(source='category.title', read_only=True)
|
||||
category_slug = serializers.CharField(source='category.slug', read_only=True)
|
||||
thumbnail = serializers.SerializerMethodField()
|
||||
tags = BlogTagSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'excerpt', 'thumbnail',
|
||||
'author_name', 'category_title', 'category_slug',
|
||||
'tags', 'published_at', 'created_at', 'updated_at',
|
||||
'views_count', 'reading_time', 'featured', 'published'
|
||||
]
|
||||
|
||||
def get_thumbnail(self, obj):
|
||||
return obj.get_thumbnail_url
|
||||
|
||||
|
||||
class BlogPostDetailSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog post detail view"""
|
||||
author = BlogAuthorSerializer(read_only=True)
|
||||
category = BlogCategorySerializer(read_only=True)
|
||||
tags = BlogTagSerializer(many=True, read_only=True)
|
||||
thumbnail = serializers.SerializerMethodField()
|
||||
featured_image = serializers.SerializerMethodField()
|
||||
related_posts = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'content', 'excerpt',
|
||||
'thumbnail', 'featured_image', 'author', 'category', 'tags',
|
||||
'meta_description', 'meta_keywords',
|
||||
'published', 'featured', 'views_count', 'reading_time',
|
||||
'published_at', 'created_at', 'updated_at', 'related_posts'
|
||||
]
|
||||
|
||||
def get_thumbnail(self, obj):
|
||||
return obj.get_thumbnail_url
|
||||
|
||||
def get_featured_image(self, obj):
|
||||
return obj.get_featured_image_url
|
||||
|
||||
def get_related_posts(self, obj):
|
||||
"""Get related posts from the same category"""
|
||||
related = BlogPost.objects.filter(
|
||||
category=obj.category,
|
||||
published=True
|
||||
).exclude(id=obj.id)[:3]
|
||||
return BlogPostListSerializer(related, many=True, context=self.context).data
|
||||
|
||||
|
||||
class BlogCommentSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog comments"""
|
||||
replies = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = BlogComment
|
||||
fields = [
|
||||
'id', 'post', 'name', 'email', 'content',
|
||||
'parent', 'is_approved', 'created_at', 'updated_at', 'replies'
|
||||
]
|
||||
read_only_fields = ['is_approved', 'created_at', 'updated_at']
|
||||
|
||||
def get_replies(self, obj):
|
||||
"""Get nested replies"""
|
||||
if obj.replies.exists():
|
||||
return BlogCommentSerializer(
|
||||
obj.replies.filter(is_approved=True),
|
||||
many=True
|
||||
).data
|
||||
return []
|
||||
|
||||
|
||||
class BlogCommentCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating blog comments"""
|
||||
class Meta:
|
||||
model = BlogComment
|
||||
fields = ['post', 'name', 'email', 'content', 'parent']
|
||||
|
||||
3
backEnd/blog/tests.py
Normal file
3
backEnd/blog/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
21
backEnd/blog/urls.py
Normal file
21
backEnd/blog/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
BlogPostViewSet,
|
||||
BlogCategoryViewSet,
|
||||
BlogAuthorViewSet,
|
||||
BlogTagViewSet,
|
||||
BlogCommentViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'posts', BlogPostViewSet, basename='blog-post')
|
||||
router.register(r'categories', BlogCategoryViewSet, basename='blog-category')
|
||||
router.register(r'authors', BlogAuthorViewSet, basename='blog-author')
|
||||
router.register(r'tags', BlogTagViewSet, basename='blog-tag')
|
||||
router.register(r'comments', BlogCommentViewSet, basename='blog-comment')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
189
backEnd/blog/views.py
Normal file
189
backEnd/blog/views.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from .models import BlogPost, BlogCategory, BlogAuthor, BlogTag, BlogComment
|
||||
from .serializers import (
|
||||
BlogPostListSerializer,
|
||||
BlogPostDetailSerializer,
|
||||
BlogCategorySerializer,
|
||||
BlogAuthorSerializer,
|
||||
BlogTagSerializer,
|
||||
BlogCommentSerializer,
|
||||
BlogCommentCreateSerializer
|
||||
)
|
||||
|
||||
|
||||
class BlogPagination(PageNumberPagination):
|
||||
"""Custom pagination for blog posts"""
|
||||
page_size = 9
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for blog posts
|
||||
Supports filtering by category, tags, author, and search
|
||||
"""
|
||||
queryset = BlogPost.objects.filter(published=True)
|
||||
pagination_class = BlogPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['category__slug', 'author', 'featured', 'tags__slug']
|
||||
search_fields = ['title', 'content', 'excerpt']
|
||||
ordering_fields = ['published_at', 'views_count', 'created_at']
|
||||
ordering = ['-published_at']
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return BlogPostDetailSerializer
|
||||
return BlogPostListSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Filter by category
|
||||
category_slug = self.request.query_params.get('category', None)
|
||||
if category_slug and category_slug != 'all':
|
||||
queryset = queryset.filter(category__slug=category_slug)
|
||||
|
||||
# Filter by tag
|
||||
tag_slug = self.request.query_params.get('tag', None)
|
||||
if tag_slug:
|
||||
queryset = queryset.filter(tags__slug=tag_slug)
|
||||
|
||||
# Filter by author
|
||||
author_id = self.request.query_params.get('author', None)
|
||||
if author_id:
|
||||
queryset = queryset.filter(author_id=author_id)
|
||||
|
||||
# Search query
|
||||
search = self.request.query_params.get('search', None)
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(title__icontains=search) |
|
||||
Q(content__icontains=search) |
|
||||
Q(excerpt__icontains=search)
|
||||
)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Override retrieve to increment view count"""
|
||||
instance = self.get_object()
|
||||
instance.increment_views()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def featured(self, request):
|
||||
"""Get featured blog posts"""
|
||||
featured_posts = self.get_queryset().filter(featured=True)[:6]
|
||||
serializer = self.get_serializer(featured_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def latest(self, request):
|
||||
"""Get latest blog posts"""
|
||||
limit = int(request.query_params.get('limit', 5))
|
||||
latest_posts = self.get_queryset()[:limit]
|
||||
serializer = self.get_serializer(latest_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def popular(self, request):
|
||||
"""Get popular blog posts by views"""
|
||||
limit = int(request.query_params.get('limit', 5))
|
||||
popular_posts = self.get_queryset().order_by('-views_count')[:limit]
|
||||
serializer = self.get_serializer(popular_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def related(self, request, slug=None):
|
||||
"""Get related posts for a specific post"""
|
||||
post = self.get_object()
|
||||
related_posts = self.get_queryset().filter(
|
||||
category=post.category
|
||||
).exclude(id=post.id)[:4]
|
||||
serializer = self.get_serializer(related_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for blog categories"""
|
||||
queryset = BlogCategory.objects.filter(is_active=True)
|
||||
serializer_class = BlogCategorySerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def with_posts(self, request):
|
||||
"""Get categories that have published posts"""
|
||||
categories = self.get_queryset().filter(posts__published=True).distinct()
|
||||
serializer = self.get_serializer(categories, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogAuthorViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for blog authors"""
|
||||
queryset = BlogAuthor.objects.filter(is_active=True)
|
||||
serializer_class = BlogAuthorSerializer
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def posts(self, request, pk=None):
|
||||
"""Get all posts by a specific author"""
|
||||
author = self.get_object()
|
||||
posts = BlogPost.objects.filter(author=author, published=True)
|
||||
page = self.paginate_queryset(posts)
|
||||
if page is not None:
|
||||
serializer = BlogPostListSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = BlogPostListSerializer(posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogTagViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for blog tags"""
|
||||
queryset = BlogTag.objects.filter(is_active=True)
|
||||
serializer_class = BlogTagSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def posts(self, request, slug=None):
|
||||
"""Get all posts with a specific tag"""
|
||||
tag = self.get_object()
|
||||
posts = BlogPost.objects.filter(tags=tag, published=True)
|
||||
page = self.paginate_queryset(posts)
|
||||
if page is not None:
|
||||
serializer = BlogPostListSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = BlogPostListSerializer(posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogCommentViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for blog comments"""
|
||||
queryset = BlogComment.objects.filter(is_approved=True)
|
||||
serializer_class = BlogCommentSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['post']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return BlogCommentCreateSerializer
|
||||
return BlogCommentSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create a new comment (requires moderation)"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
return Response(
|
||||
{
|
||||
'message': 'Comment submitted successfully. It will be visible after moderation.',
|
||||
'data': serializer.data
|
||||
},
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
0
backEnd/career/__init__.py
Normal file
0
backEnd/career/__init__.py
Normal file
BIN
backEnd/career/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/email_service.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/email_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/models.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/career/__pycache__/views.cpython-312.pyc
Normal file
BIN
backEnd/career/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
149
backEnd/career/admin.py
Normal file
149
backEnd/career/admin.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from .models import JobPosition, JobApplication
|
||||
|
||||
|
||||
@admin.register(JobPosition)
|
||||
class JobPositionAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title',
|
||||
'department',
|
||||
'location',
|
||||
'open_positions',
|
||||
'employment_type',
|
||||
'status_badge',
|
||||
'featured',
|
||||
'posted_date',
|
||||
'applications_count'
|
||||
]
|
||||
list_filter = ['status', 'employment_type', 'location_type', 'featured', 'department', 'posted_date']
|
||||
search_fields = ['title', 'department', 'location', 'short_description']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
readonly_fields = ['posted_date', 'updated_date', 'applications_count']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('title', 'slug', 'department', 'status', 'featured', 'priority')
|
||||
}),
|
||||
('Employment Details', {
|
||||
'fields': ('employment_type', 'location_type', 'location', 'open_positions', 'experience_required')
|
||||
}),
|
||||
('Salary Information', {
|
||||
'fields': ('salary_min', 'salary_max', 'salary_currency', 'salary_period', 'salary_additional')
|
||||
}),
|
||||
('Job Description', {
|
||||
'fields': ('short_description', 'about_role')
|
||||
}),
|
||||
('Requirements & Qualifications', {
|
||||
'fields': ('requirements', 'responsibilities', 'qualifications', 'bonus_points', 'benefits'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Dates', {
|
||||
'fields': ('start_date', 'deadline', 'posted_date', 'updated_date')
|
||||
}),
|
||||
)
|
||||
|
||||
def status_badge(self, obj):
|
||||
colors = {
|
||||
'active': 'green',
|
||||
'closed': 'red',
|
||||
'draft': 'orange'
|
||||
}
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{}</span>',
|
||||
colors.get(obj.status, 'black'),
|
||||
obj.get_status_display()
|
||||
)
|
||||
status_badge.short_description = 'Status'
|
||||
|
||||
def applications_count(self, obj):
|
||||
count = obj.applications.count()
|
||||
return format_html(
|
||||
'<span style="font-weight: bold;">{} application(s)</span>',
|
||||
count
|
||||
)
|
||||
applications_count.short_description = 'Applications'
|
||||
|
||||
|
||||
@admin.register(JobApplication)
|
||||
class JobApplicationAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'full_name',
|
||||
'email',
|
||||
'job',
|
||||
'status_badge',
|
||||
'applied_date',
|
||||
'resume_link'
|
||||
]
|
||||
list_filter = ['status', 'job', 'applied_date']
|
||||
search_fields = ['first_name', 'last_name', 'email', 'job__title']
|
||||
readonly_fields = ['applied_date', 'updated_date', 'resume_link']
|
||||
|
||||
fieldsets = (
|
||||
('Job Information', {
|
||||
'fields': ('job', 'status')
|
||||
}),
|
||||
('Applicant Information', {
|
||||
'fields': ('first_name', 'last_name', 'email', 'phone')
|
||||
}),
|
||||
('Professional Information', {
|
||||
'fields': ('current_position', 'current_company', 'years_of_experience')
|
||||
}),
|
||||
('Application Details', {
|
||||
'fields': ('cover_letter', 'resume', 'resume_link', 'portfolio_url')
|
||||
}),
|
||||
('Social Links', {
|
||||
'fields': ('linkedin_url', 'github_url', 'website_url'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Availability & Salary', {
|
||||
'fields': ('available_from', 'notice_period', 'expected_salary', 'salary_currency'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('applied_date', 'updated_date', 'consent', 'notes')
|
||||
}),
|
||||
)
|
||||
|
||||
def status_badge(self, obj):
|
||||
colors = {
|
||||
'new': 'blue',
|
||||
'reviewing': 'orange',
|
||||
'shortlisted': 'purple',
|
||||
'interviewed': 'teal',
|
||||
'accepted': 'green',
|
||||
'rejected': 'red'
|
||||
}
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{}</span>',
|
||||
colors.get(obj.status, 'black'),
|
||||
obj.get_status_display()
|
||||
)
|
||||
status_badge.short_description = 'Status'
|
||||
|
||||
def resume_link(self, obj):
|
||||
if obj.resume:
|
||||
return format_html(
|
||||
'<a href="{}" target="_blank">Download Resume</a>',
|
||||
obj.resume.url
|
||||
)
|
||||
return '-'
|
||||
resume_link.short_description = 'Resume'
|
||||
|
||||
actions = ['mark_as_reviewing', 'mark_as_shortlisted', 'mark_as_rejected']
|
||||
|
||||
def mark_as_reviewing(self, request, queryset):
|
||||
queryset.update(status='reviewing')
|
||||
self.message_user(request, f'{queryset.count()} application(s) marked as reviewing.')
|
||||
mark_as_reviewing.short_description = 'Mark as Reviewing'
|
||||
|
||||
def mark_as_shortlisted(self, request, queryset):
|
||||
queryset.update(status='shortlisted')
|
||||
self.message_user(request, f'{queryset.count()} application(s) marked as shortlisted.')
|
||||
mark_as_shortlisted.short_description = 'Mark as Shortlisted'
|
||||
|
||||
def mark_as_rejected(self, request, queryset):
|
||||
queryset.update(status='rejected')
|
||||
self.message_user(request, f'{queryset.count()} application(s) marked as rejected.')
|
||||
mark_as_rejected.short_description = 'Mark as Rejected'
|
||||
|
||||
7
backEnd/career/apps.py
Normal file
7
backEnd/career/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CareerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'career'
|
||||
|
||||
110
backEnd/career/email_service.py
Normal file
110
backEnd/career/email_service.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CareerEmailService:
|
||||
"""Service for handling career-related emails"""
|
||||
|
||||
def send_application_confirmation(self, application):
|
||||
"""
|
||||
Send confirmation email to applicant
|
||||
"""
|
||||
try:
|
||||
subject = f"Application Received - {application.job.title}"
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
to_email = [application.email]
|
||||
|
||||
# Create context for email template
|
||||
context = {
|
||||
'applicant_name': application.full_name,
|
||||
'job_title': application.job.title,
|
||||
'job_location': application.job.location,
|
||||
'application_date': application.applied_date,
|
||||
}
|
||||
|
||||
# Render email templates
|
||||
text_content = render_to_string('career/application_confirmation.txt', context)
|
||||
html_content = render_to_string('career/application_confirmation.html', context)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=from_email,
|
||||
to=to_email
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
# Send email
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f"Confirmation email sent to {application.email}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send confirmation email: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
def send_application_notification_to_admin(self, application):
|
||||
"""
|
||||
Send notification email to company about new application
|
||||
"""
|
||||
try:
|
||||
subject = f"New Job Application: {application.job.title} - {application.full_name}"
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
to_email = [settings.COMPANY_EMAIL]
|
||||
|
||||
# Create context for email template
|
||||
context = {
|
||||
'applicant_name': application.full_name,
|
||||
'applicant_email': application.email,
|
||||
'applicant_phone': application.phone,
|
||||
'job_title': application.job.title,
|
||||
'current_position': application.current_position,
|
||||
'current_company': application.current_company,
|
||||
'years_of_experience': application.years_of_experience,
|
||||
'cover_letter': application.cover_letter,
|
||||
'portfolio_url': application.portfolio_url,
|
||||
'linkedin_url': application.linkedin_url,
|
||||
'github_url': application.github_url,
|
||||
'website_url': application.website_url,
|
||||
'expected_salary': application.expected_salary,
|
||||
'salary_currency': application.salary_currency,
|
||||
'available_from': application.available_from,
|
||||
'notice_period': application.notice_period,
|
||||
'application_date': application.applied_date,
|
||||
'resume_url': application.resume.url if application.resume else None,
|
||||
}
|
||||
|
||||
# Render email templates
|
||||
text_content = render_to_string('career/application_notification.txt', context)
|
||||
html_content = render_to_string('career/application_notification.html', context)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=from_email,
|
||||
to=to_email,
|
||||
reply_to=[application.email]
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
# Attach resume if available
|
||||
if application.resume:
|
||||
email.attach_file(application.resume.path)
|
||||
|
||||
# Send email
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f"Admin notification email sent for application from {application.email}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send admin notification email: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
0
backEnd/career/management/__init__.py
Normal file
0
backEnd/career/management/__init__.py
Normal file
BIN
backEnd/career/management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/career/management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
0
backEnd/career/management/commands/__init__.py
Normal file
0
backEnd/career/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
305
backEnd/career/management/commands/populate_jobs.py
Normal file
305
backEnd/career/management/commands/populate_jobs.py
Normal file
@@ -0,0 +1,305 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from career.models import JobPosition
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populate database with sample job positions'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write('Creating sample job positions...')
|
||||
|
||||
jobs_data = [
|
||||
{
|
||||
'title': 'UI/UX Designer',
|
||||
'department': 'Design',
|
||||
'employment_type': 'full-time',
|
||||
'location_type': 'remote',
|
||||
'location': 'Remote',
|
||||
'open_positions': 2,
|
||||
'experience_required': '3+ years',
|
||||
'salary_min': 1500,
|
||||
'salary_max': 2500,
|
||||
'salary_currency': 'USD',
|
||||
'salary_period': 'per month',
|
||||
'salary_additional': '+ VAT (B2B) + bonuses',
|
||||
'short_description': 'Join our design team to create beautiful and intuitive user experiences.',
|
||||
'about_role': 'We are looking for a talented UI/UX Designer who is passionate about creating exceptional user experiences. You will work closely with our product and engineering teams to design and implement user-friendly interfaces for our web and mobile applications.',
|
||||
'requirements': [
|
||||
'At least 3 years of commercial UI/UX design experience',
|
||||
'Strong portfolio showcasing web/UI projects',
|
||||
'Fluent English in verbal and written communication',
|
||||
'Proficiency in design tools (Sketch, Figma, or Adobe XD)',
|
||||
'Understanding of responsive design principles',
|
||||
'Experience with user research and usability testing',
|
||||
],
|
||||
'responsibilities': [
|
||||
'Create wireframes, prototypes, and high-fidelity designs',
|
||||
'Conduct user research and usability testing',
|
||||
'Collaborate with developers to implement designs',
|
||||
'Maintain and evolve design systems',
|
||||
'Present design concepts to stakeholders',
|
||||
],
|
||||
'qualifications': [
|
||||
'Portfolio demonstrating strong UI/UX design skills',
|
||||
'Experience with design systems',
|
||||
'Knowledge of HTML/CSS basics',
|
||||
'Understanding of accessibility standards',
|
||||
],
|
||||
'bonus_points': [
|
||||
'Experience with motion design',
|
||||
'Knowledge of front-end development',
|
||||
'Illustration skills',
|
||||
'Experience with design tokens',
|
||||
],
|
||||
'benefits': [
|
||||
'Remote work flexibility',
|
||||
'Competitive salary package',
|
||||
'Professional development budget',
|
||||
'Latest design tools and software',
|
||||
'Health insurance',
|
||||
'Flexible working hours',
|
||||
],
|
||||
'start_date': 'ASAP',
|
||||
'status': 'active',
|
||||
'featured': True,
|
||||
'priority': 10,
|
||||
},
|
||||
{
|
||||
'title': 'Senior Full Stack Developer',
|
||||
'department': 'Engineering',
|
||||
'employment_type': 'full-time',
|
||||
'location_type': 'hybrid',
|
||||
'location': 'Hybrid / Remote',
|
||||
'open_positions': 3,
|
||||
'experience_required': '5+ years',
|
||||
'salary_min': 3000,
|
||||
'salary_max': 5000,
|
||||
'salary_currency': 'USD',
|
||||
'salary_period': 'per month',
|
||||
'salary_additional': '+ Annual bonus + Stock options',
|
||||
'short_description': 'Build scalable applications with cutting-edge technologies.',
|
||||
'about_role': 'We are seeking an experienced Full Stack Developer to join our engineering team. You will be responsible for developing and maintaining our web applications using modern technologies and best practices.',
|
||||
'requirements': [
|
||||
'5+ years of full-stack development experience',
|
||||
'Strong proficiency in React, Next.js, and TypeScript',
|
||||
'Experience with Python/Django or Node.js',
|
||||
'Solid understanding of RESTful APIs and GraphQL',
|
||||
'Experience with SQL and NoSQL databases',
|
||||
'Familiarity with cloud platforms (AWS, GCP, or Azure)',
|
||||
],
|
||||
'responsibilities': [
|
||||
'Develop and maintain web applications',
|
||||
'Write clean, maintainable, and efficient code',
|
||||
'Participate in code reviews',
|
||||
'Collaborate with cross-functional teams',
|
||||
'Optimize applications for performance',
|
||||
'Mentor junior developers',
|
||||
],
|
||||
'qualifications': [
|
||||
'Bachelor\'s degree in Computer Science or related field',
|
||||
'Strong problem-solving skills',
|
||||
'Experience with version control (Git)',
|
||||
'Good understanding of software development lifecycle',
|
||||
],
|
||||
'bonus_points': [
|
||||
'Experience with Docker and Kubernetes',
|
||||
'Knowledge of CI/CD pipelines',
|
||||
'Contributions to open-source projects',
|
||||
'Experience with microservices architecture',
|
||||
],
|
||||
'benefits': [
|
||||
'Competitive salary with stock options',
|
||||
'Flexible work arrangements',
|
||||
'Latest MacBook Pro or custom PC',
|
||||
'Learning and development budget',
|
||||
'Health and dental insurance',
|
||||
'Gym membership',
|
||||
],
|
||||
'start_date': 'Within 1 month',
|
||||
'status': 'active',
|
||||
'featured': True,
|
||||
'priority': 9,
|
||||
},
|
||||
{
|
||||
'title': 'Digital Marketing Manager',
|
||||
'department': 'Marketing',
|
||||
'employment_type': 'full-time',
|
||||
'location_type': 'on-site',
|
||||
'location': 'New York, NY',
|
||||
'open_positions': 1,
|
||||
'experience_required': '4+ years',
|
||||
'salary_min': 2500,
|
||||
'salary_max': 4000,
|
||||
'salary_currency': 'USD',
|
||||
'salary_period': 'per month',
|
||||
'short_description': 'Lead our digital marketing efforts and grow our online presence.',
|
||||
'about_role': 'We are looking for a creative and data-driven Digital Marketing Manager to lead our marketing initiatives. You will be responsible for developing and executing marketing strategies to increase brand awareness and drive customer acquisition.',
|
||||
'requirements': [
|
||||
'4+ years of digital marketing experience',
|
||||
'Proven track record of successful marketing campaigns',
|
||||
'Strong understanding of SEO, SEM, and social media marketing',
|
||||
'Experience with marketing automation tools',
|
||||
'Excellent analytical and communication skills',
|
||||
'Data-driven mindset with strong analytical abilities',
|
||||
],
|
||||
'responsibilities': [
|
||||
'Develop and execute digital marketing strategies',
|
||||
'Manage social media channels and campaigns',
|
||||
'Oversee content marketing initiatives',
|
||||
'Analyze campaign performance and ROI',
|
||||
'Manage marketing budget',
|
||||
'Collaborate with sales and product teams',
|
||||
],
|
||||
'qualifications': [
|
||||
'Bachelor\'s degree in Marketing or related field',
|
||||
'Experience with Google Analytics and marketing tools',
|
||||
'Strong project management skills',
|
||||
'Creative thinking and problem-solving abilities',
|
||||
],
|
||||
'bonus_points': [
|
||||
'Experience with B2B marketing',
|
||||
'Knowledge of marketing automation platforms',
|
||||
'Video production skills',
|
||||
'Experience with influencer marketing',
|
||||
],
|
||||
'benefits': [
|
||||
'Competitive salary package',
|
||||
'Marketing conferences and events budget',
|
||||
'Professional development opportunities',
|
||||
'Health insurance',
|
||||
'Paid time off',
|
||||
],
|
||||
'start_date': 'ASAP',
|
||||
'status': 'active',
|
||||
'featured': False,
|
||||
'priority': 7,
|
||||
},
|
||||
{
|
||||
'title': 'Data Analyst',
|
||||
'department': 'Analytics',
|
||||
'employment_type': 'full-time',
|
||||
'location_type': 'remote',
|
||||
'location': 'Remote',
|
||||
'open_positions': 2,
|
||||
'experience_required': '2+ years',
|
||||
'salary_min': 2000,
|
||||
'salary_max': 3500,
|
||||
'salary_currency': 'USD',
|
||||
'salary_period': 'per month',
|
||||
'short_description': 'Turn data into actionable insights to drive business decisions.',
|
||||
'about_role': 'We are seeking a Data Analyst to join our analytics team. You will be responsible for analyzing complex datasets, creating reports, and providing insights to support business decision-making.',
|
||||
'requirements': [
|
||||
'2+ years of data analysis experience',
|
||||
'Proficiency in SQL and data visualization tools',
|
||||
'Experience with Python or R for data analysis',
|
||||
'Strong statistical and analytical skills',
|
||||
'Ability to communicate insights to non-technical stakeholders',
|
||||
],
|
||||
'responsibilities': [
|
||||
'Analyze large datasets to identify trends and patterns',
|
||||
'Create dashboards and reports',
|
||||
'Collaborate with stakeholders to understand data needs',
|
||||
'Perform statistical analysis',
|
||||
'Present findings to leadership team',
|
||||
],
|
||||
'qualifications': [
|
||||
'Bachelor\'s degree in Statistics, Mathematics, or related field',
|
||||
'Experience with Tableau, Power BI, or similar tools',
|
||||
'Strong attention to detail',
|
||||
'Excellent problem-solving skills',
|
||||
],
|
||||
'bonus_points': [
|
||||
'Experience with machine learning',
|
||||
'Knowledge of big data technologies',
|
||||
'Experience with A/B testing',
|
||||
],
|
||||
'benefits': [
|
||||
'Remote work',
|
||||
'Competitive compensation',
|
||||
'Learning and development budget',
|
||||
'Health insurance',
|
||||
'Flexible hours',
|
||||
],
|
||||
'start_date': 'Within 2 weeks',
|
||||
'status': 'active',
|
||||
'featured': False,
|
||||
'priority': 6,
|
||||
},
|
||||
{
|
||||
'title': 'Product Manager',
|
||||
'department': 'Product',
|
||||
'employment_type': 'full-time',
|
||||
'location_type': 'hybrid',
|
||||
'location': 'San Francisco, CA / Remote',
|
||||
'open_positions': 1,
|
||||
'experience_required': '5+ years',
|
||||
'salary_min': 4000,
|
||||
'salary_max': 6000,
|
||||
'salary_currency': 'USD',
|
||||
'salary_period': 'per month',
|
||||
'salary_additional': '+ Stock options + Annual bonus',
|
||||
'short_description': 'Lead product strategy and development for our flagship products.',
|
||||
'about_role': 'We are looking for an experienced Product Manager to drive the vision and execution of our products. You will work closely with engineering, design, and marketing teams to deliver exceptional products that delight our customers.',
|
||||
'requirements': [
|
||||
'5+ years of product management experience',
|
||||
'Proven track record of successful product launches',
|
||||
'Strong understanding of agile methodologies',
|
||||
'Excellent communication and leadership skills',
|
||||
'Data-driven decision-making approach',
|
||||
'Experience with product analytics tools',
|
||||
],
|
||||
'responsibilities': [
|
||||
'Define product vision and strategy',
|
||||
'Create and maintain product roadmap',
|
||||
'Gather and prioritize requirements',
|
||||
'Work with engineering team on implementation',
|
||||
'Conduct market research and competitive analysis',
|
||||
'Analyze product metrics and user feedback',
|
||||
],
|
||||
'qualifications': [
|
||||
'Bachelor\'s degree (MBA preferred)',
|
||||
'Strong analytical and problem-solving skills',
|
||||
'Experience with product management tools',
|
||||
'Understanding of UX principles',
|
||||
],
|
||||
'bonus_points': [
|
||||
'Technical background',
|
||||
'Experience in SaaS products',
|
||||
'Knowledge of growth strategies',
|
||||
],
|
||||
'benefits': [
|
||||
'Competitive salary with equity',
|
||||
'Hybrid work model',
|
||||
'Professional development budget',
|
||||
'Health and wellness benefits',
|
||||
'Team offsites and events',
|
||||
],
|
||||
'start_date': 'Within 1 month',
|
||||
'status': 'active',
|
||||
'featured': True,
|
||||
'priority': 8,
|
||||
},
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
for job_data in jobs_data:
|
||||
job, created = JobPosition.objects.get_or_create(
|
||||
slug=job_data['title'].lower().replace(' ', '-').replace('/', '-'),
|
||||
defaults=job_data
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Created job: {job.title}')
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'- Job already exists: {job.title}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'\nSuccessfully created {created_count} job position(s)!'
|
||||
)
|
||||
)
|
||||
|
||||
88
backEnd/career/migrations/0001_initial.py
Normal file
88
backEnd/career/migrations/0001_initial.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-07 14:45
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='JobPosition',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(help_text='Job title', max_length=255)),
|
||||
('slug', models.SlugField(blank=True, max_length=255, unique=True)),
|
||||
('department', models.CharField(blank=True, help_text='Department or category', max_length=100)),
|
||||
('employment_type', models.CharField(choices=[('full-time', 'Full Time'), ('part-time', 'Part Time'), ('contract', 'Contract'), ('internship', 'Internship'), ('remote', 'Remote')], default='full-time', max_length=20)),
|
||||
('location_type', models.CharField(choices=[('remote', 'Remote'), ('on-site', 'On-site'), ('hybrid', 'Hybrid')], default='remote', max_length=20)),
|
||||
('location', models.CharField(default='Remote', help_text='Work location', max_length=255)),
|
||||
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions')),
|
||||
('experience_required', models.CharField(blank=True, help_text='e.g., 3+ years', max_length=100)),
|
||||
('salary_min', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('salary_max', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('salary_currency', models.CharField(default='USD', max_length=10)),
|
||||
('salary_period', models.CharField(default='per month', help_text='e.g., per month, per year', max_length=20)),
|
||||
('salary_additional', models.TextField(blank=True, help_text='Additional salary info like bonuses, benefits')),
|
||||
('short_description', models.TextField(blank=True, help_text='Brief description for listing page')),
|
||||
('about_role', models.TextField(blank=True, help_text='About this role / Who we are')),
|
||||
('requirements', models.JSONField(blank=True, default=list, help_text='List of requirements')),
|
||||
('responsibilities', models.JSONField(blank=True, default=list, help_text='List of responsibilities')),
|
||||
('qualifications', models.JSONField(blank=True, default=list, help_text='List of qualifications')),
|
||||
('bonus_points', models.JSONField(blank=True, default=list, help_text='Nice to have skills')),
|
||||
('benefits', models.JSONField(blank=True, default=list, help_text='What you get')),
|
||||
('start_date', models.CharField(default='ASAP', help_text='Expected start date', max_length=100)),
|
||||
('posted_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('deadline', models.DateTimeField(blank=True, help_text='Application deadline', null=True)),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('closed', 'Closed'), ('draft', 'Draft')], default='active', max_length=20)),
|
||||
('featured', models.BooleanField(default=False, help_text='Feature this job on homepage')),
|
||||
('priority', models.IntegerField(default=0, help_text='Higher number = higher priority in listing')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Job Position',
|
||||
'verbose_name_plural': 'Job Positions',
|
||||
'ordering': ['-priority', '-posted_date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobApplication',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('current_position', models.CharField(blank=True, help_text='Current job title', max_length=255)),
|
||||
('current_company', models.CharField(blank=True, max_length=255)),
|
||||
('years_of_experience', models.CharField(blank=True, max_length=50)),
|
||||
('cover_letter', models.TextField(blank=True, help_text='Cover letter or message')),
|
||||
('resume', models.FileField(help_text='Upload your resume (PDF, DOC, DOCX)', upload_to='career/resumes/%Y/%m/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx'])])),
|
||||
('portfolio_url', models.URLField(blank=True, help_text='Link to portfolio or LinkedIn')),
|
||||
('linkedin_url', models.URLField(blank=True)),
|
||||
('github_url', models.URLField(blank=True)),
|
||||
('website_url', models.URLField(blank=True)),
|
||||
('available_from', models.DateField(blank=True, help_text='When can you start?', null=True)),
|
||||
('notice_period', models.CharField(blank=True, help_text='Notice period if applicable', max_length=100)),
|
||||
('expected_salary', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('salary_currency', models.CharField(default='USD', max_length=10)),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('reviewing', 'Reviewing'), ('shortlisted', 'Shortlisted'), ('interviewed', 'Interviewed'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='new', max_length=20)),
|
||||
('applied_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('notes', models.TextField(blank=True, help_text='Internal notes (not visible to applicant)')),
|
||||
('consent', models.BooleanField(default=False, help_text='Consent to data processing')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='career.jobposition')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Job Application',
|
||||
'verbose_name_plural': 'Job Applications',
|
||||
'ordering': ['-applied_date'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backEnd/career/migrations/__init__.py
Normal file
0
backEnd/career/migrations/__init__.py
Normal file
Binary file not shown.
BIN
backEnd/career/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/career/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
166
backEnd/career/models.py
Normal file
166
backEnd/career/models.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.core.validators import FileExtensionValidator
|
||||
|
||||
|
||||
class JobPosition(models.Model):
|
||||
"""Model for job positions/openings"""
|
||||
|
||||
EMPLOYMENT_TYPE_CHOICES = [
|
||||
('full-time', 'Full Time'),
|
||||
('part-time', 'Part Time'),
|
||||
('contract', 'Contract'),
|
||||
('internship', 'Internship'),
|
||||
('remote', 'Remote'),
|
||||
]
|
||||
|
||||
LOCATION_TYPE_CHOICES = [
|
||||
('remote', 'Remote'),
|
||||
('on-site', 'On-site'),
|
||||
('hybrid', 'Hybrid'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('active', 'Active'),
|
||||
('closed', 'Closed'),
|
||||
('draft', 'Draft'),
|
||||
]
|
||||
|
||||
# Basic Information
|
||||
title = models.CharField(max_length=255, help_text="Job title")
|
||||
slug = models.SlugField(max_length=255, unique=True, blank=True)
|
||||
department = models.CharField(max_length=100, blank=True, help_text="Department or category")
|
||||
|
||||
# Employment Details
|
||||
employment_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=EMPLOYMENT_TYPE_CHOICES,
|
||||
default='full-time'
|
||||
)
|
||||
location_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=LOCATION_TYPE_CHOICES,
|
||||
default='remote'
|
||||
)
|
||||
location = models.CharField(max_length=255, default='Remote', help_text="Work location")
|
||||
|
||||
# Position Details
|
||||
open_positions = models.PositiveIntegerField(default=1, help_text="Number of open positions")
|
||||
experience_required = models.CharField(max_length=100, blank=True, help_text="e.g., 3+ years")
|
||||
|
||||
# Salary Information
|
||||
salary_min = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
salary_max = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
salary_currency = models.CharField(max_length=10, default='USD')
|
||||
salary_period = models.CharField(max_length=20, default='per month', help_text="e.g., per month, per year")
|
||||
salary_additional = models.TextField(blank=True, help_text="Additional salary info like bonuses, benefits")
|
||||
|
||||
# Job Description
|
||||
short_description = models.TextField(blank=True, help_text="Brief description for listing page")
|
||||
about_role = models.TextField(blank=True, help_text="About this role / Who we are")
|
||||
requirements = models.JSONField(default=list, blank=True, help_text="List of requirements")
|
||||
responsibilities = models.JSONField(default=list, blank=True, help_text="List of responsibilities")
|
||||
qualifications = models.JSONField(default=list, blank=True, help_text="List of qualifications")
|
||||
bonus_points = models.JSONField(default=list, blank=True, help_text="Nice to have skills")
|
||||
benefits = models.JSONField(default=list, blank=True, help_text="What you get")
|
||||
|
||||
# Dates and Status
|
||||
start_date = models.CharField(max_length=100, default='ASAP', help_text="Expected start date")
|
||||
posted_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
deadline = models.DateTimeField(null=True, blank=True, help_text="Application deadline")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
|
||||
# SEO and Metadata
|
||||
featured = models.BooleanField(default=False, help_text="Feature this job on homepage")
|
||||
priority = models.IntegerField(default=0, help_text="Higher number = higher priority in listing")
|
||||
|
||||
class Meta:
|
||||
ordering = ['-priority', '-posted_date']
|
||||
verbose_name = 'Job Position'
|
||||
verbose_name_plural = 'Job Positions'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.open_positions} positions)"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
# Ensure unique slug
|
||||
original_slug = self.slug
|
||||
counter = 1
|
||||
while JobPosition.objects.filter(slug=self.slug).exists():
|
||||
self.slug = f"{original_slug}-{counter}"
|
||||
counter += 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class JobApplication(models.Model):
|
||||
"""Model for job applications"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('new', 'New'),
|
||||
('reviewing', 'Reviewing'),
|
||||
('shortlisted', 'Shortlisted'),
|
||||
('interviewed', 'Interviewed'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
]
|
||||
|
||||
# Related Job
|
||||
job = models.ForeignKey(JobPosition, on_delete=models.CASCADE, related_name='applications')
|
||||
|
||||
# Applicant Information
|
||||
first_name = models.CharField(max_length=100)
|
||||
last_name = models.CharField(max_length=100)
|
||||
email = models.EmailField()
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
|
||||
# Professional Information
|
||||
current_position = models.CharField(max_length=255, blank=True, help_text="Current job title")
|
||||
current_company = models.CharField(max_length=255, blank=True)
|
||||
years_of_experience = models.CharField(max_length=50, blank=True)
|
||||
|
||||
# Application Details
|
||||
cover_letter = models.TextField(blank=True, help_text="Cover letter or message")
|
||||
resume = models.FileField(
|
||||
upload_to='career/resumes/%Y/%m/',
|
||||
validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx'])],
|
||||
help_text="Upload your resume (PDF, DOC, DOCX)"
|
||||
)
|
||||
portfolio_url = models.URLField(blank=True, help_text="Link to portfolio or LinkedIn")
|
||||
|
||||
# Additional Information
|
||||
linkedin_url = models.URLField(blank=True)
|
||||
github_url = models.URLField(blank=True)
|
||||
website_url = models.URLField(blank=True)
|
||||
|
||||
# Availability
|
||||
available_from = models.DateField(null=True, blank=True, help_text="When can you start?")
|
||||
notice_period = models.CharField(max_length=100, blank=True, help_text="Notice period if applicable")
|
||||
|
||||
# Salary Expectations
|
||||
expected_salary = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
salary_currency = models.CharField(max_length=10, default='USD')
|
||||
|
||||
# Application Metadata
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='new')
|
||||
applied_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
notes = models.TextField(blank=True, help_text="Internal notes (not visible to applicant)")
|
||||
|
||||
# Privacy
|
||||
consent = models.BooleanField(default=False, help_text="Consent to data processing")
|
||||
|
||||
class Meta:
|
||||
ordering = ['-applied_date']
|
||||
verbose_name = 'Job Application'
|
||||
verbose_name_plural = 'Job Applications'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name} - {self.job.title}"
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
131
backEnd/career/serializers.py
Normal file
131
backEnd/career/serializers.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from rest_framework import serializers
|
||||
from .models import JobPosition, JobApplication
|
||||
|
||||
|
||||
class JobPositionListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for job position list view"""
|
||||
|
||||
class Meta:
|
||||
model = JobPosition
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'slug',
|
||||
'department',
|
||||
'employment_type',
|
||||
'location_type',
|
||||
'location',
|
||||
'open_positions',
|
||||
'short_description',
|
||||
'salary_min',
|
||||
'salary_max',
|
||||
'salary_currency',
|
||||
'salary_period',
|
||||
'posted_date',
|
||||
'status',
|
||||
'featured',
|
||||
]
|
||||
|
||||
|
||||
class JobPositionDetailSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for job position detail view"""
|
||||
|
||||
class Meta:
|
||||
model = JobPosition
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'slug',
|
||||
'department',
|
||||
'employment_type',
|
||||
'location_type',
|
||||
'location',
|
||||
'open_positions',
|
||||
'experience_required',
|
||||
'salary_min',
|
||||
'salary_max',
|
||||
'salary_currency',
|
||||
'salary_period',
|
||||
'salary_additional',
|
||||
'short_description',
|
||||
'about_role',
|
||||
'requirements',
|
||||
'responsibilities',
|
||||
'qualifications',
|
||||
'bonus_points',
|
||||
'benefits',
|
||||
'start_date',
|
||||
'posted_date',
|
||||
'updated_date',
|
||||
'deadline',
|
||||
'status',
|
||||
]
|
||||
|
||||
|
||||
class JobApplicationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for job application submission"""
|
||||
|
||||
job_title = serializers.CharField(source='job.title', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = JobApplication
|
||||
fields = [
|
||||
'id',
|
||||
'job',
|
||||
'job_title',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'phone',
|
||||
'current_position',
|
||||
'current_company',
|
||||
'years_of_experience',
|
||||
'cover_letter',
|
||||
'resume',
|
||||
'portfolio_url',
|
||||
'linkedin_url',
|
||||
'github_url',
|
||||
'website_url',
|
||||
'available_from',
|
||||
'notice_period',
|
||||
'expected_salary',
|
||||
'salary_currency',
|
||||
'consent',
|
||||
'applied_date',
|
||||
]
|
||||
read_only_fields = ['id', 'applied_date', 'job_title']
|
||||
|
||||
def validate_resume(self, value):
|
||||
"""Validate resume file size"""
|
||||
if value.size > 5 * 1024 * 1024: # 5MB
|
||||
raise serializers.ValidationError("Resume file size cannot exceed 5MB")
|
||||
return value
|
||||
|
||||
def validate_consent(self, value):
|
||||
"""Ensure user has given consent"""
|
||||
if not value:
|
||||
raise serializers.ValidationError("You must consent to data processing to apply")
|
||||
return value
|
||||
|
||||
|
||||
class JobApplicationListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for listing job applications (admin view)"""
|
||||
|
||||
job_title = serializers.CharField(source='job.title', read_only=True)
|
||||
full_name = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = JobApplication
|
||||
fields = [
|
||||
'id',
|
||||
'job',
|
||||
'job_title',
|
||||
'full_name',
|
||||
'email',
|
||||
'phone',
|
||||
'status',
|
||||
'applied_date',
|
||||
'resume',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
background-color: #f9f9f9;
|
||||
padding: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.details {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #4A90E2;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Application Received</h1>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Dear <strong>{{ applicant_name }}</strong>,</p>
|
||||
|
||||
<p>Thank you for applying for the <strong>{{ job_title }}</strong> position at GNX!</p>
|
||||
|
||||
<p>We have received your application and our team will review it carefully. We appreciate your interest in joining our team.</p>
|
||||
|
||||
<div class="details">
|
||||
<h3>Application Details</h3>
|
||||
<ul>
|
||||
<li><strong>Position:</strong> {{ job_title }}</li>
|
||||
<li><strong>Location:</strong> {{ job_location }}</li>
|
||||
<li><strong>Applied on:</strong> {{ application_date|date:"F d, Y" }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>What happens next?</h3>
|
||||
<p>Our hiring team will review your application and resume. If your qualifications match our requirements, we will contact you within 1-2 weeks to discuss the next steps.</p>
|
||||
|
||||
<p>If you have any questions, please don't hesitate to reach out to us.</p>
|
||||
|
||||
<p>Best regards,<br><strong>The GNX Team</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>This is an automated message. Please do not reply to this email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22
backEnd/career/templates/career/application_confirmation.txt
Normal file
22
backEnd/career/templates/career/application_confirmation.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
Dear {{ applicant_name }},
|
||||
|
||||
Thank you for applying for the {{ job_title }} position at GNX!
|
||||
|
||||
We have received your application and our team will review it carefully. We appreciate your interest in joining our team.
|
||||
|
||||
Application Details:
|
||||
- Position: {{ job_title }}
|
||||
- Location: {{ job_location }}
|
||||
- Applied on: {{ application_date|date:"F d, Y" }}
|
||||
|
||||
What happens next?
|
||||
Our hiring team will review your application and resume. If your qualifications match our requirements, we will contact you within 1-2 weeks to discuss the next steps.
|
||||
|
||||
If you have any questions, please don't hesitate to reach out to us.
|
||||
|
||||
Best regards,
|
||||
The GNX Team
|
||||
|
||||
---
|
||||
This is an automated message. Please do not reply to this email.
|
||||
|
||||
160
backEnd/career/templates/career/application_notification.html
Normal file
160
backEnd/career/templates/career/application_notification.html
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #2C3E50;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
background-color: #f9f9f9;
|
||||
padding: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.section {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.section h3 {
|
||||
margin-top: 0;
|
||||
color: #2C3E50;
|
||||
}
|
||||
.info-row {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
.cover-letter {
|
||||
background-color: #f0f0f0;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.links a {
|
||||
display: inline-block;
|
||||
margin: 5px 10px 5px 0;
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎯 New Job Application</h1>
|
||||
<p>{{ job_title }}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="section">
|
||||
<h3>👤 Applicant Information</h3>
|
||||
<div class="info-row">
|
||||
<span class="label">Name:</span> {{ applicant_name }}
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Email:</span> <a href="mailto:{{ applicant_email }}">{{ applicant_email }}</a>
|
||||
</div>
|
||||
{% if applicant_phone %}
|
||||
<div class="info-row">
|
||||
<span class="label">Phone:</span> {{ applicant_phone }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>💼 Professional Information</h3>
|
||||
{% if current_position %}
|
||||
<div class="info-row">
|
||||
<span class="label">Current Position:</span> {{ current_position }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if current_company %}
|
||||
<div class="info-row">
|
||||
<span class="label">Current Company:</span> {{ current_company }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if years_of_experience %}
|
||||
<div class="info-row">
|
||||
<span class="label">Years of Experience:</span> {{ years_of_experience }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>📋 Application Details</h3>
|
||||
<div class="info-row">
|
||||
<span class="label">Position Applied:</span> {{ job_title }}
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Application Date:</span> {{ application_date|date:"F d, Y at h:i A" }}
|
||||
</div>
|
||||
{% if expected_salary %}
|
||||
<div class="info-row">
|
||||
<span class="label">Expected Salary:</span> {{ expected_salary }} {{ salary_currency }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if available_from %}
|
||||
<div class="info-row">
|
||||
<span class="label">Available From:</span> {{ available_from|date:"F d, Y" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if notice_period %}
|
||||
<div class="info-row">
|
||||
<span class="label">Notice Period:</span> {{ notice_period }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if portfolio_url or linkedin_url or github_url or website_url %}
|
||||
<div class="section">
|
||||
<h3>🔗 Links</h3>
|
||||
<div class="links">
|
||||
{% if portfolio_url %}<a href="{{ portfolio_url }}" target="_blank">📁 Portfolio</a>{% endif %}
|
||||
{% if linkedin_url %}<a href="{{ linkedin_url }}" target="_blank">💼 LinkedIn</a>{% endif %}
|
||||
{% if github_url %}<a href="{{ github_url }}" target="_blank">💻 GitHub</a>{% endif %}
|
||||
{% if website_url %}<a href="{{ website_url }}" target="_blank">🌐 Website</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if cover_letter %}
|
||||
<div class="section">
|
||||
<h3>✉️ Cover Letter</h3>
|
||||
<div class="cover-letter">{{ cover_letter }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>Resume is attached to this email.</strong></p>
|
||||
<p>Please log in to the admin panel to review the full application and update its status.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
48
backEnd/career/templates/career/application_notification.txt
Normal file
48
backEnd/career/templates/career/application_notification.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
New Job Application Received
|
||||
|
||||
A new application has been submitted for the {{ job_title }} position.
|
||||
|
||||
Applicant Information:
|
||||
--------------------
|
||||
Name: {{ applicant_name }}
|
||||
Email: {{ applicant_email }}
|
||||
Phone: {{ applicant_phone }}
|
||||
|
||||
Professional Information:
|
||||
-----------------------
|
||||
Current Position: {{ current_position }}
|
||||
Current Company: {{ current_company }}
|
||||
Years of Experience: {{ years_of_experience }}
|
||||
|
||||
Application Details:
|
||||
------------------
|
||||
Applied for: {{ job_title }}
|
||||
Application Date: {{ application_date|date:"F d, Y at h:i A" }}
|
||||
|
||||
{% if expected_salary %}
|
||||
Expected Salary: {{ expected_salary }} {{ salary_currency }}
|
||||
{% endif %}
|
||||
|
||||
{% if available_from %}
|
||||
Available From: {{ available_from|date:"F d, Y" }}
|
||||
{% endif %}
|
||||
|
||||
{% if notice_period %}
|
||||
Notice Period: {{ notice_period }}
|
||||
{% endif %}
|
||||
|
||||
Links:
|
||||
------
|
||||
{% if portfolio_url %}Portfolio: {{ portfolio_url }}{% endif %}
|
||||
{% if linkedin_url %}LinkedIn: {{ linkedin_url }}{% endif %}
|
||||
{% if github_url %}GitHub: {{ github_url }}{% endif %}
|
||||
{% if website_url %}Website: {{ website_url }}{% endif %}
|
||||
|
||||
Cover Letter:
|
||||
------------
|
||||
{{ cover_letter }}
|
||||
|
||||
---
|
||||
Resume is attached to this email.
|
||||
Please log in to the admin panel to review the full application.
|
||||
|
||||
4
backEnd/career/tests.py
Normal file
4
backEnd/career/tests.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
12
backEnd/career/urls.py
Normal file
12
backEnd/career/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import JobPositionViewSet, JobApplicationViewSet
|
||||
|
||||
router = DefaultRouter(trailing_slash=False)
|
||||
router.register(r'jobs', JobPositionViewSet, basename='job')
|
||||
router.register(r'applications', JobApplicationViewSet, basename='application')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
167
backEnd/career/views.py
Normal file
167
backEnd/career/views.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.shortcuts import get_object_or_404
|
||||
import logging
|
||||
|
||||
from .models import JobPosition, JobApplication
|
||||
from .serializers import (
|
||||
JobPositionListSerializer,
|
||||
JobPositionDetailSerializer,
|
||||
JobApplicationSerializer,
|
||||
JobApplicationListSerializer
|
||||
)
|
||||
from .email_service import CareerEmailService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JobPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for job positions
|
||||
GET /api/career/jobs/ - List all active job positions
|
||||
GET /api/career/jobs/{slug}/ - Get job position by slug
|
||||
"""
|
||||
|
||||
queryset = JobPosition.objects.filter(status='active')
|
||||
permission_classes = [AllowAny]
|
||||
lookup_field = 'slug'
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['department', 'employment_type', 'location_type', 'featured']
|
||||
search_fields = ['title', 'department', 'location', 'short_description']
|
||||
ordering_fields = ['posted_date', 'priority', 'title']
|
||||
ordering = ['-priority', '-posted_date']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return JobPositionDetailSerializer
|
||||
return JobPositionListSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def featured(self, request):
|
||||
"""Get featured job positions"""
|
||||
featured_jobs = self.queryset.filter(featured=True)
|
||||
serializer = self.get_serializer(featured_jobs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def applications_count(self, request, slug=None):
|
||||
"""Get number of applications for a job"""
|
||||
job = self.get_object()
|
||||
count = job.applications.count()
|
||||
return Response({'count': count})
|
||||
|
||||
|
||||
class JobApplicationViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for job applications
|
||||
POST /api/career/applications/ - Submit a job application
|
||||
GET /api/career/applications/ - List applications (admin only)
|
||||
"""
|
||||
|
||||
queryset = JobApplication.objects.all()
|
||||
serializer_class = JobApplicationSerializer
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['job', 'status']
|
||||
search_fields = ['first_name', 'last_name', 'email', 'job__title']
|
||||
ordering_fields = ['applied_date', 'status']
|
||||
ordering = ['-applied_date']
|
||||
|
||||
def get_permissions(self):
|
||||
"""
|
||||
Allow anyone to create (submit) applications
|
||||
Only admins can list/view/update/delete applications
|
||||
"""
|
||||
if self.action == 'create':
|
||||
return [AllowAny()]
|
||||
return [IsAdminUser()]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list':
|
||||
return JobApplicationListSerializer
|
||||
return JobApplicationSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Submit a job application"""
|
||||
try:
|
||||
# Build data dict - Django QueryDict returns lists, so we need to get first item
|
||||
data = {}
|
||||
|
||||
# Get POST data (text fields)
|
||||
for key in request.POST.keys():
|
||||
value = request.POST.get(key)
|
||||
data[key] = value
|
||||
|
||||
# Get FILES data
|
||||
for key in request.FILES.keys():
|
||||
file_obj = request.FILES.get(key)
|
||||
data[key] = file_obj
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing request: {str(e)}")
|
||||
return Response(
|
||||
{'error': 'Error parsing request data'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(data=data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
logger.error(f"Validation errors: {serializer.errors}")
|
||||
return Response(
|
||||
{'error': 'Validation failed', 'details': serializer.errors},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
# Save the application
|
||||
application = serializer.save()
|
||||
logger.info(f"New job application received: {application.full_name} for {application.job.title}")
|
||||
|
||||
# Try to send email notifications (non-blocking - don't fail if emails fail)
|
||||
try:
|
||||
email_service = CareerEmailService()
|
||||
email_service.send_application_confirmation(application)
|
||||
email_service.send_application_notification_to_admin(application)
|
||||
logger.info(f"Email notifications sent successfully for application {application.id}")
|
||||
except Exception as email_error:
|
||||
# Log email error but don't fail the application submission
|
||||
logger.warning(f"Failed to send email notifications for application {application.id}: {str(email_error)}")
|
||||
|
||||
return Response(
|
||||
{
|
||||
'message': 'Application submitted successfully',
|
||||
'data': serializer.data
|
||||
},
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting job application: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{'error': 'Failed to submit application. Please try again.'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def update_status(self, request, pk=None):
|
||||
"""Update application status"""
|
||||
application = self.get_object()
|
||||
new_status = request.data.get('status')
|
||||
|
||||
if new_status not in dict(JobApplication.STATUS_CHOICES):
|
||||
return Response(
|
||||
{'error': 'Invalid status'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
application.status = new_status
|
||||
application.save()
|
||||
|
||||
serializer = self.get_serializer(application)
|
||||
return Response(serializer.data)
|
||||
|
||||
208
backEnd/case_studies/README.md
Normal file
208
backEnd/case_studies/README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Case Studies API
|
||||
|
||||
## Overview
|
||||
|
||||
The Case Studies API provides a comprehensive backend and frontend solution for managing and displaying case study content. This feature includes categories, clients, process steps, gallery images, and related case studies.
|
||||
|
||||
## Backend Structure
|
||||
|
||||
### Models
|
||||
|
||||
1. **CaseStudyCategory**
|
||||
- Category management for case studies
|
||||
- Fields: name, slug, description, display_order, is_active
|
||||
|
||||
2. **Client**
|
||||
- Client information management
|
||||
- Fields: name, slug, logo, description, website
|
||||
|
||||
3. **CaseStudy**
|
||||
- Main case study model
|
||||
- Fields: title, slug, subtitle, description, excerpt
|
||||
- Images: thumbnail, featured_image, poster_image, project_image
|
||||
- Relations: category, client
|
||||
- Content: project_overview, site_map_content
|
||||
|
||||
4. **CaseStudyImage**
|
||||
- Gallery images for case studies
|
||||
- Fields: image, caption, display_order
|
||||
|
||||
5. **CaseStudyProcess**
|
||||
- Process steps for case studies
|
||||
- Fields: step_number, title, description
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Base URL: `/api/case-studies/`
|
||||
|
||||
#### Case Studies
|
||||
|
||||
- `GET /case-studies/` - List all case studies (with pagination and filtering)
|
||||
- Query params: `category`, `client`, `search`, `featured`, `ordering`, `page`, `page_size`
|
||||
- `GET /case-studies/{slug}/` - Get case study details
|
||||
- `GET /case-studies/featured/` - Get featured case studies
|
||||
- `GET /case-studies/latest/?limit=6` - Get latest case studies
|
||||
- `GET /case-studies/popular/?limit=6` - Get popular case studies
|
||||
- `GET /case-studies/{slug}/related/` - Get related case studies
|
||||
|
||||
#### Categories
|
||||
|
||||
- `GET /categories/` - List all categories
|
||||
- `GET /categories/{slug}/` - Get category details
|
||||
- `GET /categories/with_case_studies/` - Get categories with case studies
|
||||
|
||||
#### Clients
|
||||
|
||||
- `GET /clients/` - List all clients
|
||||
- `GET /clients/{slug}/` - Get client details
|
||||
- `GET /clients/{slug}/case_studies/` - Get case studies for a client
|
||||
|
||||
## Frontend Structure
|
||||
|
||||
### API Service (`lib/api/caseStudyService.ts`)
|
||||
|
||||
Provides TypeScript functions for all API endpoints with proper typing.
|
||||
|
||||
### Hooks (`lib/hooks/useCaseStudy.ts`)
|
||||
|
||||
React hooks for data fetching:
|
||||
- `useCaseStudies()` - Fetch all case studies
|
||||
- `useCaseStudy(slug)` - Fetch single case study
|
||||
- `useFeaturedCaseStudies()` - Fetch featured case studies
|
||||
- `useLatestCaseStudies()` - Fetch latest case studies
|
||||
- `usePopularCaseStudies()` - Fetch popular case studies
|
||||
- `useRelatedCaseStudies(slug)` - Fetch related case studies
|
||||
- `useCaseStudyCategories()` - Fetch categories
|
||||
- `useClients()` - Fetch clients
|
||||
|
||||
### Components
|
||||
|
||||
1. **CaseItems** (`components/pages/case-study/CaseItems.tsx`)
|
||||
- Lists all case studies in a grid
|
||||
- Includes tab navigation for "Case Study" and "Client" views
|
||||
- Dynamically rendered from API data
|
||||
|
||||
2. **CaseSingle** (`components/pages/case-study/CaseSingle.tsx`)
|
||||
- Displays detailed case study information
|
||||
- Shows poster image, project overview, and gallery
|
||||
- Dynamically rendered from API data
|
||||
|
||||
3. **Process** (`components/pages/case-study/Process.tsx`)
|
||||
- Displays process steps for a case study
|
||||
- Dynamically rendered from API data
|
||||
|
||||
4. **RelatedCase** (`components/pages/case-study/RelatedCase.tsx`)
|
||||
- Shows related case studies
|
||||
- Dynamically rendered from API data
|
||||
|
||||
### Pages
|
||||
|
||||
- `/case-study` - Lists all case studies
|
||||
- `/case-study/[slug]` - Displays individual case study
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### Backend Setup
|
||||
|
||||
1. Add `'case_studies'` to `INSTALLED_APPS` in `settings.py` ✅
|
||||
2. Run migrations:
|
||||
```bash
|
||||
python manage.py makemigrations case_studies
|
||||
python manage.py migrate case_studies
|
||||
```
|
||||
|
||||
3. Populate sample data:
|
||||
```bash
|
||||
python manage.py populate_case_studies
|
||||
```
|
||||
|
||||
4. Add URL patterns to main `urls.py`:
|
||||
```python
|
||||
path('api/case-studies/', include('case_studies.urls')),
|
||||
```
|
||||
|
||||
### Frontend Setup
|
||||
|
||||
The frontend is already integrated and ready to use. The components will automatically fetch data from the API when the pages load.
|
||||
|
||||
## Admin Panel
|
||||
|
||||
Access the Django admin panel to manage:
|
||||
- Case Study Categories
|
||||
- Clients
|
||||
- Case Studies
|
||||
- Case Study Images
|
||||
- Case Study Process Steps
|
||||
|
||||
URL: `/admin/`
|
||||
|
||||
## Data Population
|
||||
|
||||
The `populate_case_studies` management command creates:
|
||||
- 6 case study categories
|
||||
- 4 clients
|
||||
- 8 case studies
|
||||
- 32 gallery images
|
||||
- 40 process steps
|
||||
|
||||
## Features
|
||||
|
||||
✅ Full CRUD operations via Django admin
|
||||
✅ RESTful API with filtering and pagination
|
||||
✅ Dynamic frontend with React hooks
|
||||
✅ Image support (local and external URLs)
|
||||
✅ Related case studies
|
||||
✅ Process steps with ordering
|
||||
✅ Gallery images with captions
|
||||
✅ Category and client management
|
||||
✅ Featured and popular case studies
|
||||
✅ SEO fields (meta description, keywords)
|
||||
✅ View count tracking
|
||||
|
||||
## Testing the API
|
||||
|
||||
You can test the API using:
|
||||
|
||||
1. Django REST Framework browsable API:
|
||||
- Navigate to `http://localhost:8000/api/case-studies/case-studies/`
|
||||
|
||||
2. Swagger UI:
|
||||
- Navigate to `http://localhost:8000/swagger/`
|
||||
|
||||
3. Frontend:
|
||||
- Navigate to `http://localhost:3000/case-study`
|
||||
|
||||
## Example API Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Artificial intelligence is the simulation of human processes",
|
||||
"slug": "artificial-intelligence-is-the-simulation-of-human-processes",
|
||||
"subtitle": "AI-Powered Business Solutions",
|
||||
"excerpt": "This artificial intelligence project demonstrates...",
|
||||
"thumbnail": "/images/case/one.png",
|
||||
"category": {
|
||||
"id": 4,
|
||||
"name": "AI",
|
||||
"slug": "ai"
|
||||
},
|
||||
"client": {
|
||||
"id": 1,
|
||||
"name": "Tarapio",
|
||||
"slug": "tarapio"
|
||||
},
|
||||
"gallery_images": [...],
|
||||
"process_steps": [...],
|
||||
"related_case_studies": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All images support both uploaded files and external URLs
|
||||
- Slugs are automatically generated from titles
|
||||
- Case studies are ordered by display_order and published_at
|
||||
- Only published case studies are returned via the API
|
||||
- View counts are automatically incremented when viewing details
|
||||
|
||||
0
backEnd/case_studies/__init__.py
Normal file
0
backEnd/case_studies/__init__.py
Normal file
BIN
backEnd/case_studies/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/case_studies/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/case_studies/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/case_studies/__pycache__/models.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/case_studies/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/case_studies/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/case_studies/__pycache__/views.cpython-312.pyc
Normal file
BIN
backEnd/case_studies/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
105
backEnd/case_studies/admin.py
Normal file
105
backEnd/case_studies/admin.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from django.contrib import admin
|
||||
from .models import CaseStudy, CaseStudyCategory, Client, CaseStudyImage, CaseStudyProcess
|
||||
|
||||
|
||||
@admin.register(CaseStudyCategory)
|
||||
class CaseStudyCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'display_order', 'is_active', 'case_studies_count']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'slug']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['display_order', 'name']
|
||||
|
||||
def case_studies_count(self, obj):
|
||||
return obj.case_studies.count()
|
||||
case_studies_count.short_description = 'Case Studies Count'
|
||||
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'website', 'is_active', 'case_studies_count']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
search_fields = ['name', 'slug', 'website']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['name']
|
||||
|
||||
def case_studies_count(self, obj):
|
||||
return obj.case_studies.count()
|
||||
case_studies_count.short_description = 'Case Studies Count'
|
||||
|
||||
|
||||
class CaseStudyImageInline(admin.TabularInline):
|
||||
model = CaseStudyImage
|
||||
extra = 1
|
||||
fields = ['image', 'image_url', 'caption', 'display_order']
|
||||
|
||||
|
||||
class CaseStudyProcessInline(admin.TabularInline):
|
||||
model = CaseStudyProcess
|
||||
extra = 1
|
||||
fields = ['step_number', 'title', 'description']
|
||||
ordering = ['step_number']
|
||||
|
||||
|
||||
@admin.register(CaseStudy)
|
||||
class CaseStudyAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title', 'category', 'client', 'published',
|
||||
'featured', 'views_count', 'display_order', 'published_at'
|
||||
]
|
||||
list_filter = [
|
||||
'published', 'featured', 'category',
|
||||
'client', 'published_at', 'created_at'
|
||||
]
|
||||
search_fields = ['title', 'description', 'excerpt']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
date_hierarchy = 'published_at'
|
||||
ordering = ['display_order', '-published_at', '-created_at']
|
||||
inlines = [CaseStudyImageInline, CaseStudyProcessInline]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('title', 'slug', 'subtitle', 'category', 'client')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('excerpt', 'description', 'project_overview', 'site_map_content')
|
||||
}),
|
||||
('Images', {
|
||||
'fields': (
|
||||
'thumbnail', 'thumbnail_url',
|
||||
'featured_image', 'featured_image_url',
|
||||
'poster_image', 'poster_image_url',
|
||||
'project_image', 'project_image_url'
|
||||
)
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('meta_description', 'meta_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status & Visibility', {
|
||||
'fields': ('published', 'featured', 'display_order', 'published_at')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('views_count',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['views_count']
|
||||
|
||||
|
||||
@admin.register(CaseStudyImage)
|
||||
class CaseStudyImageAdmin(admin.ModelAdmin):
|
||||
list_display = ['case_study', 'caption', 'display_order', 'created_at']
|
||||
list_filter = ['case_study', 'created_at']
|
||||
search_fields = ['case_study__title', 'caption']
|
||||
ordering = ['case_study', 'display_order']
|
||||
|
||||
|
||||
@admin.register(CaseStudyProcess)
|
||||
class CaseStudyProcessAdmin(admin.ModelAdmin):
|
||||
list_display = ['case_study', 'step_number', 'title', 'created_at']
|
||||
list_filter = ['case_study', 'created_at']
|
||||
search_fields = ['case_study__title', 'title', 'description']
|
||||
ordering = ['case_study', 'step_number']
|
||||
|
||||
7
backEnd/case_studies/apps.py
Normal file
7
backEnd/case_studies/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CaseStudiesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'case_studies'
|
||||
|
||||
0
backEnd/case_studies/management/__init__.py
Normal file
0
backEnd/case_studies/management/__init__.py
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user