第十三章 上线

在上一章,为其他程序与我们的Web应用交互创建了RESTful API。本章将学习如何创建生产环境让我们的网站正式上线,主要内容有:

1创建生产环境

现在该将Django项目正式部署到生产环境中了。我们将按照下列步骤将站点部署到生产环境中:

  1. 为生产环境配置项目设置
  2. 使用PostgreSQL数据库
  3. 使用uWSGI和NGINX建立web服务器
  4. 管理静态资源
  5. 使用SSL加强站点安全管理

1.1管理用于多个环境的配置

在实际的项目中,很可能要面对不同的环境。一般至少有一个本地开发环境和一个生产环境,也可能有其他环境比如测试环境,预上线环境等。对于不同的环境,有些设置是通用的,有些则因环境而异。让我们将项目设置为可以适合不同环境,又可以保证项目结构不会被改变。

educa/educa/目录下建立settings目录(包),与settings.py同级,将settings.py文件重命名为base.py然后移动到settings目录中来,再创建其他文件,setting/目录如下所示:

settings/
    __init__.py
    base.py
    local.py
    pro.py

这些文件用途如下:

编辑settings/base.py,找到下列这行:

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

将其替换成下边这行:

BASE_DIR =
os.path.dirname(os.path.dirname(os.path.abspath(os.path.join(__file__, os.pardir))))

由于我们将settings.py文件又往下级目录放了一级,必须让BASE_DIR指向正确的路径,所以使用了os.pardir指向父目录,来让最后的路径依然是原来的项目根目录。

编辑settings/local.py,添加下列代码:

from .base import *

DEBUG = True
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

这是代表我们本地环境的配置文件。在其中导入了所有base.py中的设置内容,然后写了DEBUGDATABASES两个设置,这两个设置会覆盖原来base.py中的设置,成为本文件中的设置。由于DEBUG设置和DATABASES设置在每个配置文件中都会修改,也可以将这两个设置从base.py中删除。

再来编辑settings/pro.py,如下所示:

from .base import *

DEBUG = False
ADMINS = (
    ('Antonio M', 'email@mydomain.com'),
)
ALLOWED_HOSTS = ['*']
DATABASES = {
    'default': {
    }
}

这是生产环境的配置文件,来详细看一下其中的内容:

在需要面对多种环境时,建立一个基础配置文件并为每种环境编写单独的配置文件。用于具体环境的配置文件继承基础配置并重写与环境相关的配置即可。

由于我们现在没有把配置文件放在原来settings.py所在的位置,所以无法运行manage.py,必须为其指定settings模块的所在路径,即使用--settings参数或者设置环境变量DJANGO_SETTINGS_MODULE

打开系统命令行窗口输入:

export DJANGO_SETTINGS_MODULE=educa.settings.pro

这条命令会为当前的会话窗口设置DJANGO_SETTINGS_MODULE环境变量。如果不想每次运行shell都执行一遍,可以把这条命令加入到shell配置文件如.bashrc或者.bash_profile中。

如果不想对系统进行任何设置,那么在启动站点的时候必须加上--settings参数,如下:

python manage.py migrate --settings=educa.settings.pro

现在我们就为多环境做好了基础设置。

1.2使用PostgreSQL数据库

在整本书中,我们大部分都使用了Python自带的SQLite数据库,只要在博客全文检索的时候推荐使用了PostgreSQL数据库。SQLite轻量而且易于使用,但对于生产环境而言太过简陋,必须需要一个更强力的数据库比如PostgreSQL和MySQL或者Oracle。PostgreSQL的安装在第三章中已经介绍过,不再赘述。

让我们为我们的应用创建一个PostgreSQL用户,打开系统命令行输入如下命令:

su postgres
createuser -dP educa

系统会提示输入用户密码和权限。输入密码并且给予用户权限,然后使用下列命令建立一个新的数据库:

createdb -E utf8 -U educa educa

这样就建立好了一个新的数据库并且将其分配给educa用户,之后编辑settings/pro.py,修改数据库的设置如下:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'educa',
        'USER': 'educa',
        'PASSWORD': '********',
    }
}

