加载中

This is the eleventh article in the series in which I document my experience writing web applications in Python using the Flask microframework.

The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to callmicroblog.

Here is an index of all the articles in the series that have been published to date:

这篇文章是我记录我使用Python的微型框架Flask来编写Web应用的系列文章的第十一篇.

这系列教程的目的是开发一个功能像样的微博应用,并展示所有并非独创的,我决定称之为微博。

这里是系列教程的目录,迄今为止已经发布的如下:

Recap

In the most recent installments of this tutorial we've been looking at improvements that mostly had to do with our database.

Today we are letting our database rest for a bit, and instead we'll look at another important function that most web applications have: the ability to send emails to its users.

In our littlemicroblogapplication we are going to implement one email related function, we will send an email to a user each time he/she gets a new follower. There are several more ways in which email support can be useful, so we'll make sure we design a generic framework for sending emails that can be reused.

简述

在大多数此类教程中都会不遗余力的介绍如何使用数据库。今天我们对数据库暂且不表,而是来关注另一个在web应用中很重要的特性:如何推送邮件给用户。

在某个轻量级应用中我们可能会添加一个如下的邮件服务功能:当用户有了新的粉丝后,我们发送一封邮件通知用户。有很多方法可以实现这个特性,而我们希望提供出一种可复用的通用框架来处理。

Introducing Flask-Mail

Luckily for us, Flask already has an extension that handles email, and while it will not take us 100% of the way, it gets pretty close.

Installing Flask-Mail into our virtual environment is very simple. Users on anything other than Windows do it as follows:

flask/bin/pip install flask-mail

Windows users do it a little different, due to the fact that one of the supporting modules used by Flask-Mail does not work in that OS. On Windows you do this instead:

flask\Scripts\pip install --no-deps lamson chardet flask-mail

Flask-Mail介绍

对于我们来说是幸运的,现在已经有很多外部插件来处理邮件,虽说不能百分百按照我们的想法去处理,但已经相当接近了。  

在虚拟环境中安装 Flask-Mail是相当简单的。Windows以外的用户可以利用以下命令来安装:

flask/bin/pip install flask-mail

Windows用户的安装稍有不同,因为Flask-Mail所使用的一些模块不能再Windows系统上运行,你可以使用以下命令: 

flask\Scripts\pip install --no-deps lamson chardet flask-mail

Configuration

Back when we looked at unit testing, we added configuration for Flask to send us an email should an error occur in the production version of our application. It turns out that same information is used for sending application related emails.

Just as a reminder, what we need is two pieces of information:

  • the email server that will be used to send the emails
  • the email address(es) of the admins

This is what we did in the previous article (fileconfig.py):

# email server
MAIL_SERVER = 'your.mailserver.com'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_USERNAME = 'you'
MAIL_PASSWORD = 'your-password'

# administrator list
ADMINS = ['you@example.com']

It goes without saying that you have enter the details of an actual email server and administrator above before the application can actually send emails. For example, if you want the application to send emails via your gmail account you would enter the following:

# email server
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'your-gmail-username'
MAIL_PASSWORD = 'your-gmail-password'

# administrator list
ADMINS = ['your-gmail-username@gmail.com']

We also need to initialize aMailobject, as this will be the object that will connect to the SMTP server and send the emails for us (fileapp/__init__.py):

from flask.ext.mail import Mail
mail = Mail(app)

配置:

回想一下前文中单元测试部分的案例,我们通过添加配置支持了一个这样的功能:当应用的某个版本测试出错时可以邮件通知我们。从这个例子就可以看出如何配置使用邮件支持。

再次提醒大家,我们需要设置两个方面的内容:

  • 邮件服务器信息
  • 用户邮箱地址
如下正是前文中所用到的配置

# email server
MAIL_SERVER = 'your.mailserver.com'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_USERNAME = 'you'
MAIL_PASSWORD = 'your-password'

# administrator list
ADMINS = ['you@example.com']
其中并没有设置切实可用的邮件服务器和邮箱。现在我们通过一个例子来看如何使用gmail邮箱账户来发送邮件:

# email server
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'your-gmail-username'
MAIL_PASSWORD = 'your-gmail-password'

