Django Web 开发学习手册(二)

原文:zh.annas-archive.org/md5/C7E16835D8AC71A567CF7E772213E9F7

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:引入标签

标签是 Web 2.0 应用程序中最突出的功能之一。标签是与信息(如文章、图片或链接)相关联的关键词。标记是将标签分配给内容的过程。通常由作者或用户完成,并允许用户定义的内容分类。

我们还将在我们的项目中使用标签,并将其称为hashtags。标签在 Web 应用程序中变得非常流行,因为它们使用户能够轻松分类、查看和共享内容。如果您不熟悉标签,可以通过访问 Twitter、Facebook 或 Google Plus 等社交网站的示例来了解。在这些网站上,标签被固定到每个状态或对话中,以帮助我们找到热门话题。由于我们将构建一个微型博客网站,标签将帮助我们对用户之间的对话进行分类。

为了将标签引入我们的系统,我们需要一种机制,使用户能够将带有标签的 tweet 提交到数据库中。稍后,我们还需要一种浏览特定标签下分类的 tweet 的方法。

在本章中,您将学习以下内容:

  • 设计标签数据模型

  • 构建一个从 tweet 表单中分离出标签的算法

  • 为特定标签下的 tweet 列表创建页面

  • 构建标签云

标签数据模型

标签需要存储在数据库中并与 tweet 关联。因此,引入标签到我们的项目的第一步是为标签创建一个数据模型。一个标签对象只会保存一个数据,一个代表标签的字符串。此外,我们需要维护与特定 tweet 关联的标签列表。

你可能还记得第四章中,构建类似 Twitter 的应用程序,我们使用外键将 tweet 与用户关联起来,并将其称为一对多关系。然而,标签和 tweet 之间的关系不是一对多,因为一个标签可以与多个 tweet 关联,一个 tweet 也可以与多个标签关联。这被称为多对多关系,并且在 Django 模型中使用models.ManyToManyField参数表示。

到目前为止,您应该很清楚数据模型放在mytweet | models.py文件中。因此,打开文件并将以下HashTag类添加到其中:

class HashTag(models.Model):
  """
  HashTag model
  """
  name = models.CharField(max_length=64, unique=True)
  tweet = models.ManyToManyField(Tweet)
  def __unicode__(self):
    return self.name

相当简单,不是吗?我们只是为标签定义了一个数据模型。该模型在其ManyToManyField参数中保存了标签名称及其 tweet。当您完成输入代码后,不要忘记运行以下命令,以便在数据库中为模型创建一个表:

$ python manage.py syncdb

输出:

 Creating tables ...
 Creating table tweet_hashtag_tweet
 Creating table tweet_hashtag
 Installing custom SQL ...
 Installing indexes ...
 Installed 0 object(s) from 0 fixture(s)

现在,要查看 Django 如何创建和实现所有关系的详细 SQL 查询,以及如何为它们创建表,我们只需使用manage.py中的模型名称发出sql命令。它将显示要运行以创建对象实例的 SQL 查询。熟悉 SQL 的人都知道,多对多关系通常是通过创建连接两个相关表的第三个表来实现的。现在,让我们看看 Django 如何实现这种类型的关系。在终端中,发出以下命令:

$ python manage.py sql tweet

输出:

 BEGIN;
 CREATE TABLE "tweet_tweet" (
 "id" integer NOT NULL PRIMARY KEY,
 "user_id" integer NOT NULL REFERENCES "user_profile_user" ("id"),
 "text" varchar(160) NOT NULL,
 "created_date" datetime NOT NULL,
 "country" varchar(30) NOT NULL,
 "is_active" bool NOT NULL
 )
 ;
 CREATE TABLE "tweet_hashtag_tweet" (
 "id" integer NOT NULL PRIMARY KEY,
 "hashtag_id" integer NOT NULL,
 "tweet_id" integer NOT NULL REFERENCES "tweet_tweet" ("id"),
 UNIQUE ("hashtag_id", "tweet_id")
 )
 ;
 CREATE TABLE "tweet_hashtag" (
 "id" integer NOT NULL PRIMARY KEY,
 "name" varchar(64) NOT NULL UNIQUE
 )
 ;
 COMMIT;

输出可能会因您的数据库引擎而略有不同。事实上,Django 会自动创建一个名为tweet_hashtag_tweet的额外表来维护多对多关系。

在 Django 的模型 API 中定义多对多关系时,值得注意的是,models.ManyToMany字段可以放置在两个相关模型中的任何一个。我们本可以将这个字段放在 tweet 模型中而不是 hashtag;因为我们后来创建了 hashtag 模型,所以我们把models.ManyToMany字段放在了它里面。

为了测试目的,我们将转到管理面板并创建带有标签的推文,就像我们为用户和推文创建一样。但首先,我们需要在admin.py文件中为管理面板注册标签。

修改后的admin.py文件将如下所示:

  from django.contrib import admin
  from models import Tweet,Hashtag
  # Register your models here.
  admin.site.register(Tweet)
  admin.site.register(HashTag)

现在我们可以转到/administration URL 的管理面板。

在为推文创建标签之前,我们需要创建一个带有标签的推文。稍后,我们将编写一个程序,它将解析推文并自动创建与之关联的标签实例。

参考我们在第四章中展示的创建推文的演示图,并创建一条带有以下文本的推文:

Hello, #Django! you are awesome.

使用我们之前使用的相同用户ratancs,然后转到标签模型并创建标签#Django并将其与我们创建的推文关联起来。这将让你了解我们如何将标签分配给推文。

让我们创建一个合适的推文提交表单,它将要求用户将推文写入输入框。它将创建与推文相关的所有标签,并保存推文。

查看我们创建的用户个人资料页面。在页面的顶部中央,将有一个已与用户关联的输入框;因此,当他写一条推文并点击提交按钮时,推文将被保存与他的 ID 关联。

现在,访问这个 URL:http://localhost:8000/user/ratancs/。你会看到我们之前创建的推文。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00299.jpeg

我们将返回到profile.html代码,并追加一个文本区域和一个提交按钮,为用户发布一条推文。设计将与我们选择显示推文的方式相同,也就是说,我们将使用Twitter bootstrap的相同的 well box。