将密码部分替换成为educa用户设置的密码。由于新数据库是空的,运行:

python manage.py migrate

然后创建一个超级用户:

python manage.py createsuperuser

译者注,安装PostgreSQL远没有这么简单,尤其是通过第三方程序远程管理PostgreSQL,需要修改PostgreSQL的配置文件,将认证方式修改为md5或者trust,然后启用允许访问的IP,建议查看官方文档和各种安装教程进行配置。

1.3部署前检查

Django提供了一个check命令,可以在任何时候检查项目。通常检查过程包括检查所有注册的应用,输出所有错误和警告信息。如果包含--deploy参数,还会额外执行针对生产环境的检查。

打开系统终端然后输入如下命令进行检查:

python manage.py check --deploy

译者注:作者这里遗漏了配置文件的路径,应该写成python manage.py check --deploy --settings=educa.settings.***,其中***为base,local或pro

如果站点编写正确的话,会看到没有错误输出,但是会有一些警告信息。这说明站点通过了检查,但这些警告信息应该得到处理,以让站点更加安全。本书不会深入这里的内容,但是要记得在正式部署之前一定要进行部署前检查。

1.4通过WSGI程序提供Django服务

Django的主要部署平台就是WSGI,WSGI是Web Server Gateway Interface的简称,是基于Python的程序提供Web服务的标准格式。由于Django也是Python程序,也需要通过WSGI对外提供服务。

当通过startproject命令新建一个项目的时候,Django会在项目目录内新建一个wsgi.py。这个文件包含了一个WSGI可调用函数,为我们的Django应用提供了一个接口。无论是我们之前采用本机8000端口的开发服务器,还是正式生产环境,都需要通过这个接口。关于WSGI的详细知识可以看https://wsgi.readthedocs.io/en/latest/及Python的PEP333

1.5安装WSGI

直到本节之前,我们的所有开发都是在django在本地环境运行的开发服务器上进行的。在生产环境中,需要一个真正的web服务器才能部署django服务。

uWSGI是一个非常快的Python应用程序WSGI服务器,使用WSGI标准与Python应用进行通信。uWSGI把HTTP请求翻译成Django程序能够处理的格式。

安装uSWGI:

pip install uwsgi==2.0.17

在pip安装之后,会built uWSGI(编译安装),需要一个C编译器,比如GCC或者clang,在linux环境下可以输入命令:apt-get install build-essential

如果是MacOS X,可以通过Homebrew安装,执行命令:brew install uwsgi。如果在windows下安装,需要Cygwin https://www.cygwin.com

。推荐在基于UNIX的操作系统上安装uWSGI。

UNIX环境下如果看到Successfully built uwsgi就说明成功安装了uWSGI。关于uWSGI的文档可以在https://uwsgi-docs.readthedocs.io/en/latest/找到。

1.6配置uWSGI

可以通过命令行配置uWSGI,打开系统命令行模式,进入educa项目的根目录,然后输入:

sudo uwsgi --module=educa.wsgi:application --env=DJANGO_SETTINGS_MODULE=educa.settings.pro --master --pidfile=/tmp/project-master.pid --http=127.0.0.1:8000 --uid=1000 --virtualenv=/home/env/educa/

必须需要su权限才可以。通过这条命令,为本机上的uWSGI设置了如下的内容:

  1. 使用educa.wsgi:application作为调用接口
  2. 载入生产环境的设置文件
  3. 使用virtualenv设置的虚拟环境,注意将/home/env/educa/替换为实际的虚拟环境所在路径。如果未使用虚拟环境,该配置可以不填。

如果不是在项目目录内执行的上述命令,需要额外加一个参数指定具体的项目目录--chdir=/path/to/educa/,将其中的/path/to/educa/替换成educa的项目路径。

通过浏览器访问http://127.0.0.1:8000/(无需启动django服务),可以看到站点内容显示了出来,但没有任何CSS样式,也无法显示图片,这是因为还没有配置uWSGI来提供静态文件服务。