# administrator list
ADMINS = ['your-gmail-username@gmail.com']
另外我们也可以初始化一个Mail对象来连接SMTP邮件服务器,发送邮件:
from flask.ext.mail import Mail
mail = Mail(app)
Let's send an email!

To learn how Flask-Mail works we'll just send an email from the command line. So let's fire up Python from our virtual environment and run the following:

>>> from flask.ext.mail import Message
>>> from app import mail
>>> from config import ADMINS
>>> msg = Message('test subject', sender = ADMINS[0], recipients = ADMINS)
>>> msg.body = 'text body'
>>> msg.html = '<b>HTML</b> body'
>>> mail.send(msg)

The snippet of code above will send an email to the list of admins that are configured inconfig.py. The sender will be the first admin in the list. The email will have text and HTML versions, so depending on how your email client is setup you may see one or the other.

Pretty, neat. Now it's time to integrate this code into our application!

发个邮件试试!

为了了解flask-mail如何工作的,我们可以从命令行发一封邮件看看。进入python shell并执行如下的脚本:

>>> from flask.ext.mail import Message
>>> from app import mail
>>> from config import ADMINS
>>> msg = Message('test subject', sender = ADMINS[0], recipients = ADMINS)
>>> msg.body = 'text body'
>>> msg.html = '<b>HTML</b> body'
>>> mail.send(msg)

上面这段代码会根据inconfig.py中配置的邮箱地址列表,以首个邮箱作为发件人给所有邮箱发送一封邮件。邮件内容会以文本和html两种格式呈现,而你能看到哪种格式取决于你的邮件客户端。

多么简单小巧!你完全可以现在就把它集成到你的应用中。


A simple email framework

We will now write a helper function that sends an email. This is just a generic version of the above test. We'll put this function in a new source file that will be dedicated to our email support functions (fileapp/emails.py):

from flask.ext.mail import Message
from app import mail

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender, recipients)
    msg.body = text_body
    msg.html = html_body
    mail.send(msg)

Note that Flask-Mail support goes beyond what we are using. Bcc lists and attachments are available, for example, but we won't use them in this application.

邮件框架

我们现在可以编写一个帮助函数来发送邮件。这是以上测试中一个通用版的测试。我们把这个函数放进一个新的原文件中用作邮件支持(fileapp/emails.py):

from flask.ext.mail import Message
from app import mail

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender, recipients)
    msg.body = text_body
    msg.html = html_body
    mail.send(msg)

Flask-Mail的邮件支持超出了我们目前的使用范围,像密件抄送和附件的功能并不会在此应用中得以使用。

Follower notifications

Now that we have the basic framework to send an email in place, we can write the function that sends out the follower notification (fileapp/emails.py):

from flask import render_template
from config import ADMINS

def follower_notification(followed, follower):
    send_email("[microblog] %s is now following you!" % follower.nickname,
        ADMINS[0],
        [followed.email],
        render_template("follower_email.txt", 
            user = followed, follower = follower),
        render_template("follower_email.html", 
            user = followed, follower = follower))

Do you find any surprises in here? Our old friend therender_templatefunction is making an appearance. If you recall, we used this function to render all the HTML templates from our views. Like the HTML from our views, the bodies of email messages are an ideal candidate for using templates. As much as possible we want to keep logic separate from presentation, so emails will also go into thetemplatesfolder along with our views.

Follower 提醒

现在,我们已经有了发邮件的基本框架,我们可以写发送follower提醒的函数了 (fileapp/emails.py):

from flask import render_template
from config import ADMINS

def follower_notification(followed, follower):
    send_email("[microblog] %s is now following you!" % follower.nickname,
        ADMINS[0],
        [followed.email],
        render_template("follower_email.txt", 
            user = followed, follower = follower),
        render_template("follower_email.html", 
            user = followed, follower = follower))

你在这里找到任何惊喜了吗?我们的老朋友render_template函数有一次出现了。 

如果你还记得,我们使用这个函数在views渲染模版. 就像在views里写html不好一样,使用邮件模版是理想的选择。我们要可能的将逻辑和表现分开,所以email模版也会和其它试图模版一起放到在模版文件夹里.

