feat: add Organization model with tree structure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3466f4866
commit
7e089bd5ec
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 4.2.20 on 2026-03-24 09:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Organization',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='公司名称')),
|
||||||
|
('logo', models.ImageField(blank=True, null=True, upload_to='org_logos/')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='公司简介')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='联系邮箱')),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='organizations.organization', verbose_name='上级公司')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '组织架构',
|
||||||
|
'verbose_name_plural': '组织架构',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -2,12 +2,23 @@ from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class Organization(models.Model):
|
class Organization(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=100, verbose_name='公司名称')
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
'self', null=True, blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='children',
|
||||||
|
verbose_name='上级公司'
|
||||||
|
)
|
||||||
|
logo = models.ImageField(upload_to='org_logos/', null=True, blank=True)
|
||||||
|
description = models.TextField(blank=True, verbose_name='公司简介')
|
||||||
|
email = models.EmailField(blank=True, verbose_name='联系邮箱')
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'organizations'
|
app_label = 'organizations'
|
||||||
verbose_name = '组织'
|
verbose_name = '组织架构'
|
||||||
verbose_name_plural = '组织'
|
verbose_name_plural = '组织架构'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import Organization
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = ['id', 'name', 'parent', 'logo', 'description', 'email', 'is_active']
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationTreeSerializer(serializers.ModelSerializer):
|
||||||
|
"""带子公司列表,用于门户展示"""
|
||||||
|
children = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = ['id', 'name', 'logo', 'description', 'email', 'children']
|
||||||
|
|
||||||
|
def get_children(self, obj):
|
||||||
|
return OrganizationSerializer(
|
||||||
|
obj.children.filter(is_active=True), many=True
|
||||||
|
).data
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import pytest
|
||||||
|
from apps.organizations.models import Organization
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestOrganizationModel:
|
||||||
|
def test_create_group(self):
|
||||||
|
org = Organization.objects.create(name='示例集团', email='group@example.com')
|
||||||
|
assert org.parent is None
|
||||||
|
assert org.is_active is True
|
||||||
|
|
||||||
|
def test_create_subsidiary(self):
|
||||||
|
parent = Organization.objects.create(name='示例集团', email='group@example.com')
|
||||||
|
child = Organization.objects.create(
|
||||||
|
name='子公司A', email='a@example.com', parent=parent
|
||||||
|
)
|
||||||
|
assert child.parent == parent
|
||||||
|
|
||||||
|
def test_list_subsidiaries(self):
|
||||||
|
parent = Organization.objects.create(name='集团', email='g@example.com')
|
||||||
|
Organization.objects.create(name='子A', email='a@example.com', parent=parent)
|
||||||
|
Organization.objects.create(name='子B', email='b@example.com', parent=parent)
|
||||||
|
assert parent.children.count() == 2
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import OrganizationPublicViewSet, OrganizationManageViewSet
|
||||||
|
|
||||||
urlpatterns = []
|
router = DefaultRouter()
|
||||||
|
router.register('public', OrganizationPublicViewSet, basename='org-public')
|
||||||
|
router.register('manage', OrganizationManageViewSet, basename='org-manage')
|
||||||
|
|
||||||
|
urlpatterns = [path('', include(router.urls))]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from .models import Organization
|
||||||
|
from .serializers import OrganizationSerializer, OrganizationTreeSerializer
|
||||||
|
from apps.accounts.permissions import IsSuperAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationPublicViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""公开只读:门户展示用"""
|
||||||
|
queryset = Organization.objects.filter(is_active=True, parent__isnull=False)
|
||||||
|
serializer_class = OrganizationSerializer
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationManageViewSet(viewsets.ModelViewSet):
|
||||||
|
"""超管:完整增删改查"""
|
||||||
|
queryset = Organization.objects.all()
|
||||||
|
serializer_class = OrganizationSerializer
|
||||||
|
permission_classes = [IsSuperAdmin]
|
||||||
Loading…
Reference in New Issue