我们的profile.html文件模板如下:

  {% extends "base.html" %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-12 column">
      {% for tweet in tweets %}
      <div class="well">
        <span>{{ tweet.text }}</span>
      </div>
      {% endfor %}
    </div>
  </div>
  {% endblock %}

这个{%for ...}块用于表示多条推文,每条推文都在下面,因为它们有div标签。

现在我们将在{% for ...}块的上方创建一个div标签,并添加我们的推文提交表单。

在我们编写表单之前,让我们了解一下 Django 表单以及它们的使用方法。

Django 表单

创建、验证和处理表单是一项非常常见的任务。Web 应用程序通过 Web 表单接收输入并收集用户数据。因此,自然地,Django 自带了处理这些任务的库。你所要做的就是导入这个库并开始编写你的表单:

from django import forms

Django 表单库处理三个常见任务:

  • HTML 表单生成

  • 用户输入的服务器端验证

  • 在输入错误的情况下重新显示 HTML 表单

这个库的工作方式类似于 Django 的数据模型的工作方式。你首先定义一个代表你的表单的类。这个类必须派生自forms.Form基类。这个类中的属性代表表单字段。forms包提供了许多字段类型。

当你从派生自forms.Form基类的类创建对象时,你可以使用各种方法与它交互。有用于生成 HTML 代码的方法,用于访问输入数据的方法,以及用于验证表单的方法。

在下一节中,我们将通过创建一个推文发布表单来学习表单库。

设计推文发布表单

让我们从创建我们的第一个 Django 表单开始。在推文应用程序文件夹中创建一个新文件,命名为forms.py。然后,在代码编辑器中打开文件并输入以下代码:

  from django import forms
  class TweetForm(forms.Form):
    text = forms.CharField(widget=forms.Textarea(attrs={'rows': 1, 'cols': 85}), max_length=160)
    country = forms.CharField(widget=forms.HiddenInput())

在检查了代码之后,你会注意到我们定义这个类的方式类似于我们定义模型类的方式。我们从forms.Form派生了TweetForm类。所有表单类都需要继承自这个类。接下来,我们定义这个表单包含的字段:

  text = forms.CharField(widget=forms.Textarea(attrs={'rows': 1, 'cols': 85}), max_length=160)

表单包含一个文本字段,它将具有文本区域的 HTML 标签,一个用于行和列的附加属性,以及一个输入的最大大小限制,与 tweet 的最大长度相同。

  country = forms.CharField(widget=forms.HiddenInput())

请注意,表单还包含一个名为country的隐藏字段,它将是一个 char 字段。

forms包中有许多字段类型。以下是一些参数,可以传递给任何字段类型的构造函数。一些专门的字段类型除了这些参数之外还可以接受其他参数。

  • label:生成 HTML 代码时字段的标签。

  • required:用户是否必须输入值。默认设置为True。要更改它,将required=False传递给构造函数。

  • widget:这个参数让你控制字段在 HTML 中的呈现方式。我们刚刚用它来使密码的CharField参数成为密码输入字段。

  • help_text:在表单呈现时,字段的描述将被显示。

以下是常用字段类型的表格:

| 字段类型:描述 |
| — | — |
| CharField:返回一个字符串。 |
| IntegerField:返回一个整数。 |
| DateField:返回 Python 的datetime.date对象。 |
| DateTimeField:返回 Python 的datetime.datetime对象。 |
| EmailField:返回一个有效的电子邮件地址字符串。 |
| URLField:返回一个有效的 URL 字符串。 |

以下是可用的表单小部件的部分列表:

小部件类型描述
PasswordInput:密码文本字段。
HiddenInput:隐藏输入字段。
Textarea:允许在多行上输入文本的文本区域。
FileInput:文件上传字段。

现在,我们需要根据form.py文件修改profile.html文件。更新profile.html文件如下:

  {% extends "base.html" %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-12 column">
      <form method="post" action="post/">{% csrf_token %}
        <div class="col-md-8 col-md-offset-2 fieldWrapper">
          {{ form.text.errors }}
          {{ form.text }}
        </div>
        {{ form.country.as_hidden }}
        <div>
          <input type="submit" value="post">
        </div>
      </form>
    </div>
    <h3>&nbsp;</h3>
    <div class="col-md-12 column">
      {% for tweet in tweets %}
      <div class="well">
        <span>{{ tweet.text }}</span>
      </div>
      {% endfor %}
    </div>
  </div>
  {% endblock %}

通过一个简单的表单实现了发布 tweet,即<form method="post" action="post/">{% csrf_token %}。表单将被提交的方法是"post",发布 tweet 表单的相对 URL 将是post/

  {% csrf_token %}

这段代码生成了 CSRF 令牌,实际上解决了一个安全问题;它保护这个post URL 免受另一个服务器的攻击;关于这一点将在本章的后面部分进行解释。

我们在 tweet <div>之前添加了一个div标签,这个div标签包含一个表单,当单击发布按钮时将保存 tweet。

<div class="col-md-8 col-md-offset-2 fieldWrapper">
  {{ form.text.errors }}
  {{ form.text }}
</div>

fieldWrapper类在div标签中被 Django 的表单库使用,用于呈现我们在表单类中提到的文本的 HTML 标签(即文本区域),随后是表单呈现的任何错误情况。

这将呈现如下截图所示的表单:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00300.jpeg

现在,我们需要做两件事来使这个表单工作:

  1. 我们必须在控制器中定义一个方法,该方法将接受这个表单提交请求,并将 tweet 数据保存到我们的 tweet 模型类对象中。

  2. 我们必须定义一个 URL 模式,以便将该表单提交为 tweet 内容。

为了处理请求,我们将添加一个新的类,该类将接受来自表单的 tweet。我们将把这个类命名为PostTweet。这个类被添加在tweet/view.py中,有一个导入依赖from tweet.forms import TweetForm

  class PostTweet(View):
    """Tweet Post form available on page /user/<username> URL"""
    def post(self, request, username):
      form = TweetForm(self.request.POST)
      if form.is_valid():
        user = User.objects.get(username=username)
        tweet = Tweet(text=form.cleaned_data['text'],
        user=user,
        country=form.cleaned_data['country'])
        tweet.save()
        words = form.cleaned_data['text'].split(" ")
        for word in words:
        if word[0] == "#":
          hashtag, created = HashTag.objects.get_or_create(name=word[1:])
          hashtag.tweet.add(tweet)
        return HttpResponseRedirect('/user/'+username)

我们只需要定义 post 方法,因为我们只需要这个类来接受数据。这里的逻辑非常清楚;如果表单有效,那么数据才会被持久化。重定向总是发生。代码还执行了另一个特殊任务;即从 tweet 中分离出所有的 hashtags。这与分割 tweet 中的所有单词的方式类似,如果单词以#(井号)开头,它将创建该单词的 hashtag(在这里考虑一个正则表达式)。对于第二部分,我们将在我们的urls.py文件中添加一个条目,如下所示:

from django.conf.urls import patterns, include, url
from django.contrib import admin
from tweet.views import Index, Profile, PostTweet

admin.autodiscover()

urlpatterns = patterns('',
  url(r'^$', Index.as_view()),
  url(r'^user/(\w+)/$', Profile.as_view()),
  url(r'^admin/', include(admin.site.urls)),
  url(r'^user/(\w+)/post/$', PostTweet.as_view())
)

如果你仔细看最后一行,我们有:

  url(r'^user/(\w+)/post/$', PostTweet.as_view())

这意味着所有形式为/user/<username>/post的请求将由PostTweet渲染。

通过这个,我们已经制作了一个简单的 Django 表单,用户可以从他的 Twitter 页面发布推文,如下图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00301.jpeg

一旦推文发布,页面将显示所有推文,如下图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00302.jpeg

创建标签页面

接下来,我们将创建一个类似于 Twitter 标签列表的页面。对于这个任务,我们几乎会遵循我们为用户个人资料所遵循的相同架构。让我们从为标签页面添加 URL 条目开始。打开urls.py文件并插入以下条目(最好是在用户页面条目下方,以保持表格有序):

  url(r'^hashTag/(\w+)/$', HashTagCloud.as_view()),

这个正则表达式捕获的部分与用户页面的相同。我们只允许标签中包含字母数字字符。

我们将在控制器中定义hashtag类如下:

  class HashTagCloud(View):
    """Hash Tag  page reachable from /hastag/<hashtag> URL"""
    def get(self, request, hashtag):
      params = dict()
      hashtag = HashTag.objects.get(name=hashtag)
      params["tweets"] = hashtag.tweet
      return render(request, 'hashtag.html', params)

我们将使用的 HTML 模板页面几乎与个人资料页面相同,除了我们用于发布推文的表单部分。

我们需要使用以下代码创建hashtag.html文件:

  {% extends "base.html" %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-12 column">
      {% for tweet in tweets.all %}
      <div class="well">
        <span>{{ tweet.text }}</span>
      </div>
      {% endfor %}
    </div>
  </div>
  {% endblock %}

这将列出所有通过 URL 传递的标签的推文。

总结

我们已经学会了如何设计标签数据模型以及从推文中分离标签所需的算法。然后,我们创建了用于列出特定标签下推文的页面。我们看到了如何构建带有标签的推文的代码片段,就像许多博客网站中看到的标签云一样。

在下一章中,我们将看到如何使用 Django 和 AJAX 增强 UI 体验。

第六章:使用 AJAX 增强用户界面

AJAX的到来是 Web 2.0 历史上的一个重要里程碑。AJAX 是一组技术,使开发人员能够构建交互式、功能丰富的 Web 应用程序。在 AJAX 本身出现之前,这些技术多年前就已经存在。然而,AJAX 的出现代表了 Web 从需要在数据交换时刷新的静态页面向动态、响应迅速和交互式用户界面的转变。

由于我们的项目是一个 Web 2.0 应用程序,它应该更加注重用户体验。我们的应用程序的成功取决于用户在上面发布和分享内容。因此,我们的应用程序的用户界面是我们的主要关注点之一。本章将通过引入 AJAX 功能来改进我们的应用程序界面,使其更加用户友好和交互性。

在本章中,您将学习以下主题:

  • AJAX 及其优势

  • 在 Django 中使用 AJAX

  • 如何使用开源 jQuery 框架

  • 实现推文的搜索

  • 在不加载单独页面的情况下编辑推文

  • 提交推文时自动完成标签

AJAX 及其优势

AJAX 代表异步 JavaScript 和 XML,包括以下技术:

  • 用于结构化和样式信息的 HTML 和 CSS

  • JavaScript 用于动态访问和操作信息

  • 一个由现代浏览器提供的对象,用于在不重新加载当前网页的情况下与服务器交换数据

  • 在客户端和服务器之间传输数据的格式

有时会使用 XML,但它可以是 HTML、纯文本或基于 JavaScript 的格式 JSON。

AJAX 技术使您能够在不重新加载整个页面的情况下在客户端和服务器之间交换数据。通过使用 AJAX,Web 开发人员能够增加 Web 页面的交互性和可用性。

在正确的地方实现 AJAX 时,它提供了以下优势:

  • 更好的用户体验:通过 AJAX,用户可以在不刷新页面的情况下完成很多操作,这使得 Web 应用程序更接近常规桌面应用程序

  • 更好的性能:通过与服务器交换所需的数据,AJAX 节省了带宽并提高了应用程序的速度

有许多使用 AJAX 的 Web 应用程序的例子。谷歌地图和 Gmail 可能是最突出的两个例子。事实上,这两个应用程序在推广 AJAX 的使用方面起到了重要作用,因为它们取得了成功。Gmail 与其他网络邮件服务的区别在于其用户界面,它使用户能够在不等待页面在每个操作后重新加载的情况下交互式地管理他们的电子邮件。这创造了更好的用户体验,使 Gmail 感觉更像是一个响应迅速且功能丰富的应用程序,而不是一个简单的网站。

本章将解释如何在 Django 中使用 AJAX,以使我们的应用程序更具响应性和用户友好性。我们将实现当今 Web 应用程序中发现的三种最常见的 AJAX 功能。但在此之前,我们将了解使用 AJAX 框架的好处,而不是使用原始 JavaScript 函数。

在 Django 中使用 AJAX 框架

由于我们已经在项目中使用了 Bootstrap,因此我们无需为 AJAX 和 jQuery 单独配置它。

使用 AJAX 框架的许多优点:

  • JavaScript 的实现因浏览器而异。一些浏览器提供更完整和功能丰富的实现,而其他浏览器包含不完整或不符合标准的实现。

没有 AJAX 框架,开发人员必须跟踪浏览器对他们使用的 JavaScript 功能的支持,并必须解决一些浏览器对 JavaScript 实现的限制。

另一方面,当使用 AJAX 框架时,框架会为我们处理这一点;它抽象了对 JavaScript 实现的访问,并处理了不同浏览器之间的差异和怪癖。这样,我们可以专注于开发功能,而不必担心浏览器的差异和限制。

  • 标准的 JavaScript 函数和类集合对于完整的 Web 应用程序开发有些不足。各种常见任务需要许多行代码,即使它们可以包装在简单的函数中。

因此,即使您决定不使用 AJAX 框架,您也会发现自己在编写一个函数库,该函数库封装了 JavaScript 功能并使其更易于使用。然而,既然已经有许多优秀的开源库可用,为什么要重新发明轮子呢?

今天市场上可用的 AJAX 框架范围从提供服务器端和客户端组件的综合解决方案到简化使用 JavaScript 的轻量级客户端库。鉴于我们已经在服务器端使用 Django,我们只需要一个客户端框架。除此之外,该框架应该易于与 Django 集成,而不需要任何额外的依赖。最后,最好选择一个轻量级和快速的框架。有许多优秀的框架符合我们的要求,例如PrototypeYahoo! UI LibraryjQuery

但是,对于我们的应用程序,我将选择 jQuery,因为它是这三种框架中最轻量级的。它还拥有一个非常活跃的开发社区和广泛的插件范围。如果您已经有其他框架的经验,可以在本章中继续使用它。的确,您将不得不将本章中的 JavaScript 代码适应到您的框架中,但是无论您选择哪种框架,服务器端的 Django 代码都将保持不变。

注意

您还需要导入 Bootstrap 和 jQuery。因此,在我们的 Django 项目中使用 AJAX 功能不需要特定的安装或导入。

使用开源的 jQuery 框架

在我们开始在项目中实现 AJAX 增强功能之前,让我们快速介绍一下 jQuery 框架。

jQuery JavaScript 框架

jQuery 是一个 JavaScript 函数库,它简化了与 HTML 文档的交互并对其进行操作。该库旨在减少编写代码和实现跨浏览器兼容性所需的时间和精力,同时充分利用 JavaScript 提供的功能来构建交互式和响应式的 Web 应用程序。

使用 jQuery 的一般工作流程包括以下两个步骤:

  1. 选择要处理的 HTML 元素或一组元素。

  2. 将 jQuery 方法应用于所选组。

元素选择器

jQuery 提供了一种简单的选择元素的方法:通过将 CSS 选择器字符串传递给名为$()的函数。以下是一些示例,说明了此函数的用法:

  • 如果您想选择页面上的所有锚(<a>)元素,可以使用$("a")函数调用

  • 如果您想选择具有.title CSS 类的锚元素,请使用

$("a.title")
  • 要选择 ID 为#nav的元素,可以使用$("#nav")

  • 要选择#nav内部的所有列表项(<li>)元素,请使用$("#nav li")

$()函数构造并返回一个 jQuery 对象。之后,您可以在此对象上调用方法以与所选的 HTML 元素交互。

jQuery 方法

jQuery 提供了各种方法来操作 HTML 文档。您可以隐藏或显示元素,将事件处理程序附加到事件,修改 CSS 属性,操作页面结构,最重要的是执行 AJAX 请求。

为了调试,我们选择 Chrome 浏览器作为我们的首选浏览器。 Chrome 是最先进的 JavaScript 调试器之一,以其 Chrome 开发者工具的形式。要启动它,请在键盘上按下Ctrl+Shift+J

要尝试本节中概述的方法,请启动开发服务器并导航到用户配置文件页面(http://127.0.0.1:8000/user/ratan/)。通过按下键盘上的Ctrl+Shift+J打开 Chrome 开发者工具(按F12),并尝试选择元素并操作它们。

隐藏和显示元素

让我们从简单的事情开始。要在页面上隐藏一个元素,请在其上调用hide()方法。要再次显示它,请调用show()方法。例如,尝试在您的应用程序的 Bootstrap 中称为navbar的导航菜单上尝试这个:

>>> $(".navbar").hide()
>>> $(".navbar").show() 

您还可以在隐藏和显示元素时对元素进行动画处理。尝试使用fadeOut()fadeIn()slideUp()slideDown()方法来查看这两种动画效果中的两种。

当然,如果一次选择多个元素,这些方法(就像所有其他 jQuery 方法一样)也会起作用。例如,如果打开用户配置文件并在 Chrome 开发人员工具控制台中输入以下方法调用,则所有推文都将消失:

>>> $('.well').slideUp()

访问 CSS 属性和 HTML 属性

接下来,我们将学习如何更改元素的 CSS 属性。jQuery 提供了一个名为css()的方法来执行 CSS 操作。如果您以字符串形式传递 CSS 属性名称调用此方法,它将返回此属性的值:

>>> $(".navbar").css("display")

这样的结果如下:

block

如果向此方法传递第二个参数,它将将所选元素的指定 CSS 属性设置为附加参数:

>>> $(".navbar").css("font-size", "0.8em")

这样的结果如下:

<div id="nav" style="font-size: 0.8em;">

实际上,您可以操纵任何 HTML 属性,而不仅仅是 CSS 属性。要这样做,请使用attr()方法,它的工作方式与css()方法类似。使用属性名称调用它会返回属性值,而使用属性名称或值对调用它会将属性设置为传递的值:

>>> $("input").attr("size", "48")

这将导致以下结果:

<input type="hidden" name="csrfmiddlewaretoken" value="xxx" size="48">
<input id="id_country" name="country" type="hidden" value="Global" size="48">
<input type="submit" value="post" size="48">

这将一次性将页面上所有输入元素的大小更改为48

除此之外,还有一些快捷方法可以获取和设置常用的属性,例如val(),当不带参数调用时返回输入字段的值,并在传递一个参数时将该值设置为参数。还有控制元素内部 HTML 代码的html()方法。

最后,有两种方法可以用来附加或分离 CSS 类到一个元素:它们是addClass()removeClass()方法。还提供了第三种方法来切换 CSS 类,称为toggleClass()方法。所有这些类方法都将要更改的类的名称作为参数。

操作 HTML 文档

现在您已经熟悉了如何操作 HTML 元素,让我们看看如何添加新元素或删除现有元素。要在元素之前插入 HTML 代码,请使用before()方法,要在元素之后插入代码,请使用after()方法。请注意 jQuery 方法的命名方式非常直观,易于记忆!

让我们通过在用户页面上的标签列表周围插入括号来测试这些方法。

打开您的用户页面,并在 Chrome 开发者工具控制台中输入以下内容:

>>> $(".well span").before("<strong>(</strong>")
>>> $(".well span").after("<strong>)</strong>")

您可以向before()after()方法传递任何您想要的字符串。该字符串可以包含纯文本、一个 HTML 元素或更多。这些方法提供了一种非常灵活的方式来动态添加 HTML 元素到 HTML 文档中。

如果要删除一个元素,请使用remove()方法。例如:

$("#navbar").remove()

这种方法不仅隐藏了元素,还将其从文档树中完全删除。如果在使用remove()方法后尝试重新选择元素,您将得到一个空集:

>>> $("#nav")

这样的结果如下:

[]

当然,这只是从当前页面实例中删除元素。如果重新加载页面,元素将再次出现。

遍历文档树

尽管 CSS 选择器提供了一种非常强大的选择元素的方式,但有时您希望从特定元素开始遍历文档树。

对此,jQuery 提供了几种方法。parent()方法返回当前选定元素的父元素。children()方法返回所选元素的所有直接子元素。最后,find()方法返回当前选定元素的所有后代元素。所有这些方法都接受一个可选的 CSS 选择器字符串,以限制结果为与选择器匹配的元素。例如,$(".column").find("span")返回类 column 的所有<span>后代。

如果要访问一组中的单个元素,请使用get()方法,该方法将元素的索引作为参数。例如,$("span").get(0)方法返回所选组中的第一个<span>元素。

处理事件

接下来我们将学习事件处理程序。事件处理程序是在特定事件发生时调用的 JavaScript 函数,例如,当单击按钮或提交表单时。jQuery 提供了一系列方法来将处理程序附加到事件上;在我们的应用程序中特别感兴趣的事件是鼠标点击和表单提交。要处理单击元素的事件,我们选择该元素并在其上调用click()方法。该方法将事件处理程序函数作为参数。让我们在 Chrome 开发者控制台中尝试一下。

打开应用程序的用户个人资料页面,并在推文后插入一个按钮:

>>> $(".well span").after("<button id=\"test-button\">Click me!</button>")

注意

请注意,我们必须转义传递给after()方法的字符串中的引号。

如果您尝试单击此按钮,将不会发生任何事情,因此让我们为其附加一个事件处理程序:

>>> $("#test-button").click(function () { alert("You clicked me!"); })

现在,当您点击按钮时,将出现一个消息框。这是如何工作的?

我们传递给click()方法的参数可能看起来有点复杂,因此让我们再次检查一下:

function () { alert("You clicked me!"); }

这似乎是一个函数声明,但没有函数名。事实上,这个构造在 JavaScript 术语中创建了所谓的匿名函数,当您需要即时创建一个函数并将其作为参数传递给另一个函数时使用。我们本可以避免使用匿名函数,并将事件处理程序声明为常规函数:

>>> function handler() { alert("You clicked me!"); }
>>> $("#test-button").click(handler)

前面的代码实现了相同的效果,但第一个更简洁、紧凑。我强烈建议您熟悉 JavaScript 中的匿名函数(如果您还没有),因为我相信您在使用一段时间后会欣赏这种构造并发现它更易读。

处理表单提交与处理鼠标点击非常相似。首先选择表单,然后在其上调用submit()方法,然后将处理程序作为参数传递。在后面的部分中,我们将在项目中添加 AJAX 功能时多次使用这种方法。

发送 AJAX 请求

在完成本节之前,让我们谈一下 AJAX 请求。jQuery 提供了许多发送 AJAX 请求到服务器的方法。例如,load()方法接受一个 URL,并将该 URL 的页面加载到所选元素中。还有发送 GET 或 POST 请求以及接收结果的方法。在实现项目中的 AJAX 功能时,我们将更深入地研究这些方法。

接下来呢?

这就结束了我们对 jQuery 的快速介绍。本节提供的信息足以继续本章,一旦您完成本章,您将能够自己实现许多有趣的 AJAX 功能。但是,请记住,这个 jQuery 介绍只是冰山一角。如果您想全面了解 jQuery 框架,我强烈建议您阅读 Packt Publishing 的Learning jQuery,因为它更详细地介绍了 jQuery。您可以在www.packtpub.com/jQuery了解更多关于这本书的信息。

实现推文搜索

我们将通过实现实时搜索来引入 AJAX 到我们的应用程序中。这个功能背后的想法很简单:当用户在文本字段中输入一些关键词并点击搜索时,一个脚本在后台工作,获取搜索结果并在同一个页面上呈现它们。搜索页面不会重新加载,从而节省带宽,并提供更好、更具响应性的用户体验。

在我们开始实现这个功能之前,我们需要牢记一个重要的规则,即在使用 AJAX 时编写应用程序,确保它在没有 AJAX 支持的浏览器和没有启用 JavaScript 的用户中也能正常工作。如果你这样做,你就确保每个人都能使用你的应用程序。

实现搜索

因此,在我们使用 AJAX 之前,让我们编写一个简单的视图,通过标题搜索书签。首先,我们需要创建一个搜索表单,所以打开tweets/forms.py文件,并添加以下类:

class SearchForm(forms.Form):
query = forms.CharField(label='Enter a keyword to search for',
widget=forms.TextInput(attrs={'size': 32, 'class':'form-control'}))

正如你所看到的,这是一个非常简单的表单类,只有一个文本字段。用户将使用这个字段输入搜索关键词。接下来,让我们创建一个视图来进行搜索。打开tweets/views.py文件,并输入以下代码:

class Search(View):
  """Search all tweets with query /search/?query=<query> URL"""
  def get(self, request):
    form = SearchForm()
    params = dict()
    params["search"] = form
  return render(request, 'search.html', params)

  def post(self, request):
    form = SearchForm(request.POST)
    if form.is_valid():
    query = form.cleaned_data['query']
    tweets = Tweet.objects.filter(text__icontains=query)
    context = Context({"query": query, "tweets": tweets})
    return_str = render_to_string('partials/_tweet_search.html', context)
  return HttpResponse(json.dumps(return_str), content_type="application/json")
  else:
    HttpResponseRedirect("/search")

除了一些方法调用,这个视图应该非常容易理解。如果你看一下get请求,它非常简单,因为它准备搜索表单,然后呈现它。

post()方法是所有魔法发生的地方。当我们呈现搜索结果时,它只是一个带有搜索表单的布局呈现,也就是说,如果你看一下我们创建的名为search.html的新文件,你会看到以下内容:

{% extends "base.html" %}
{% load staticfiles %}
{% block content %}

<div class="row clearfix">
  <div class="col-md-6 col-md-offset-3 column">
    <form id="search-form" action="" method="post">{% csrf_token %}
      <div class="input-group input-group-sm">
      {{ search.query.errors }}
      {{ search.query }}
        <span class="input-group-btn">
          <button class="btn btn-search" type="submit">search</button>
        </span>
      </div><!-- /input-group -->
    </form>
  </div>
  <div class="col-md-12 column tweets">
  </div>
</div>
{% endblock %}
{% block js %}
  <script src="img/search.js' %}"></script>
{% endblock %}

如果你仔细观察,你会看到一个名为{% block js %}的新部分的包含。这里使用的概念与{% block content %}块相同,也就是说,这里声明的内容将在base.html文件中呈现。进一步看,再看修改后的base.html文件,我们可以看到以下内容:

{% load staticfiles %}
  <html>
    <head>
      <link href="{% static 'css/bootstrap.min.css' %}"
        rel="stylesheet" media="screen">
        {% block css %}
        {% endblock %}
    </head>
    <body>
      <nav class="navbar navbar-default" role="navigation">
        <a class="navbar-brand" href="#">MyTweets</a>
        <p class="navbar-text navbar-right">User Profile Page</p>
      </nav>
      <div class="container">
        {% block content %}
        {% endblock %}
      </div>
      <nav class="navbar navbar-default navbar-fixed-bottom" role="navigation">
        <p class="navbar-text navbar-right">Footer </p>
      </nav>
      <script src="img/jquery-2.1.1.min.js' %}"></script>
      <script src="img/bootstrap.min.js' %}"></script>
      <script src="img/base.js' %}"></script>
        {% block js %}
        {% endblock %}
    </body>
  </html>

上述代码清楚地显示了两个新的内容块,如下所示:

{% block css %}
  {% endblock %}
  {% block js %}
{% endblock %}

它们用于包含相应的文件类型,并将文件类型与基础一起呈现,因此使用简单的规则,每页只声明一个 CSS 和 JavaScript 文件,从而使项目的维护变得更加简单。我们将在本书的后面使用调用assets pipeline的概念来实现这一点。

现在,回到我们的 AJAX 搜索功能,你会发现这个search.html文件与tweet.html文件类似。

对于搜索功能,我们将创建一个新的 URL,需要将其附加到以下的urls.py文件中:

url(r'^search/$', Search.as_view()),
urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin
from tweet.views import Index, Profile, PostTweet, HashTagCloud, Search

admin.autodiscover()

urlpatterns = patterns('',
url(r'^$', Index.as_view()),
url(r'^user/(\w+)/$', Profile.as_view()),
url(r'^admin/', include(admin.site.urls)),
url(r'^user/(\w+)/post/$', PostTweet.as_view()),
url(r'^hashTag/(\w+)/$', HashTagCloud.as_view()),
url(r'^search/$', Search.as_view()),
)

search.html文件中,我们定义了search.js方法;让我们创建这个 JavaScript 文件,它实际上发出了 AJAX 请求:

search.js

$('#search-form').submit(function(e){
$.post('/search/', $(this).serialize(), function(data){
$('.tweets').html(data);
});
e.preventDefault();
});

当表单提交时,这段 JavaScript 代码会被触发,它会向/search用户发出一个 AJAX post 请求,带有序列化的表单数据,并获得响应。然后,根据获得的响应,它将数据附加到具有类别 tweets 的元素。

如果我们在浏览器中打开用户搜索,它会看起来像下面的截图:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00303.jpeg

现在,等等!当这个表单提交时会发生什么?

AJAX 请求发送到搜索类的post()方法,如下所示:

def post(self, request):
  form = SearchForm(request.POST)
  if form.is_valid():
    query = form.cleaned_data['query']
    tweets = Tweet.objects.filter(text__icontains=query)
    context = Context({"query": query, "tweets": tweets})
    return_str = render_to_string('partials/_tweet_search.html', context)
  return HttpResponse(json.dumps(return_str), content_type="application/json")
  else:
    HttpResponseRedirect("/search")

我们从request.POST方法中提取表单验证;如果表单有效,就从表单对象中提取查询。

然后,tweets = Tweet.objects.filter(text__icontains===query)方法搜索给定查询项的子字符串匹配。

搜索是使用Tweets.objects模块中的filter方法进行的。你可以把它看作是 Django 模型中SELECT语句的等价物。它接收搜索条件作为参数,并返回搜索结果。每个参数的名称必须遵循以下命名约定:

field__operator

请注意,fieldoperator 变量之间用两个下划线分隔:field 是我们想要搜索的字段的名称,operator 是我们想要使用的查找方法。以下是常用操作符的列表:

  • exact: 参数的值与字段的精确匹配

  • contains: 该字段包含参数的值

  • startswith: 该字段以参数的值开头

  • lt: 该字段小于参数的值

  • gt: 该字段大于参数的值

此外,还有前三个操作符的不区分大小写版本:iexacticontainsistartswith,也可以包括在列表中。

我们现在正在做的一件完全不同的事情是:

context = Context({"query": query, "tweets": tweets})
return_str = render_to_string('partials/_tweet_search.html', context)
return HttpResponse(json.dumps(return_str), content_type="application/json")

我们的目标是在不重新加载或刷新搜索页面的情况下加载搜索结果。如果是这样,我们之前的渲染方法将如何帮助我们?它不能。我们需要一些方法,可以帮助我们在不重新加载页面的情况下将数据发送到浏览器。

我们广泛使用网页开发中称为partials的概念。它们通常是在服务器端生成的小段 HTML 代码片段,以 JSON 格式呈现,然后通过 JavaScript 添加到现有 DOM 中。

为了实现这个方法,我们首先会在现有模板文件夹中创建一个名为 partials 的文件夹,以及一个名为 _tweet_search.html 的文件,内容如下:

{% for tweet in tweets %}
  <div class="well">
    <span>{{ tweet.text }}</span>
  </div>
{% endfor %}
{% if not tweets %}
  <div class="well">
    <span> No Tweet found.</span>
  </div>
{% endif %}

该代码将在一个良好的框中渲染整个推文对象,或者如果找不到推文对象,它将在框中渲染 未找到推文

前面的概念是在视图中将一个 partial 渲染为字符串,如果我们需要为渲染传递任何参数,我们需要在调用从 partials 生成字符串的地方首先传递它们。要为 partials 传递参数,我们需要创建一个上下文对象,然后传递我们的参数:

context = Context({"query": query, "tweets": tweets})
return_str = render_to_string('partials/_tweet_search.html', context)

首先,我们将创建包含 query(稍后将使用)和 tweets 参数的上下文,并使用 render_to_string() 函数。然后,我们可以使用 JSON 将字符串转储到 HttpResponse() 函数,如下所示:

return HttpResponse(json.dumps(return_str), content_type="application/json")

导入列表如下:

from django.views.generic import View
from django.shortcuts import render
from user_profile.models import User
from models import Tweet, HashTag
from tweet.forms import TweetForm, SearchForm
from django.http import HttpResponseRedirect
from django.template.loader import render_to_string
from django.template import Context
from django.http import HttpResponse
import json

就是这样!我们完成了一个基于 AJAX 的推文搜索。搜索 django 列出了我们创建的两条推文,如下截图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00304.jpeg

继续使用搜索引擎,并且我相信你会更加喜欢 Django。

现在我们有了一个功能性的(尽管非常基本的)搜索页面。搜索功能本身将在后面的章节中得到改进,但对我们来说现在重要的是将 AJAX 引入搜索表单,以便在幕后获取结果并呈现给用户,而无需重新加载页面。由于我们的模块化代码,这个任务将比看起来要简单得多。

实现实时搜索推文

在上一节中进行了简单的搜索,现在我们将实现实时搜索,技术上是相同的,但唯一的区别是搜索表单将随着每次按键而提交,并且结果将实时加载。

要实现实时搜索,我们需要做以下两件事:

  • 我们需要拦截并处理提交搜索表单的事件。这可以使用 jQuery 的 submit() 方法来完成。

  • 我们需要使用 AJAX 在后台加载搜索结果,并将它们插入页面中。

jQuery 提供了一个名为 load() 的方法,用于从服务器检索页面并将其内容插入到所选元素中。在其最简单的形式中,该函数将远程页面的 URL 作为参数。

我们将在标签上实现实时搜索,也就是说,我们将创建一个新页面,与我们刚刚创建的搜索页面相同,但这将用于标签,并且我们将使用实时标签建议(标签自动完成)。在开始之前,我们需要相同的 Twitter typeahead JavaScript 库。

twitter.github.io/typeahead.js/ 下载这个库的最新版本。

在本章中,我们下载了版本为 10.05 的库。下载并保存到当前的 JavaScript 文件夹中。

首先,让我们稍微修改我们的搜索视图,以便在接收到名为 AJAX 的额外 GET 变量时,仅返回搜索结果而不是搜索页面的其余部分。我们这样做是为了使客户端的 JavaScript 代码能够轻松地检索搜索结果,而不需要搜索页面的其余部分的 HTML 格式。这可以通过在请求时简单地使用 bookmark_list.html 模板而不是 search.html 模板来实现。

GET 包含关键的 AJAX 参数。打开 bookmarks/views.py 文件并修改 search_page 参数(朝文件末尾),使其如下所示:

def search_page(request):
  [...]
  variables = RequestContext(request, {
    'form': form,
    'bookmarks': bookmarks,
    'show_results': show_results,
    'show_tags': True,
    'show_user': True
  })
  if request.GET.has_key('AJAX'):):):
    return render_to_response('bookmark_list.html', variables)
  else:
    return render_to_response('search.html', variables)

接下来,在 site_media 目录中创建一个名为 search.js 的文件,并将其链接到 templates/search.html 文件,如下所示:

{% extends "base.html" %}
  {% block external %}
    <script type="text/javascript" src="img/search.js">
    </script>
  {% endblock %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
[...]

现在是有趣的部分!让我们创建一个函数,加载搜索结果并将它们插入相应的 div 标签中。在 site_media/search.js 文件中写入以下代码:

function search_submit() {
  var query = $("#id_query").val();
  $("#search-results").load(
    "/search/?AJAX&query=" + encodeURIComponent(query)
  );
return false;
}

让我们逐行浏览这个函数:

  • 该函数首先使用 val() 方法从文本字段中获取查询字符串。

  • 我们使用 load() 方法从 search_page 视图获取搜索结果,并将搜索结果插入到 #search-results div 中。首先对查询调用 encodeURIComponent 参数构造请求 URL,它的工作方式与我们在 Django 模板中使用的 urlencode 过滤器完全相同。调用这个函数很重要,以确保即使用户在文本字段中输入特殊字符,如 &,构造的 URL 仍然有效。在转义查询后,我们将其与 /search/?AJAX&query= 参数连接起来。这个 URL 调用 search_page 视图,并将 GET 变量的 AJAX 参数和查询传递给它。视图返回搜索结果,load() 方法便将结果加载到 #search-results div 中。

  • 我们从函数中返回 False,告诉浏览器在调用处理程序后不要提交表单。如果我们在函数中不返回 False,浏览器将继续像往常一样提交表单,而我们不希望这样。

还有一个小细节:在何处以及何时应该将 search_submit 参数附加到搜索表单的提交事件上?在编写 JavaScript 时的一个经验法则是,在文档完成加载之前,我们不能操作文档树中的元素。因此,我们的函数必须在搜索页面加载完成后立即调用。幸运的是,jQuery 提供了一种在 HTML 文档加载时执行函数的方法。让我们通过将以下代码附加到 site_media/search.js 文件来利用它:

$(document).ready(function () {
  $("#search-form").submit(search_submit);
});

$(document) 函数选择当前页面的文档元素。请注意,document 变量周围没有引号;它是浏览器提供的变量,而不是字符串。

ready() 方法接受一个函数,并在所选元素完成加载后立即执行它。因此,实际上,我们告诉 jQuery 在 HTML 文档加载完成后立即执行传递的函数。我们将一个匿名函数传递给 ready() 方法,这个函数简单地将 search_submit 参数绑定到 #search-form 表单的提交事件上。

就是这样。我们用不到十五行的代码实现了实时搜索。要测试新功能,转到 http://127.0.0.1:8000/search/,提交查询,并注意结果如何在不重新加载页面的情况下显示。

本节涵盖的信息可以应用于任何需要在后台处理而无需重新加载页面的表单。例如,您可以创建一个带有预览按钮的评论表单,该按钮在同一页面上加载预览而无需重新加载。在下一节中,我们将增强用户页面,使用户可以在原地编辑书签而无需离开用户页面。

在不加载单独页面的情况下原地编辑推文

编辑发布的内容是网站上非常常见的任务。通常通过在内容旁边提供一个“编辑”链接来实现。当用户点击链接时,该链接会将用户带到另一个页面上的一个表单,用户可以在那里编辑内容。用户提交表单后,会被重定向回内容页面。

另一方面,想象一下,您可以在不离开内容页面的情况下编辑内容。当您点击“编辑”按钮时,内容会被一个表单替换。当您提交表单时,它会消失,更新后的内容会出现在原来的位置。所有操作都在同一个页面上进行;使用 JavaScript 和 AJAX 来完成表单的渲染和提交。这样的工作流程会更直观和响应更快吗?

上述描述的技术称为原地编辑。它现在在 Web 应用程序中变得更加普遍。我们将通过让用户在用户页面上原地编辑书签来实现此功能。

由于我们的应用程序尚不支持编辑书签,我们将首先实现这一点,然后修改编辑过程以在原地工作。

实现书签编辑

我们已经拥有大部分需要实现书签编辑的部分。如果您回忆一下前一章,我们在 bookmarks/views.py 文件中实现了 bookmark_save_page 视图,以便如果用户尝试多次保存相同的 URL,则更新相同的书签而不是创建副本。这得益于数据模型提供的 get_or_create() 方法,这个小细节极大地简化了书签编辑的实现。我们需要做的是:

  • 我们将要编辑的书签的 URL 作为名为 URL 的 GET 变量传递给 bookmark_save_page 视图。

  • 我们修改 bookmark_save_page 视图,以便在接收到 GET 变量时填充书签表单的字段。该表单将填充与传递的 URL 对应的书签的数据。

当填充的表单被提交时,书签将被更新,就像我们之前解释的那样,因为它看起来好像用户又提交了相同的 URL。

在我们实现上述描述的技术之前,让我们通过将保存书签的部分移动到一个单独的函数中来减少 bookmark_save_page 视图的大小。我们将称此函数为 _bookmark_save。名称开头的下划线告诉 Python 在导入视图模块时不要导入此函数。该函数期望请求和有效的表单对象作为参数;它将根据表单数据保存书签并返回该书签。

打开 bookmarks/views.py 文件并创建以下函数;如果愿意,可以从 bookmark_save_page 视图中复制并粘贴代码,因为我们除了最后的 return 语句外不会对其进行任何更改:

def _bookmark_save(request, form):
  # Create or get link.
  link, dummy = \
  Link.objects.get_or_create(url=form.clean_data['url'])
  # Create or get bookmark.
  bookmark, created = Bookmark.objects.get_or_create(
    user=request.user,
    link=link
  )
  # Update bookmark title.
  bookmark.title = form.clean_data['title']
  # If the bookmark is being updated, clear old tag list.
  if not created:
    bookmark.tag_set.clear()
    # Create new tag list.
    tag_names = form.clean_data['tags'].split()
    for tag_name in tag_names:
      tag, dummy = Tag.objects.get_or_create(name=tag_name)
      bookmark.tag_set.add(tag)
      # Save bookmark to database and return it.
      bookmark.save()
    return bookmark
    Now in the same file, replace the code that you removed from bookmark_save_page
    with a call to _bookmark_save :
      @login_required
      def bookmark_save_page(request):
        if request.method == 'POST':
          form = BookmarkSaveForm(request.POST)
        if form.is_valid():
          bookmark = _bookmark_save(request, form)
          return HttpResponseRedirect(
            '/user/%s/' % request.user.username
          )
        else:
          form = BookmarkSaveForm()
          variables = RequestContext(request, {
            'form': form
          })
        return render_to_response('bookmark_save.html', variables)

bookmark_save_page 视图中的当前逻辑如下:

[伪代码]

if there is POST data:
  Validate and save bookmark.
  Redirect to user page.
else:
  Create an empty form.
Render page.

要实现书签编辑,我们需要稍微修改逻辑,如下所示:

[伪代码]

if there is POST data:
  Validate and save bookmark.
  Redirect to user page.
  else if there is a URL in GET data:
    Create a form an populate it with the URL's bookmark.
  else:
    Create an empty form.
Render page.

让我们将上述伪代码翻译成 Python。修改 bookmarks/views.py 文件中的 bookmark_save_page 视图,使其看起来像以下代码(新代码已突出显示):

from django.core.exceptions import ObjectDoesNotExist
@login_required
def bookmark_save_page(request):
  if request.method == 'POST':
    form = BookmarkSaveForm(request.POST)
      if form.is_valid():
        bookmark = _bookmark_save(request, form)
        return HttpResponseRedirect(
          '/user/%s/' % request.user.username)
        elif request.GET.has_key('url'):):):
          url = request.GET['url']
          title = ''
          tags = ''
        try:
          link = Link.objects.get(url=url)
          bookmark = Bookmark.objects.get(
            link=link,
            user=request.user
          )
        title = bookmark.title
        tags = ' '.join(
          tag.name for tag in bookmark.tag_set.all()
        )
        except ObjectDoesNotExist:
          pass
        form = BookmarkSaveForm({
          'url': url,
          'title': title,
          'tags': tags
        })
        else:
          form = BookmarkSaveForm()
          variables = RequestContext(request, {
            'form': form
          })
        return render_to_response('bookmark_save.html', variables)

代码的这一新部分首先检查是否存在名为 URL 的 GET 变量。如果是这样,它将加载此 URL 的相应LinkBookmark对象,并将所有数据绑定到书签保存表单。您可能会想知道为什么我们在 try-except 结构中加载LinkBookmark对象,并默默地忽略异常。

确实,如果没有找到请求的 URL 的书签,引发 HTTP 404 异常是完全有效的。然而,我们的代码选择在这种情况下只填充 URL 字段,留下标题和标签字段为空。

现在,在用户页面的每个书签旁边添加编辑链接。打开templates/bookmark_list.html文件并插入突出显示的代码:

{% if bookmarks %}
  <ul class="bookmarks">
    {% for bookmark in bookmarks %}
      <li>
        <a href="{{ bookmark.link.url }}" class="title">
        {{ bookmark.title|escape }}</a>
        {% if show_edit %}
          <a href="/save/?url={{ bookmark.link.url|urlencode }}"
          class="edit">[edit]</a>
        {% endif %}
      <br />
      {% if show_tags %}
        Tags:
          {% if bookmark.tag_set.all %}
            <ul class="tags">
              {% for tag in bookmark.tag_set.all %}
                <li><a href="/tag/{{ tag.name|urlencode }}/">
              {{ tag.name|escape }}</a></li>
              {% endfor %}
            </ul>
      {% else %}
        None.
      {% endif %}
      <br />
[...]

注意我们是如何通过将书签的 URL 附加到/save/?url= {{ bookmark.link.url|urlencode }}来构建编辑链接的。

此外,由于我们只想在用户页面上显示编辑链接,模板只在show_edit标志设置为True时呈现这些链接。否则,让用户编辑其他人的链接是没有意义的。现在打开bookmarks/views.py文件,并在user_page标志的模板变量中添加show_edit标志:

def user_page(request, username):
  user = get_object_or_404(User, username=username)
  bookmarks = user.bookmark_set.order_by('-id')
  variables = RequestContext(request, {
    'bookmarks': bookmarks,
    'username': username,
    'show_tags': True,
    'show_edit': username == request.user.username,
  })
return render_to_response('user_page.html', variables)

username == request.user.username表达式仅在用户查看自己的页面时评估为True,这正是我们想要的。

最后,我建议您稍微减小编辑链接的字体大小。打开site_media/style.css文件并将以下内容附加到其末尾:

ul.bookmarks .edit {
  font-size: 70%;
}

我们完成了!在继续之前,随意导航到您的用户页面并尝试编辑书签。

实现书签的原地编辑

现在我们已经实现了书签编辑,让我们转向令人兴奋的部分:使用 AJAX 添加原地编辑!

我们的方法是:

  • 我们将拦截点击编辑链接的事件,并使用 AJAX 从服务器加载书签编辑表单。然后我们将用编辑表单替换页面上的书签。

  • 当用户提交编辑表单时,我们将拦截提交事件,并使用 AJAX 将更新后的书签发送到服务器。

  • 服务器保存书签并返回新书签的 HTML 表示。然后我们将用服务器返回的标记替换页面上的编辑表单。

我们将使用与实时搜索非常相似的方法来实现前面的过程。首先,我们将修改bookmark_save_page视图,以便在 GET 变量称为 AJAX 存在时响应 AJAX 请求。接下来,我们将编写 JavaScript 代码,从视图中检索编辑表单,当用户提交此表单时,将书签数据发送回服务器。

由于我们希望从bookmark_save_page视图返回一个编辑表单的标记给 AJAX 脚本,让我们稍微重构一下我们的模板。在模板中创建一个名为bookmark_save_form.html的文件,并将书签保存表单从bookmark_save.html文件移动到这个新文件中:

<form id="save-form" method="post" action="/save/">
  {{ form.as_p }}
  <input type="submit" value="save" />
</form>

请注意,我们还更改了表单的 action 属性为/save/并为其赋予了一个 ID。这对于表单在用户页面以及书签提交页面上的工作是必要的。

接下来,在bookmark_save.html文件中包含这个新模板:

{%extends "base.html" %}
{%block title %}Save Bookmark{% endblock %}
{%block head %}Save Bookmark{% endblock %}
{%block content %}
{%include 'bookmark_save_form.html' %}
{%endblock %}

,现在我们将表单放在一个单独的模板中。让我们更新bookmark_save_page视图,以处理正常和 AJAX 请求。打开bookmarks/views.py文件并更新视图,使其看起来像下面修改后的样子(用新加粗的行):

def bookmark_save_page(request):
  AJAX = request.GET.has_key('AJAX')))
  if request.method == 'POST':
    form = BookmarkSaveForm(request.POST)
    if form.is_valid():
      bookmark = _bookmark_save(form)
        if AJAX:
          variables = RequestContext(request, {
            'bookmarks': [bookmark],
            'show_edit': True,
            'show_tags': True
        })
      return render_to_response('bookmark_list.html', variables)
      else:
        return HttpResponseRedirect(
          '/user/%s/' % request.user.username
        )
      else:
        if AJAX:
          return HttpResponse('failure')
          elif request.GET.has_key('url'):
            url = request.GET['url']
            title = ''
            tags = ''
        try:
          link = Link.objects.get(url=url)
          bookmark = Bookmark.objects.get(link=link, user=request.user)
          title = bookmark.title
          tags = ' '.join(tag.name for tag in bookmark.tag_set.all())
        except:::
          pass
          form = BookmarkSaveForm({
            'url': url,
            'title': title,
            'tags': tags
          })
        else:
          form = BookmarkSaveForm()
          variables = RequestContext(request, {
            'form': form
          })
          if AJAX:
            return render_to_response(
              'bookmark_save_form.html',
              variables
            )
            else:
              return render_to_response(
                'bookmark_save.html',
                variables
              )