So we now need to write the templates for the text and HTML versions of our follower notification email. Here is the text version (fileapp/templates/follower_email.txt):

Dear {{user.nickname}},

{{follower.nickname}} is now a follower. Click on the following link to visit {{follower.nickname}}'s profile page:

{{url_for("user", nickname = follower.nickname, _external = True)}}

Regards,

The microblog admin

For the HTML version we can do a little bit better and even show the follower's avatar and profile information (fileapp/templates/follower_email.html):

<p>Dear {{user.nickname}},</p>
<p><a href="{{url_for("user", nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a> is now a follower.</p>
<table>
    <tr valign="top">
        <td><img src="{{follower.avatar(50)}}"></td>
        <td>
            <a href="{{url_for('user', nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a><br />
            {{follower.about_me}}
        </td>
    </tr>
</table>
<p>Regards,</p>
<p>The <code>microblog</code> admin</p>

Note the_external = Trueargument tourl_forin the above templates. By default, theurl_forfunction generates URLs that are relative to the domain from which the current page comes from. For example, the return value fromurl_for("index")will be/index, while in this case we wanthttp://localhost:5000/index. In an email there is no domain context, so we have to force fully qualified URLs that include the domain, and the_externalargument is just for that.

所以,我们需要为follower提醒邮件写纯文本和网页版的邮件模版,下面这个是纯文本的版本 (fileapp/templates/follower_email.txt):

Dear {{user.nickname}},

{{follower.nickname}} is now a follower. Click on the following link to visit {{follower.nickname}}'s profile page:

{{url_for("user", nickname = follower.nickname, _external = True)}}

Regards,

The microblog admin

下面这个是网页版的邮件,效果会更好(fileapp/templates/follower_email.html):

<p>Dear {{user.nickname}},</p>
<p><a href="{{url_for("user", nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a> is now a follower.</p>
<table>
    <tr valign="top">
        <td><img src="{{follower.avatar(50)}}"></td>
        <td>
            <a href="{{url_for('user', nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a><br />
            {{follower.about_me}}
        </td>
    </tr>
</table>
<p>Regards,</p>
<p>The <code>microblog</code> admin</p>

注解:模版中的url_for函数的 _external = True 参数的意义.默认情况下,url_for 函数生成url是相对我们的域名的。例如,url_for("index")函数返回值是/index, 但是,发邮件是我们想要http://localhost:5000/index这种url,email中是没有域上下文的,所以,我们必须强制生成带域名的url,_external argument就是干这个工作的。

The final step is to hook up the sending of the email with the actual view function that processes the "follow" (fileapp/views.py):

from emails import follower_notification

@app.route('/follow/<nickname>')
@login_required
def follow(nickname):
    user = User.query.filter_by(nickname = nickname).first()
    # ...
    follower_notification(user, g.user)
    return redirect(url_for('user', nickname = nickname))

Now you can create two users (if you haven't yet) and make one follow the other to see how the email notification works.

最后一步是处理“follow”过程,即触发邮件提醒时的视图函数,(fileapp/views.py):

from emails import follower_notification

@app.route('/follow/<nickname>')
@login_required
def follow(nickname):
    user = User.query.filter_by(nickname = nickname).first()
    # ...
    follower_notification(user, g.user)
    return redirect(url_for('user', nickname = nickname))

现在你可以创建两个用户(如果还没有用户的话)尝试着用让一个用户follow另一个用户,理解邮件提醒是怎样工作的。 

So that's it? Are we done?

We could now pat ourselves in the back for a job well done and take email notifications out of our list of features yet to implement.

But if you played with the application for some time and payed attention you may have noticed that now that we have email notifications when you click thefollowlink it takes 2 to 3 seconds for the browser to refresh the page, whereas before it was almost instantaneous.

就是这样吗?我们做完了吗?

我们可能心底里很兴奋完成了这项工作并且把邮件提醒功能同未完成列表里删除。

但是,如果你现在测试下应用,你会发现当你单击follow链接的时候,页面会2到3秒才会响应,浏览器才会刷新,这在之前是没有的。 

返回顶部
顶部