uWSGI允许使用一个.ini配置文件进行自定义配置,比使用命令行要方便很多。在educa项目根目录下建立:

config/
    uwsgi.ini

编辑uwsgi.ini,添加如下代码:

[uwsgi]
# variables
projectname = educa
base = /home/projects/educa

# configuration
master = true
virtualenv = /home/env/%(projectname)
pythonpath = %(base)
chdir = %(base)
env = DJANGO_SETTINGS_MODULE=%(projectname).settings.pro
module = educa.wsgi:application
socket = /tmp/%(projectname).sock

在这个.ini文件里我们定义了两个变量:

上边定义的这两个变量是自定义变量,还可以定义任意其他变量,只要不和内置的名称冲突。接下来是具体设置的解释:

其中的socket套接字是用于和第三方路由软件进行通信,比如NGINX。命令行模式中我们使用的--http 127.0.0.1:8000指的是让uWSGI自己接受HTTP请求并自己负责路由这些请求。我们需要把uWSGI作为socket启动(在.ini文件设置中并没有设置--http参数),因为我们要使用NGINX作为我们的web服务器,NGINX通过刚才设置的文件套接字与uWSGI进行通信。

关于uWSGI的详细设置可以看 https://uwsgi-docs.readthedocs.io/en/latest/Options.html

现在可以通过使用配置文件来启动uWSGI(先关闭原来运行的uWSGI服务):

uwsgi --ini config/uwsgi.ini

这样运行之后,可以发现暂时无法通过浏览器访问http://127.0.0.1:8000/,因为此时uWSGI监听文件套接字而不是HTTP端口,我们还需要继续完善生产环境配置。

1.7配置uWSGI

当启动一个Web服务的时候,很显然必须提供动态的内容服务,但也需要静态的文件服务,比如CSS,JavaScript文件,图像等。如果用uWSGI来管理静态文件,会为HTTP请求增加不必要的开销,所以最好在uWSGI之前加一个Web服务,比如NGINX。

NGINX是一个高并发,低内存占用的Web服务端,也具有反向代理功能,即接受一个HTTP请求,然后把这个请求路由给不同的后端。通常来说,你需要一个web服务端如NGINX,用于快速高效的提供静态文件,然后把动态的请求转发给uWSGI。通过使用NGINX,还可以设置其反向代理功能从而更好的提供web服务。

安装NGINX可以使用下列命令:

sudo apt-get install nginx

如果使用MacOS X,可以通过brew install nginx来安装。Windows下的NGINX可以通过https://nginx.org/en/download.html下载。

译者注:安装NGINX后不会立刻启动,译者使用的Centos 7.5 1804还需要启动NGINX服务和开机启动:

systemctl start nginx.service
systemctl enable nginx.service

正常情况下在启动NGINX之后,直接访问本机IP地址,可以看到NGINX欢迎页面,表示基础配置成功运行,之后可以先停用NGINX服务,以配置生产环境。

1.8生产环境

下面的图表示了我们最终配置的生产环境的结构:

当一个浏览器发起一个HTTP请求的时候,发生如下事情:

  1. NGINX接收HTTP请求
  2. 如果请求静态文件,NGINX直接提供服务。如果请求动态页面,NGINX通过SOCKET与uWSGI通信,将请求转交给uWSGI处理
  3. uWSGI将请求转交给Django后端进行处理,返回的响应被传递给NGINX,NGINX再发回给浏览器。

1.9配置NGINX

config/目录下创建nginx.conf文件,在其中添加如下代码:

# the upstream component nginx needs to connect to
upstream educa {
    server unix:///tmp/educa.sock;
}
server {
    listen 80;
    server_name www.educaproject.com educaproject.com;
    location / {
        include /etc/nginx/uwsgi_params;
        uwsgi_pass educa;
    }
}

这是NGINX的基础配置。我们建立了一个upstream名叫educa,指定了uWSGI使用的socket名称 ,然后使用server指令,其中的设置有:

NGINX还有很多复杂的设置,文档可以参考https://nginx.org/en/docs/

NGINX主要的设置文件位于/etc/nginx/nginx.conf,该文件包含/etc/nginx/sites-enabled/下的所有配置文件。为了让NGINX使用我们刚才编写的配置文件,打开系统命令行窗口建立一个软连接:

sudo ln -s /home/projects/educa/config/nginx.conf /etc/nginx/sites-enabled/educa.conf

将其中的/home/projects/educa/替换成实际的绝对路径。注意,这里如果没有/sites-enabled/目录,要先手工建立。

如果还没有运行uWSGI,打开系统命令行窗口,在educa项目根目录先运行uWSGI:

uwsgi --ini config/uwsgi.ini

当前窗口会被uWSGI占用,再开一个命令行窗口,然后执行:

service nginx start

由于我们使用了自定义的域名,还必须修改/etc/hosts,添加如下两行:

127.0.0.1 educaproject.com
127.0.0.1 www.educaproject.com

这样我们就把这两个域名都路由到本地回环地址上,由于我们是从本机访问本机,所以要更改HOSTS,实际生产环境不必做本步修改,因为生产环境会有固定的IP,域名和对应的DNS解析。

打开浏览器,输入http://educaproject.com/,应该可以看到站点了,但是所有的静态文件依然没有被加载,没关系,即将完成生产环境的配置。

如果系统是Centos 7,这里显示502错误,查看/var/log/nginx/error.log,如果其中的错误是[crit] 4036#4036: *1 connect() to unix:///tmp/educa.sock failed (13: Permission denied),就先执行/usr/sbin/sestatus查看SELINUX的状态,如果为开启,就编辑SELINUX的设置,将其关闭,如下:

vi /etc/selinux/config

#SELINUX=enforcing
SELINUX=disabled

之后reboot重启系统才行。之后应该就可以正常显示站点了。

之后为了安全起见,到settings/pro.py中,修改ALLOWED_HOSTS设置为NGINX配置文件中的两个域名:

ALLOWED_HOSTS = ['educaproject.com', 'www.educaproject.com']

现在Django就只为这两个主机名提供服务了。关于ALLOWED_HOSTS的更多信息可以看https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts

1.10让NGINX提供静态文件和媒体资源服务

NGINX提供静态文件的速度很快。刚才我们把所有的地址转发,都交给了uWSGI,现在要将所有的静态文件通过NGINX提供服务,对于我们站点来说,就是把所有的CSS JS文件和用户上传的媒体文件都交给NGINX来代理。

编辑settings/base.py,增加下边一行:

STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

这行表示存放站点静态文件的地址,还记得之前学习过使用python manage.py collectstatic吗?现在就需要将所有的静态文件收集过来放在此目录中,在命令行中输入:

python manage.py collectstatic --settings=educa.settings.pro

注意,原书的命令缺少了 --settings=educa.settings.pro

可以看到下列输出:

160 static files copied to '/educa/static'.

静态文件目录设置好了,现在需要将这个目录设置到NGINX中,编辑config/nginx.conf,在server指令后的大括号中增加下列内容:

location /static/ {
    alias /home/projects/educa/static/;
}
location /media/ {
    alias /home/projects/educa/media/;
}

将其中的/home/projects/educa/static//home/projects/educa/media/替换成你项目的实际staticmedia目录的绝对路径。这两个参数解释如下:

重新启动NGINX服务,以便让配置文件生效:

service nginx reload

在浏览器中打开http://educaproject.com/,现在可以看到整个站点包含静态资源都正确的显示了。对于站点的静态文件请求,NGINX将绕开uWSGI,把文件直接返回给浏览器。

现在生产环境就初步配置完毕。整个站点现在可以说运行在生产环境之下了。

1.11使用SSL安全连接

在配置完初步的生产环境之后,下一个话题是站点的安全性。Secure Sockets Layer现在逐渐成为提供Web安全连接服务的规范。强烈建议对于正式的网站使用HTTPS协议,现在就在NGINX中配置SSL认证来让站点变得更加安全。

