Django生态:Django-admin、Django-Compressor、Django-grappelli、django-rules

在Python生态下,虽然有Flask、FastAPI等后来者居上的Web框架;但不可否认的是,依然有很多企业、组织和开发者仍在使用Django框架,或是有遗留的项目在使用Django。

本文汇总整理几个Django生态的开发框架。

Django Admin

Django-Compressor

开源(GitHub,2.9K Star,608 Fork)静态资源压缩器,可在Django模板里把CSS、JS合并并压缩成可缓存的静态文件,也支持离线压缩,把资源处理从请求链路里提前挪到部署阶段,官方文档

特点

  • 接入位置自然:直接工作在Django模板层。用{% compress css %}{% compress js %}包住资源片段,自动解析link、style、script,再输出压缩后的静态文件引用;
  • 缓存友好:压缩后的文件名和内容有关,内容变化才会生成新的文件名。浏览器可以放心做长缓存,线上资源更新时也不容易遇到"缓存没刷新"的问题。
  • 扩展空间足:支持CSS/JS过滤器、预编译器、不同HTML解析器、远程存储和离线压缩。简单项目可先用默认配置,复杂项目也能继续扩展到SASS、LESS、S3、CDN等场景。

适用场景

  • Django项目里CSS/JS较多,希望减少请求数和静态文件体积
  • 需要把资源文件名和内容绑定,方便浏览器做长缓存
  • 部署链路里可以提前执行 manage.py compress
  • 项目需要接入SASS、LESS、JS minifier或自定义资源处理流程

实战

基于pip安装:pip install django-compressor

用模板标签压缩CSS和JS:当一个页面里同时引用多个 CSS/JS 文件和少量内联代码时,希望最终输出变成更少的静态资源请求,同时让文件名随内容变化。

py 复制代码
from pathlib import Path
from tempfile import TemporaryDirectory
from django.conf import settings
import django
from django.template.loader import render_to_string

with TemporaryDirectory() as tmp:
	root = Path(tmp)
	static_dir = root / "static"
	out_dir = root / "compressed"
	tpl_dir = root / "templates"
	(static_dir / "css").mkdir(parents=True)
	(static_dir / "js").mkdir(parents=True)
	tpl_dir.mkdir()
	
	(static_dir / "css" / "base.css").write_text("body { color: #333; }\n.card { padding: 16px; }\n", encoding="utf-8")
	(static_dir / "js" / "app.js").write_text("function hello(name) { console.log('hello ' + name); }\nhello('mumu');\n", encoding="utf-8")
	(tpl_dir / "demo.html").write_text("""{% load compress %}
	{% compress css %}
	<link rel="stylesheet" href="/static/css/base.css">
	<style>.title { font-weight: 700; color: #0b5; }</style>
	{% endcompress %}
	{% compress js %}
	<script src="/static/js/app.js"></script>
	<script>console.log("inline js");</script>
	{% endcompress %}
	""", encoding="utf-8")
	
	settings.configure(
		SECRET_KEY="demo",
		DEBUG=False,
		INSTALLED_APPS=["django.contrib.staticfiles", "compressor"],
		STATIC_URL="/static/",
		STATICFILES_DIRS=[str(static_dir)],
		STATIC_ROOT=str(out_dir),
		COMPRESS_ROOT=str(out_dir),
		COMPRESS_URL="/static/",
		STATICFILES_FINDERS=[
			"django.contrib.staticfiles.finders.FileSystemFinder",
			"django.contrib.staticfiles.finders.AppDirectoriesFinder",
			"compressor.finders.CompressorFinder",
		],
		TEMPLATES=[{
			"BACKEND": "django.template.backends.django.DjangoTemplates",
			"DIRS": [str(tpl_dir)],
			"APP_DIRS": True,
		}],
		STORAGES={
			"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
			"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
			"compressor": {"BACKEND": "compressor.storage.CompressorFileStorage"},
		},
		COMPRESS_ENABLED=True,
		COMPRESS_OFFLINE=False,
	)
	django.setup()
	print(render_to_string("demo.html").strip())

compressor.finders.CompressorFinder让compressor能找到静态资源,COMPRESS_ROOT决定压缩后的文件写到哪里。真实项目里通常还会配合 collectstatic、CDN和静态文件存储一起使用。

功能二:把小段样式直接内联压缩

有些首屏关键样式很短,不一定值得单独生成文件。使用inline模式,让压缩结果以内联方式输出。

py 复制代码
from pathlib import Path
from tempfile import TemporaryDirectory
from django.template.loader import render_to_string

