feat: add Application model with status tracking and email notifications
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc39c22e87
commit
0ccd943255
|
|
@ -0,0 +1,22 @@
|
|||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
|
||||
STATUS_LABELS = {
|
||||
'viewed': '已查看',
|
||||
'interviewing': '面试邀请',
|
||||
'hired': '恭喜录用',
|
||||
'rejected': '很遗憾未通过',
|
||||
}
|
||||
|
||||
def notify_status_change(application):
|
||||
label = STATUS_LABELS.get(application.status)
|
||||
if not label or not application.applicant.email:
|
||||
return
|
||||
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@offer.com')
|
||||
send_mail(
|
||||
subject=f'【招聘通知】您投递的"{application.job.title}"状态更新:{label}',
|
||||
message=f'您好 {application.applicant.username},\n\n您投递的职位"{application.job.title}"状态已更新为:{label}。\n\n请登录平台查看详情。',
|
||||
from_email=from_email,
|
||||
recipient_list=[application.applicant.email],
|
||||
fail_silently=True,
|
||||
)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2.20 on 2026-03-24 09:45
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('jobs', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('resume_snapshot', models.JSONField(verbose_name='简历快照')),
|
||||
('status', models.CharField(choices=[('pending', '待查看'), ('viewed', '已查看'), ('interviewing', '面试中'), ('hired', '已录用'), ('rejected', '已拒绝')], default='pending', max_length=20)),
|
||||
('note', models.TextField(blank=True, verbose_name='HR备注')),
|
||||
('applied_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('applicant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to=settings.AUTH_USER_MODEL)),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='jobs.job')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '投递记录',
|
||||
'ordering': ['-applied_at'],
|
||||
'unique_together': {('job', 'applicant')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class Application(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('pending', '待查看'),
|
||||
('viewed', '已查看'),
|
||||
('interviewing', '面试中'),
|
||||
('hired', '已录用'),
|
||||
('rejected', '已拒绝'),
|
||||
]
|
||||
job = models.ForeignKey('jobs.Job', on_delete=models.CASCADE, related_name='applications')
|
||||
applicant = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='applications'
|
||||
)
|
||||
resume_snapshot = models.JSONField(verbose_name='简历快照')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||
note = models.TextField(blank=True, verbose_name='HR备注')
|
||||
applied_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [['job', 'applicant']]
|
||||
ordering = ['-applied_at']
|
||||
verbose_name = '投递记录'
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Application
|
||||
|
||||
class ApplicationCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ['job']
|
||||
|
||||
def create(self, validated_data):
|
||||
request = self.context['request']
|
||||
try:
|
||||
resume = request.user.resume
|
||||
except Exception:
|
||||
raise serializers.ValidationError(
|
||||
{'detail': '请先完善简历后再投递'}
|
||||
)
|
||||
if not resume.name:
|
||||
raise serializers.ValidationError(
|
||||
{'detail': '请先完善简历后再投递'}
|
||||
)
|
||||
return Application.objects.create(
|
||||
job=validated_data['job'],
|
||||
applicant=request.user,
|
||||
resume_snapshot=resume.to_snapshot(),
|
||||
)
|
||||
|
||||
class ApplicationSerializer(serializers.ModelSerializer):
|
||||
job_title = serializers.CharField(source='job.title', read_only=True)
|
||||
company_name = serializers.CharField(source='job.organization.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ['id', 'job', 'job_title', 'company_name',
|
||||
'resume_snapshot', 'status', 'note', 'applied_at']
|
||||
read_only_fields = ['resume_snapshot', 'applied_at']
|
||||
|
||||
class ApplicationStatusSerializer(serializers.ModelSerializer):
|
||||
"""HR 更新状态"""
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ['status', 'note']
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from apps.organizations.models import Organization
|
||||
from apps.jobs.models import Job
|
||||
from apps.resumes.models import Resume
|
||||
from apps.applications.models import Application
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@pytest.fixture
|
||||
def setup(db):
|
||||
org = Organization.objects.create(name='公司', email='co@test.com')
|
||||
seeker = User.objects.create_user(username='seeker1', password='pass', role='seeker')
|
||||
job = Job.objects.create(
|
||||
organization=org, title='测试职位', category='技术',
|
||||
location='北京', salary='15k', status='published'
|
||||
)
|
||||
resume = Resume.objects.create(user=seeker, name='张三')
|
||||
return {'org': org, 'seeker': seeker, 'job': job, 'resume': resume}
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestApplicationModel:
|
||||
def test_create_application(self, setup):
|
||||
app = Application.objects.create(
|
||||
job=setup['job'],
|
||||
applicant=setup['seeker'],
|
||||
resume_snapshot=setup['resume'].to_snapshot(),
|
||||
)
|
||||
assert app.status == 'pending'
|
||||
assert app.resume_snapshot['name'] == '张三'
|
||||
|
||||
def test_cannot_apply_twice(self, setup):
|
||||
Application.objects.create(
|
||||
job=setup['job'],
|
||||
applicant=setup['seeker'],
|
||||
resume_snapshot=setup['resume'].to_snapshot(),
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
Application.objects.create(
|
||||
job=setup['job'],
|
||||
applicant=setup['seeker'],
|
||||
resume_snapshot={},
|
||||
)
|
||||
|
|
@ -1,3 +1,13 @@
|
|||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ApplyView, MyApplicationsView, ApplicationManageViewSet, ApplicationStatusUpdateView
|
||||
|
||||
urlpatterns = []
|
||||
router = DefaultRouter()
|
||||
router.register('manage', ApplicationManageViewSet, basename='application-manage')
|
||||
|
||||
urlpatterns = [
|
||||
path('apply/', ApplyView.as_view()),
|
||||
path('mine/', MyApplicationsView.as_view()),
|
||||
path('manage/<int:pk>/status/', ApplicationStatusUpdateView.as_view()),
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
from rest_framework import generics, viewsets
|
||||
from .models import Application
|
||||
from .serializers import ApplicationCreateSerializer, ApplicationSerializer, ApplicationStatusSerializer
|
||||
from .emails import notify_status_change
|
||||
from apps.accounts.permissions import IsSeeker, IsAdminOrSuperAdmin
|
||||
|
||||
class ApplyView(generics.CreateAPIView):
|
||||
serializer_class = ApplicationCreateSerializer
|
||||
permission_classes = [IsSeeker]
|
||||
|
||||
class MyApplicationsView(generics.ListAPIView):
|
||||
serializer_class = ApplicationSerializer
|
||||
permission_classes = [IsSeeker]
|
||||
|
||||
def get_queryset(self):
|
||||
return Application.objects.filter(applicant=self.request.user).select_related('job__organization')
|
||||
|
||||
class ApplicationManageViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""HR 查看本公司投递"""
|
||||
serializer_class = ApplicationSerializer
|
||||
permission_classes = [IsAdminOrSuperAdmin]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.is_superadmin:
|
||||
return Application.objects.all().select_related('job__organization', 'applicant')
|
||||
return Application.objects.filter(
|
||||
job__organization=user.organization
|
||||
).select_related('job__organization', 'applicant')
|
||||
|
||||
class ApplicationStatusUpdateView(generics.UpdateAPIView):
|
||||
serializer_class = ApplicationStatusSerializer
|
||||
permission_classes = [IsAdminOrSuperAdmin]
|
||||
queryset = Application.objects.all()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = serializer.save()
|
||||
notify_status_change(instance)
|
||||
Loading…
Reference in New Issue