1.11.1创建一个SSL认证

educa项目根目录下建立一个ssl目录,然后通过openssl生成我们的SSL证书:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl/educa.key -out ssl/educa.crt

用这条命令生成一个365天有效的2048位的SSL证书,然后系统会提示输入一些信息:

Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []: educaproject.com
Email Address []: email@domain.com

这其中最关键的是Common Name,必须将主机域名名称输入:这里使用educaproject.com

之后会在ssl/目录下生成两个文件,educa.key是私钥,educa.crt是实际的SSL证书。

1.11.2配置NGINX使用SSL

编辑config/nginx.conf,在server设置中加入下列内容:

server {
listen 80;
listen 443 ssl;
ssl_certificate /home/projects/educa/ssl/educa.crt;
ssl_certificate_key /home/projects/educa/ssl/educa.key;
server_name www.educaproject.com educaproject.com;
# ...
}

将其中的路径都修改为SSL证书所在的实际绝对路径。

这么设置之后,NGINX将同时监听80端口(HTTP协议)和443端口(HTTPS协议),然后指定了SSL的验证信息ssl_certificate与对应的密钥ssl_certificate_key

现在重新启动NGINX服务,访问 https://educaproject.com/ ,会看到类似如下提示:

这个提示因浏览器而异。意思是警告当前站点并没有使用一个值得信任的验证方式,浏览器无法确定该站点安全与否。这是因为我们使用的SSL证书是由我们自行签发的,而不是从一个受信任的机构(Certification Authority)获得的证书。当我们有了实际的公开域名之后,就可以向一个受信任的证书颁发机构申请一个SSL证书,这样浏览器就能识别该站点的HTTPS认证。

如果想为实际的站点申请证书,可以使用Linux基金会Linux Foundation的Let's Encrypt项目。这是一个致力于免费获得和更新SSL证书的计划,该计划的站点在 https://letsencrypt.org/

点击 "Add Exception" 按钮可以让浏览器知道可以信任该站点,这时浏览器的显示可能如下:

点击小锁按钮,就可以看到SSL的详细信息。

译者注:这里也因浏览器而异,有的浏览器依旧会提示证书不可信或者存在问题,毕竟这个证书是我们自行签发的。

1.11.3配置Django使用SSL

Django也有针对SSL的配置,编辑settings/pro.py,增加下边的代码:

SECURE_SSL_REDIRECT = True
CSRF_COOKIE_SECURE = True

这两个设置的含义如下:

现在我们就配置好了一个高效的提供Web服务的生产环境。

2自定义中间件

在之前我们已经了解了中间件MIDDLEWARE的设置,该设置包含项目中所有使用到的中间件。关于中间件,可以认为其是一个底层的插件系统,为在请求/响应的过程中提供钩子。每一个中间件都负责一个特定的行为,会在HTTP请求和响应的过程中得到执行。

注意不要添加开销非常大的中间件,因为中间件会在项目的所有请求和响应的过程中被执行。

当一个HTTP请求进来的时候,中间件会按照其在MIDDLEWARE设置中从上到下的顺序执行,当HTTP响应被生成且发送的过程中,中间件会按照设置中从下到上的顺序执行。

一个符合标准的函数可以作为一个中间件被注册在settings.py中。类似下边的函数就可以作为一个中间件:

def my_middleware(get_response):
    def middleware(request):
        # 对于每个HTTP请求,在视图和之后的中间件执行之前执行的代码
        response = get_response(request)
        # 对于每个HTTP请求和响应,在视图执行之后执行的代码
        return response
    return middleware

一个中间件工厂函数接受一个get_response可调用对象,然后返回一个中间件函数。一个中间件接受一个请求然后返回一个响应,类似于视图。这里的get_response可以是下一个中间件,如果自己就是中间件列表中的最后一个,也可以是一个视图名称。

如果任何一个中间件在尚未调用get_response这个可调用对象之前就返回了一个响应,这个时候就会短路整个中间件链条的处理:其后的中间件不再被执行,这个响应开始从同级的中间件向上返回。

