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