把一个Django应用从1.4升级到1.11

升级的动机

基本上,一张图就可以解释升级Django的全部动机。

Django Roadmap

由上图可见,近期LTS(long-term support)的版本有1.8、1.11、2.2三个。 另外,Django官网的文档已经不再支持1.7以前的了。 所以,无论从安全、维护、还是开发的角度考虑,都至少要把Django升级到这三个版本之一。

2017年来研究这个问题很奇怪。

正如孤在《Django项目从1.3.7升级到1.4.22》中所说, 在工作中,什么奇怪的项目都会遇到。 当这些个奇怪的项目,还有维护与再开发的需求时,这种连升N级的诡异需求就产生了。

孤有一种攒了一个多月的作业,到暑假的最后一天来写的感觉,仿佛少年。

1.4升级到1.5

pip uninstall -y Django
pip install 'Django<1.6'

模板中的url问题

'url' requires a non-empty first argument. The syntax changed in Django 1.5

这是由于原先的url使用方式是直接接上函数名。

<a href="{% url foo.view.hello sth.id %}">{{ sth.name }}</a>

而新的方式,需要把它变成字符串参数。

<a href="{% url "foo.view.hello" sth.id %}">{{ sth.name }}</a>

原先的方式,在1.3版本里,解释时会出一些问题。 foo.view.hello到底是一个view的函数名,还是一个模板的变量名,不能很好地区分。 因此,引入了一个{% load url from future %},支持新的使用方式。 详见Django 1.5 release notes

如果原先用的是{% load url from future %},那么改成{% load url %}即可。 如果原先用的是旧的方式,那么需要给函数名加上双引号。

find . -name '*.html' | xargs sed -i 's/{% url \([^" >][^ >]*\)/{% url "\1"/g'

参考《stackoverflow.com/questions/14882491》。

1.5升级到1.6

pip uninstall -y Django
pip install 'Django<1.7'

1.6版本对SQLite数据库、ORM方面,做了很多改动。 详见《Django 1.6 release notes》。

启动问题

ImportError: No module named markup

在1.3、1.4里有django.contrib.markup这个App。 它在1.5里被废弃,在1.6中被加速删除。 详见《#18054 (deprecate contrib.markup) – Django》。

最根本的解决办法,就是不要去使用它。 但是在已经大量使用它的老旧项目中,没办法不用。 只能自行安装django-markup-deprecated,再顶一顶。

pip install django-markup-deprecated
find . -name '*.py' | xargs sed -i 's/django.contrib.markup/markup_deprecated/g'

在需要包名的地方,用markup_deprecated代替django.contrib.markup,模板文件中的{% load markup %}可以不变。

数据库问题

TransactionManagementError at /admin/

Your database backend doesn't behave properly when autocommit is off. Turn it on before using 'atomic'.

settings.py中,数据库的配置位置,添加一行'ATOMIC_REQUESTS': True就可解决。 详见《stackoverflow.com/questions/20039250》。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'ATOMIC_REQUESTS': True,
        ...
    }
}

django.conf.urls.defaults

No module named defaults

在1.6中,早已被废弃的django.conf.urls.defaults被正式删除了。 相关import,应替换为django.conf.urls

find . -name '*.py' | xargs sed -i 's/django.conf.urls.defaults/django.conf.urls/g'

1.6升级到1.7

pip uninstall -y Django
pip install 'Django<1.8'

参考:《Django 1.7 release notes | Django documentation》。

System check identified some issues

System check identified some issues: ... System check identified 15 issues (0 silenced).

You have unapplied migrations; your app may not work properly until they are applied. Run 'python manage.py migrate' to apply them.

1_6.W002

14个issue都是一样的内容:

(1_6.W002) BooleanField does not have a default value. HINT: Django 1.6 changed the default value of BooleanField from False to None. See https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield for more information.

当然,这个网址已经无法访问了。 前面说过,Django已经不再支持1.7以前的文档。 把URL中的1.6换成1.11,即可查看到最新文档。

这类issue的意思是,原先默认保存为FalseBooleanField,今后会默认保存None。 这种事可大可小,问题可能会出在读取新保存的数据库字段的时候。

只要修改代码中所有无默认值的BooleanField,把默认值设为False,即可消除所有警告,并且保持行为的一致。

1_6.W001

还有一个issue是关于测试的。

?: (1_6.W001) Some project unittests may not execute as expected. HINT: Django 1.6 introduced a new default test runner. It looks like this project was generated using Django 1.5 or earlier. You should ensure your tests are all running & behaving as expected. See https://docs.djangoproject.com/en/dev/releases/1.6/#new-test-runner for more information.

然而,一个在2017年把项目移交给我时,仍然保持在1.3版本的老外,有可能会写测试吗?

如果仅仅是消除警告,可以参考《stackoverflow.com/questions/25871261》。 添加以下代码进settings.py