所以MIDDLEWARE设置中的中间件顺序非常重要,因为中间件依赖于上下中间件的数据进行工作。

在向MIDDLEWARE中添加一个中间件时必须注意将其放置在正确的位置,反复强调,中间件在HTTP请求进来的时候从上到下执行,HTTP响应发出的时候从下到上执行。

原书在这里只是比较简单的说了一下执行顺序,详细的中间件执行顺序请参考Django进阶-中间件以及https://docs.djangoproject.com/en/2.0/topics/http/middleware/

2.1创建二级域名中间件

我们来创建一个自定义中间件,用于通过一个自定义的二级域名来访问课程资源。例如:某个显示课程的URL:https://educaproject.com/course/django/,可以通过一个二级域名django.educaproject.com来访问。这样用户就可以使用二级域名作为快捷方式快速访问课程,也比较容易记忆该路径。所有发往这个二级域名的请求,都会被重定向到实际的educaproject.com/course/django/这个URL。

与视图,模型,表单等组件一样,中间件也可以写在项目的任何位置。推荐在应用目录内建立middleware.py文件来编写中间件。

courses应用目录内创建middleware.py文件,并编写如下代码:

from django.urls import reverse
from django.shortcuts import get_object_or_404, redirect
from .models import Course

def subdomain_course_middleware(get_response):
    """
    为课程提供二级域名
    """

    def middleware(request):
        host_parts = request.get_host().split('.')
        if len(host_parts) > 2 and host_parts[0] != 'www':
            # 通过指定的二级域名查询课程对象
            course = get_object_or_404(Course, slug=host_parts[0])
            course_url = reverse('course_detail', args=[course.slug])
            # 将二级域名请求重定向至实际的URL
            url = '{}://{}{}'.format(request.scheme, '.'.join(host_parts[1:]), course_url)
            return redirect(url)
        response = get_response(request)
        return response
    return middleware

当一个HTTP请求进来的时候,这个中间件执行如下任务:

  1. 取得这个HTTP请求中的域名,然后将其分割成几部分;例如mycourse.educaproject.com会被分割得到一个列表['mycourse', 'educaproject', 'com']
  2. 检查这个域名是否包含二级域名,判断分割后的域名是否包含多于2个元素。如果包含,就取出第一个元素也就是二级域名,如果这个域名不是www,那就通过根据slug查询并取得该课程对象。
  3. 如果找不到对应的课程,就返回404错误;如果找到了,就重定向到课程对象对应的规范化URL。

编辑settings/base.py,把自定义中间件添加到MIDDLEWARE设置中:

MIDDLEWARE = [
    # ......
    'courses.middleware.subdomain_course_middleware',
]

还需要看一下ALLOWED_HOSTS中的域名设置,这里我们将其设置为可以是任何eduproject.com的二级域名:

ALLOWED_HOSTS = ['.educaproject.com']

ALLOWED_HOSTS中以一个.开始的域名,例如.educaproject.com,会匹配educaproject.com及所有的educaproject.com的二级域名,比如course.educaproject.comdjango.educaproject.com

2.3配置NGINX的二级域名

编辑config/nginx.conf,将以下这行:

server_name www.educaproject.com educaproject.com;

修改成:

server_name *.educaproject.com educaproject.com;

通过增加通配符设置,让NGINX也可以代理所有的二级域名,为了测试中间件,还必须在etc/hosts中配置相关内容,比如如果要测试二级域名django.educaproject.com,需要增加一行:

127.0.0.1 django.educaproject.com

然后启动站点到https://django.educaproject.com/,可以发现中间件现在将其重定向到 https://educaproject.com/course/django/

3实现自定义的管理命令

Django允许应用向manage.py管理工具中注册自定义的管理命令。所谓管理命令就是通过manage.py使用的指令,例如,我们曾经使用在第9章使用过makemessagescompilemessages命令。

