feat: add Job model with search/filter and role-based access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc2cd40532
commit
f228ff0697
|
|
@ -0,0 +1,13 @@
|
||||||
|
import django_filters
|
||||||
|
from .models import Job
|
||||||
|
|
||||||
|
|
||||||
|
class JobFilter(django_filters.FilterSet):
|
||||||
|
title = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
category = django_filters.CharFilter(lookup_expr='exact')
|
||||||
|
location = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
organization = django_filters.NumberFilter(field_name='organization__id')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Job
|
||||||
|
fields = ['title', 'category', 'location', 'organization']
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 4.2.20 on 2026-03-24 09:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('organizations', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Job',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=100, verbose_name='职位名称')),
|
||||||
|
('category', models.CharField(max_length=50, verbose_name='职位类别')),
|
||||||
|
('location', models.CharField(max_length=100, verbose_name='工作地点')),
|
||||||
|
('salary', models.CharField(max_length=50, verbose_name='薪资范围')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='职位描述')),
|
||||||
|
('status', models.CharField(choices=[('draft', '草稿'), ('published', '已发布'), ('closed', '已关闭')], default='draft', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='organizations.organization')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '职位',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Job(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('draft', '草稿'),
|
||||||
|
('published', '已发布'),
|
||||||
|
('closed', '已关闭'),
|
||||||
|
]
|
||||||
|
organization = models.ForeignKey(
|
||||||
|
'organizations.Organization',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='jobs'
|
||||||
|
)
|
||||||
|
title = models.CharField(max_length=100, verbose_name='职位名称')
|
||||||
|
category = models.CharField(max_length=50, verbose_name='职位类别')
|
||||||
|
location = models.CharField(max_length=100, verbose_name='工作地点')
|
||||||
|
salary = models.CharField(max_length=50, verbose_name='薪资范围')
|
||||||
|
description = models.TextField(verbose_name='职位描述', blank=True)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created_at']
|
||||||
|
verbose_name = '职位'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import Job
|
||||||
|
from apps.organizations.serializers import OrganizationSerializer
|
||||||
|
from apps.organizations.models import Organization
|
||||||
|
|
||||||
|
|
||||||
|
class JobListSerializer(serializers.ModelSerializer):
|
||||||
|
organization_name = serializers.CharField(source='organization.name', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Job
|
||||||
|
fields = ['id', 'title', 'category', 'location', 'salary',
|
||||||
|
'organization', 'organization_name', 'status', 'created_at']
|
||||||
|
|
||||||
|
|
||||||
|
class JobDetailSerializer(serializers.ModelSerializer):
|
||||||
|
organization = OrganizationSerializer(read_only=True)
|
||||||
|
organization_id = serializers.PrimaryKeyRelatedField(
|
||||||
|
source='organization',
|
||||||
|
queryset=Organization.objects.all(),
|
||||||
|
write_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Job
|
||||||
|
fields = ['id', 'title', 'category', 'location', 'salary',
|
||||||
|
'description', 'organization', 'organization_id', 'status', 'created_at']
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import pytest
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from apps.organizations.models import Organization
|
||||||
|
from apps.jobs.models import Job
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def org():
|
||||||
|
return Organization.objects.create(name='测试公司', email='test@test.com')
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def admin_user(org):
|
||||||
|
return User.objects.create_user(
|
||||||
|
username='admin1', password='pass123',
|
||||||
|
role='admin', organization=org
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestJobModel:
|
||||||
|
def test_create_job(self, org):
|
||||||
|
job = Job.objects.create(
|
||||||
|
organization=org, title='Python工程师',
|
||||||
|
category='技术', location='北京', salary='20k-30k',
|
||||||
|
description='职位描述', status='published'
|
||||||
|
)
|
||||||
|
assert job.status == 'published'
|
||||||
|
assert str(job) == 'Python工程师'
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestJobAPI:
|
||||||
|
def test_public_can_list_published_jobs(self, client, org):
|
||||||
|
Job.objects.create(
|
||||||
|
organization=org, title='已发布职位',
|
||||||
|
status='published', category='技术',
|
||||||
|
location='北京', salary='10k'
|
||||||
|
)
|
||||||
|
Job.objects.create(
|
||||||
|
organization=org, title='草稿职位',
|
||||||
|
status='draft', category='技术',
|
||||||
|
location='北京', salary='10k'
|
||||||
|
)
|
||||||
|
response = client.get('/api/jobs/public/')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data['count'] == 1
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import JobPublicViewSet, JobManageViewSet
|
||||||
|
|
||||||
urlpatterns = []
|
router = DefaultRouter()
|
||||||
|
router.register('public', JobPublicViewSet, basename='job-public')
|
||||||
|
router.register('manage', JobManageViewSet, basename='job-manage')
|
||||||
|
|
||||||
|
urlpatterns = [path('', include(router.urls))]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
from rest_framework import viewsets, permissions
|
||||||
|
from .models import Job
|
||||||
|
from .serializers import JobListSerializer, JobDetailSerializer
|
||||||
|
from .filters import JobFilter
|
||||||
|
from apps.accounts.permissions import IsAdminOrSuperAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class JobPublicViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""公开只读,仅返回已发布职位"""
|
||||||
|
queryset = Job.objects.filter(status='published').select_related('organization')
|
||||||
|
filterset_class = JobFilter
|
||||||
|
search_fields = ['title', 'description', 'location']
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == 'retrieve':
|
||||||
|
return JobDetailSerializer
|
||||||
|
return JobListSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class JobManageViewSet(viewsets.ModelViewSet):
|
||||||
|
"""管理端:公司管理员管理本公司职位"""
|
||||||
|
permission_classes = [IsAdminOrSuperAdmin]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action in ['retrieve', 'create', 'update', 'partial_update']:
|
||||||
|
return JobDetailSerializer
|
||||||
|
return JobListSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.is_superadmin:
|
||||||
|
return Job.objects.all().select_related('organization')
|
||||||
|
return Job.objects.filter(organization=user.organization).select_related('organization')
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
if self.request.user.is_admin:
|
||||||
|
serializer.save(organization=self.request.user.organization)
|
||||||
|
else:
|
||||||
|
serializer.save()
|
||||||
Loading…
Reference in New Issue