with TemporaryDirectory() as tmp:
    root = Path(tmp)
    out_dir = root / "compressed"
    tpl_dir = root / "templates"
    tpl_dir.mkdir()
    (tpl_dir / "inline.html").write_text("""{% load compress %}
{% compress css inline %}
<style>
  .notice { color: #0b5; padding: 12px; }
  .notice strong { font-weight: 700; }
</style>
{% endcompress %}
""", encoding="utf-8")
	// 省略部分重复代码片段
	print(render_to_string("inline.html").strip())

内联模式适合很小的关键样式或特殊片段。不要把大量 CSS 都塞进 HTML 里,否则页面体积会变大,也会削弱浏览器缓存静态文件的收益。

线上环境更推荐把压缩过程提前到部署阶段,用manage.py compress生成压缩文件和manifest。这样请求到来时不必再临时解析模板和写文件,运行时行为更稳定。开启 COMPRESS_OFFLINE=True,在部署时统一生成资源文件。

py 复制代码
from pathlib import Path
from tempfile import TemporaryDirectory
from django.core.management import call_command

with TemporaryDirectory() as tmp:
	root = Path(tmp)
	static_dir = root / "static"
	out_dir = root / "compressed"
	tpl_dir = root / "templates"
	(static_dir / "css").mkdir(parents=True)
	tpl_dir.mkdir()
	(static_dir / "css" / "dashboard.css").write_text(".card { padding: 16px; color: #245; }\n", encoding="utf-8")
	(tpl_dir / "dashboard.html").write_text("""{% load compress %}
	{% compress css %}
	<link rel="stylesheet" href="/static/css/dashboard.css">
	<style>.badge { border-radius: 999px; padding: 4px 8px; }</style>
	{% endcompress %}
	""", encoding="utf-8")
	
	settings.configure(
		// 省略部分代码
		COMPRESS_ENABLED=True,
		COMPRESS_OFFLINE=True,
	)
	
	django.setup()
	call_command("compress", verbosity=1, force=True)

离线压缩的核心产物是压缩文件和manifest。部署时需要把这些产物一起发布到静态文件存储或CDN。

注意事项

  1. 确认 compressor.finders.CompressorFinder 已加入 STATICFILES_FINDERS
  2. 确认 COMPRESS_ROOTSTATIC_ROOTCOMPRESS_URL 和 CDN 地址一致。
  3. 线上启用离线压缩时,把 manage.py compress 放进部署流程。
  4. 检查压缩输出目录、manifest 和静态文件存储是否一起发布。
  5. 避免在模板里混入过多动态上下文,减少离线压缩找不到缓存的风险。

Django-Grappelli

官网,开源(GitHub,3.9K Star,651 Fork)Django Admin增强:一边把交互和布局做得更顺手,一边把很多后台常见的小痛点,比如原始字段联想、可拖拽 inline、dashboard 组合这些能力,一起收拢成了成体系的扩展点,官方文档

特点

  • 后台体验:界面更顺眼,调整后台导航、关联选择、过滤器等交互;
  • 很多增强点都做成了可配置接口:从后台标题、切换用户,到 autocomplete 查找和 change list 模板,Grappelli 不是靠魔改 admin 模板硬怼,而是留了清晰的 settings、ModelAdmin 属性和 dashboard API。
  • 已有Django Admin项目很友好:迁移成本比重新做管理后台小得多。

适用场景

  • 已经在用 Django Admin,但觉得默认后台体验太素、太散
  • 想增强关联选择、inline 排序、dashboard 这些后台高频交互
  • 团队希望保留原生 admin 的稳定性,又想把管理后台做得更像产品
  • 需要一套比"纯换主题"更完整、但又不至于重做后台的方案

实战

基于pip安装:pip install django-grappelli

环境验证:

py 复制代码
import django
import tempfile
from django.conf import settings
from pathlib import Path
from django.contrib import admin
from django.urls import include, path, reverse
from grappelli import settings as grp_settings

DB = Path(tempfile.gettempdir()) / "grappelli_demo1.sqlite3"
if DB.exists():
    DB.unlink()