一个管理命令由一个Python模块组成,这个模块里包含一个Command类,这个Command类继承django.core.management.base.BaseCommand或者BaseCommand的子类。我们可以创建一个简单的包含参数和选项的自定义命令。

对于每个在INSTALLED_APPS内注册的应用,Django会在应用目录下边的management/commands/目录下搜索管理命令,搜索到的每个命令模块,都会被注册成为一个同名的命令。

更多自定义管理命令的信息可以查看https://docs.djangoproject.com/en/2.0/howto/custom-management-commands/

我们准备来创建一个提醒学生至少选一个课程的命令。这个命令会向所有已经注册超过一定时间,但还没有选任何一门课程的学生发送一封邮件。

student应用下建立如下的目录和文件结构:

management/
    __init__.py
    commands/
        __init__.py
        enroll_reminder.py

编辑enroll_reminder.py,添加下列代码:

import datetime
from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.mail import send_mass_mail
from django.contrib.auth.models import User
from django.db.models import Count

class Command(BaseCommand):
    help = 'Sends an e-mail reminder to users registered more than N days that are not enrolled into any courses yet'

def add_arguments(self, parser):
    parser.add_argument('--days', dest='days', type=int)

def handle(self, *args, **options):
    emails = []
    subject = 'Enroll in a course'
    date_joined = datetime.date.today() - datetime.timedelta(days=options['days'])
    users = User.objects.annotate(course_count=Count('courses_joined')).filter(course_count=0,
                                                                               date_joined__lte=date_joined)
    for user in users:
        message = "Dear {},\n\n We noticed that you didn't enroll in any courses yet. What are you waiting for?".format(
            user.first_name)
        emails.append((subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]))
    send_mass_mail(emails)
    self.stdout.write('Sent {} reminders'.format(len(emails)))

这是enroll_reminder命令,解释如下:

编写好上述代码后,打开系统命令行来运行命令:

python manage.py enroll_reminder --days=20

如果还没有配置SMTP服务器,可以参考第二章中的内容。如果确实没有SMTP服务器,可以在settings.py中加上:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

以让Django将邮件内容显示在控制台而不实际发送邮件。

还可以通过系统让这个命令每天早上8点运行,如果使用了基于UNIX的操作系统,可以打开系统命令行模式,输入crontab -e来编辑crontab,在其中增加下边这行:

0 8 * * * python /path/to/educa/manage.py enroll_reminder --days=20 --settings=educa.settings.pro

将其中的/path/to/educa/manage.py替换成实际的manage.py所在的绝对路径。如果不熟悉cron的使用,可以参考http://www.unixgeeks.org/security/newbie/unix/cron-1.html

如果使用的是Windows,可以使用系统的计划任务功能,具体可以参考https://docs.microsoft.com/zh-cn/windows/desktop/TaskSchd/task-scheduler-start-page

还有一个方法是使用Celery定期执行任务。我们在第7章使用过Celery,可以使用Celery beat scheduler来建立定期执行的异步任务,具体可以参考https://celery.readthedocs.io/en/latest/userguide/periodic-tasks.html

对于想通过cron或者Windows的计划任务执行的单独脚本,都可以通过自定义管理命令的方式来进行。

Django还提供了一个使用Python执行管理命令的方法,可以通过Python代码来运行管理命令,例如:

from django.core import management
management.call_command('enroll_reminder', days=20)

程序在执行到这里的时候,就会去运行这个命令。现在我们就可以为自己的应用定制管理命令并且计划运行了。

总结

这一章里使用uWSGI和NGINX配置完成了生产环境,还实现了自定义中间件和管理命令。

到这里本书已经结束。祝贺你,本书通过创建实际的项目和将其他软件与Django集成的方式,指引你学习使用Django建立Web应用所需的技能。无论一个简单的项目原型还是大型的Web应用,你现在都具备使用Django创建它们的能力。

祝你未来的Django之旅愉快!

其他感兴趣的书

如果你发现本书很有用,你可能还会对下列书籍感兴趣。

Python Programming Blueprints

Django RESTful Web Services