Contents
-
Removing the magic
- 如何得到这个分枝
- 概述
- Status
- 你需要做的数据库改变
-
你需要做的代码更改
- Model 类 及 Field 类 重命名/重定位
- 直接访问你的 model 类, 而不是通过那个令人费解的中间模块(magic modules)
- 名字空间简化
- 'packages' 模块不再存在
- model 语法的变更
- Moved admin options to 'class Admin'
- 数据库连接重命名/重定位
- Model 方法不再自动访问 datetime 和 db 模块
- Descriptor fields
- 覆盖默认的管理器名称: ('objects')
- 自定义管理器及多管理器
- 添加了一个更强大的覆盖 model 方法的方式, 删除了硬编码的 _pre_save(), _post_save(), 等等.
- 改名! DoesNotExist 异常
- admin URLconf path 变得更短
- get_object_or_404 及 get_list_or_404 现在接受 model 类做为参数,而不再是模块
- 传递给 generic views 的参数做了改变
- generic views 中的模板名字改变
- 将设置文件 settings 移到一个实例中
- 删除 SilentVariableFailure 异常
- request.user 现在设置成通过 middleware 访问
- Authentication 做了整理,变得更加统一
- 操纵器界面的改动
- django.VERSION
- 你可以使用的新功能
- 团队仍然需要做的
Removing the magic
"magic-removal" 分支力图扫除 Django 代码库中的近两年来积攒的缺陷. 大部分变化集中在数据库 API 及移除一些不必要的 magic, 其它一些变化主要在于提高了框架的简单性和可用性.
这些变化将被整合在下一个 Django 发行版中, 也就是0.92.
本文档说明了该分枝带来的变化.
如何得到这个分枝
欢迎试用! 通过 Subversion 你可以得到该分支. 地址是: http://code.djangoproject.com/svn/django/branches/magic-removal .
在一个机器上同时运行两个版本的 Django
Here's one way to use Django's trunk and magic-removal branch on the same machine. This assumes Django's trunk (or a release such as 0.90 or 0.91) is installed:
# Get the magic-removal code somewhere on your filesystem. In this example, we use /home/python/django. $ cd /home/python/django $ svn co http://code.djangoproject.com/svn/django/branches/magic-removal # This will have created a "magic-removal" directory. # Whenever you want to use magic-removal, set the environment variable {{{PYTHONPATH
to the directory containing magic-removal.export PYTHONPATH=/home/python/django/magic-removal }}}
概述
magic-removal 中最大的改变是:
去除了另人费解的 django.models 包. 要使用 models, 不论它是否在 Python 路径中, 直接导入 model 类即可. 同样的,另人费解的 modules (比如教程中的 django.models.polls) 也不再存在了, 你现在直接操作 model class 就可以.
- 删除了所有的自动复数行为.
- 数据库 API 以不同的方式进行了改变.
多个包, 比如 Django 模板系统(以前位于 django.core.template), 现在访问这些包更直接和更易记了.
Status
希望这个消息不会让你昏倒, Django 仍在紧锣密鼓的开发当中.
你需要做的数据库改变
要升级你以前安装的 Django , 你需要对数据库做一些改变. 特别是, 当你数据库已经有了很多数据时.
对核心数据表进行了重新命名
我们对一系列核心 Django 表进行了重命名. 要更新 MySQL 和 SQLite, 执行这些SQL语句:
ALTER TABLE auth_groups RENAME TO auth_group; ALTER TABLE auth_groups_permissions RENAME TO auth_group_permissions; ALTER TABLE auth_messages RENAME TO auth_message; ALTER TABLE auth_permissions RENAME TO auth_permission; ALTER TABLE auth_users RENAME TO auth_user; ALTER TABLE auth_users_groups RENAME TO auth_user_groups; ALTER TABLE auth_users_user_permissions RENAME TO auth_user_user_permissions; ALTER TABLE content_types RENAME TO django_content_type; ALTER TABLE core_sessions RENAME TO django_session; ALTER TABLE django_flatpages RENAME TO django_flatpage; ALTER TABLE django_flatpages_sites RENAME TO django_flatpage_sites; ALTER TABLE django_redirects RENAME TO django_redirect; ALTER TABLE sites RENAME TO django_site; DROP TABLE packages; ALTER TABLE django_content_type rename package to app_label; ALTER TABLE django_content_type rename python_module_name to model;
PostgreSQL 则不同, 要更新 PostgreSQL, 执行下列 SQL 语句:
BEGIN; ALTER TABLE auth_groups RENAME TO auth_group; ALTER TABLE auth_groups_id_seq RENAME TO auth_group_id_seq; ALTER TABLE auth_group ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_group ALTER COLUMN id SET DEFAULT nextval('public.auth_group_id_seq'::text); ALTER TABLE auth_groups_permissions RENAME TO auth_group_permissions; ALTER TABLE auth_groups_permissions_id_seq RENAME TO auth_group_permissions_id_seq; ALTER TABLE auth_group_permissions ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_group_permissions ALTER COLUMN id SET DEFAULT nextval('public.auth_group_permissions_id_seq'::text); ALTER TABLE auth_messages RENAME TO auth_message; ALTER TABLE auth_messages_id_seq RENAME TO auth_message_id_seq; ALTER TABLE auth_message ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_message ALTER COLUMN id SET DEFAULT nextval('public.auth_message_id_seq'::text); ALTER TABLE auth_permissions RENAME TO auth_permission; ALTER TABLE auth_permissions_id_seq RENAME TO auth_permission_id_seq; ALTER TABLE auth_permission ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_permission ALTER COLUMN id SET DEFAULT nextval('public.auth_permission_id_seq'::text); ALTER TABLE auth_users RENAME TO auth_user; ALTER TABLE auth_users_id_seq RENAME TO auth_user_id_seq; ALTER TABLE auth_user ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_user ALTER COLUMN id SET DEFAULT nextval('public.auth_user_id_seq'::text); ALTER TABLE auth_users_groups RENAME TO auth_user_groups; ALTER TABLE auth_users_groups_id_seq RENAME TO auth_user_groups_id_seq; ALTER TABLE auth_user_groups ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_user_groups ALTER COLUMN id SET DEFAULT nextval('public.auth_user_groups_id_seq'::text); ALTER TABLE auth_users_user_permissions RENAME TO auth_user_user_permissions; ALTER TABLE auth_users_user_permissions_id_seq RENAME TO auth_user_user_permissions_id_seq; ALTER TABLE auth_user_user_permissions ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_user_user_permissions ALTER COLUMN id SET DEFAULT nextval('public.auth_user_user_permissions_id_seq'::text); ALTER TABLE content_types RENAME TO django_content_type; ALTER TABLE content_types_id_seq RENAME TO django_content_type_id_seq; ALTER TABLE django_content_type ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_content_type ALTER COLUMN id SET DEFAULT nextval('public.django_content_type_id_seq'::text); ALTER TABLE core_sessions RENAME TO django_session; ALTER TABLE django_flatpages RENAME TO django_flatpage; ALTER TABLE django_flatpages_id_seq RENAME TO django_flatpage_id_seq; ALTER TABLE django_flatpage ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_flatpage ALTER COLUMN id SET DEFAULT nextval('public.django_flatpage_id_seq'::text); ALTER TABLE django_flatpages_sites RENAME TO django_flatpage_sites; ALTER TABLE django_flatpages_sites_id_seq RENAME TO django_flatpage_sites_id_seq; ALTER TABLE django_flatpage_sites ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_flatpage_sites ALTER COLUMN id SET DEFAULT nextval('public.django_flatpage_sites_id_seq'::text); ALTER TABLE django_redirects RENAME TO django_redirect; ALTER TABLE django_redirects_id_seq RENAME TO django_redirect_id_seq; ALTER TABLE django_redirect ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_redirect ALTER COLUMN id SET DEFAULT nextval('public.django_redirect_id_seq'::text); ALTER TABLE auth_permission DROP COLUMN package; ALTER TABLE django_content_type DROP CONSTRAINT "$1"; DROP TABLE packages; ALTER TABLE sites RENAME TO django_site; ALTER TABLE sites_id_seq RENAME TO django_site_id_seq; ALTER TABLE django_site ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_site ALTER COLUMN id SET DEFAULT nextval('public.django_site_id_seq'::text); ALTER TABLE django_content_type rename package to app_label; ALTER TABLE django_content_type rename python_module_name to model; COMMIT;
Database 表命名方式做了改动
以前 Database 表的名字通过连接 app_label 和 module_name 来生成. 比如: polls_polls.
由于现在已经没有了 module_name 的概念, 数据表的名字现在被设计成通过连接 app_label 和 model 类的名字(小写)来自动生成.比如: polls_poll.
和以前一样, 这种行为可以通过在你的 model 中的内嵌类 class Meta 的指定db_table属性来改变..
你要么显式的指定 db_table 以适应新的命名方式, 也可以直接在数据库中对数据表进行改名.我们建议设置 db_table, 因为这个更容易一些.
你需要做的代码更改
Model 类 及 Field 类 重命名/重定位
导入 models 由原来的 from django.core.meta 变更为 from django.db.models.
直接访问你的 model 类, 而不是通过那个令人费解的中间模块(magic modules)
直接从定义model类的模块导入 model 类, 去掉了 django.models.*.
名字空间简化
django.utils.httpwrappers 变更为 django.http.
django.core.exceptions.Http404 变更为 django.http.Http404.
django.core.template 变更为 django.template.
django.core.formfields 变更为 django.forms.
django.core.extensions 变更为 django.shortcuts.
django.core.extensions.DjangoContext 变更为 django.template.RequestContext.
需要从你的settings文件中的 TEMPLATE_LOADERS 设置值中的将 ".core" 删除, 旧代码:
新代码:
自定义模板标签也需要做一个类似的修改.旧代码:
新代码:
"auth" 和 "core" models 被分开且被移动到 django.contrib, 具体如下:
django.models.auth 变更为 django.contrib.auth.models.
django.models.core.sites 变更为 django.contrib.sites.models.
django.models.core.contenttypes 变更为 django.contrib.contenttypes.models.
django.models.core.packages 变更为 django.contrib.contenttypes.models. (注意在 magic-removal 完成之前,那个 "packages" 正在逐步被去除.)
如果你正使用session的话, Session 中间件从 django.middleware.sessions.SessionMiddleware 被移动到 django.contrib.sessions.middleware.SessionMiddleware. 请确保更新你的 MIDDLEWARE_CLASSES 设置.
同样的, Session model 由 django/models/core.py 被移动到 django/contrib/sessions/models.py. 如果你正使用 Session model ,一定要注意这个改变.
'packages' 模块不再存在
Packages 不再存在 (它们实在多余). 如果已经完成依据 content-types 或 permissions 查询功能,你需要稍微修改一下你的代码:
旧代码 |
新代码 |
contenttypes.get_list(package__label__exact='foo') |
ContentType.objects.filter(package__exact='foo') |
permissions.get_list(package__label__exact='foo') |
Permission.objects.filter(package__exact='foo') |
model 语法的变更
class META 变成了 class Meta.
以下不再是 class Meta 的合法参数:
module_name
admin (参阅下文中的 "Moved admin options to 'class Admin'" 小节.)
exceptions (只要在将你的异常放到包含 models 的模块中访问即可)
module_constants (只要在将定义的常数放到包含 models 的模块中访问即可)
where_constraints (用一个自定管理器就可以. 参阅下文中的 "自定义管理器,及多管理器")
Moved admin options to 'class Admin'
不要再从 class META 定义 admin=meta.Admin 参数 , 所有的管理选项都放到了 class Admin 内.
旧代码:
新代码:
如果你使用 admin 界面却没有指定任何 admin 选项, 只要在那个内嵌类中放一个 pass 语句.
旧代码:
新代码:
数据库连接重命名/重定位
对于任何使用原始数据库连接的代码, 使用 django.db.connection ,不再使用 django.core.db.db.
旧代码:
新代码:
如果你需要那些 Backend-specific 函数, 他们现在位于 django.db.backend.
旧代码:
新代码:
同样的, 不同的 backend 功能已经分到三个不同的模块中 -- base.py, creation.py 和 introspection.py. 之所以这样修改是为了改善性能和节省内存, 这样一来, Django 日常使用就不必将内省模块载入内存.
Model 方法不再自动访问 datetime 和 db 模块
以前, 每个 model 方法都能访问 datetime 模块及 db 变量(表示当前的数据库连接). 现在你必须显式的导入它们.
旧代码:
新代码:
Descriptor fields
所有 "table-level" 的函数 -- (在表范围内得到数据记录的方式胜于 performing instance-specific tasks 的方式) -- 现在通过一个 model 类的 objects 属性访问.它们不是一个 model 实例对象的直接方法, 因为我们希望保持 "表范围" 和 "行范围" 两个不同的名字空间.
一个 model 类的 objects 属性是一个 django.db.models.manager.Manager 类的实例. 一个管理器有一系列方法,所有这些方法都返回一个 QuerySet 实例.
all() -- 返回一个包含数据表中所有对象的 QuerySet 对象. 它就象过去的老的 get_list() 方法. 它不接受任何参数.
filter(**kwargs) -- 返回一个 QuerySet 对象, 按给定的关键字参数过滤结果集. 查询参数的风格与以前一样, 比如 pubdate__year=2005, 一个小改动就是你现在可以在关键字中加省略 __exact 了(更加方便). 举例来说, name='John' 与 name__exact='John' 是等价的. 注意一点如果跨应用程序查询时你不能省略 __exact.
exclude(**kwargs) 类似 filter(), 不过它是当给定参数为假时返回结果集.
order_by(*fieldnames) -- 返回一个 QuerySet
count() -- 返回指定数据库中的对象个数.
dates(field_name, kind) -- 类似过去的用于日期字段的 get_FIELD_list() . 举例来说, 过去的 get_pubdate_list('year') 现在变成了 dates('pubdate', 'year').
delete() -- 删除所有对象.
distinct() -- 返回一个无重复记录的 QuerySet .
extra(select=None, where=None, params=None, tables=None) -- 设置 select, where, params 和 tables 参数, 参数格式与以前相同.
get(**kwargs) -- 类似旧的 get_object(). 返回一个对象或引发 DoesNotExist 异常.
in_bulk(id_list) -- 类似旧的 get_in_bulk().
iterator() -- 返回一个生成器以迭代结果集.
select_related() -- 返回一个带有 "select related" 选项的 QuerySet(未做改动).
values(*fieldnames) -- 类似旧的 get_values().
每个 QuerySet 都有下列方法, 返回一个经过适当处理的当前结果集的克隆:
filter(**kwargs)
order_by(*fieldnames)
iterator()
count()
get(**kwargs)
delete()
filter(**kwargs)
select_related()
order_by(*fieldnames)
distinct()
extra(select=None, where=None, params=None, tables=None)
下面是一些例子,使用下面的 models:
1 class Reporter(models.Model):
2 fname = models.CharField(maxlength=30)
3 lname = models.CharField(maxlength=30)
4
5 class Site(models.Model):
6 name = models.CharField(maxlength=20)
7
8 class Article(models.Model):
9 headline = models.CharField(maxlength=50)
10 reporter = models.ForeignKey(Reporter)
11 pub_date = models.DateField()
12 sites = models.ManyToManyField(Site)
旧的语法 |
新的语法 |
reporters.get_list() |
Reporter.objects.all() |
reporters.get_list(fname__exact='John') |
Reporter.objects.filter(fname='John') |
reporters.get_list(order_by=('-lname', 'fname')) |
Reporter.objects.order_by('-lname', 'fname') |
reporters.get_list(fname__exact='John', order_by=('lname',)) |
Reporter.objects.filter(fname='John').order_by('lname') |
reporters.get_object(pk=3) |
Reporter.objects.get(pk=3) |
reporters.get_object(complex=(Q(...)|Q(...))) |
Reporter.objects.get(Q(...)|Q(...)) |
reporters.get_object(fname__contains='John') |
Reporter.objects.get(fname__contains='John') |
reporters.get_list(fname__ne='John') |
Reporter.objects.exclude(fname='John') (note that ne is no longer a valid lookup type) |
(not previously possible) |
Reporter.objects.exclude(fname__contains='n') |
reporters.get_list(distinct=True) |
Reporter.objects.distinct() |
reporters.get_list(offset=10, limit=5) |
Reporter.objects.all()[10:15] |
reporters.get_values() |
Reporter.objects.values() |
reporters.get_in_bulk([1, 2]) |
Reporter.objects.in_bulk([1, 2]) |
reporters.get_in_bulk([1, 2], fname__exact='John') |
Reporter.objects.filter(fname='John').in_bulk([1, 2]) |
Date lookup |
|
articles.get_pub_date_list('year') |
Article.objects.dates('pub_date', 'year') |
Latest-object lookup |
|
articles.get_latest() (required get_latest_by in model) |
Article.objects.latest() (with get_latest_by in model) |
(Not previously possible) |
Article.objects.latest('pub_date') # Latest by pub_date (overrides get_latest_by field in model) |
Many-to-one related lookup |
|
article_obj.reporter_id |
article_obj.reporter.id |
article_obj.get_reporter() |
article_obj.reporter |
reporter_obj.get_article_list() |
reporter_obj.article_set.all() |
reporter_obj.get_article_list(headline__exact='Hello') |
reporter_obj.article_set.filter(headline='Hello') |
reporter_obj.get_article_count() |
reporter_obj.article_set.count() |
reporter_obj.add_article(headline='Foo') |
reporter_obj.article_set.add(headline='Foo') |
(Alternate syntax) |
reporter_obj.article_set.add(article_obj) |
("values" lookup, etc., not previously possible) |
reporter_obj.article_set.values() |
Many-to-many related lookup |
|
article_obj.get_site_list() |
article_obj.sites.all() |
article_obj.set_sites([s1.id, s2.id]) |
article_obj.sites.clear(); article_obj.sites.add(s1); article_obj.sites.add(s2) |
article_obj.set_sites([s1.id]) # deletion |
article_obj.sites.remove(s2) |
site_obj.get_reporter_list() |
site_obj.reporter_set.all() |
注意关联对象查询使用关联对象的默认管理器, 这意味着访问关联对象的API与通过管理器访问关联对象的API已经完全一致.
还要注意不能从以下实例中访问管理器:
覆盖默认的管理器名称: ('objects')
如果一个 model 已经有一个 objects 属性, 你需要给 objects 管理器指定一个另外的名字.
1 class Person(models.Model):
2 first_name = models.CharField(maxlength=30)
3 last_name = models.CharField(maxlength=30)
4 objects = models.TextField()
5 people = models.Manager()
6
7 p = Person(first_name='Mary', last_name='Jones', objects='Hello there.')
8 p.save()
9 p.objects == 'Hello there.'
10 Person.people.all()
自定义管理器及多管理器
只要你愿意,你可以创建任意多个管理器. 在必要时(比如在 admin 中), Django 会按照管理器定义顺序使用第一个定义的管理器.
如果你定义了至少一个自定义管理器, 就不能使用默认的 "objects" 管理器.
如果一个管理器需要访问它的关联 model 类, 应该使用 self.model. 例子:
让 admin 使用自定义管理器
有时你需要使用一个不同的管理器以用于 admin 的特定显示(比如在 admin 中只显示符合某些条件的对象). 你可以通过在Admin 声明中定义 manager 选项来实现:
1 class LivingPeopleManager(models.Manager):
2 def get_query_set(self):
3 return super(LivingPeopleManager, self).get_query_set().filter(is_alive=True)
4
5 class Person(models.Model):
6 name = models.CharField(maxlength=50)
7 is_alive = models.BooleanField()
8
9 class Admin:
10 manager = LivingPeopleManager()
(参阅 "覆盖默认的 QuerySets" 了解 QuerySets 的更多信息)
添加了一个更强大的覆盖 model 方法的方式, 删除了硬编码的 _pre_save(), _post_save(), 等等.
Proper subclassing of methods now works, 你可以子类化自动的 save() 和 delete() 方法. 删除了 _pre_save(), _post_save(), _pre_delete() 和 _post_delete() 钩子. 例子:
你甚至可以跳过保存这一步 (如同 requested #1014).
改名! DoesNotExist 异常
不再是 people.PersonDoesNotExist, 现在是 Person.DoesNotExist.
旧代码:
新代码:
admin URLconf path 变得更短
URLconf 中的 include 变得更短.
旧代码: django.contrib.admin.urls.admin
新代码: django.contrib.admin.urls
get_object_or_404 及 get_list_or_404 现在接受 model 类做为参数,而不再是模块
旧代码:
1 get_object_or_404(polls, pk=1)
新代码:
1 get_object_or_404(Poll, pk=1)
传递给 generic views 的参数做了改变
由于不再有 module_name 的概念, 传递给 generic views 的 "info_dicts" 不再接受 "app_label" 和 "module_name" 参数而是使用新的 "queryset" 参数(一个 QuerySet 的实例).
旧代码:
新代码:
generic views 中的模板名字改变
由于不再有 module_name 的概念, generic views 不再创建基于 "module_name" 的模板.无论何处用到了 module_name, 现在他们都变成了 model_name, 一个 model 名字的小写版本.
注意在这里仍然保留了 app_label.
下面的例子假定 models 定义在 myproject/blog/models.py.
旧的: blog/entries_archive.html
新的: blog/entry_archive.html
将设置文件 settings 移到一个实例中
Settings 从一个专门的模块django.conf.settings 中被移动到一个django.conf 模块实例中.现在你需要导入 settings 对象并以该实例的一个属性的方式引用 settings .
旧代码: from django.conf.settings import LANGUAGE_CODE
新代码: from django.conf import settings
Django machinery 封装可以通过变更 settings 实例(带有一个访问 per-thread 或 per-location 全局变量代理 proxy 实例使用这一功能.
删除 SilentVariableFailure 异常
旧的行为: 模板中子类django.core.template.SilentVariableFailure的任何异常将静默.
新的行为: 模板系统中任何拥有 silent_variable_failure 属性的异常将静默. django.core.template.SilentVariableFailure不再存在.
request.user 现在设置成通过 middleware 访问
过去常常在 mod_python 和 wsgi 处理器中设置. 你需要在你的 settings.py 文件中的MIDDLEWARE_CLASSES 里的"django.contrib.sessions.middleware.SessionMiddleware" 之后 的某处 添加 "django.contrib.auth.middleware.AuthenticationMiddleware" . 否则访问 request.user 会引发 AttributeError.
Authentication 做了整理,变得更加统一
以前, 授权系统至少要在四个不同的位置进行设置. 现在所有的东西都被整理到 django.contrib.auth 中. 如下:
django.parts.auth.formfields.AuthenticationForm 变更为 django.contrib.auth.forms
django.parts.auth.anonymoususers.AnonymousUser 变更为 django.contrib.auth.models
django.views.auth.login.* 变更为 django.contrib.auth.views
django.views.decorators.auth.* 变更为 django.contrib.auth.decorators
django.views.registration.passwords.PasswordResetForm 变更为 django.contrib.auth.forms
django.views.registration.passwords.PasswordChangeForm 变更为 django.contrib.auth.forms
django.views.registration.passwords.password_reset 变更为 django.contrib.auth.views
django.views.registration.passwords.password_reset_done 变更为 django.contrib.auth.views
django.views.registration.passwords.password_change 变更为 django.contrib.auth.views
django.views.registration.passwords.password_change_done 变更为 django.contrib.auth.views
如果你用到了这些类或函数, 你需要根据上文更新代码.
操纵器界面的改动
旧的:
新的:
django.VERSION
变量 django.VERSION 由一个四元素 tuple 变更为一个三元素 tuple.
Old: VERSION = (0, 9, 1, 'magic-removal')
New: VERSION = (0, 91, 'magic-removal')
你可以使用的新功能
Models 支持属性
与以前不同, 现在models也支持属性了.
你能覆盖默认的 QuerySets
你能指定一个管理者使用的默认 QuerySet (参阅上文的 "Descriptor fields"). 例子:
1 class PublishedBookManager(models.Manager):
2 def get_query_set(self):
3 return super(PublishedBookManager, self).get_query_set().filter(is_published=True)
4
5 class Book(models.Model):
6 title = models.CharField(maxlength=50)
7 author = models.CharField(maxlength=30)
8 is_published = models.BooleanField()
9 published_objects = PublishedBookManager()
团队仍然需要做的
删除自动的 manipulators, 有利于 validation-aware models
状态: 尚未完工
改变子类语法
状态: 沿未完工