让我们分别检查每个突出显示的部分:

AJAX = request.GET.has_key('AJAX')

在方法的开头,我们将检查是否存在名为 AJAX 的 GET 变量。我们将结果存储在名为 AJAX 的变量中。稍后在方法中,我们可以使用这个变量来检查我们是否正在处理 AJAX 请求:

if condition:
  if form.is_valid():
    bookmark = _bookmark_save(form)
    if AJAX:
      variables = RequestContext(request, {
        'bookmarks': [bookmark],
         'show_edit': True,
         'show_tags': True
      })
    return render_to_response('bookmark_list.html', variables)
    else:
      return HttpResponseRedirect('/user/%s/' % request.user.username)
    else:
      if AJAX:
        return HttpResponse('failure')

如果我们收到 POST 请求,我们检查提交的表单是否有效。如果有效,我们保存书签。接下来,我们检查这是否是一个 AJAX 请求。如果是,我们使用bookmark_list.html模板呈现保存的书签,并将其返回给请求脚本。否则,这是一个正常的表单提交,因此我们将用户重定向到他们的用户页面。另一方面,如果表单无效,我们只会像处理 AJAX 请求一样返回字符串'failure',我们将通过在 JavaScript 中显示错误对话框来响应。如果是正常请求,则无需执行任何操作,因为页面将重新加载,并且表单将显示输入中的任何错误:

if AJAX:
  return render_to_response('bookmark_save_form.html', variables)
  else:
    return render_to_response('bookmark_save.html', variables)

这在方法的末尾进行检查。如果没有 POST 数据,即执行到这一点,这意味着我们应该呈现一个表单并返回它。如果是 AJAX 请求,则使用bookmark_save_form.html模板,否则将其保存为 HTML 文件。

我们的视图现在已准备好为 AJAX 请求提供服务,也可以处理正常的页面请求。让我们编写 JavaScript 代码,以利用更新后的视图。在site_media文件夹中创建一个名为bookmark_edit.js的新文件。但是,在向其中添加任何代码之前,让我们将bookmark_edit.js文件链接到user_page.html模板。打开user_page.html文件,并进行以下修改:

{% extends "base.html" %}
  {% block external %}
    <script type="text/javascript" src="img/bookmark_edit.js">
    </script>
  {% endblock %}
  {% block title %}{{ username }}{% endblock %}
  {% block head %}Bookmarks for {{ username }}{% endblock %}
  {% block content %}
    {% include 'bookmark_list.html' %}
  {% endblock %}

我们需要在bookmark_edit.js文件中编写两个函数:

  • bookmark_edit:此函数处理编辑链接的点击。它从服务器加载编辑表单,并用此表单替换书签。

  • bookmark_save:此函数处理编辑表单的提交。它将表单数据发送到服务器,并用服务器返回的书签 HTML 替换表单。

让我们从第一个函数开始。打开site_media/bookmark_edit.js文件,并在其中编写以下代码:

function bookmark_edit() {
  var item = $(this).parent();
  var url = item.find(".title").attr("href");
  item.load("/save/?AJAX&url=" + escape(url), null, function () {
    $("#save-form").submit(bookmark_save);
  });
  return false;
}

因为这个函数处理编辑链接上的点击事件,所以this变量指的是编辑链接本身。将其包装在 jQuery $()函数中并调用parent()函数返回编辑链接的父元素,即书签的<li>元素(在 Firebug 控制台中尝试一下,看看自己是否能看到相同的结果)。

在获取书签的<li>元素的引用之后,我们获取书签的标题的引用,并使用attr()方法从中提取书签的 URL。

接下来,我们使用load()方法将编辑表单放置在书签的 HTML 文件中。这次,我们在 URL 之外调用load()方法时,还使用了两个额外的参数。load()函数接受两个可选参数,如下所示:

  • 如果我们发送 POST 请求,则它接受键或值对的对象。由于我们使用 GET 请求从服务器端视图获取编辑表单,因此对于此参数,我们传递 null。

  • 它接受一个函数,当 jQuery 完成将 URL 加载到所选元素时调用该函数。我们传递的函数将bookmark_save()方法(接下来我们将要编写的方法)附加到刚刚检索到的表单上。

最后,该函数返回False,告诉浏览器不要跟随编辑链接。现在我们需要使用$(document).ready()bookmark_edit()函数附加到单击编辑链接的事件上:

$(document).ready(function () {
  $("ul.bookmarks .edit").click(bookmark_edit);
});

如果您在编写此函数后尝试在用户页面中编辑书签,则应该会出现编辑表单,但是您还应该在 Firebug 控制台中收到 JavaScript 错误消息,因为bookmark_save()函数未定义,所以让我们来编写它:

function bookmark_save() {
  var item = $(this).parent();
  var data = {
    url: item.find("#id_url").val(),
    title: item.find("#id_title").val(),
    tags: item.find("#id_tags").val()
  };
  $.post("/save/?AJAX", data, function (result) {
    if (result != "failure") {
      item.before($("li", result).get(0));
      item.remove();
      $("ul.bookmarks .edit").click(bookmark_edit);
    }
    else {
      alert("Failed to validate bookmark before saving.");
    }
  });
  return false;
}

在这里,this变量指的是编辑表单,因为我们处理提交表单的事件。该函数首先通过检索对表单的父元素(再次是书签的<li>元素)的引用来开始。接下来,该函数使用每个表单字段的 ID 和val()方法从表单中检索更新的数据。

然后它使用一个名为$.post()的方法将数据发送回服务器。最后,它返回False以防止浏览器提交表单。

您可能已经猜到,$.post()函数是一个发送 POST 请求到服务器的 jQuery 方法。它有三个参数,如下:

  • POST 请求目标的 URL。

  • 表示 POST 数据的键/值对对象。

  • 当请求完成时调用的函数。服务器响应作为字符串参数传递给此函数。

值得一提的是,jQuery 提供了一个名为$.get()的方法,用于向服务器发送 GET 请求。它接受与$.post()函数相同类型的参数。我们使用$.post()方法将更新的书签数据发送到bookmark_save_page视图。正如前面几段讨论的那样,如果视图成功保存书签,则返回更新的书签 HTML。否则,它返回failure字符串。

因此,我们检查服务器返回的结果是否是“失败”。如果请求成功,我们使用before()方法在旧书签之前插入新书签,并使用remove()方法从 HTML 文档中删除旧书签。另一方面,如果请求失败,我们会显示一个显示失败的警报框。

在我们完成本节之前还有一些小事情。为什么我们插入$("li",result).get(0)方法而不是结果本身?如果您检查bookmark_save_page视图,您会看到它使用bookmark_list.html模板来构建书签的 HTML。然而,bookmark_list.html模板返回包装在<ul>标签中的书签<li>元素。基本上,$("li", result).get(0)方法告诉 jQuery 从结果中提取第一个<li>元素,这就是我们想要的元素。正如您从前面的片段中看到的,您可以使用 jQuery $()函数通过将该字符串作为函数的第二个参数传递来选择 HTML 字符串中的元素。

bookmark_submit模板是从bookmark_edit模板中的事件附加的,因此我们不需要在$(document).ready()方法中做任何事情。

最后,在将更新的书签加载到页面后,我们再次调用$("ul.bookmarks.edit").click(bookmark_edit)方法,将bookmark_edit模板附加到新加载的编辑链接上。如果不这样做并尝试两次编辑书签,第二次点击编辑链接将带您到一个单独的表单页面。

当您完成编写 JavaScript 代码后,打开浏览器并转到您的用户页面,尝试使用新功能。编辑书签,保存它们,并注意到如何在页面上立即反映出更改而无需重新加载。

现在您已经完成了这一部分,应该对就地编辑的实现有很好的理解。还有许多其他情况下,这个功能可以很有用,例如,可以用来在同一页上编辑文章或评论,而不必跳转到位于不同 URL 上的表单进行编辑。

在下一节中,我们将实现一个帮助用户在提交书签时输入标签的第三个常见的 AJAX 功能。

在提交推文时自动完成标签

我们将在本章中要实现的最后一个 AJAX 增强功能是标签的自动完成。自动完成的概念是在 Google 发布其 Suggest 搜索界面时进入 Web 应用程序的。Suggest 通过根据用户到目前为止输入的内容,在搜索输入字段下方显示最受欢迎的搜索查询。这也类似于集成开发环境中的代码编辑器根据您的输入提供代码完成建议。这个功能通过让用户输入他们想要的单词的几个字符,然后让他们从列表中选择而不必完全输入来节省时间。

我们将通过在提交书签时提供建议来实现此功能,但我们不打算从头开始编写此功能,而是要使用 jQuery 插件来实现它。jQuery 拥有一个不断增长的大型插件列表,提供各种功能。安装插件与安装 jQuery 本身没有什么不同。您下载一个(或多个)文件并将它们链接到您的模板,然后编写几行 JavaScript 代码来激活插件。

您可以通过导航到docs.jquery.com/Plugins来浏览可用的 jQuery 插件列表。在列表中搜索 autocomplete 插件并下载它,或者您可以直接从bassistance.de/jquery-plugins/jquery-plugin-autocomplete/获取它。

您将收到一个包含许多文件的 zip 存档文件。将以下文件(可以在jquery/autocomplete/scroll目录中找到)提取到site_media目录中:

  • jquery.autocomplete.css

  • dimensions.js

  • jquery.bgiframe.min.js

  • jquery.autocomplete.js

由于我们希望在书签提交页面上提供自动完成功能,请在site_media文件夹中创建一个名为tag_autocomplete.js的空文件。然后打开templates/bookmark_save.html文件,并将所有前述文件链接到它:

{% extends "base.html" %}
  {% block external %}
  <link rel="stylesheet"
  href="/site_media/jquery.autocomplete.css" type="text/css" />
  <script type="text/javascript"
  src="img/dimensions.js"> </script>
  <script type="text/javascript"
  src="img/jquery.bgiframe.min.js"> </script>
  <script type="text/javascript"
  src="img/jquery.autocomplete.js"> </script>
  <script type="text/javascript"
  src="img/tag_autocomplete.js"> </script>
  {% endblock %}
  {% block title %}Save Bookmark{% endblock %}
  {% block head %}Save Bookmark{% endblock %}
[...]

我们现在已经完成了插件的安装。如果你阅读它的文档,你会发现这个插件是通过在选定的输入元素上调用一个名为autocomplete()的方法来激活的。autocomplete()函数接受以下参数:

  • 服务器端 URL:对于这一点,插件向这个 URL 发送一个 GET 请求,其中包含到目前为止已经输入的内容,并期望服务器返回一组建议。

  • 可用于指定各种选项的对象:我们感兴趣的选项有很多。这个选项有一个布尔变量,告诉插件输入字段用于输入多个值(记住我们使用同一个文本字段输入所有标签),以及用于告诉插件哪个字符串分隔多个条目的多个分隔符。在我们的情况下,它是一个单个空格字符。

因此,在激活插件之前,我们需要编写一个视图,接收用户输入并返回一组建议。打开bookmarks/views.py文件,并将以下内容追加到文件末尾:

def AJAX_tag_autocomplete(request):
  if request.GET.has_key('q'):):):
    tags = \
    Tag.objects.filter(name__istartswith=request.GET['q'])[:10]
  return HttpResponse('\n'.join(tag.name for tag in tags))
return HttpResponse()

autocomplete()插件将用户输入发送到名为q的 GET 变量。因此,我们可以验证该变量是否存在,并构建一个以该变量值开头的标签列表。这是使用我们在本章前面学到的filter()方法和istartswith运算符完成的。我们只取前十个结果,以避免给用户带来过多的建议,并减少带宽和性能成本。最后,我们将建议连接成一个由换行符分隔的单个字符串,将字符串包装成一个HttpResponse对象,并返回它。

有了建议视图准备好后,在urls.py文件中为插件添加一个 URL 条目,如下所示:

urlpatterns = patterns('',
  # AJAX
  (r'^AJAX/tag/autocomplete/$', AJAX_tag_autocomplete),
)

现在在site_media/tag_autocomplete.js文件中输入以下代码,激活标签输入字段上的插件:

$(document).ready(function () {
  $("#id_tags").autocomplete(
    '/AJAX/tag/autocomplete/',
    {multiple: true, multipleSeparator: ' '}
  );
});

该代码将一个匿名函数传递给$(document).ready()方法。这个函数在标签输入字段上调用autocomplete()函数,并传递了我们之前讨论过的参数。

这几行代码就是我们实现标签自动完成所需要的全部内容。要测试新功能,请导航到http://127.0.0.1:8000/save/的书签提交表单,并尝试在标签字段中输入一个或两个字符。根据数据库中可用的标签,应该会出现建议。

有了这个功能,我们完成了这一章。我们涵盖了很多材料,并学习了许多令人兴奋的技术和技巧。阅读完本章后,你应该能够想到并实现许多其他用户界面的增强功能,比如在用户页面上删除书签的能力,或者通过标签实时浏览书签等等。

下一章将转向一个不同的主题:我们将让用户对他们最喜欢的书签进行投票和评论,我们的应用程序的首页将不再像现在这样空荡荡了!

总结

呼,这是一个很长的章节,但希望你从中学到了很多!我们从学习 jQuery 框架和如何将其整合到我们的 Django 项目开始了这一章。之后,我们在我们的书签应用程序中实现了三个令人兴奋的功能:实时搜索、就地编辑和自动完成。

下一章将是另一个令人兴奋的章节。我们将让用户提交书签到首页并为他们最喜欢的书签投票。我们还将让用户对书签进行评论。所以,请继续阅读!

第七章:关注和评论

我们应用程序的主要思想是为用户提供一个通过推文分享他们的想法的平台。让用户创建新推文只是其中的一部分,如果用户无法与现有推文进行交互,则应用程序将被认为是不完整的。在本章中,我们将完成另一部分,即使用户能够关注特定用户并评论现有推文。在此过程中,您还将学习到几个新的 Django 功能。

在本章中,您将学习以下内容:

  • 让用户关注另一个用户

  • 显示最受关注的用户

让用户关注另一个用户

到目前为止,我们的用户可以通过浏览标签和用户页面发现新的推文。让我们为用户提供一种方法来关注另一个用户,这样他们就可以在他们各自的主页上看到来自他们关注的所有用户的聚合推文。让我们还使用户能够评论新的推文。

我们还将创建一个页面,用户可以按关注者数量列出受欢迎的用户。这个功能对我们的应用程序很重要,因为它将把主页从一个基本的欢迎页面变成一个经常更新的用户列表,用户可以在其中找到热门用户和他们有趣的推文。

我们实现此功能的策略如下:

  • 创建一个数据模型来存储用户及其关注者。这个模型将跟踪与用户相关的各种信息。

  • 在他们的标题旁边给每个用户一个关注按钮。我们还将创建一个视图,显示计数,比如用户发表的推文数量和他们的关注者数量。这需要大量的工作,但结果将是值得的,我们将在这个过程中学到很多有用的信息。

让我们开始吧!

首先,我们要做的是为每个推文添加一个转发计数,并跟踪用户投票赞成的所有推文。为了实现这一点,我们需要创建一个新的UserFollowers数据模型。

UserFollowers 数据模型

当一个用户被另一个用户关注时,我们需要在数据库中存储以下信息:

  • 用户被关注的日期。我们需要这个信息来显示在一段时间内拥有最多关注者的用户。

  • 用户拥有的关注者数量。

  • 关注我们用户的用户列表。

这是为了防止用户两次关注同一个用户。

为此,我们将创建一个名为UserFollowers的新数据模型。打开user_profile/model.py并将以下类添加到其中:

class UserFollowers(models.Model):
  user = models.ForeignKey(User, unique=True))
  date = models.DateTimeField(auto_now_add=True)
  count = models.IntegerField(default=1))
  followers = models.ManyToManyField(User, related_name='followers')
  def __str__(self):
    return '%s, %s' % self.user, self.count

这个数据模型利用了一些重要的特性,所以我们将逐个介绍它的字段。用户字段是一个外键,指回被关注的用户。我们希望它是唯一的,这样同一个用户就不能被多次关注。

日期字段的类型是models.DateTimeField。顾名思义,您可以使用此字段存储日期/时间值。参数auto_now_add告诉 Django 在首次创建此数据模型的对象时自动将此字段设置为当前日期/时间。

计数字段的类型是models.IntegerField。该字段保存一个整数值。通过在该字段使用default=1参数,我们告诉 Django 在首次创建此数据模型的对象时将字段的值设置为 1。

以下的ManyToManyField参数包含了关注此用户的用户列表。

注意

这里,related_name='followers'参数必须作为第二个参数给出。用户和关注者都指向相同的类user,如果通过相关名称进行区分,可能会出现错误,例如,访问字段user的访问器与相关的 m2m 字段User.userfollowers_set冲突。

将数据模型代码输入到user_profile/models.py文件后,运行以下命令在数据库中创建相应的表:

$ python manage.py syncdb

有了这个,我们就可以存储所有我们需要维护关注者的信息。

接下来,我们将创建一个视图,用户可以通过单击其个人资料名称旁边的关注按钮来关注其他用户。

如果访问的用户不是已经关注你的用户,那么应该有一个按钮来关注该用户。如果用户已经被关注,同样的按钮应该允许取消关注。

让我们编辑现有的用户个人资料,profile.html

对用户名添加用户图标,我们可以使用以下 Bootstrap 图标。这是默认 Bootstrap 附带的图标集。

  {% block navbar %}
  <p class="navbar-text navbar-right">
    <span class="glyphicon glyphicon-user"></span> {{ user.username }}
  </p>
  {% endblock %}

