update
This commit is contained in:
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)
|
||||
|
||||
Reference in New Issue
Block a user