TEST_RUNNER = 'django.test.runner.DiscoverRunner'

RemovedInDjango18Warning

除了已经改动的警告,还有未来1.8版本的警告。 如果孤的目标是升级到1.7,这类问题倒是不用处理。然而……

RemovedInDjango18Warning: Creating a ModelForm without either the 'fields' attribute or the 'exclude' attribute is deprecated - form ComponentAdminForm needs updating

参考《stackoverflow.com/questions/28306288》,需要在类定义位置加上fields = '__all__'。 例如:

class ExampleForm(forms.ModelForm):
    class Meta:
        model = Something
        fields = '__all__'

MessageFailure at /admin/auth/group/add/

MessageFailure at /admin/auth/group/add/ You cannot add messages without installing django.contrib.messages.middleware.MessageMiddleware

与此同时,还有以下两个RemovedInDjango18Warning。

RemovedInDjango18Warning: XViewMiddleware has been moved to django.contrib.admindocs.middleware. RemovedInDjango18Warning: TransactionMiddleware is deprecated in favor of ATOMIC_REQUESTS.

参考《stackoverflow.com/questions/15852317》,需要在settings.py中替换相关部件。

MIDDLEWARE_CLASSES = (
    ...
-   'django.middleware.doc.XViewMiddleware',
-   'django.middleware.transaction.TransactionMiddleware',
+   'django.contrib.messages.middleware.MessageMiddleware',
)

INSTALLED_APPS = (
    ...
+   'django.contrib.messages',
)

unapplied migrations

You have unapplied migrations; your app may not work properly until they are applied. Run 'python manage.py migrate' to apply them.

代码不需要改动,而数据库结构却需要变化。 在1.7,原先syncdb的机制,被migratemakemigrations所替换。 需要执行python manage.py migrate,确保一些内置App的数据库结构被更新。

如果有交互式的提问,那么最好选no。 因为,在没有测试的情况下,数据库结构变化这种事,还是一动不如一静。

The following content types are stale and need to be deleted:

    auth | message

Any objects related to these content types by a foreign key will also
be deleted. Are you sure you want to delete these content types?
If you're unsure, answer 'no'.

    Type 'yes' to continue, or 'no' to cancel:

自从有了这个机制,Django的自带App,对数据库结构的改动,愈发地肆无忌惮。 几乎每一个大版本,都要来一次python manage.py migrate

1.7升级到1.8

pip uninstall -y Django
pip install 'Django<1.9'

参考:《Django 1.8 release notes | Django documentation》。

RemovedInDjango19Warning

RemovedInDjango19Warning: The django.forms.util module has been renamed. Use django.forms.utils instead.

from django.forms.util import ErrorList

简单地说,就是多个s。 这样的改动,其实挺任性。 不过,也能看出Django的开发者们对代码优化的执着。

-from django.forms.util import ErrorList
+from django.forms.utils import ErrorList

1_8.W001

(18.W001) The standalone TEMPLATE* settings were deprecated in Django 1.8 and the TEMPLATES dictionary takes precedence. You must put the values of the following settings into your default TEMPLATES dict: TEMPLATE_DIRS, TEMPLATE_DEBUG.

在1.8版本中,TEMPLATE_*的设置方式被废弃,改为一个汇总的TEMPLATES。 参考《Settings#TEMPLATES》。

TEMPLATE_DEBUG = DEBUG

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.Loader',
    'django.template.loaders.app_directories.Loader',
)

TEMPLATE_DIRS = (
    os.path.join(BASE_DIR, 'templates'),
)

TEMPLATE_CONTEXT_PROCESSORS = (
    "django.contrib.auth.context_processors.auth",
    "django.core.context_processors.debug",
    "django.core.context_processors.i18n",
    "django.core.context_processors.media",
    "django.core.context_processors.request",
)

以上settings.py中的配置,应替换为:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'APP_DIRS': False,
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        'OPTIONS': {
            'debug': DEBUG,
            'context_processors': (
                'django.contrib.auth.context_processors.auth',
                'django.template.context_processors.debug',
                'django.template.context_processors.i18n',
                'django.template.context_processors.media',
                'django.template.context_processors.static',
                'django.template.context_processors.tz',
                'django.template.context_processors.request',
                'django.contrib.messages.context_processors.messages',
            ),
            'loaders': (
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
            )
        }
    },
]

'APP_DIRS'不能为True,否则会出现以下错误。

app_dirs must not be set when loaders is defined.

ImportError: No module named doc

ImportError: No module named doc

关于这个问题,全网搜索都没有现成的答案。 其实,它是一个中间件改名导致的。

-   'django.middleware.doc.XViewMiddleware',
+   'django.contrib.admindocs.middleware.XViewMiddleware',

ImportError: No module named transaction