settings.configure(
    SECRET_KEY="demo",
    ROOT_URLCONF=__name__,
    STATIC_URL="/static/",
    USE_TZ=True,
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": str(DB)}},
    INSTALLED_APPS=[
        "django.contrib.admin",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "grappelli",
    ],
    MIDDLEWARE=[],
    TEMPLATES=[{"BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "OPTIONS": {"context_processors": []}}],
    GRAPPELLI_ADMIN_TITLE="Mumu Ops Hub",
    GRAPPELLI_SWITCH_USER=True,
)

django.setup()
urlpatterns = [
    path("grappelli/", include("grappelli.urls")),
    path("admin/", admin.site.urls),
]

print("admin title :", grp_settings.ADMIN_TITLE)
print("admin index :", reverse("admin:index"))
print("lookup url  :", reverse("grp_related_lookup"))
print("switch user :", reverse("grp_switch_user", args=[7]))

GrappelliSortableHiddenMixin

py 复制代码
from django import forms
from grappelli.forms import GrappelliSortableHiddenMixin

class DummyBase:
    def formfield_for_dbfield(self, db_field, request, **kwargs):
        widget = kwargs.get("widget") or forms.TextInput()
        return type("Field", (), {"widget": widget})()

class DemoInline(GrappelliSortableHiddenMixin, DummyBase):
    sortable_field_name = "position"

class DbField:
    def __init__(self, name):
        self.name = name

inline = DemoInline()
print("position widget:", type(inline.formfield_for_dbfield(DbField("position"), None).widget).__name__)
print("title widget   :", type(inline.formfield_for_dbfield(DbField("title"), None).widget).__name__)
print("sortable field :", inline.sortable_field_name)

把后台首页当成了可以编排的 dashboard,而不是一块固定模板。你可以把快捷入口、监控分组、最近操作这些模块自由拼起来,做出更像"团队工作台"的管理首页。

原生 admin 首页通常更像入口列表,但很多团队真正想要的是"打开后台就先看到关键入口和关键状态"。Grappelli 的 dashboard API 正是为了把首页做成可组合模块。

py 复制代码
from django.conf import settings
if not settings.configured:
    settings.configure(SECRET_KEY="demo", USE_I18N=False, INSTALLED_APPS=["grappelli"])

import django
django.setup()
from grappelli.dashboard import Dashboard, modules

class OpsDashboard(Dashboard):
    title = "Ops Dashboard"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.children.append(modules.LinkList(
            title="Quick Links",
            column=1,
            children=[
                ("Django Docs", "https://docs.djangoproject.com/", True, "official docs", "_blank"),
                ("Release Board", "/admin/releases/"),
            ],
        ))
        self.children.append(modules.Group(
            title="Watchers",
            column=2,
            children=[
                modules.LinkList(title="Core Services", column=2, children=[("Nginx", "/admin/nginx/")]),
                modules.LinkList(title="Task Queue", column=2, children=[("Celery", "/admin/celery/")]),
            ],
        ))

board = OpsDashboard()
quick = board.children[0]
quick.init_with_context({})
group = board.children[1]
print("dashboard title:", board.title)
print("module count   :", len(board.children))
print("first link     :", quick.children[0]["title"], quick.children[0]["target"])
print("group css      :", group.render_css_classes())

注意事项

  • 先确认 grappelli 在 INSTALLED_APPS 里的位置和 urls.py 的挂载顺序正确,再去改更细的 admin 行为。
  • 用少量 ModelAdmin 和 inline 先试跑排序、autocomplete、change list 模板,别一口气全后台替换。
  • 如果要做 dashboard,自定义模块时优先保证入口清晰和权限边界明确,不要把首页堆成信息垃圾场。

django-rules

权限规则编排方向开源(GitHub,2K Star,152 Fork)项目,把用户权限拆成可组合的谓词(predicate) ,可把谓词挂到Django的权限检查链路里。相比把判断逻辑散落在viewserializermodel各个层,更利于复用、审计、问题排查。

特点

  • 规则组合:基于谓词,可把每个判断拆成最小单元,再用&|~组合成真正的业务规则;
  • 可独立运行:底层是通用规则引擎,适用于各种Python项目;
  • 不依赖额外权限表:更偏向运行时判定,基于用户、对象和上下文信息直接判断,维护成本会轻不少。

适用场景

  • Django 项目已经有认证体系,但对象级权限逻辑开始变复杂
  • 想把权限判断从业务代码里抽出来统一管理
  • 需要一套既能独立测试、又能挂到has_perm上的规则表达方式
  • 后续再接Admin、CBV、DRF时还能延续同一套规则

实战

安装:pip install rules

接入 Django,别忘了把 rules 放进 INSTALLED_APPS,并把 rules.permissions.ObjectPermissionBackend 加到 AUTHENTICATION_BACKENDS 里。

用 predicate 把这些条件收拢起来

py 复制代码
import rules
from dataclasses import dataclass, field

@dataclass
class Ticket:
    owner: str
    reviewers: set[str] = field(default_factory=set)
    closed: bool = False

@rules.predicate
def is_owner(user, ticket):
    return ticket.owner == user

@rules.predicate
def is_reviewer(user, ticket):
    return user in ticket.reviewers

@rules.predicate
def not_closed(user, ticket):
    return not ticket.closed

rules.set_rule("can_edit_ticket", (is_owner | is_reviewer) & not_closed)

draft = Ticket(owner="mumu", reviewers={"ops", "qa"})
archived = Ticket(owner="mumu", reviewers={"ops"}, closed=True)

for actor, ticket in [
    ("mumu", draft),
    ("ops", draft),
    ("guest", draft),
    ("ops", archived),
]:
    print(f"{actor:>5} -> {rules.test_rule('can_edit_ticket', actor, ticket)}")

把同一套规则直接接进 Django 权限链路

py 复制代码
import django
import rules
import tempfile
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.core.management import call_command
from pathlib import Path

DB = Path(tempfile.gettempdir()) / "rules_demo2.sqlite3"
if DB.exists():
    DB.unlink()

settings.configure(
    SECRET_KEY="demo",
    USE_TZ=True,
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": str(DB)}},
    INSTALLED_APPS=[
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "rules",
    ],
    AUTHENTICATION_BACKENDS=(
        "rules.permissions.ObjectPermissionBackend",
        "django.contrib.auth.backends.ModelBackend",
    ),
)

django.setup()
call_command("migrate", run_syncdb=True, verbosity=0)

class Book:
    def __init__(self, author, state):
        self.author = author
        self.state = state

@rules.predicate
def is_book_author(user, book):
    return bool(user and book and book.author == user)

is_editor = rules.is_group_member("editors")
rules.add_perm("library.change_book", is_book_author | is_editor)
rules.add_perm("library.publish_book", is_book_author & rules.is_staff)

alice = User.objects.create_user("alice")
bob = User.objects.create_user("bob")
carol = User.objects.create_user("carol")
editors = Group.objects.create(name="editors")
bob.groups.add(editors)
book = Book(author=alice, state="draft")

print("alice change :", alice.has_perm("library.change_book", book))
print("bob   change :", bob.has_perm("library.change_book", book))
print("carol change :", carol.has_perm("library.change_book", book))
print("alice publish:", alice.has_perm("library.publish_book", book))

invocation context,可以让同一轮规则求值过程共享上下文数据。很适合把一次昂贵计算缓存下来,后面的 predicate 直接复用。当你的权限判断依赖上下文信息时,不一定要在每个 predicate 里重复计算。先缓存,再复用,组合表达式会更清晰。

py 复制代码
import rules

@rules.predicate(bind=True)
def cache_region(self, user, service):
    region = service["region"]
    self.context["region"] = region
    print("cache region      :", region)
    return True

@rules.predicate(bind=True)
def same_region(self, user, service):
    print("reuse cached data :", self.context.get("region"))
    return user["region"] == self.context.get("region")

can_deploy = cache_region & same_region

print("cn user ->", can_deploy.test({"region": "cn"}, {"region": "cn"}))
print("us user ->", can_deploy.test({"region": "us"}, {"region": "cn"}))

前一个 predicate 把 region 放进上下文,后一个 predicate 直接复用,不需要把相同数据再查一遍。

最佳实践

  1. 先把 predicate 命名成业务语言,再组合成 rule,避免后面只剩一坨布尔表达式。
  2. 接入 Django 时确认 AUTHENTICATION_BACKENDS 顺序和 INSTALLED_APPS 配置正确,再做一次真实的 has_perm 回归。
  3. 对复杂规则补充单元测试,尤其是 owner、group、上下文条件叠加时的边界分支。
相关推荐
weixin_BYSJ19871 天前
django在线图书销售平台---附源码16192
java·javascript·spring boot·python·django·flask·php
kobe_OKOK_2 天前
django外键字段会自动在数据库字段后面加上_id
数据库·django·sqlite
梅雅达编程笔记2 天前
零基础学 Python 第14章 | 模块、包与第三方库
开发语言·python·django·numpy·pandas
梅雅达编程笔记2 天前
零基础学 Python 第15章 | 类与对象:面向对象编程入门
开发语言·python·django·numpy·pandas
2401_868534783 天前
OSPF经典案例分析
python·django
不瘦80斤不改名3 天前
全家桶、乐高积木与类型引擎:重新认识 Django、Flask 与 FastAPI
django·flask·fastapi
hanxiuchao8 天前
告别客户端臃肿!网页端 M3U8 播放调试方案,适配全办公场景
运维·python·django·m3u8·m3u8播放
程序员羽痕8 天前
基于深度学习的眼疾识别系统
人工智能·pytorch·深度学习·分类·django
流云鹤8 天前
1. 配置环境、创建导航栏
python·django