我们还将在个人资料页面上设计一个新的推文发布文本框。更新后的user_profile.html文件如下:

  {% extends "base.html" %}
  {% block navbar %}
  <p class="navbar-text navbar-right">
    <span class="glyphicon glyphicon-user"></span> {{ user.username }}
  </p>
  {% endblock %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-6 col-md-offset-3 column">
      <form id="search-form" action="post/" method="POST">{% csrf_token %}
        <div class="input-group">
          {{ form.text.errors }}
          {{ form.text }}
          {{ form.country.as_hidden }}
          <span class="input-group-btn">
            <button class="btn btn-default" type="submit">Post</button>
          </span>
        </div><!-- /input-group -->
      </form>
    </div>
    <h1>&nbsp;</h1>
    <div class="col-md-12 column">
      {% for tweet in tweets %}
      <div class="well">
        <span>{{ tweet.text }}</span>
      </div>
      {% endfor %}
    </div>
  </div>
  {% endblock %}

更新forms.py文件以呈现一个新表单:

class TweetForm(forms.Form):
  text = forms.CharField(widget=forms.Textarea(attrs={'rows': 1, 'cols': 85, 'class':'form-control', 'placeholder': 'Post a new Tweet'}), max_length=160)
  country = forms.CharField(widget=forms.HiddenInput())

表单的更新 UI 将如下所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00305.jpeg

要添加关注用户的功能,我们首先需要创建另一个用户。我们将遵循之前使用的相同方法,即通过 Django 管理员。

我们一直在推迟的一个非常重要的事情是用户登录和注册。没有它,关注功能无法使用。我们将首先实现 Django 登录,然后再转向关注功能。

用户登录模型

为了实现用户登录,我们需要为登录和注册添加默认 URL。我们将在urls.py文件中添加以下 URL 模式:

  url(r'^login/$', 'django.contrib.auth.views.login'),
  url(r'^logout/$', 'django.contrib.auth.views.logout')

现在,我们的urls.py文件将如下所示:

  from django.conf.urls import patterns, include, url
  from django.contrib import admin
  from tweet.views import Index, Profile, PostTweet, HashTagCloud, Search, SearchHashTag, HashTagJson
  admin.autodiscover()

  urlpatterns = patterns('',
    url(r'^$', Index.as_view()),
    url(r'^user/(\w+)/$', Profile.as_view()),
    url(r'^admin/', include(admin.site.urls)),
    url(r'^user/(\w+)/post/$', PostTweet.as_view()),
    url(r'^hashTag/(\w+)/$', HashTagCloud.as_view()),
    url(r'^search/$', Search.as_view()),
    url(r'^search/hashTag$', SearchHashTag.as_view()),
    url(r'^hashtag.json$', HashTagJson.as_view()),
    url(r'^login/$', 'django.contrib.auth.views.login'),
    url(r'^logout/$', 'django.contrib.auth.views.logout')   
  )

登录和注销视图都有默认模板名称,分别为registration/login.htmlregistration/logged_out.html。因为这些视图是特定于用户而不是我们可重用的应用程序,我们将使用以下命令在mytweets项目内创建一个新的模板/registration 目录:

 $  mkdir -p mytweets/templates/registration

然后,创建一个简单的登录和注销页面。在login.html文件中使用以下代码片段:

  {% extends "base.html" %}
  {% block content %}
  {% if form.errors %}
  <p>Your username and password didn't match. Please try again.</p>
  {% endif %}
  <form method="post" action="{% url 'django.contrib.auth.views.login' %}">
    {% csrf_token %}
    <table>
      <tr>
        <td>{{ form.username.label_tag }}</td>
        <td>{{ form.username }}</td>
      </tr>
      <tr>
        <td>{{ form.password.label_tag }}</td>
        <td>{{ form.password }}</td>
      </tr>
    </table>
    <input type="submit" value="login"/>
    <input type="hidden" name="next" value="{{ next }}"/>
  </form>
  {% endblock %}

logout.html文件中使用以下代码片段:

  {% extends "base.html" %}
  {% block content %}
    You have been Logged out!
  {% endblock %}

我们刚刚启用了 Django 的默认身份验证系统。由于这是基本授权,它具有特定重定向的预定义 URL。例如,我们已经知道/login将把用户带到/registration/login.html页面。同样,一旦用户经过身份验证,他们将被重定向到 URLaccounts/profile。在我们的项目中,每个用户都有一个自定义的 URL。我们将在settings.py文件中更新这些条目

LOGIN_REDIRECT_URL = '/profile'
LOGIN_URL = 'django.contrib.auth.views.login'

为了保持简单,我们将只创建一个视图,该视图将带着经过身份验证的用户到个人资料,然后将用户重定向到他们的个人资料页面。基本上,我们将在有效身份验证后构造用户名的参数;换句话说,将在单独的类视图中生成/profile | /profile/<username>。为此,我们还需要创建以下 URL 条目:

  url(r'^profile/$', UserRedirect.as_view()),

以及Profile重定向类和get()方法如下:

class UserRedirect(View):
  def get(self, request):
  return HttpResponseRedirect('/user/'+request.user.username)

就是这样。现在每个已登录用户都将被重定向到他的个人资料页面。

现在,回到最初的问题,当用户访问另一个用户的个人资料时,他们将有选择关注该用户的个人资料;这意味着关注者将在他们的主页上获取所有发布的推文的更新。

一旦关注了用户,关注者将有选项取消关注该用户,如果用户访问自己的个人资料,他们应该根本看不到任何东西。

用户个人资料的更新代码如下:

  {% extends "base.html" %}
  {% block navbar %}
  <p class="navbar-text navbar-left">
    <span class="glyphicon glyphicon-user"> </span> {{ profile.username }}'s Profile Page
    {% if profile.username != user.username %}
    <span class="btn btn-xs btn-default follow-btn" title="Click to follow {{ profile.username }}">
    <input id="follow" type="hidden" name="follow" value="{{ profile.username }}">
    <span class="glyphicon glyphicon-plus"> </span> {% if following %} Unfollow {% else %} Follow {% endif %}</span>
    {% endif %}
  </p>
  <p class="navbar-text navbar-right">
    <span class="glyphicon glyphicon-user"></span> {{ user.username }}
  </p>
  {% endblock %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-6 col-md-offset-3 column">
      <form id="search-form" action="post/" method="POST">{% csrf_token %}
        <div class="input-group">
          {{ form.text.errors }}
          {{ form.text }}
          {{ form.country.as_hidden }}
          <span class="input-group-btn">
            <button class="btn btn-default" type="submit">Post</button>
          </span>
        </div>
        <!-- /input-group -->
      </form>
    </div>
    <h1>&nbsp;</h1>
    <div class="col-md-12 column">
      {% for tweet in tweets %}
      <div class="well">
        <span>{{ tweet.text }}</span>
      </div>
      {% endfor %}
    </div>
  </div>
  {% endblock %}

以下代码检查用户是否正在查看自己的个人资料;如果是,他们将不会看到关注按钮。它还检查已登录的用户是否正在关注他们访问的个人资料;如果是,将显示取消关注按钮,如果不是,将显示关注按钮。

  {% if profile.username != user.username %}
  <span class="btn btn-xs btn-default follow-btn" title="Click to follow {{ profile.username }}">
    <input id="follow" type="hidden" name="follow" value="{{ profile.username }}">
  <span class="glyphicon glyphicon-plus"> </span> {% if following %} Unfollow {% else %} Follow {% endif %}</span>
  {% endif %}

为了呈现更新后的视图,class Profile()也已更新如下:

class Profile(LoginRequiredMixin, View):
  """User Profile page reachable from /user/<username> URL"""
  def get(self, request, username):
    params = dict()
    userProfile = User.objects.get(username=username))
    userFollower = UserFollower.objects.get(user=userProfile)
    if userFollower.followers.filter(username=request.user.username).exists():
      params["following"] = True
    else:
      params["following"] = False
      form = TweetForm(initial={'country': 'Global'})
      search_form = SearchForm()
      tweets = Tweet.objects.filter(user=userProfile).order_by('-created_date')
      params["tweets"] = tweets
      params["profile"] = userProfile
      params["form"] = form
      params["search"] = search_form
      return render(request, 'profile.html', params)

以下代码检查已登录用户是否是正在访问的用户的关注者:

  if userFollower.followers.filter(username=request.user.username).exists():

添加或删除关注者

让我们为个人资料创建一个post()方法,根据参数添加或删除关注者:

  def post(self, request, username):
    follow = request.POST['follow']
    user = User.objects.get(username= request.user.username)))
    userProfile === User.objects.get(username=username)
    userFollower, status = UserFollower.objects.get_or_create(user=userProfile)
    if follow=='true':
      #follow user
      userFollower.followers.add(user)
    else:
      #unfollow user
      userFollower.followers.remove(user)
    return HttpResponse(json.dumps(""), content_type="application/json")

这是一个简单的函数,用于检查参数以将用户添加到或从关注者列表中删除。

profile.html文件中的关注按钮部分应更新为类名,以便我们可以触发 JavaScript 事件功能,如下所示:

<p class="navbar-text navbar-left">
  <span class="glyphicon glyphicon-user"> </span> {{ profile.username }}'s Profile Page
    {% if profile.username != user.username %}
    <span class="btn btn-xs btn-default follow-btn" title="Click to follow {{ profile.username }}" value="{{ following }}" username="{{ profile.username }}">
      <span class="glyphicon glyphicon-plus"></span><span class="follow-text">
      {{ following|yesno:"Unfollow,Follow" }}
    </span>
  </span>
  {% endif %}
</p>

最后,让我们创建profile.js文件,其中包含post()方法,每当单击关注/取消关注按钮时都会调用该方法:

创建一个名为profile.js的 JavaScript 文件,并添加以下代码:

$(".follow-btn").click(function () {
  var username = $(this).attr('username');
  var follow = $(this).attr('value') != "True";
  $.ajax({
    type: "POST",
    url:  "/user/"+username+"/",
    data: { username: username , follow : follow  },
    success: function () {
      window.location.reload();
    },
    error: function () {
      alert("ERROR !!");
    }
  })
});

不要忘记在页面底部的profile.html文件中添加此 JavaScript 文件,如下面的代码所示:

  {% block js %}
  <script src="img/profile.js' %}"></script>
  {% endblock %}

显示最受关注的用户

在我们实现了关注用户的功能之后,我们可以继续进行新页面设计,我们将在其中列出最受关注的用户。这个页面的逻辑可以被重用来设计具有最多评论数量的页面。

这个页面设计的基本组件包括:

  • 视图users.html文件

  • 控制器:最受关注的用户

  • URL 映射

view.html文件中添加以下内容:

  {% extends "base.html" %}
  {% load staticfiles %}
  {% block navbar %}
  <p class="navbar-text navbar-right">
    <span class="glyphicon glyphicon-user"></span> {{ user.username }}
  </p>
  {% endblock %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-12 column">
      {% for userFollower in userFollowers %}
      <div class="well">
        <span class="username">{{ userFollower.user.username }}</span>
        <span class="count text-muted"> ({{ userFollower.count }} followers)</span>
      </div>
      {% endfor %}
    </div>
  </div>
  {% endblock %}

在控制器中添加以下类:

class MostFollowedUsers(View):
  def get(self, request):
    userFollowers = UserFollower.objects.order_by('-count')
    params = dict()
    params['userFollowers'] = userFollowers
    return render(request, 'users.html', params)

以下一行按照拥有最多关注者的顺序对关注者进行排序:

  userFollowers = UserFollower.objects.order_by('-count')

我们还需要更新 URL 映射,如下所示:

  url(r'^mostFollowed/$', MostFollowedUsers.as_view()),

就这些了!我们已经完成了一个页面,其中所有用户都按关注者数量列出。如果数量太高,您还可以使用这种基本的 Python 列表语法进行限制:

  userFollowers = UserFollower.objects.order_by('-count')[:10]

这将只列出前 10 名用户。

摘要

在本章中,我们学习了如何创建登录、注销和注册页面模板。我们还学会了如何允许关注另一个用户并显示最受关注的用户。

下一章将转到新的主题。迟早,您将需要一个管理界面来管理应用程序的数据模型。幸运的是,Django 带有一个成熟的管理界面,可以立即使用。我们将在下一章中学习如何启用和自定义此界面,所以请继续阅读!

第八章:创建管理界面

在本章中,我们将学习使用 Django 的内置功能的管理员界面的特性。我们还将介绍如何以自定义方式显示推文,包括侧边栏或启用分页。本章将涉及以下主题:

  • 自定义管理界面

  • 自定义列表页面

  • 覆盖管理模板

  • 用户、组和权限

  • 用户权限

  • 组权限

  • 在视图中使用权限

  • 将内容组织成页面(分页)

自定义管理界面

Django 提供的管理界面非常强大和灵活,从 1.6 版本开始,默认情况下就已激活。这将为您的站点提供一个功能齐全的管理工具包。尽管管理应用程序对大多数需求应该足够了,但 Django 提供了几种自定义和增强它的方法。除了指定哪些模型可用于管理界面外,您还可以指定如何呈现列表页面,甚至覆盖用于呈现管理页面的模板。因此,让我们了解这些功能。

自定义列表页面

正如我们在上一章中看到的,我们使用以下方法将我们的模型类注册到管理界面:

  • admin.site.register (Tweet)

  • admin.site.register (Hashtag)

  • admin.site.register (UserFollower)

我们还可以自定义管理页面的几个方面。让我们通过示例来了解这一点。推文列表页面显示每条推文的字符串表示,如下面的屏幕截图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00306.jpeg

如果此页面能够显示发布推文的用户的名称以及发布时间,那不是更有用吗?事实证明,实现这个功能只需要添加几行代码。

编辑tweet/admin.py文件中的推文模型如下:

  from django.contrib import admin
  from models import Tweet, HashTag
  from user_profile.models import UserFollower
  # Register your models here.
  admin.site.register(Tweet)
  admin.site.register(HashTag)
  admin.site.register(UserFollower)

在“#在此注册您的模型”上方添加新的代码行,更新后的代码将如下所示:

  from django.contrib import admin
  from models import Tweet, HashTag
  from user_profile.models import UserFollower
 class TweetAdmin(admin.ModelAdmin):
 list_display = ('user', 'text', 'created_date')
  # Register your models here.
  admin.site.register(Tweet, TweetAdmin)))
  admin.site.register(HashTag)
  admin.site.register(UserFollower)

此代码为TweetAdmin()类的管理员视图添加了额外的列:

  class TweetAdmin(admin.ModelAdmin):
    list_display = ('user', 'text', 'created_date')

此外,我们为管理员推文传递了一个额外的参数;即admin.site.register(Tweet)现在变成了admin.site.register(Tweet, TweetAdmin)。刷新同一页面,注意变化,如下面的屏幕截图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00307.jpeg

表现得更有条理了!我们只需在Tweet模型的TweetAdmin()类中定义一个名为list_display的元组属性。该元组包含要在列表页面中使用的字段的名称。

在 Admin 类中还有其他属性可以定义;每个属性应该定义为一个或多个字段名称的元组。

  • list_filter:如果定义了,这将创建一个侧边栏,其中包含可以根据模型中一个或多个字段来过滤对象的链接。

  • ordering:用于在列表页面中对对象进行排序的字段。

  • search_fields:如果定义了,它将创建一个可用于搜索的搜索字段。字段名称前面加上减号,并且根据一个或多个字段的数据模型中的可用对象,使用降序而不是升序。

让我们在推文列表页面中利用前述每个属性。再次编辑tweet/admin.py文件中的推文模型,并追加以下突出显示的行:

  from django.contrib import admin
  from models import Tweet, HashTag
  from user_profile.models import UserFollower

  class TweetAdmin(admin.ModelAdmin):
    list_display = ('user', 'text', 'created_date')
 list_filter = ('user', )
 ordering = ('-created_date', )
 search_fields = ('text', )

  # Register your models here.
  admin.site.register(Tweet, TweetAdmin)
  admin.site.register(HashTag)
  admin.site.register(UserFollower)

使用这些属性后的效果如下:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00308.jpeg

正如您所看到的,我们能够只用几行代码来自定义和增强推文列表页面。接下来,我们将学习如何自定义用于呈现管理页面的模板,这将使我们对管理界面有更大的控制权。

覆盖管理模板

有时您想要更改管理界面的外观或移动各种管理页面上的元素并重新排列它们。幸运的是,管理界面足够灵活,可以通过允许我们覆盖其模板来执行所有这些操作及更多操作。自定义管理模板的过程很简单。首先,您将模板从管理应用程序文件夹复制到项目的模板文件夹中,然后编辑此模板并根据您的喜好进行自定义。管理模板的位置取决于 Django 的安装位置。以下是 Django 在主要操作系统下的默认安装路径列表:

  • Windows:C:\PythonXX\Lib\site-packages\django

  • UNIX 和 Linux:/usr/lib/pythonX.X/site-packages/django

  • Mac OS X:/Library/Python/X.X/site-packages/django

(这里,X.X是您系统上 Python 的版本。site-packages文件夹也可以被称为dist-packages。)

如果您在操作系统的默认安装路径中找不到 Django,请执行文件系统搜索django-admin.py。您会得到多个结果,但您想要的结果将在 Django 安装路径下,位于名为bin的文件夹内。

找到 Django 安装路径后,打开django/contrib/admin/templates/,您将找到管理应用程序使用的模板。

此目录中有许多文件,但最重要的文件是这些:

  • admin/base_site.html:这是管理的基本模板。此模板生成界面。所有页面都继承自以下模板。

  • admin/change_list.html:此模板生成可用对象的列表。

  • admin/change_form.html:此模板生成用于添加或编辑对象的表单。

  • admin/delete_confirmation.html:此模板在删除对象时生成确认页面。

让我们尝试自定义其中一个模板。假设我们想要更改所有管理页面顶部的字符串Django administration。为此,在我们项目的templates文件夹内创建一个名为admin的文件夹,并将admin/base_site.html文件复制到其中。然后,编辑文件以将所有Django的实例更改为Django Tweet

  {% extends "admin/base.html" %}
  {% load i18n %}
  {% block title %}{{ title|escape }} |
  {% trans 'Django Tweet site admin' %}{% endblock %}
  {% block branding %}
  <h1 id="site-name">{% trans 'Django Tweet administration' %}</h1>
  {% endblock %}
  {% block nav-global %}{% endblock %}

结果将如下所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00309.jpeg

由于管理模板的模块化设计,通常不需要也不建议替换整个模板。通常最好只覆盖您需要更改的模板部分。

这个过程非常简单,不是吗?随意尝试其他模板。例如,您可能想要向列表或编辑页面添加帮助消息。

管理模板利用了 Django 模板系统的许多高级功能,因此如果您看到一个您不熟悉的模板标签,可以参考 Django 文档。

用户、组和权限

到目前为止,我们一直使用manage.py syncdb命令创建的超级用户帐户登录到管理界面。但实际上,您可能有其他受信任的用户需要访问管理页面。在本节中,我们将看到如何允许其他用户使用管理界面,并在此过程中了解更多关于 Django 权限系统的信息。

但在我们继续之前,我想强调一点:只有受信任的用户应该被授予对管理页面的访问权限。管理界面是一个非常强大的工具,所以只有你熟悉的人才应该被授予访问权限。

用户权限

如果数据库中除了超级用户之外没有其他用户,请使用我们在第七章中构建的注册表单创建一个新用户帐户,关注和评论。或者,您可以通过单击用户,然后单击添加用户来使用管理界面本身。

