如何编写Django命令自动生成代码?含模型配套代码生成需求
当然可以!Django不仅支持你自定义管理命令来实现这个需求,还有一些现成工具能帮你减少重复工作,下面分两部分给你详细说明:
一、现成工具推荐
如果不想从零开始写代码,这些工具可以帮你快速生成大部分 boilerplate:
- django-extensions:这个第三方包提供了很多实用的管理命令,虽然没有直接生成你要的全部内容,但可以通过
runscript扩展自己的生成逻辑,它的shell_plus等工具也能辅助模型开发; - django-crud-generator:主要用于生成CRUD视图和模板,但可以配置扩展生成admin类、基础服务函数和测试用例;
- factory-boy 配套工具:社区里有一些工具可以根据模型自动生成FactoryBoy的工厂类,省去手动编写字段默认值的麻烦。
不过如果你的项目有特定的代码规范(比如服务函数的命名、admin的展示配置),自定义命令是最灵活的选择,完全贴合你的需求。
二、自行编写自定义Django命令
下面是一个完整的实现示例,能自动生成admin类、服务函数、FactoryBoy工厂类和测试用例:
1. 创建命令目录结构
在你的Django应用(比如myapp)下创建management/commands目录,同时创建空的__init__.py文件:
myapp/ ├── management/ │ ├── __init__.py │ └── commands/ │ ├── __init__.py │ └── generate_model_boilerplate.py
2. 编写自定义命令代码
在generate_model_boilerplate.py中写入以下代码:
from django.core.management.base import BaseCommand, CommandError from django.apps import apps import os from django.template import Template, Context class Command(BaseCommand): help = 'Generates admin, services, factory and test boilerplate for a Django model' def add_arguments(self, parser): parser.add_argument('app_name', type=str, help='Name of the Django app containing the model') parser.add_argument('model_name', type=str, help='Name of the model to generate boilerplate for') def handle(self, *args, **options): app_name = options['app_name'] model_name = options['model_name'] # 动态获取模型类 try: model = apps.get_model(app_name, model_name) except LookupError: raise CommandError(f"Model {model_name} not found in app {app_name}") app_dir = os.path.join(os.getcwd(), app_name) self.generate_admin(app_name, model_name, app_dir) self.generate_services(app_name, model_name, app_dir) self.generate_factory(app_name, model_name, model, app_dir) self.generate_tests(app_name, model_name, model, app_dir) self.stdout.write(self.style.SUCCESS(f'Successfully generated boilerplate for {model_name} in {app_name}')) def generate_admin(self, app_name, model_name, app_dir): admin_path = os.path.join(app_dir, 'admin.py') template = Template(""" from django.contrib import admin from {{ app_name }}.models import {{ model_name }} @admin.register({{ model_name }}) class {{ model_name }}Admin(admin.ModelAdmin): list_display = [field.name for field in {{ model_name }}._meta.fields] search_fields = [field.name for field in {{ model_name }}._meta.fields if field.get_internal_type() in ('CharField', 'TextField')] """) context = Context({'app_name': app_name, 'model_name': model_name}) content = template.render(context).strip() # 处理文件:不存在则创建,存在则检查是否已有该类,无则追加 if os.path.exists(admin_path): with open(admin_path, 'r') as f: existing_content = f.read() if f'class {model_name}Admin' not in existing_content: with open(admin_path, 'a') as f: f.write('\n\n' + content) self.stdout.write(f'Added {model_name}Admin to {admin_path}') else: self.stdout.write(f'{model_name}Admin already exists in {admin_path}') else: with open(admin_path, 'w') as f: f.write(content) self.stdout.write(f'Created {admin_path} with {model_name}Admin') def generate_services(self, app_name, model_name, app_dir): services_path = os.path.join(app_dir, 'services.py') model_lower = model_name.lower() template = Template(""" from {{ app_name }}.models import {{ model_name }} from django.core.exceptions import ObjectDoesNotExist def get_{{ model_lower }}(pk): try: return {{ model_name }}.objects.get(pk=pk) except {{ model_name }}.DoesNotExist: return None def create_{{ model_lower }}(**kwargs): return {{ model_name }}.objects.create(**kwargs) def update_{{ model_lower }}(instance, **kwargs): for key, value in kwargs.items(): if hasattr(instance, key): setattr(instance, key, value) instance.save() return instance def delete_{{ model_lower }}(instance): instance.delete() """) context = Context({'app_name': app_name, 'model_name': model_name, 'model_lower': model_lower}) content = template.render(context).strip() if os.path.exists(services_path): with open(services_path, 'r') as f: existing_content = f.read() if f'def get_{model_lower}' not in existing_content: with open(services_path, 'a') as f: f.write('\n\n' + content) self.stdout.write(f'Added {model_name} services to {services_path}') else: self.stdout.write(f'{model_name} services already exist in {services_path}') else: with open(services_path, 'w') as f: f.write(content) self.stdout.write(f'Created {services_path} with {model_name} services') def generate_factory(self, app_name, model_name, model, app_dir): factory_path = os.path.join(app_dir, 'factories.py') fields = [f for f in model._meta.fields if not f.primary_key] template = Template(""" import factory from {{ app_name }}.models import {{ model_name }} class {{ model_name }}Factory(factory.django.DjangoModelFactory): class Meta: model = {{ model_name }} {% for field in fields %} {% if field.get_internal_type == 'CharField' and field.name != 'slug' %} {{ field.name }} = factory.Faker('word') {% elif field.get_internal_type == 'TextField' %} {{ field.name }} = factory.Faker('text') {% elif field.get_internal_type == 'IntegerField' %} {{ field.name }} = factory.Faker('random_int', min=1, max=100) {% elif field.get_internal_type == 'FloatField' %} {{ field.name }} = factory.Faker('pyfloat', positive=True) {% elif field.get_internal_type == 'BooleanField' %} {{ field.name }} = factory.Faker('boolean') {% elif field.get_internal_type == 'DateTimeField' %} {{ field.name }} = factory.Faker('date_time_this_year') {% elif field.get_internal_type == 'DateField' %} {{ field.name }} = factory.Faker('date_this_year') {% endif %} {% endfor %} """) context = Context({'app_name': app_name, 'model_name': model_name, 'fields': fields}) content = template.render(context).strip() if os.path.exists(factory_path): with open(factory_path, 'r') as f: existing_content = f.read() if f'class {model_name}Factory' not in existing_content: with open(factory_path, 'a') as f: f.write('\n\n' + content) self.stdout.write(f'Added {model_name}Factory to {factory_path}') else: self.stdout.write(f'{model_name}Factory already exists in {factory_path}') else: with open(factory_path, 'w') as f: f.write(content) self.stdout.write(f'Created {factory_path} with {model_name}Factory') def generate_tests(self, app_name, model_name, model, app_dir): test_dir = os.path.join(app_dir, 'tests') if not os.path.exists(test_dir): os.makedirs(test_dir) with open(os.path.join(test_dir, '__init__.py'), 'w') as f: f.write('') model_lower = model_name.lower() test_path = os.path.join(test_dir, f'test_{model_lower}.py') model_fields = [f for f in model._meta.fields if not f.primary_key] template = Template(""" from django.test import TestCase from {{ app_name }}.models import {{ model_name }} from {{ app_name }}.factories import {{ model_name }}Factory from {{ app_name }}.services import get_{{ model_lower }}, create_{{ model_lower }}, update_{{ model_lower }}, delete_{{ model_lower }} class {{ model_name }}ModelTests(TestCase): def test_create_{{ model_lower }}(self): obj = {{ model_name }}Factory() self.assertTrue(isinstance(obj, {{ model_name }})) self.assertIsNotNone(obj.pk) def test_get_{{ model_lower }}(self): obj = {{ model_name }}Factory() retrieved_obj = get_{{ model_lower }}(obj.pk) self.assertEqual(obj, retrieved_obj) def test_get_nonexistent_{{ model_lower }}(self): retrieved_obj = get_{{ model_lower }}(999) self.assertIsNone(retrieved_obj) def test_update_{{ model_lower }}(self): obj = {{ model_name }}Factory() update_data = {} {% for field in model_fields %} {% if field.get_internal_type == 'CharField' and field.name != 'slug' %} update_data['{{ field.name }}'] = 'Updated Test Value' {% elif field.get_internal_type == 'IntegerField' %} update_data['{{ field.name }}'] = 200 {% endif %} {% endfor %} if update_data: updated_obj = update_{{ model_lower }}(obj, **update_data) for key, value in update_data.items(): self.assertEqual(getattr(updated_obj, key), value) def test_delete_{{ model_lower }}(self): obj = {{ model_name }}Factory() obj_pk = obj.pk delete_{{ model_lower }}(obj) with self.assertRaises({{ model_name }}.DoesNotExist): {{ model_name }}.objects.get(pk=obj_pk) """) context = Context({'app_name': app_name, 'model_name': model_name, 'model_lower': model_lower, 'model_fields': model_fields}) content = template.render(context).strip() if os.path.exists(test_path): self.stdout.write(f'Test file {test_path} already exists, skipping creation') else: with open(test_path, 'w') as f: f.write(content) self.stdout.write(f'Created test file {test_path} for {model_name}')
3. 使用命令
在项目根目录下运行:
python manage.py generate_model_boilerplate your_app_name YourModelName
4. 扩展和调整
你可以根据自己的项目规范修改模板内容:
- 比如在admin类中添加
list_filter、readonly_fields; - 调整服务函数的逻辑(比如添加事务、权限校验);
- 自定义FactoryBoy的字段生成规则(比如针对特定字段使用自定义Faker provider);
- 扩展测试用例,覆盖更多场景(比如字段验证、关联模型测试)。
内容的提问来源于stack exchange,提问作者user2937998
相关产品推荐
相关产品推荐

