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:
TianyangZhang 2026-03-24 17:39:58 +08:00
parent cc2cd40532
commit f228ff0697
9 changed files with 197 additions and 2 deletions

View File

@ -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']

View File

@ -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'],
},
),
]

View File

@ -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

View File

@ -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']

View File

@ -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

View File

@ -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))]

View File

@ -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()