接下来,返回用户列表,然后单击新创建的用户的名称。您将获得一个表单,可用于编辑用户帐户的各个方面,例如姓名和电子邮件信息。在编辑表单的权限部分下,您将找到一个名为员工状态的复选框。启用此复选框将允许新用户进入管理界面。但是,他们登录后将无法做太多事情,因为此复选框仅授予对管理区域的访问权限;它不会赋予查看或更改数据的能力。

为了给新用户足够的权限来更改数据模型,您可以启用超级用户状态复选框,这将授予新用户执行任何所需功能的完全权限。此选项使帐户与manage.py syncdb命令创建的超级用户帐户一样强大。

然而,总的来说,不希望将用户对所有内容都授予完全访问权限。因此,Django 允许您通过权限系统对用户的操作进行精细控制。在超级用户状态复选框下方,您将找到可以授予用户的权限列表。如果您查看此列表,您将发现每个数据模型都有三种类型的权限:

  • 向数据模型添加对象

  • 更改数据模型中的对象

  • 从数据模型中删除对象

这些权限是由 Django 自动生成的,用于包含 Admin 类的数据模型。使用箭头按钮向我们正在编辑的帐户授予一些权限。例如,给予帐户添加、编辑和删除推文和主题标签的能力。接下来,注销然后使用新帐户再次登录到管理界面。您会注意到您只能管理推文和主题标签数据模型。

用户编辑页面的权限部分还包含一个名为活跃的复选框。此复选框可用作全局开关,用于启用或禁用帐户。取消选中时,用户将无法登录到主站点或管理区域。

组权限

如果您有大量共享相同权限的用户,编辑每个用户帐户并为他们分配相同权限将是一项繁琐且容易出错的任务。因此,Django 提供了另一个用户管理设施:组。简单来说,组是对共享相同权限的用户进行分类的一种方式。您可以创建一个组并为其分配权限。当您将用户添加到组时,该用户将被授予组的所有权限。

创建组与创建其他数据模型并没有太大的不同。在管理界面的主页上点击,然后点击添加组。接下来,输入组名并为组分配一些权限;最后,点击保存

要将用户添加到组中,请编辑用户帐户,滚动到编辑表单中的部分,然后选择要将用户添加到的任何组。

在视图中使用权限

尽管到目前为止我们只在管理界面中使用了权限,但 Django 还允许我们在编写视图时利用权限系统。在编写视图时,可以使用权限来授予一组用户对特定功能或页面的访问权限,例如私人内容。我们将在本节中了解可以用来实现此目的的方法。我们不会实际更改应用程序的代码,但如果您想尝试解释的方法,请随意这样做。

如果您想要检查用户是否具有特定权限,可以在User对象上使用has_perm()方法。该方法采用表示权限的字符串,格式如下:

app.operation_model

app参数指定了模型所在的应用程序的名称;operation参数可以是addchangedeletemodel参数指定了模型的名称。

例如,要检查用户是否可以添加推文,使用以下代码:

  user.has_perm('tweets.add_tweet')

要检查用户是否可以更改推文,使用以下代码:

  user.has_perm('tweets.change_tweet')

此外,Django 提供了一个名为decorator的函数,可以用来限制只有特定权限的用户才能访问视图。这个装饰器叫做permission_required,位于django.contrib.auth.decorators包中。

使用这个装饰器类似于我们使用login_required函数的方式。这个装饰器函数是为了限制页面只对已登录用户开放。假设我们想要将tweet_save_page视图(在tweets/views.py文件中)限制为具有tweet.add_tweet权限的用户。为此,我们可以使用以下代码:

from django.contrib.auth.decorators import permission_required
@permission_required('tweets.add_tweet', login_url="/login/")
def tweet_save_page(request):
  # [...]

这个装饰器接受两个参数:要检查的权限以及如果用户没有所需权限时要重定向用户的位置。

使用has_perm方法还是permission_required装饰器取决于您想要的控制级别。如果您需要控制对整个视图的访问权限,请使用permission_required装饰器。但是,如果您需要对视图内的权限进行更精细的控制,请使用has_perm方法。这两种方法应该足够满足任何权限相关的需求。

将内容组织成页面 - 分页

在之前的章节中,我们已经涵盖了列出用户的推文和列出最多关注的用户等内容,但是考虑到当这些小数字扩大并且我们开始获得大量结果时的使用情况。为了应对这种情况,我们应该调整我们的代码以支持分页。

页面的大小会增加,而在页面中找到项目将变得困难。幸运的是,这有一个简单直观的解决方案:分页。分页是将内容分成页面的过程。而且,正如以往一样,Django 已经有一个实现这个功能的组件,可以供我们使用!

如果我们有一大堆推文,我们将这些推文分成每页十个(左右)项目的页面,并向用户呈现第一页,并提供链接以浏览其他页面。

Django 分页功能封装在一个名为Paginator的类中,该类位于django.core.paginator包中。让我们使用交互式控制台来学习这个类的接口:

  from tweet.models import *
  from django.core.paginator import Paginator
  query_set = Tweet.objects.all()
  paginator = Paginator(query_set, 10)

注意

使用python manage.py shell命令打开 Django shell。

在这里,我们导入一些类,构建一个包含所有书签的查询集,并实例化一个名为Paginator的对象。这个类的构造函数接受要分页的查询集,以及每页的项目数。

让我们看看如何从Paginator对象中检索信息(当然,结果会根据您拥有的书签数量而有所不同):

>>> paginator.num_pages # Number of pages
1
>>> paginator.count # Total number of items
5
# Items in first page (index is zero-based)
>>> paginator.object_list
[<Tweet: #django is awesome.>, <Tweet: I love Django too.>, <Tweet: Django makes my day.>, <Tweet: #Django is fun.>, <Tweet: #Django is fun.>]

# Does the first page have a previous page?
>>> page1 = paginator.page(1)
# Stores the first page object to page1
>>> page1.has_previous()
False
# Does the first page have a next page?
>>> page1.has_next()
True

正如您所看到的,Paginator为我们做了大部分的工作。它接受一个查询集,将其分成页面,并使我们能够将查询集呈现为多个页面。

让我们将分页功能实现到我们的一个视图中,例如推文页面。打开tweet/views.py并修改user_page视图如下:

我们有我们的用户个人资料页面列表,其中包含以下类:

  class Profile(LoginRequiredMixin, View):
    """User Profile page reachable from /user/<username> URL"""
    def get(self, request, username):
      params = dict()
      userProfile = User.objects.get(username=username)
      userFollower = UserFollower.objects.get(user=userProfile)
      if userFollower.followers.filter(username=request.user.username).exists():
        params["following"] = True
      else:
        params["following"] = False
        form = TweetForm(initial={'country': 'Global'})
        search_form = SearchForm()
        tweets = Tweet.objects.filter(user=userProfile).order_by('-created_date')
        params["tweets"] = tweets
        params["profile"] = userProfile
        params["form"] = form
        params["search"] = search_form
        return render(request, 'profile.html', params)

我们需要修改前面的代码以使用分页:

  class Profile(LoginRequiredMixin, View):
    """User Profile page reachable from /user/<username> URL"""
    def get(self, request, username):
      params = dict()
      userProfile = User.objects.get(username=username)
      userFollower = UserFollower.objects.get(user=userProfile)
      if userFollower.followers.filter(username=request.user.username).exists():
        params["following"] = True
      else:
        params["following"] = False
        form = TweetForm(initial={'country': 'Global'})
        search_form = SearchForm()
        tweets = Tweet.objects.filter(user=userProfile).order_by('-created_date')
        paginator = Paginator(tweets, TWEET_PER_PAGE)
        page = request.GET.get('page')
      try:
        tweets = paginator.page(page)
        except PageNotAnInteger:
          # If page is not an integer, deliver first page.
          tweets = paginator.page(1)
      except EmptyPage:
        # If page is out of range (e.g. 9999), deliver last page of results.
        tweets = paginator.page(paginator.num_pages)
        params["tweets"] = tweets
        params["profile"] = userProfile
        params["form"] = form
        params["search"] = search_form
        return render(request, 'profile.html', params)

以下代码片段主要在前面的代码中实现了分页的魔法:

        tweets = Tweet.objects.filter(user=userProfile).order_by('-created_date')
        paginator = Paginator(tweets, TWEET_PER_PAGE)
        page = request.GET.get('page')
        try:
          tweets = paginator.page(page)
        except PageNotAnInteger:
          # If page is not an integer, deliver first page.
          tweets = paginator.page(1)
        except EmptyPage:
          # If page is out of range (e.g. 9999), deliver last page of results.
          tweets = paginator.page(paginator.num_pages)

为了使这段代码工作,需要在settings.py文件中添加TWEET_PER_PAGE = 5参数,并在前面的代码中,只需在代码顶部添加import settings.py语句。

我们从请求中读取了一个名为pageget变量,告诉 Django 请求了哪个页面。我们还在settings.py文件中设置了TWEET_PER_PAGE参数,以显示单个页面上的推文数量。对于这种特定情况,我们选择了5

paginator = Paginator(tweets, TWEET_PER_PAGE)方法创建一个分页对象,其中包含有关查询的所有信息。

现在,只需使用 URL user/<username>/?page=<page_numer>,页面将如下截图所示。第一张图片显示了带有 URL 中页面编号的用户推文。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00310.jpeg

以下截图显示了用户主页上的推文列表:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-dj-webdev/img/image00311.jpeg

总结

尽管本章相对较短,但我们学会了如何实现许多事情。这强调了 Django 让您只需几行代码就能做很多事情的事实。您学会了如何利用 Django 强大的管理界面,如何自定义它,以及如何利用 Django 提供的全面权限系统。

在下一章中,您将了解到几乎每个 Web 2.0 应用程序中都有的一些令人兴奋的功能。

第九章:扩展和部署

在本章中,我们将通过利用各种 Django 框架功能来准备我们的应用程序以在生产中部署。我们将添加对多种语言的支持,通过缓存和自动化测试来提高性能,并为生产环境配置项目。本章中有很多有趣和有用的信息,因此在将应用程序发布到网上之前,请确保您仔细阅读!

在本章中,您将学习以下主题:

  • 向朋友发送邀请电子邮件

  • 国际化(i18n)-提供多种语言的站点

  • 缓存-在高流量期间提高站点性能

  • 单元测试-自动化测试应用程序的过程

向朋友发送邀请电子邮件

使我们的用户邀请他们的朋友具有许多好处。如果他们的朋友已经使用我们的网站,那么他们更有可能加入我们的网站。加入后,他们还会邀请他们的朋友,依此类推,这意味着我们的应用程序会有越来越多的用户。因此,在我们的应用程序中包含“邀请朋友”的功能是一个好主意。

构建此功能需要以下组件:

  • 一个邀请数据模型,用于在数据库中存储邀请

  • 用户可以在其中输入他们朋友的电子邮件 ID 并发送邀请的表单

  • 带有激活链接的邀请电子邮件

  • 处理电子邮件中发送的激活链接的机制

在本节中,我们将实现这些组件中的每一个。但是,因为本节涉及发送电子邮件,我们首先需要通过向settings.py文件添加一些选项来配置 Django 发送电子邮件。因此,打开settings.py文件并添加以下行:

  SITE_HOST = '127.0.0.1:8000'
  DEFAULT_FROM_EMAIL = 'MyTwitter <noreply@mytwitter.com>'
  EMAIL_HOST = 'mail.yourisp.com'
  EMAIL_PORT = ''
  EMAIL_HOST_USER = 'username+mail.yourisp.com'
  EMAIL_HOST_PASSWORD = ''

让我们看看前面代码中的每个变量都做了什么:

  • SITE_HOST:这是您服务器的主机名。现在将其保留为127.0.0.1:8000。在下一章中部署服务器时,我们将更改此设置。

  • DEFAULT_FROM_EMAIL:这是出站电子邮件服务器From字段中显示的电子邮件地址。对于主机用户名,请输入您的用户名加上您的电子邮件服务器,如前面的代码片段所示。如果您的 ISP 不需要这些字段,请将其留空。

  • EMAIL_HOST:这是您的电子邮件服务器的主机名。

  • EMAIL_PORT:这是出站电子邮件服务器的端口号。如果将其留空,则将使用默认值(25)。您还需要从 ISP 那里获取此信息。

  • EMAIL_HOST_USEREMAIL_HOST_PASSWORD:这是 Django 发送的电子邮件的用户名和密码。

如果您的开发计算机没有运行邮件服务器,很可能是这种情况,那么您需要输入 ISP 的出站电子邮件服务器。联系您的 ISP 以获取更多信息。

要验证您的设置是否正确,请启动交互式 shell 并输入以下内容:

>>> from django.core.mail import EmailMessage
>>> email = EmailMessage('Hello', 'World', to=['your_email@example.com'])
>>> email.send()

your_email@example.com参数替换为您的实际电子邮件地址。如果前面的发送邮件调用没有引发异常并且您收到了邮件,那么一切都设置好了。否则,您需要与 ISP 验证您的设置并重试。

但是,如果您没有从 ISP 那里获得任何信息怎么办?然后我们尝试另一种方式:使用 Gmail 发送邮件(当然,不是作为noreply@mytweet.com,而是从您的真实电子邮件 ID)。让我们看看您需要对MyTweeets项目的settings.py文件进行哪些更改。

完全删除以前的settings.py文件条目,并添加以下内容:

  EMAIL_USE_TLS = True
  EMAIL_HOST = 'smtp.gmail.com'
  EMAIL_HOST_USER = 'your-gmail-email-id'
  EMAIL_HOST_PASSWORD = 'your-gmail-application-password'
  EMAIL_PORT = 587
  SITE_HOST = '127.0.0.1:8000'

如果您遇到错误,例如:

 (534, '5.7.9 Application-specific password required. Learn more at\n5.7.9 http://support.google.com/accounts/bin/answer.py?answer=185833 zr2sm8629305pbb.83 - gsmtp')

这意味着EMAIL_HOST_PASSWORD参数需要一个应用程序授权密码,而不是您的电子邮件密码。请按照主机部分中提到的链接获取有关如何创建的更多详细信息。

设置好这些东西后,尝试使用以下命令从 shell 再次发送邮件:

>>> from django.core.mail import EmailMessage
>>> email = EmailMessage('Hello', 'World', to=['your_email@example.com'])
>>> email.send()

在这里,your_email@example.com参数是您想发送邮件的任何电子邮件地址。邮件的发件人地址将是我们传递给以下变量的 Gmail 电子邮件地址:

 EMAIL_HOST_USER = 'your-gmail-email-id'

现在,一旦设置正确,使用 Django 发送邮件就像小菜一碟!我们将使用EmailMessage函数发送邀请邮件,但首先,让我们创建一个数据模型来存储邀请。

邀请数据模型

邀请包括以下信息:

  • 收件人姓名

  • 收件人邮箱

  • 发件人的用户对象

我们还需要为邀请存储一个激活码。该代码将在邀请邮件中发送。该代码将有两个目的:

  • 在接受邀请之前,我们可以使用该代码验证邀请是否实际存在于数据库中

  • 接受邀请后,我们可以使用该代码从数据库中检索邀请信息,并跟踪发件人和收件人之间的关系

考虑到上述信息,让我们创建邀请数据模型。打开user_profile/models.py文件,并将以下代码追加到其中:

  class Invitation(models.Model):
    name = models.CharField(maxlength=50)
    email = models.EmailField()
    code = models.CharField(maxlength=20)
    sender = models.ForeignKey(User)
    def __unicode__(self):
        return u'%s, %s' % (self.sender.username, self.email)

在这个模型中没有什么新的或难以理解的。我们只是为收件人姓名、收件人电子邮件、激活码和邀请发件人定义了字段。我们还为调试创建了一个__unicode__方法,并在管理界面中启用了该模型。不要忘记运行python manage.py syncdb命令来在数据库中创建新模型的表。

我们还将为此创建邀请表单。在user_profile目录中创建一个名为forms.py的文件,并使用以下代码进行更新:

from django import forms

class InvitationForm(forms.Form):
  email = forms.CharField(widget=forms.TextInput(attrs={'size': 32, 'placeholder': 'Email Address of Friend to invite.', 'class':'form-control search-query'}))

创建发送邀请的视图页面类似于创建我们为搜索和推文表单创建的其他页面,通过创建一个名为template/invite.html的新文件:

  {% extends "base.html" %}
  {% load staticfiles %}
  {% block content %}
  <div class="row clearfix">
    <div class="col-md-6 col-md-offset-3 column">
      {% if success == "1" %}
        <div class="alert alert-success" role="alert">Invitation Email was successfully sent to {{ email }}</div>
      {% endif %}
      {% if success == "0" %}
        <div class="alert alert-danger" role="alert">Failed to send Invitation Email to {{ email }}</div>
      {% endif %}
      <form id="search-form" action="" method="post">{% csrf_token %}
        <div class="input-group input-group-sm">
        {{ invite.email.errors }}
        {{ invite.email }}
          <span class="input-group-btn">
            <button class="btn btn-search" type="submit">Invite</button>
          </span>
        </div>
      </form>
    </div>
  </div>
  {% endblock %}

此方法的 URL 输入如下:

  url(r'^invite/$', Invite.as_view()),

现在,我们需要创建getpost方法来使用此表单发送邀请邮件。

由于发送邮件比推文更具体于用户,我们将在user_profile视图中创建此方法,而不是之前使用的推文视图。

使用以下代码更新user_profile/views.py文件:

from django.views.generic import View
from django.conf import settings
from django.shortcuts import render
from django.template import Context
from django.template.loader import render_to_string
from user_profile.forms import InvitationForm
from django.core.mail import EmailMultiAlternatives
from user_profile.models import Invitation, User
from django.http import HttpResponseRedirect
import hashlib

class Invite(View):
  def get(self, request):
    params = dict()
    success = request.GET.get('success')
    email = request.GET.get('email')
    invite = InvitationForm()
    params["invite"] = invite
    params["success"] = success
    params["email"] = email
    return render(request, 'invite.html', params)

  def post(self, request):
    form = InvitationForm(self.request.POST)
    if form.is_valid():
      email = form.cleaned_data['email']
      subject = 'Invitation to join MyTweet App'
      sender_name = request.user.username
      sender_email = request.user.email
      invite_code = Invite.generate_invite_code(email)
      link = 'http://%s/invite/accept/%s/' % (settings.SITE_HOST, invite_code)
      context = Context({"sender_name": sender_name, "sender_email": sender_email, "email": email, "link": link})
      invite_email_template = render_to_string('partials/_invite_email_template.html', context)
      msg = EmailMultiAlternatives(subject, invite_email_template, settings.EMAIL_HOST_USER, [email], cc=[settings.EMAIL_HOST_USER])
      user = User.objects.get(username=request.user.username)
      invitation = Invitation()
      invitation.email = email
      invitation.code = invite_code
      invitation.sender = user
      invitation.save()
      success = msg.send()
      return HttpResponseRedirect('/invite?success='+str(success)+'&email='+email)

  @staticmethod
  def generate_invite_code(email):
    secret = settings.SECRET_KEY
    if isinstance(email, unicode):
      email = email.encode('utf-8')
      activation_key = hashlib.sha1(secret+email).hexdigest()
      return activation_key

在这里,get()方法就像使用invite.html文件渲染邀请表单一样简单,并且初始未设置successemail变量。

post()方法使用通常的表单检查和变量提取概念;您将首次看到的代码如下:

  invite_code = Invite.generate_invite_code(email)

这实际上是一个静态函数调用,为每个受邀用户生成具有唯一密钥的激活令牌。当您加载名为_invite_email_template.html的模板并将以下变量传递给它时,render_to_string()方法将起作用:

  • sender_name:这是邀请或发件人的姓名

  • sender_email:这是发件人的电子邮件地址

  • email:这是被邀请人的电子邮件地址

  • link:这是邀请接受链接

然后使用该模板来渲染邀请邮件的正文。之后,我们使用EmailMultiAlternatives()方法发送邮件,就像我们在上一节的交互式会话中所做的那样。

这里有几点需要注意:

  • 激活链接的格式为http://SITE_HOST/invite/accept/CODE/。我们将在本节后面编写一个视图来处理此类 URL。

  • 这是我们第一次使用模板来渲染除网页以外的其他内容。正如您所见,模板系统非常灵活,允许我们构建电子邮件,以及网页或任何其他文本。

  • 我们使用render_to_string()render()方法构建消息正文,而不是通常的render_to_response调用。如果你还记得,这就是我们在本书早期渲染模板的方式。我们这样做是因为我们不是在渲染网页。

由于send方法加载名为_invite_email_template.html的模板,请在模板文件夹中创建一个同名文件并插入以下内容:

  Hi,
    {{ sender_name }}({{ sender_email }}) has invited you to join Mytweet.
    Please click {{ link }} to join.
This email was sent to {{ email }}. If you think this is a mistake Please ignore.

我们已经完成了“邀请朋友”功能的一半实现。目前,点击激活链接会产生 404 页面未找到错误,因此,接下来,我们将编写一个视图来处理它。

处理激活链接

我们取得了良好的进展;用户现在能够通过电子邮件邀请他们的朋友。下一步是构建一个处理邀请中激活链接的机制。以下是我们将要做的概述。

我们将构建一个视图来处理激活链接。此视图验证邀请码实际上是否存在于数据库中,并且注册的用户自动关注发送链接的用户并被重定向到注册页面。

让我们从为视图编写 URL 条目开始。打开urls.py文件并添加以下突出显示的行:

 url(r'^invite/accept/(\w+)/$', InviteAccept.as_view()),

user_profile/view.py文件中创建一个名为InviteAccept()的类。

从逻辑上讲,邀请接受将起作用,因为用户将被要求注册应用程序,如果他们已经注册,他们将被要求关注邀请他们的用户。

为了简单起见,我们将用户重定向到带有激活码的注册页面,这样当他们注册时,他们将自动成为关注者。让我们看一下以下代码:

class InviteAccept(View):
  def get(self, request, code):
    return HttpResponseRedirect('/register?code='+code)

然后,我们将用以下代码编写注册页面:

class Register(View):
  def get(self, request):
    params = dict()
    registration_form = RegisterForm()
    code = request.GET.get('code')
    params['code'] = code
    params['register'] = registration_form
    return render(request, 'registration/register.html', params)

  def post(self, request):
    form = RegisterForm(request.POST)
    if form.is_valid():
      username = form.cleaned_data['username']
      email = form.cleaned_data['email']
      password = form.cleaned_data['password']
      try:
        user = User.objects.get(username=username)                
      except:
        user = User()
        user.username = username
        user.email = email
        commit = True
        user = super(user, self).save(commit=False)
        user.set_password(password)
        if commit:
          user.save()
        return HttpResponseRedirect('/login')

如你所见,视图遵循邀请电子邮件中发送的 URL 格式。激活码是使用正则表达式从 URL 中捕获的,然后作为参数传递给视图。

这有点耗时,但我们能够充分利用我们的 Django 知识来实现它。您现在可以点击通过电子邮件收到的邀请链接,看看会发生什么。您将被重定向到注册页面;您可以在那里创建一个新账户,登录,并注意新账户和您的原始账户如何成为发送者的关注者。

国际化(i18n)-提供多种语言的网站

如果人们无法阅读我们应用的页面,他们就不会使用我们的应用。到目前为止,我们只关注说英语的用户。然而,全世界有许多人不懂英语或更喜欢使用他们的母语。为了吸引这些人,将我们应用的界面提供多种语言是个好主意。这将克服语言障碍,并为我们的应用打开新的前沿,特别是在英语不常用的地区。

正如你可能已经猜到的那样,Django 提供了将项目翻译成多种语言所需的所有组件。负责提供此功能的系统称为国际化系统i18n)。翻译 Django 项目的过程非常简单。