这个,是中间件'django.middleware.transaction.TransactionMiddleware'导致的,直接可以删除。 详见《#deprecation-removed-in-1.8》。

其实,如果在升级到1.7时把他俩删掉,就不会出问题。

'module' object has no attribute 'commit_on_success'

'module' object has no attribute 'commit_on_success'

在1.7时,其实就会发现警告。

RemovedInDjango18Warning: commit_on_success is deprecated in favor of atomic.

@transaction.commit_on_success

这需要把所有的@transaction.commit_on_success,都改为@transaction.atomic。 参考《stackoverflow.com/questions/21861207》。

find . -name '*.py' | xargs sed -i 's/@transaction.commit_on_success/@transaction.atomic/g'

1.8升级到1.9

pip uninstall -y Django
pip install 'Django<1.10'

RemovedInDjango110Warning

共有以下两类警告,都是django.conf.urls接口变化所致。

RemovedInDjango110Warning: django.conf.urls.patterns() is deprecated and will be removed in Django 1.10. Update your urlpatterns to be a list of django.conf.urls.url() instances instead.

RemovedInDjango110Warning: Support for string view arguments to url() is deprecated and will be removed in Django 1.10

这个需要对所有urls.py文件,进行较大改动。 原先使用patterns()实现的URL指定,现在需要改成url列表。

urlpatterns = patterns(
    (r'^admin/', include(admin.site.urls)),
)

以上样例,需要改为:

urlpatterns = [
    url(r'^admin/', admin.site.urls),
]

1.9升级到1.10

pip uninstall -y Django
pip install 'Django<1.11'

参考《Django 1.10 release notes》。

NoReverseMatch

NoReverseMatch at /accounts/login/

Reverse for 'django.contrib.auth.views.login' with arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []

登录时,遇到以上信息。 这个问题其实是受url的改动影响,需要改动的地方非常多。 以下引用,来自《Django 1.10 release notes》。

  • The LOGOUT_URL setting is removed as Django hasn’t made use of it since pre-1.0. If you use it in your project, you can add it to your project’s settings. The default value was '/accounts/logout/'.
  • The ability to reverse() URLs using a dotted Python path is removed.
  • The ability to use a dotted Python path for the LOGIN_URL and LOGIN_REDIRECT_URL settings is removed.
  • Support for string view arguments to url() is removed.

这部分,其实是在1.9的RemovedInDjango110Warning中有提到的。 只是,那时不需要修改模板文件,警告就消除了。

在1.10的模板中,使用url需要一个标识符(identification,参考《#reverse-resolution-of-urls》),也即指定url函数的name参数。

url(r'accounts/', include([
    url(r'^login/$', login, name='login'),
    url(r'^logout/$', logout, name='logout'),
])),

上例中,把login这个view,标识符为'login'。 在模板文件中,使用{% url 'login' %}即可得到计算后的URL。

<form method="post" action="{% url 'login' %}">

所有被模板引用的url,都需要做这样的修改。 这就没办法用什么findsed来处理了,只能纯手工操作。 不过好在有Vim的record/execute功能,几十处修改也就几分钟的事。

No named cycles in template

No named cycles in template. 'row2,row1' is not defined

参考《stackoverflow.com/questions/40603961》与《#cycle》,可知应该把原先模板中的cycle使用进行替换。

主要原因,见《Django 1.10 release notes》。

Support for the syntax of {% cycle %} that uses comma-separated arguments is removed.

比如:

<tr class="{% cycle row2,row1 %}">

就应该替换为:

<tr class="{% cycle 'row2' 'row1' %}">

这个也需要Vim进行批量处理。

1.10升级到1.11

pip uninstall -y Django
pip install 'Django<1.12'

执行python manage.py runserver后,发现完全没有警告,直接可以运行!

然而,查看《Django 1.11 release notes》,发现是空欢喜一场。 执行python -Wd manage.py runserver后,大量的RemovedInDjango20Warning如约而至。

Deprecating warnings are no longer loud by default

Unlike older versions of Django, Django’s own deprecation warnings are no longer displayed by default. This is consistent with Python’s default behavior.

不过,还好1.11是LTS版本,维护周期比2.0还长。 升级到了终点。

最终的数据库汇总升级

开发用的数据库,是跟着一个个小版本一路升上来的。 而生产环境的数据库,直接执行python manage.py migrate,可能会有问题。

django.db.utils.OperationalError: table "django_content_type" already exists

参考《stackoverflow.com/questions/29760817》,使用--fake-initial参数可以解决问题。

python manage.py migrate --fake-initial

总结

呼呼……且让孤喘一会儿!

本质上,这是一个把废弃接口与用法,替换成新版本形式的一个过程。 通过这次升级历险记,孤对Django近年的发展,有了一个笼统的了解。

重要参考


相关笔记