按照以下三个步骤进行:

  1. 指定应用程序中应翻译的字符串,例如,状态和错误消息是可翻译的,而用户名则不是。

  2. 为要支持的每种语言创建一个翻译文件。

  3. 启用和配置 i18n 系统。

我们将在以下各小节中详细介绍每个步骤。在本章节的最后,我们的应用将支持多种语言,您将能够轻松翻译任何其他 Django 项目。

将字符串标记为可翻译的

翻译应用程序的第一步是告诉 Django 哪些字符串应该被翻译。一般来说,视图和模板中的字符串需要被翻译,而用户输入的字符串则不需要。将字符串标记为可翻译是通过函数调用完成的。函数的名称以及调用方式取决于字符串的位置:在视图、模板、模型或表单中。

这一步比起一开始看起来要容易得多。让我们通过一个例子来了解它。我们将翻译应用程序中的“邀请关注者”功能。翻译应用程序的其余部分的过程将完全相同。打开user_profile/views.py文件,并对邀请视图进行突出显示的更改:

from django.utils.translation import ugettext as _
from django.views.generic import View
from django.conf import settings
from django.shortcuts import render
from django.template import Context
from django.template.loader import render_to_string
from user_profile.forms import InvitationForm
from django.core.mail import EmailMultiAlternatives
from user_profile.models import Invitation, User
from django.http import HttpResponseRedirect
import hashlib

class Invite(View):
  def get(self, request):
    params = dict()
    success = request.GET.get('success')
    email = request.GET.get('email')
    invite = InvitationForm()
    params["invite"] = invite
    params["success"] = success
    params["email"] = email
    return render(request, 'invite.html', params)

  def post(self, request):
    form = InvitationForm(self.request.POST)
    if form.is_valid():
      email = form.cleaned_data['email']
      subject = _('Invitation to join MyTweet App')
      sender_name = request.user.username
      sender_email = request.user.email
      invite_code = Invite.generate_invite_code(email)
      link = 'http://%s/invite/accept/%s/' % (settings.SITE_HOST, invite_code)
      context = Context({"sender_name": sender_name, "sender_email": sender_email, "email": email, "link": link})
      invite_email_template = render_to_string('partials/_invite_email_template.html', context)
      msg = EmailMultiAlternatives(subject, invite_email_template, settings.EMAIL_HOST_USER, [email], cc=[settings.EMAIL_HOST_USER])
      user = User.objects.get(username=request.user.username)
      invitation = Invitation()
      invitation.email = email
      invitation.code = invite_code
      invitation.sender = user
      invitation.save()
      success = msg.send()
    return HttpResponseRedirect('/invite?success='+str(success)+'&email='+email)

  @staticmethod
  def generate_invite_code(email):
    secret = settings.SECRET_KEY
    if isinstance(email, unicode):
      email = email.encode('utf-8')
      activation_key = hashlib.sha1(secret+email).hexdigest()
    return activation_key

请注意,主题字符串以“_”开头;或者,您也可以这样写:

from django.utils.translation import ugettext
  subject = ugettext('Invitation to join MyTweet App')

无论哪种方式,它都运行良好。

正如您所看到的,更改是微不足道的:

  • 我们从django.utils.translation中导入了一个名为ugettext的函数。

  • 我们使用了as关键字为函数(下划线字符)分配了一个更短的名称。我们这样做是因为这个函数将用于在视图中标记字符串为可翻译的,而且由于这是一个非常常见的任务,给函数一个更短的名称是个好主意。

  • 我们只需将一个字符串传递给_函数即可将其标记为可翻译。

这很简单,不是吗?然而,这里有一个小观察需要做。第一条消息使用了字符串格式化,并且在调用_()函数后应用了%运算符。这是为了避免翻译电子邮件地址。最好使用命名格式,这样在实际翻译时可以更好地控制。因此,您可能想要定义以下代码:

message= \
_('An invitation was sent to %(email)s.') % {
'email': invitation.email}

既然我们知道如何在视图中标记字符串为可翻译的,让我们转到模板。在模板文件夹中打开invite.html文件,并修改如下:

{% extends "base.html" %}
{% load staticfiles %}
{% load i18n %}
{% block content %}
<div class="row clearfix">
  <div class="col-md-6 col-md-offset-3 column">
    {% if success == "1" %}
    <div class="alert alert-success" role="alert">
      {% trans Invitation Email was successfully sent to  %}{{ email }}
    </div>
    {% endif %}
    {% if success == "0" %}
    <div class="alert alert-danger" role="alert">Failed to send Invitation Email to {{ email }}</div>
    {% endif %}
      <form id="search-form" action="" method="post">{% csrf_token %}
        <div class="input-group input-group-sm">
        {{ invite.email.errors }}
        {{ invite.email }}
          <span class="input-group-btn">
            <button class="btn btn-search" type="submit">Invite</button>
          </span>
        </div>
      </form>
    </div>
  </div>
  {% endblock %}

在这里,我们在模板的开头放置了{% load i18n %}参数,以便让它可以访问翻译标签。<load>标签通常用于启用默认情况下不可用的额外模板标签。您需要在使用翻译标签的每个模板的顶部放置它。i18n 是国际化的缩写,这是 Django 框架的名称,它提供了翻译功能。

接下来,我们使用了一个名为trans的模板标签来标记字符串为可翻译的。这个模板标签与视图中的gettext函数完全相同。值得注意的是,如果字符串包含模板变量,trans标签将不起作用。在这种情况下,您需要使用blocktrans标签,如下所示:

{% blocktrans %} 

您可以在{% endblocktrans %}块中传递一个变量块,即{{ variable }},以使其对读者更有意义。

现在您知道如何在模板中处理可翻译的字符串了。那么,让我们转到表单和模型。在表单或模型中标记字符串为可翻译与在视图中略有不同。要了解如何完成这一点,请打开user_profile/forms.py文件,并修改邀请表单如下:

from django.utils.translation import gettext_lazy as _
class InvitationForm(forms.Form):
  email = forms.CharField(widget=forms.TextInput(attrs={'size': 32, 'placeholder': _('Email Address of Friend to invite.'), 'class':'form-control'}))

唯一的区别是我们导入了gettext_lazy函数而不是gettextgettext_lazy会延迟直到访问其返回值时才翻译字符串。这在这里是必要的,因为表单的属性只在应用程序启动时创建一次。如果我们使用普通的gettext函数,翻译后的标签将以默认语言(通常是英语)存储在表单属性中,并且永远不会再次翻译。但是,如果我们使用gettext_lazy函数,该函数将返回一个特殊对象,每次访问时都会翻译字符串,因此翻译将正确进行。这使得gettext_lazy函数非常适合表单和模型属性。

有了这个,我们完成了为“邀请朋友”视图标记字符串以进行翻译。为了帮助您记住本小节涵盖的内容,这里是标记可翻译字符串所使用的技术的快速总结:

  • 在视图中,使用gettext函数标记可翻译的字符串(通常导入为_

  • 在模板中,使用trans模板标记标记不包含变量的可翻译字符串,使用blocktrans标记标记包含变量的字符串。

  • 在表单和模型中,使用gettext_lazy函数标记可翻译的字符串(通常导入为_

当然,也有一些特殊情况可能需要单独处理。例如,您可能希望使用gettext_lazy函数而不是gettext函数来翻译视图中的默认参数值。只要您理解这两个函数之间的区别,您就应该能够决定何时需要这样做。

创建翻译文件

现在我们已经完成了标记要翻译的字符串,下一步是为我们想要支持的每种语言创建一个翻译文件。这个文件包含所有可翻译的字符串及其翻译,并使用 Django 提供的实用程序创建。

让我们创建一个翻译文件。首先,您需要在 Django 安装文件夹内的bin目录中找到一个名为make-messages.py的文件。找到它的最简单方法是使用操作系统中的搜索功能。找到它后,将其复制到系统路径(在 Linux 和 Mac OS X 中为/usr/bin/,在 Windows 中为c:\windows\)。

此外,确保在 Linux 和 Mac OS X 中运行以下命令使其可执行(对 Windows 用户来说,这一步是不需要的):

$ sudo chmod +x /usr/bin/make-messages.py

make-messages.py实用程序使用一个名为 GNU gettext 的软件包从源代码中提取可翻译的字符串。因此,您需要安装这个软件包。对于 Linux,搜索您的软件包管理器中的软件包并安装它。Windows 用户可以在gnuwin32.sourceforge.net/packages/gettext.htm找到该软件包的安装程序。

最后,Mac OS X 用户将在gettext.darwinports.com/找到适用于其操作系统的软件包版本以及安装说明。

安装 GNU gettext 软件包后,打开终端,转到您的项目文件夹,在那里创建一个名为locale的文件夹,然后运行以下命令:

$ make-messages.py -l de

这个命令为德语语言创建了一个翻译文件。de变量是德语的语言代码。如果您想要翻译其他语言,将其语言代码放在de的位置,并继续为本章的其余部分执行相同的操作。除此之外,如果您想要支持多种语言,为每种语言运行上一个命令,并将说明应用到本节的所有语言。

一旦您运行了上述命令,它将在locale/de/LC_MESSAGES/下创建一个名为django.po的文件。这是德语语言的翻译文件。在文本编辑器中打开它,看看它是什么样子的。文件以一些元数据开头,比如创建日期和字符集。之后,您会发现每个可翻译字符串的条目。每个条目包括字符串的文件名和行号,字符串本身,以及下面的空字符串,用于放置翻译。以下是文件中的一个示例条目:

#: user_profile/forms.py
msgid "Friend's Name"
msgstr ""

要翻译字符串,只需使用文本编辑器在第三行的空字符串中输入翻译。您也可以使用专门的翻译编辑器,比如Poedit(在www.poedit.net/上提供所有主要操作系统的版本),但对于我们的简单文件,普通文本编辑器就足够了。确保在文件的元数据部分设置一个有效的字符。我建议您使用UTF-8

"Content-Type: text/plain; charset=UTF-8\n"

您可能会注意到翻译文件包含一些来自管理界面的字符串。这是因为admin/base_site.html管理模板使用trans模板标记将其字符串标记为可翻译的。无需翻译这些字符串;Django 已经为它们提供了翻译文件。

翻译完成后,您需要将翻译文件编译为 Django 可以使用的格式。这是使用 Django 提供的另一个实用程序compile-messages.py命令完成的。找到并将此文件移动到系统路径,并确保它是可执行的,方法与我们使用make-messages.py命令相同。

接下来,在项目文件夹中运行以下命令:

$ compile-messages.py

如果实用程序报告文件中的错误(例如缺少引号),请更正错误并重试。一旦成功,实用程序将在同一文件夹中创建一个名为django.mo的已编译翻译文件,并为本节的下一步做好一切准备。

启用和配置 i18n 系统

Django 默认启用了 i18n 系统。您可以通过在settings.py文件中搜索以下行来验证这一点:

USE_I18N = True

有两种配置 i18n 系统的方法。您可以为所有用户全局设置语言,也可以让用户单独指定其首选语言。我们将在本小节中看到如何同时进行这两种配置。

要全局设置活动语言,请在settings.py文件中找到名为LANGUAGE_CODE的变量,并将您喜欢的语言代码分配给它。例如,如果您想将德语设置为项目的默认语言,请将语言代码更改如下:

LANGUAGE_CODE = 'de'

现在,如果开发服务器尚未运行,请启动它,并转到“邀请朋友”页面。在那里,您会发现字符串已根据您在德语翻译文件中输入的内容进行了更改。现在,将LANGUAGE_CODE变量的值更改为’en’,并注意页面如何恢复为英语。

第二种配置方法是让用户选择语言。为此,我们应该启用一个名为LocaleMiddleware的类。简而言之,中间件是处理请求或响应对象的类。Django 的许多组件都使用中间件类来实现功能。要查看这一点,请打开settings.py文件并搜索MIDDLEWARE_CLASSES变量。您会在那里找到一个字符串列表,其中一个是django.contrib.sessions.middleware.SessionMiddleware,它将会话数据附加到请求对象上。在使用中间件之前,我们不需要了解中间件类是如何实现的。要启用LocaleMiddleware,只需将其类路径添加到MIDDLEWARE_CLASSES列表中。确保将LocaleMiddleware放在SessionMiddleware之后,因为区域设置中间件利用会话 API,我们将在下面看到。打开settings.py文件并按照以下代码片段中的突出显示的内容修改文件:

MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.doc.XViewMiddleware',
'django.middleware.locale.LocaleMiddleware',
)

区域设置中间件通过以下步骤确定用户的活动语言:

  1. 它在会话数据中查找名为django_language的键。

  2. 如果键不存在,则查找名为django_language的 cookie。

  3. 如果 cookie 不存在,则查看 Accept-Language HTTP 标头中的语言代码。此标头由浏览器发送到 Web 服务器,指示您希望以哪种语言接收内容。

  4. 如果一切都失败了,将使用settings.py文件中的LANGUAGE_CODE变量。

在所有前面的步骤中,Django 会寻找与可用翻译文件匹配的语言代码。为了有效地利用区域设置中间件,我们需要一个视图,使用户能够选择语言并相应地更新会话数据。幸运的是,Django 已经为我们提供了这样的视图。该视图称为setlanguage,并且它期望在名为 language 的 GET 变量中包含语言代码。它使用此变量更新会话数据,并将用户重定向到原始页面。要启用此视图,请编辑urls.py文件,并向其中添加以下突出显示的行:

urlpatterns = patterns('',
# i18n
(r'^i18n/', include('django.conf.urls.i18n')),
)

添加上述行类似于我们为管理界面添加 URL 条目的方式。如果您还记得之前的章节,include()函数可以用于在特定路径下包含来自另一个应用程序的 URL 条目。现在,我们可以通过提供链接(例如/i18n/setlang/language=de)让用户将语言更改为德语。我们将修改基本模板以在所有页面上添加此类链接。打开templates/base.html文件,并向其中添加以下突出显示的行:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
  <head>
    [...]
  </head>
  <body>
    [...]
    <div id="footer">
    Django Mytweets <br />
    Languages:
      <a href="/i18n/setlang/?language=en">en</a>
      <a href="/i18n/setlang/?language=de">de</a>
      [ 218 ]Chapter 11
    </div>
  </body>
</html>

此外,我们将通过将以下 CSS 代码附加到site_media/style.css文件来为新的页脚设置样式:

#footer {
margin-top: 2em;
text-align: center;
}

现在,我们的应用程序的 i18n 功能已经准备就绪。将浏览器指向“邀请朋友”页面,并尝试页面底部的新语言链接。语言应该根据点击的链接而改变。

在我们结束本节之前,这里有一些观察结果:

  • 您可以在视图中使用请求LANGUAGE_CODE属性访问当前活动的语言。

  • Django 本身被翻译成多种语言。您可以通过在激活英语以外的语言时触发表单错误来查看这一点。错误消息将以所选语言显示,即使您自己没有进行翻译。

  • 在模板中,当使用RequestContext变量时,可以使用LANGUAGE_CODE模板变量访问当前活动的语言。

这一部分有点长,但您从中学到了一个非常重要的功能。通过以多种语言提供我们的应用程序,我们使其能够吸引更广泛的受众,从而具有吸引更多用户的潜力。这实际上适用于任何 Web 应用程序,现在,我们将能够轻松地将任何 Django 项目翻译成多种语言。

在下一节中,我们将转移到另一个主题。当您的应用程序用户基数增长时,服务器的负载将增加,您将开始寻找改进应用程序性能的方法。这就是缓存发挥作用的地方。

因此,请继续阅读以了解这个非常有用的技术!

缓存-在高流量期间提高站点性能

Web 应用程序的页面是动态生成的。每次请求页面时,都会执行代码来处理用户输入并生成输出。生成动态页面涉及许多开销,特别是与提供静态 HTML 文件相比。代码可能会连接到数据库,执行昂贵的计算,处理文件等等。同时,能够使用代码生成页面正是使网站动态和交互的原因。

如果我们能同时获得两全其美岂不是太好了?这就是缓存所做的,这是大多数中高流量网站上实现的功能。当请求页面时,缓存会存储页面的生成 HTML,并在以后再次请求相同页面时重用它。这样可以通过避免一遍又一遍地生成相同页面来减少很多开销。当然,缓存页面并不是永久存储的。当页面被缓存时,会为缓存设置一个过期时间。当缓存页面过期时,它会被删除,页面会被重新生成并缓存。过期时间通常在几秒到几分钟之间,取决于网站的流量。过期时间确保缓存定期更新,并且用户接收内容更新的同时,减少生成页面的开销。

尽管缓存对于中高流量网站特别有用,低流量网站也可以从中受益。如果网站突然接收到大量高流量,可能是因为它被主要新闻网站报道,您可以启用缓存以减少服务器负载,并帮助您的网站度过高流量的冲击。稍后,当流量平息时,您可以关闭缓存。因此,缓存对小型网站也很有用。您永远不知道何时会需要它,所以最好提前准备好这些信息。

启用缓存

我们将从启用缓存系统开始这一部分。要使用缓存,您首先需要选择一个缓存后端,并在 settings.py 文件中的一个名为 CACHE_BACKEND 的变量中指定您的选择。此变量的内容取决于您选择的缓存后端。一些可用的选项包括:

  • 简单缓存:对于这种情况,缓存数据存储在进程内存中。这只对开发过程中测试缓存系统有用,不应在生产中使用。要启用它,请在 settings.py 文件中添加以下内容:
CACHE_BACKEND = 'simple:///'
  • 数据库缓存:对于这种情况,缓存数据存储在数据库表中。要创建缓存表,请运行以下命令:
$ python manage.py createcachetable cache_table

然后,在 settings.py 文件中添加以下内容:

CACHE_BACKEND = 'db://cache_table'

在这里,缓存表被称为 cache_table。只要不与现有表冲突,您可以随意命名它。

  • 文件系统缓存:在这里,缓存数据存储在本地文件系统中。要使用它,请在 settings.py 文件中添加以下内容:
CACHE_BACKEND = 'file:///tmp/django_cache'

在这里,/tmp/django_cache 变量用于存储缓存文件。如果需要,您可以指定另一个路径。

  • Memcached:Memcached 是一个先进、高效和快速的缓存框架。安装和配置它超出了本书的范围,但如果您已经有一个可用的 Memcached 服务器,可以在 settings.py 文件中指定其 IP 和端口,如下所示:
CACHE_BACKEND = 'memcached://ip:port/'

如果您不确定在本节中选择哪个后端,请选择简单缓存。然而,实际上,如果您突然遇到高流量并希望提高服务器性能,可以选择 Memcached 或数据库缓存,具体取决于服务器上可用的选项。另一方面,如果您有一个中高流量的网站,我强烈建议您使用 Memcached,因为它绝对是 Django 可用的最快的缓存解决方案。本节中提供的信息无论您选择哪种缓存后端都是一样的。

因此,决定一个缓存后端,并在 settings.py 文件中插入相应的 CACHE_BACKEND 变量。接下来,您应该指定缓存页面的过期持续时间(以秒为单位)。在 settings.py 文件中添加以下内容,以便将页面缓存五分钟:

CACHE_MIDDLEWARE_SECONDS = 60 * 5

现在,我们已经完成了启用缓存系统。继续阅读,了解如何利用缓存来提高应用程序的性能。

配置缓存

您可以配置 Django 缓存整个站点或特定视图。我们将在本小节中学习如何做到这两点。

缓存整个站点

要缓存整个网站,请将CacheMiddleware类添加到settings.py文件中的MIDDLEWARE_CLASSES类中:

MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.cache.CacheMiddleware',
'django.middleware.doc.XViewMiddleware',
'django.middleware.locale.LocaleMiddleware',
)

在这里顺序很重要,就像我们添加区域设置中间件时一样。缓存中间件类应该在会话和身份验证中间件类之后添加,在区域设置中间件类之前添加。

这就是您需要缓存 Django 网站的全部内容。从现在开始,每当请求页面时,Django 都会存储生成的 HTML 并在以后重复使用。重要的是要意识到,缓存系统只缓存没有GETPOST变量的页面。因此,我们的用户仍然可以发布推文和关注朋友,因为这些页面的视图期望 GET 或 POST 变量。另一方面,推文和标签列表等页面将被缓存。

缓存特定视图

有时,您可能只想缓存网站的特定页面-可能是一个与您的页面链接的高流量网站,因此大部分流量将被引导到这个特定页面。在这种情况下,只缓存此页面是有意义的。另一个适合缓存的好候选者是生成成本高昂的页面,因此您只希望每五分钟生成一次。我们应用程序中的标签云页面符合后一种情况。每次请求页面时,Django 都会遍历数据库中的所有标签,并计算每个标签的推文数量。这是一个昂贵的操作,因为它需要大量的数据库查询。因此,缓存这个视图是一个好主意。

要根据标签类缓存视图,只需应用一个名为cache_page的方法和与之相关的缓存参数。通过编辑mytweets/urls.py文件中的以下代码来尝试这一点:

from django.views.decorators.cache import cache_page
...
...
url(r'^search/hashTag$',  cache_page(60 * 15)(SearchHashTag.as_view())),
...
...

使用cache_page()方法很简单。它允许您指定要缓存的视图。站点缓存中提到的规则也适用于视图缓存。如果视图接收 GET 或 POST 参数,Django 将不会对其进行缓存。

有了这些信息,我们完成了本节。当您首次将网站发布到公众时,缓存是不必要的。然而,当您的网站增长,或者突然接收到大量高流量时,缓存系统肯定会派上用场。因此,在监视应用程序性能时要牢记这一点。

接下来,我们将学习 Django 测试框架。测试有时可能是一项乏味的任务。如果您可以运行一个命令来处理测试您的网站,那不是很好吗?Django 允许您这样做,我们将在下一节中学习。

模板片段可以以以下方式进行缓存:

 % load cache %}
 {% cache 500 sidebar %}
 .. sidebar ..
 {% endcache %}

单元测试-自动化测试应用程序的过程

在本书的过程中,我们有时修改了先前编写的视图。这在软件开发过程中经常发生。一个人可能会修改甚至重写一个函数来改变实现细节,因为需求已经改变,或者只是为了重构代码,使其更易读。

当您修改一个函数时,您必须再次测试它,以确保您的更改没有引入错误。然而,如果您不断重复相同的测试,测试将变得乏味。如果函数的各个方面没有很好地记录,您可能会忘记测试所有方面。显然,这不是一个理想的情况;我们绝对需要一个更好的机制来处理测试。

幸运的是,已经有了一个解决方案。它被称为单元测试。其思想是编写代码来测试您的代码。测试代码调用您的函数并验证它们的行为是否符合预期,然后打印出结果报告。您只需要编写一次测试代码。以后,每当您想要测试时,只需运行测试代码并检查生成的报告即可。

Python 自带了一个用于单元测试的框架。它位于单元测试模块中。Django 扩展了这个框架,以添加对视图测试的支持。我们将在本节中学习如何使用 Django 单元测试框架。

测试客户端

为了与视图交互,Django 提供了一个模拟浏览器功能的类。您可以使用它向应用程序发送请求并接收响应。让我们使用交互式控制台来学习。使用以下命令启动控制台:

$ python manage.py shell

导入Client()类,创建一个Client对象,并使用 GET 请求检索应用程序的主页:

>>>from django.test.client import Client
client = Client()
>>> response = client.get('/')
>>> print response

X-Frame-Options: SAMEORIGIN
Content-Type: text/html; charset=utf-8

<html>
 <head>
 <link href="/static/css/bootstrap.min.css"
 rel="stylesheet" media="screen">
 </head>
 <body>
 <nav class="navbar navbar-default" role="navigation">
 <a class="navbar-brand" href="#">MyTweets</a>
 </nav>
 <div class="container">
 </div>
 <nav class="navbar navbar-default navbar-fixed-bottom" role="navigation">
 <p class="navbar-text navbar-right">Footer </p>
 </nav>
 <script src="img/jquery-2.1.1.min.js"></script>
 <script src="img/bootstrap.min.js"></script>
 <script src="img/base.js"></script>
 </body>
</html>
>>> 

尝试向登录视图发送 POST 请求。输出将根据您是否提供正确的凭据而有所不同:

>>> print client.post('/login/',{'username': 'your_username', 'password': 'your_password'})

最后,如果有一个只允许已登录用户访问的视图,您可以像这样发送一个请求:

>>> print client.login('/friend/invite/', 'your_username', 'your_password')

如您从交互式会话中看到的,Client()类提供了三种方法:

  • get:这个方法向视图发送一个 GET 请求。它将视图的 URL 作为参数。您可以向该方法传递一个可选的 GET 变量字典。

  • post:这个方法向视图发送一个 POST 请求。它将视图的 URL 和一个 POST 变量字典作为参数。

  • login:这个方法向一个只允许已登录用户访问的视图发送一个 GET 请求。它将视图的 URL、用户名和密码作为参数。

Client()类是有状态的,这意味着它在请求之间保留其状态。一旦您登录,后续的请求将在您登录的状态下处理。Client()类的方法返回的响应对象包含以下属性:

  • status_code:这是响应的 HTTP 状态

  • content:这是响应页面的主体

  • template:这是用于渲染页面的Template实例;如果使用了多个模板,这个属性将是一个Template对象的列表

  • context:这是用于渲染模板的Context对象

这些字段对于检查测试是否成功或失败非常有用,接下来我们将看到。请随意尝试更多Client()类的用法。在继续下一小节之前,了解它的工作原理是很重要的,我们将在下一小节中创建第一个单元测试。

测试注册视图

现在您对Client()类感到满意了,让我们编写我们的第一个测试。单元测试应该位于应用程序文件夹内名为tests.py的模块中。每个测试应该是从django.test.TestCase模块派生的类中的一个方法。方法的名称必须以单词 test 开头。有了这个想法,我们将编写一个测试方法,试图注册一个新的用户帐户。因此,在bookmarks文件夹内创建一个名为tests.py的文件,并在其中输入以下内容:

from django.test import TestCase
from django.test.client import Client
class ViewTest(TestCase):
def setUp(self):
self.client = Client()
def test_register_page(self):
data = {
'username': 'test_user',
'email': 'test_user@example.com',
'password1': 'pass123',
'password2': 'pass123'
}
response = self.client.post('/register/', data)
self.assertEqual(response.status_code, 302)

让我们逐行查看代码:

  • 首先,我们导入了TestCaseClient类。

  • 接下来,我们定义了一个名为ViewTest()的类,它是从TestCase类派生的。正如我之前所说,所有测试类都必须从这个基类派生。

  • 之后,我们定义了一个名为setUp()的方法。当测试过程开始时,将调用这个方法。在这里,我们创建了一个Client对象。

  • 最后,我们定义了一个名为test_register_page的方法。方法的名称以单词 test 开头,表示它是一个测试方法。该方法向注册视图发送一个 POST 请求,并检查状态码是否等于数字302。这个数字是重定向的 HTTP 状态。

如果您回忆一下前面的章节,注册视图在请求成功时会重定向用户。

我们使用一个名为assertEqual()的方法来检查响应对象。这个方法是从TestCase类继承的。如果两个传递的参数不相等,它会引发一个异常。如果引发了异常,测试框架就知道测试失败了;否则,如果没有引发异常,它就认为测试成功了。

TestCase类提供了一组方法供测试使用。以下是一些重要的方法列表:

  • assertEqual:这期望两个值相等

  • assertNotEquals:这期望两个值不相等

  • assertTrue:这期望一个值为True

  • assertFalse:这期望一个值为False

现在您了解了测试类,让我们通过发出命令来运行实际测试:

$ python manage.py test

输出将类似于以下内容:

Creating test database...
Creating table auth_message
Creating table auth_group
Creating table auth_user
Creating table auth_permission
[...]
Loading 'initial_data' fixtures...
No fixtures found.
.
-------------------------------------------------------------
Ran 1 test in 0.170s
OK
Destroying test database...

那么,这里发生了什么?测试框架首先通过创建一个类似于真实数据库中的表的测试数据库来开始。接下来,它运行在测试模块中找到的测试。最后,它打印出结果的报告并销毁测试数据库。

在这里,我们的单个测试成功了。如果测试失败,输出会是什么样子,请修改tests.py文件中的test_register_page视图,删除一个必需的表单字段:

def test_register_page(self):
data = {
'username': 'test_user',
'email': 'test_user@example.com',
'password1': '1',
# 'password2': '1'
}
response = self.client.post('/register/', data)
self.assertEqual(response.status_code, 302)

现在,再次运行python manage.py test命令以查看结果:

=============================================================
FAIL: test_register_page (mytweets.user_profile.tests.ViewTest)
-------------------------------------------------------------
Traceback (most recent call last):
File "mytweets/user_profile/tests.py", line 19, in test_
register_page
self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302
-------------------------------------------------------------
Ran 1 test in 0.170s
FAILED (failures=1)

我们的测试有效!Django 检测到错误并给了我们发生的确切细节。完成后不要忘记将测试恢复到原始形式。现在,让我们编写另一个测试,一个稍微更高级的测试,以更好地了解测试框架。

还有许多其他情景可以编写单元测试:

  • 检查注册是否失败,如果两个密码字段不匹配

  • 测试“添加朋友”和“邀请朋友”视图

  • 测试“编辑书签”功能

  • 测试搜索返回正确结果

上面的列表只是一些例子。编写单元测试以覆盖尽可能多的用例对于保持应用程序的健康和减少错误和回归非常重要。你编写的单元测试越多,当你的应用程序通过所有测试时,你就越有信心。Django 使单元测试变得非常容易,所以要充分利用这一点。

在应用程序的生命周期中的某个时刻,它将从开发模式转移到生产模式。下一节将解释如何为生产环境准备您的 Django 项目。

部署 Django

所以,你在你的 Web 应用程序上做了很多工作,现在是时候上线了。为了确保从开发到生产的过渡顺利进行,必须在应用程序上线之前进行一些更改。本节涵盖了这些更改,以帮助您成功上线您的 Web 应用程序。

生产 Web 服务器

在本书中,我们一直在使用 Django 自带的开发 Web 服务器。虽然这个服务器非常适合开发过程,但绝对不适合作为生产 Web 服务器,因为它并没有考虑安全性或性能。因此,它绝对不适合生产环境。

在选择 Web 服务器时,有几个选项可供选择,但Apache是迄今为止最受欢迎的选择,Django 开发团队实际上也推荐使用它。如何在 Apache 上设置 Django 的详细信息取决于您的托管解决方案。一些托管计划提供预配置的 Django 托管,您只需将项目文件复制到服务器上,而其他托管计划则允许您自己配置一切。

设置 Apache 的详细信息可能会因多种因素而有所不同,超出了本书的范围。如果最终需要自己配置 Apache,请参考 Django 文档www.djangoproject.com/documentation/apache_auth/以获取详细说明。

总结

本章涵盖了各种有趣的主题。在本章中,我们为项目开发了一组重要的功能。追随者的网络对于帮助用户社交和共享兴趣非常重要。我们了解了几个在部署 Django 时有用的 Django 框架。我们还学会了如何将 Django 项目从开发环境迁移到生产环境。值得注意的是,我们学到的这些框架都非常易于使用,因此您将能够在未来的项目中有效地利用它们。这些功能在 Web 2.0 应用程序中很常见,现在,您将能够将它们整合到任何 Django 网站中。

在下一章中,我们将学习如何改进应用程序的各个方面,主要是性能和本地化。我们还将学习如何在生产服务器上部署我们的项目。下一章将提供大量有用的信息,所以请继续阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值