创建种类表单 - 学习 Web 开发

创建种类表单
======

本章节演示如何定义我们的页面，创建`Genre` 物件（这是一个很好的起点，因为类型只有一个字段，它的名称`name`，没有依赖项）。像任何其他页面一样，我们需要设置路由，控制器和视图。

引入验证与无害化方法
----------

在我们的控制器中使用 *express-validator* 验证器，我們必須导入我们想要从 **'express-validator/check**' 和 **'express-validator/filter**' 模块中使用的函数。

打开\*\*/controllers/genreController.js\*\*，并在文件顶部添加以下行：

```js
const { body, validationResult } = require("express-validator/check");
const { sanitizeBody } = require("express-validator/filter");
```

控制器—get 路由
----------

找到导出的`genre_create_get()` 控制器方法，并将其替换为以下代码。这只是渲染**genre\_form.pug**视图，传递一个 title 变量。

```js
// Display Genre create form on GET.
exports.genre\_create\_get = function (req, res, next) {
  res.render("genre\_form", { title: "Create Genre" });
};
```

控制器—post 路由
-----------

找到导出的`genre_create_post()`控制器方法，并将其替换为以下代码。

```js
// Handle Genre create on POST.
exports.genre_create_post = [
  // Validate that the name field is not empty.
  body("name", "Genre name required").isLength({ min: 1 }).trim(),

  // Sanitize (trim and escape) the name field.
  sanitizeBody("name").trim().escape(),

  // Process request after validation and sanitization.
  (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a genre object with escaped and trimmed data.
    var genre = new Genre({ name: req.body.name });

    if (!errors.isEmpty()) {
      // There are errors. Render the form again with sanitized values/error messages.
      res.render("genre\_form", {
        title: "Create Genre",
        genre: genre,
        errors: errors.array(),
      });
      return;
    } else {
      // Data from form is valid.
      // Check if Genre with same name already exists.
      Genre.findOne({ name: req.body.name }).exec(function (err, found\_genre) {
        if (err) {
          return next(err);
        }

        if (found_genre) {
          // Genre exists, redirect to its detail page.
          res.redirect(found_genre.url);
        } else {
          genre.save(function (err) {
            if (err) {
              return next(err);
            }
            // Genre saved. Redirect to genre detail page.
            res.redirect(genre.url);
          });
        }
      });
    }
  },
];
```

首先要注意的是，控制器不是单个中间件函数（带参数（`req, res, next`）），而是指定一组中间件函数。数组传递给路由器函数，并按顺序调用每个方法。

**备注：** 这种方法是必需的，因为消毒/验证器是中间件功能。

数组中的第一个方法定义了一个验证器（`body`），来检查 name 字段是否为空（在执行验证之前调用`trim()`，以删除任何尾随/前导空格）。

数组中的第二个方法（`sanitizeBody()`），创建一个清理程序来调用`trim()`修剪名称字段和调用`escape()`转义任何危险的 HTML 字符。

```js
// Validate that the name field is not empty.
body('name', 'Genre name required').isLength({ min: 1 }).trim(),

// Sanitize (trim and escape) the name field.
sanitizeBody('name').trim().escape(),
```

**备注：** 验证期间运行的清洁器不会修改请求。这就是为什么我们必须在上面的两个步骤中调用`trim()`！

在指定验证器和清理器之后，我们创建了一个中间件函数，来提取任何验证错误。我们使用`isEmpty()` 来检查验证结果中，是否有任何错误。如果有，那么我们再次渲染表单，传入我们的已清理种类对象和错误消息的数组（`errors.array()`）。

```js
// Process request after validation and sanitization.
(req, res, next) => {

    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a genre object with escaped and trimmed data.
    var genre = new Genre(
      { name: req.body.name }
    );

    if (!errors.isEmpty()) {
        // There are errors. Render the form again with sanitized values/error messages.
        res.render('genre\_form', { title: 'Create Genre', genre: genre, errors: errors.array()});
    return;
    }
    else {
        // Data from form is valid.
        ... <save the result> ...
    }
}
```

如果种类名称数据有效，那么我们检查，是否已存在具有相同名称的种类`Genre`（因为我们不想创建重复项）。

如果是，我们会重定向到现有种类的详细信息页面。如果没有，我们保存新种类，并重定向到其详细信息页面。

```js
// Check if Genre with same name already exists.
Genre.findOne({ name: req.body.name }).exec(function (err, found\_genre) {
  if (err) {
    return next(err);
  }
  if (found_genre) {
    // Genre exists, redirect to its detail page.
    res.redirect(found_genre.url);
  } else {
    genre.save(function (err) {
      if (err) {
        return next(err);
      }
      // Genre saved. Redirect to genre detail page.
      res.redirect(genre.url);
    });
  }
});
```

在我们所有的 `POST`控制器中，都使用了相同的模式：我们运行验证器，然后运行消毒器，然后检查错误，并使用错误信息重新呈现表单，或保存数据。

视图
--

当我们创建一个新的种类`Genre`时，在`GET`和`POST`控制器/路由中，都会呈现相同的视图（稍后在我们更新种类`Genre`时也会使用它）。

在`GET`情况下，表单为空，我们只传递一个 title 变量。在`POST`情况下，用户先前输入了无效数据 - 在种类变量`genre`中，我们传回了输入数据的已清理版本，并且在`errors`变量中，我们传回了一组错误消息。

```js
res.render("genre\_form", { title: "Create Genre" });
res.render("genre\_form", {
  title: "Create Genre",
  genre: genre,
  errors: errors.array(),
});
```

创建 **/views/genre\_form.pug**，并复制下面的文本。
```

extends layout

block content
  h1 #{title}

  form(method='POST' action='')
    div.form-group
      label(for='name') Genre:
      input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name))
    button.btn.btn-primary(type='submit') Submit

  if errors
    ul
      for error in errors
        li!= error.msg
```

从我们之前的教程中，可以很好地理解这个模板的大部分内容。首先，我们扩展 **layout.pug**基本模板，并覆盖名为“**content**”的块`block`。然后我们有一个标题，我们从控制器传入的标题`title`（通过`render()` 方法）。

接下来，我们有 HTML 表单的 Pug 代码，它使用`POST`方法将数据发送到服务器，并且因为操作`action`是空字符串，所以将数据发送到与页面相同的 URL。

表单定义了一个名为“name”的“text”类型的必填字段。字段的默认值，取决于是否定义了种类变量`genre`。如果从`GET`路由调用，它将为空，因为这是一个新表单。如果从`POST`路由调用，它将包含用户最初输入的（无效）值。

页面的最后一部分是错误代码。如果已定义错误变量，则只会打印错误列表（换句话说，当模板在`GET`路由上呈现时，此部分不会出现）。

**备注：** 这只是呈现错误的一种方法。你还可以从错误变量中，获取受影响字段的名称，并使用这些，来控制错误消息的呈现位置，以及是否应用自定义 CSS 等。

它看起來像是？
-------

运行应用程序，打开浏览器到http://localhost:3000/，然后选择 Create new genre 链接。如果一切设置正确，你的网站应该类似于以下屏幕截图。输入值后，应保存该值，你将进入种类详细信息页面。

![Genre Create Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Create_genre_form/locallibary_express_genre_create_empty.png)

我们针对服务器端，验证的唯一错误是种类字段不能为空。下面的屏幕截图，显示了如果你没有提供种类（以红色突出显示），错误列表会是什么样子。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Create_genre_form/locallibary_express_genre_create_error.png)

**备注：** 我们的验证使用`trim()`，来确保不接受空格作为种类名称。我们还可以在表单中 的字段定义中，添加值`required='true'`，来验证客户端字段不为空：

```js
input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name), required='true' )
```

下一步
---

* 回到 Express 教程 6: 使用表单
* 继续教程 6 下一个部分：创建作者表单


创建作者表单 - 学习 Web 开发

创建作者表单
======

本章节演示，如何为创建作者对象`Author`定义一个页面。

导入验证和清理方法
---------

为了在种类表单使用 express 验证器，我们必须用 require 导入我们想用的函式。

打开 **/controllers/authorController.js**，并在档案最上方加入底下几行：

```js
const { body, validationResult } = require("express-validator/check");
const { sanitizeBody } = require("express-validator/filter");
```

控制器—get 路由
----------

找到导出的 `author_create_get()`控制器方法，并替换为底下代码。这里单纯呈现 **author\_form.pug** 视图，传送 `title` 变数。

```js
// Display Author create form on GET.
exports.author\_create\_get = function (req, res, next) {
  res.render("author\_form", { title: "Create Author" });
};
```

控制器—post 路由
-----------

找到导出的 `author_create_post()` 控制器方法，并替换为底下代码。

```js
// Handle Author create on POST.
exports.author_create_post = [
  // Validate fields.
  body("first\_name")
    .isLength({ min: 1 })
    .trim()
    .withMessage("First name must be specified.")
    .isAlphanumeric()
    .withMessage("First name has non-alphanumeric characters."),
  body("family\_name")
    .isLength({ min: 1 })
    .trim()
    .withMessage("Family name must be specified.")
    .isAlphanumeric()
    .withMessage("Family name has non-alphanumeric characters."),
  body("date\_of\_birth", "Invalid date of birth")
    .optional({ checkFalsy: true })
    .isISO8601(),
  body("date\_of\_death", "Invalid date of death")
    .optional({ checkFalsy: true })
    .isISO8601(),

  // Sanitize fields.
  sanitizeBody("first\_name").trim().escape(),
  sanitizeBody("family\_name").trim().escape(),
  sanitizeBody("date\_of\_birth").toDate(),
  sanitizeBody("date\_of\_death").toDate(),

  // Process request after validation and sanitization.
  (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
      // There are errors. Render form again with sanitized values/errors messages.
      res.render("author\_form", {
        title: "Create Author",
        author: req.body,
        errors: errors.array(),
      });
      return;
    } else {
      // Data from form is valid.

      // Create an Author object with escaped and trimmed data.
      var author = new Author({
        first\_name: req.body.first_name,
        family\_name: req.body.family_name,
        date\_of\_birth: req.body.date_of_birth,
        date\_of\_death: req.body.date_of_death,
      });
      author.save(function (err) {
        if (err) {
          return next(err);
        }
        // Successful - redirect to new author record.
        res.redirect(author.url);
      });
    }
  },
];
```

此代码的结构和行为，几乎与创建`Genre`对象完全相同。首先，我们验证并清理数据。如果数据无效，那么我们将重新显示表单，以及用户最初输入的数据，和错误消息列表。如果数据有效，那么我们保存新的作者记录，并将用户重定向到作者详细信息页面。

**备注：** 与`Genre` post 处理程序不同，我们不会在保存之前，检查`Author`对象是否已存在。可以说，我们应该这样做，尽管现在我们可以有多个具有相同名称的作者。

验证代码演示了几个新功能：

* 我们可以用菊花链式连接验证器，使用`withMessage()`指定在前一个验证方法失败时，显示的错误消息。这使得在没有大量代码重复的情况下，提供特定的错误消息变得非常容易。

```js
// Validate fields.
body('first\_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.')
    .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'),
```

* 我们可以使用`optional()`函数，仅在输入字段时运行后续验证（这允许我们验证可选字段）。例如，下面我们检查可选的出生日期是否符合 ISO8601 标准（`checkFalsy` 旗标，表示我们接受空字符串或`null`作为空值）。

```js
body('date\_of\_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(),
```

* 参数从请求中作为字符串接收。我们可以使用`toDate()`（或`toBoolean()`等）将这些转换为正确的 JavaScript 类型。

```js
sanitizeBody('date\_of\_birth').toDate(),
```

视图
--

创建 **/views/author\_form.pug** 并复制贴上以下文字。
```

extends layout

block content
  h1=title

  form(method='POST' action='')
    div.form-group
      label(for='first_name') First Name:
      input#first_name.form-control(type='text' placeholder='First name (Christian) last' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) )
      label(for='family_name') Family Name:
      input#family_name.form-control(type='text' placeholder='Family name (surname)' name='family_name' required='true' value=(undefined===author ? '' : author.family_name))
    div.form-group
      label(for='date_of_birth') Date of birth:
      input#date_of_birth.form-control(type='date' name='date_of_birth' value=(undefined===author ? '' : author.date_of_birth) )
    button.btn.btn-primary(type='submit') Submit
  if errors
    ul
      for error in errors
        li!= error.msg
```

此视图的结构和行为与**genre\_form.pug**模板完全相同，因此我们不再对其进行描述。

**备注：** 某些浏览器不支持 input `type=“date”`，因此你不会获得日期选取部件或默认的\*`dd/mm/yyyy`\*占位符，而是获取一个空的纯文本字段。一种解决方法，是明确添加属性`placeholder='dd/mm/yyyy'`，以便在功能较少的浏览器上，仍然可以获得有关所需文本格式的信息。

### 自我挑战：加入死亡日期

上面的模板少了一个输入字段 `date_of_death` 。依照跟生日表单同样的模式，创建此字段！

它看起來像是？
-------

运行本应用，打开浏览器访问网址http://localhost:3000/，然后点击创建新作者 Create new author 连结。如果每个东西都设定正确了，你的网站看起应该像底下的截图。在你输入一个值之后，它应该会被储存，并且你将被带到作者详细信息页面。

![Author Create Page - Express Local Library site](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Create_author_form/locallibary_express_author_create_empty.png)

**备注：** 如果你尝试使用日期的各种输入格式，你可能会发现格式`yyyy-mm-dd`行为不正常。这是因为 JavaScript 将日期字符串，视为包含 0 小时的时间，但另外将该格式的日期字符串（ISO 8601 标准）视为包括 0 小时 UTC 时间，而不是本地时间。如果你的时区在 UTC 以西，则日期显示（即本地）将在你输入的日期之前一天。这是我们在这里没有解决的几个复杂问题之一（例如多字姓和有多个作者的书本）。

下一步
---

* 回到 Express 教程 6: 使用表单
* 继续教程 6 的下一个部分：创建书本表单


更新书本表单 - 学习 Web 开发

更新书本表单
======

本文最后一部分演示如何定义一个页面，以更新书本（`Book`）对象。当更新一本书的时候，表单处理更像是创建一本书，除了你必须将表单填进 `GET` 路由，并附加上来自数据库的值。

控制器—get 路由
----------

打开 **/controllers/bookController.js**. 找到 exported `book_update_get()` 控制方法，并用底下的代码替换。

```js
// Display book update form on GET.
exports.book\_update\_get = function (req, res, next) {
  // Get book, authors and genres for form.
  async.parallel(
    {
      book: function (callback) {
        Book.findById(req.params.id)
          .populate("author")
          .populate("genre")
          .exec(callback);
      },
      authors: function (callback) {
        Author.find(callback);
      },
      genres: function (callback) {
        Genre.find(callback);
      },
    },
    function (err, results) {
      if (err) {
        return next(err);
      }
      if (results.book == null) {
        // No results.
        var err = new Error("Book not found");
        err.status = 404;
        return next(err);
      }
      // Success.
      // Mark our selected genres as checked.
      for (
        var all_g_iter = 0;
        all_g_iter < results.genres.length;
        all_g_iter++
      ) {
        for (
          var book_g_iter = 0;
          book_g_iter < results.book.genre.length;
          book_g_iter++
        ) {
          if (
            results.genres[all_g_iter]._id.toString() ==
            results.book.genre[book_g_iter]._id.toString()
          ) {
            results.genres[all_g_iter].checked = "true";
          }
        }
      }
      res.render("book\_form", {
        title: "Update Book",
        authors: results.authors,
        genres: results.genres,
        book: results.book,
      });
    },
  );
};
```

这个控制器从 URL 参数 (`req.params.id`) 中，取得要更新的书本 `Book` 的 id。它使用 `async.parallel()`方法，取得指定的书本 `Book` 纪录 (填入它的种类和作者字段) ，并列出所有作者 `Author` 和种类 `Genre`对象。当所有操作都完成，它用勾选的方式，标记当前选择的种类，并呈现 **book\_form.pug** 视图，传送变数 `title`、`book`、所有 `authors`、所有 `genres`。

控制器—post 路由
-----------

找到 exported `book_update_post()` 控制器方法，并替换为底下的代码。

```js
// Handle book update on POST.
exports.book_update_post = [
  // Convert the genre to an array
  (req, res, next) => {
    if (!(req.body.genre instanceof Array)) {
      if (typeof req.body.genre === "undefined") req.body.genre = [];
      else req.body.genre = new Array(req.body.genre);
    }
    next();
  },

  // Validate fields.
  body("title", "Title must not be empty.").isLength({ min: 1 }).trim(),
  body("author", "Author must not be empty.").isLength({ min: 1 }).trim(),
  body("summary", "Summary must not be empty.").isLength({ min: 1 }).trim(),
  body("isbn", "ISBN must not be empty").isLength({ min: 1 }).trim(),

  // Sanitize fields.
  sanitizeBody("title").trim().escape(),
  sanitizeBody("author").trim().escape(),
  sanitizeBody("summary").trim().escape(),
  sanitizeBody("isbn").trim().escape(),
  sanitizeBody("genre.\*").trim().escape(),

  // Process request after validation and sanitization.
  (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a Book object with escaped/trimmed data and old id.
    var book = new Book({
      title: req.body.title,
      author: req.body.author,
      summary: req.body.summary,
      isbn: req.body.isbn,
      genre: typeof req.body.genre === "undefined" ? [] : req.body.genre,
      \_id: req.params.id, //This is required, or a new ID will be assigned!
    });

    if (!errors.isEmpty()) {
      // There are errors. Render form again with sanitized values/error messages.

      // Get all authors and genres for form.
      async.parallel(
        {
          authors: function (callback) {
            Author.find(callback);
          },
          genres: function (callback) {
            Genre.find(callback);
          },
        },
        function (err, results) {
          if (err) {
            return next(err);
          }

          // Mark our selected genres as checked.
          for (let i = 0; i < results.genres.length; i++) {
            if (book.genre.indexOf(results.genres[i]._id) > -1) {
              results.genres[i].checked = "true";
            }
          }
          res.render("book\_form", {
            title: "Update Book",
            authors: results.authors,
            genres: results.genres,
            book: book,
            errors: errors.array(),
          });
        },
      );
      return;
    } else {
      // Data from form is valid. Update the record.
      Book.findByIdAndUpdate(req.params.id, book, {}, function (err, thebook) {
        if (err) {
          return next(err);
        }
        // Successful - redirect to book detail page.
        res.redirect(thebook.url);
      });
    }
  },
];
```

这很像是创建一本书的时候，所使用的 post 路由。首先，我们验证来自表单的书本数据，并进行无害化处理，并使用它创建一个新的书本 `Book` 对象 (将它的 `_id` 值，设置给将要更新的对象的 id)。当我们验证资料，然后重新呈现表单的时候，如果存在错误，再附加显示使用者输入的资料、错误信息、以及种类和作者列表。当我们调用`Book.findByIdAndUpdate()` 去更新 `Book` ，如果没有错误，就重新导向到它的细节页面。

视图
--

打开 **/views/book\_form.pug**，并更新作者表单控制器的区段，以加入底下条件控制代码。
```

    div.form-group
      label(for='author') Author:
      select#author.form-control(type='select' placeholder='Select author' name='author' required='true' )
        for author in authors
          if book
            //- Handle GET form, where book.author is an object, and POST form, where it is a string.
            option(
              value=author._id
              selected=(
                author._id.toString()==book.author._id
                || author._id.toString()==book.author
              ) ? 'selected' : false
            ) #{author.name}
          else
            option(value=author._id) #{author.name}
```

**备注：** 此处代码的更动，是为了让书本表单 book\_form，能被创建和更新书本的对象共同使用 (如果不这么做，当创建表单时，在 `GET` 路由会发生一个错误)。

加入一个更新按钮
--------

打开 **book\_detail.pug** 视图，并确认在页面下方，有删除和更新书本的连结，如下所示。

```pug
  hr
  p
    a(href=book.url+'/delete') Delete Book
  p
    a(href=book.url+'/update') Update Book
```

你现在应该能够更新来自书本细节页面的书了。

它看起來像是？
-------

运行本应用，打开浏览器，访问网址 http://localhost:3000/，点击所有书本 All books 连结，然后点击一本书。最后点击更新书本 Update Book 连结。

表单看起来应该就像是创建书本页面，只是标题变为 'Update book'，并且事先填入纪录值。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Update_Book_form/locallibary_express_book_update_noerrors.png)

**备注：** 其他更新对象的页面，也可以用同样的方式处理。我们把这些更新页面的实作留下，做为自我挑战。

下一步
---

* 回到 Express 教程 6: 使用表单


创建书本表单 - 学习 Web 开发

创建书本表单
======

此子文档显示如何定义页面/表单以创建`Book`对象。这比相同的作者`Author`或种类`Genre`页面稍微复杂一点，因为我们需要在我们的书本表单中，获取并显示可用的作者和种类记录。

导入验证和清理方法
---------

打开 **/controllers/bookController.js**，并在文件顶部添加以下行：

```js
const { body, validationResult } = require("express-validator/check");
const { sanitizeBody } = require("express-validator/filter");
```

控制器—get 路由
----------

找到导出的`book_create_get()` 控制器方法，并将其替换为以下代码。

```js
// Display book create form on GET.
exports.book\_create\_get = function (req, res, next) {
  // Get all authors and genres, which we can use for adding to our book.
  async.parallel(
    {
      authors: function (callback) {
        Author.find(callback);
      },
      genres: function (callback) {
        Genre.find(callback);
      },
    },
    function (err, results) {
      if (err) {
        return next(err);
      }
      res.render("book\_form", {
        title: "Create Book",
        authors: results.authors,
        genres: results.genres,
      });
    },
  );
};
```

这使用异步模块 async（在教程 5：显示数据库中的数据），来获取所有作者和种类对象。然后将它们作为名为`authors`和`genres`的变量（以及页面标题`title`），传递给视图\*\*`book_form.pug`\*\*。

控制器—post 路由
-----------

找到导出的`book_create_post()`控制器方法，并将其替换为以下代码。

```js
// Handle book create on POST.
exports.book_create_post = [
  // Convert the genre to an array.
  (req, res, next) => {
    if (!(req.body.genre instanceof Array)) {
      if (typeof req.body.genre === "undefined") req.body.genre = [];
      else req.body.genre = new Array(req.body.genre);
    }
    next();
  },

  // Validate fields.
  body("title", "Title must not be empty.").isLength({ min: 1 }).trim(),
  body("author", "Author must not be empty.").isLength({ min: 1 }).trim(),
  body("summary", "Summary must not be empty.").isLength({ min: 1 }).trim(),
  body("isbn", "ISBN must not be empty").isLength({ min: 1 }).trim(),

  // Sanitize fields (using wildcard).
  sanitizeBody("\*").trim().escape(),
  sanitizeBody("genre.\*").escape(),
  // Process request after validation and sanitization.
  (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a Book object with escaped and trimmed data.
    var book = new Book({
      title: req.body.title,
      author: req.body.author,
      summary: req.body.summary,
      isbn: req.body.isbn,
      genre: req.body.genre,
    });

    if (!errors.isEmpty()) {
      // There are errors. Render form again with sanitized values/error messages.

      // Get all authors and genres for form.
      async.parallel(
        {
          authors: function (callback) {
            Author.find(callback);
          },
          genres: function (callback) {
            Genre.find(callback);
          },
        },
        function (err, results) {
          if (err) {
            return next(err);
          }

          // Mark our selected genres as checked.
          for (let i = 0; i < results.genres.length; i++) {
            if (book.genre.indexOf(results.genres[i]._id) > -1) {
              results.genres[i].checked = "true";
            }
          }
          res.render("book\_form", {
            title: "Create Book",
            authors: results.authors,
            genres: results.genres,
            book: book,
            errors: errors.array(),
          });
        },
      );
      return;
    } else {
      // Data from form is valid. Save book.
      book.save(function (err) {
        if (err) {
          return next(err);
        }
        //successful - redirect to new book record.
        res.redirect(book.url);
      });
    }
  },
];
```

此代码的结构和行为，几乎与创建种类`Genre`或作者`Author`对象完全相同。首先，我们验证并清理数据。如果数据无效，那么我们将重新显示表单，以及用户最初输入的数据，和错误消息列表。如果数据有效，我们将保存新的`Book`记录，并将用户重定向到`Book`详细信息页面。

与其他表单处理代码相关的第一个主要区别，是我们使用通配符，一次修剪和转义所有字段（而不是单独清理它们）：

```js
sanitizeBody('\*').trim().escape(),
```

与其他表单处理代码相关的下一个主要区别，是我们如何清理种类`Genre`信息。表单返回一个`Genre`项的数组（而对于其他字段，它返回一个字符串）。为了验证信息，我们首先将请求转换为数组（下一步需要）。

```js
// Convert the genre to an array.
(req, res, next) => {
    if(!(req.body.genre instanceof Array)){
        if(typeof req.body.genre==='undefined')
        req.body.genre=[];
        else
        req.body.genre=new Array(req.body.genre);
    }
    next();
},
```

然后，我们在清理器中使用通配符（\*）来单独验证每个种类数组条目。下面的代码显示了 - 这转换为“清理关键种类`genre`下的每个项目”。

```js
sanitizeBody('genre.\*').trim().escape(),
```

与其他表单处理代码的最终区别，在于我们需要将所有现有的种类和作者传递给表单。为了标记用户已经检查过的种类，我们遍历所有种类，并将`checked='true'`参数，添加到我们的 POST 数据中（如下面的代码片段中所示）。

```js
// Mark our selected genres as checked.
for (let i = 0; i < results.genres.length; i++) {
  if (book.genre.indexOf(results.genres[i]._id) > -1) {
    // Current genre is selected. Set "checked" flag.
    results.genres[i].checked = "true";
  }
}
```

视图
--

创建 **/views/book\_form.pug**，并复制下面的文本。
```

extends layout

block content
  h1= title

  form(method='POST' action='')
    div.form-group
      label(for='title') Title:
      input#title.form-control(type='text', placeholder='Name of book' name='title' required='true' value=(undefined===book ? '' : book.title) )
    div.form-group
      label(for='author') Author:
      select#author.form-control(type='select', placeholder='Select author' name='author' required='true' )
        for author in authors
          if book
            option(value=author._id selected=(author._id.toString()==book.author ? 'selected' : false) ) #{author.name}
          else
            option(value=author._id) #{author.name}
    div.form-group
      label(for='summary') Summary:
      input#summary.form-control(type='textarea', placeholder='Summary' name='summary' value=(undefined===book ? '' : book.summary) required='true')
    div.form-group
      label(for='isbn') ISBN:
      input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required='true')
    div.form-group
      label Genre:
      div
        for genre in genres
          div(style='display: inline; padding-right:10px;')
            input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked=genre.checked )
            label(for=genre._id) #{genre.name}
    button.btn.btn-primary(type='submit') Submit

  if errors
    ul
      for error in errors
        li!= error.msg
```

视图结构和行为与 **genre\_form.pug** 模板几乎相同。

主要区别在于，我们如何实现选择类型字段：作者`Author`和种类`Genre`。

* 种类集合显示为复选框，使用我们在控制器中设置的检查值`checked`，来确定是否应该选中该框。
* 作者集合显示为单选下拉列表。在这种情况下，我们通过比较当前作者选项的 id 与用户先前输入的值（作为`book`变量传入），来确定要显示的作者。这在上面突出显示！

**备注：** 如果提交的表单中存在错误，那么，当要重新呈现表单时，新的书本作者仅使用字符串（作者列表中选中选项的值）进行标识。相比之下，现有的书本作者的`_id`属性不是字符串。因此，要比较新的和现有的，我们必须将每个现有书本作者的`_id`，强制转换为字符串，如上所示。
它看起來像是？
-------

运行应用程序，将浏览器打开到 `http://localhost:3000/`，然后选择 Create new book 链接。如果一切设置正确，你的网站应该类似于以下屏幕截图。提交有效的图书后，应将其保存，然后你将进入图书详细信息页面。

![](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Create_book_form/locallibary_express_book_create_empty.png)

下一步
---

回到 Express 教程 6: 使用表单

继续教程 6 的下一个部分：创建书本实例表单


Django 教程 9: 使用表单 - 学习 Web 开发

Django 教程 9: 使用表单
=================

* 上一页  (en-US)
* Overview: Django
* 下一页

在本教程中，我们将向你展示如何在 Django 中使用 HTML 表单，特别是编写表单以创建，更新和删除模型实例的最简单方法。作为本演示的一部分，我们将扩展LocalLibrary网站，以便图书馆员可以使用我们自己的表单（而不是使用管理员应用程序）更新图书，创建，更新和删除作者。

|  |  |
| --- | --- |
| 前置条件： |
 完成所有先前的教程主题，包含
 Django 教程 8: 使用者授权与许可 (en-US)。
  |
| 目標： | 了解如何撰写表单，向使用者取得资料，并更新资料库。了解通用类别表单编辑视图，如何大量地简化用于单一模型的新表单制作。 |

概览
--

一张 HTML 表单 ，是由一个或多个栏位/widget 在一个网页上组成的，以用于向使用者收集资料，并提交至伺服器。表单是一个弹性的机制，用于收集使用者输入，有合适的 widgets 可输入许多不同型态的资料，包含文字框、复选框、单选按钮、日期选取组件等等。若是允许我们用 `POST` 方式传送资料，并附加 CSRF 跨站要求伪造保护，表单也是与伺服器分享资料的一种相对安全的方式。

在这个教程目前为止，我们还没有创造任何表单，但我们已经在 Django 管理站点遇到这些表单了— 例如以下的撷图展示了一张表单，用于编辑我们的一个 Book 书本模型，包含一些选择列表以及文字编辑框。

![Admin Site - Book Add](/zh-CN/docs/Learn/Server-side/Django/Forms/admin_book_add.png)

表单的使用可以很复杂！开发者需要为表单撰写 HTML 语法，在服务端验证输入的资料并经过充分的安全处理（并且可能在浏览器端也需要），回到表单呈现错误信息，告知使用者任何无效的栏位，当成功提交时处理资料，在最后用某些方式回应使用者表单提交成功的信息。经由提供一个框架，让你程序化定义表单以及其中的栏位，Django 表单接手处理了以上这些步骤的大量工作，比如使用这些物件，产生表单的 HTML 源码，并处理大量的验证、使用者互动的工作。

在本教程中，我们将展示一些方法，用以创造并使用表单，特别是，当你创造用以操作资料模型的表单，通用编辑表单视图如何显著降低你的工作量。在此过程中，我们将通过添加表单，来扩展我们的 LocalLibrary 应用程序，以允许图书馆员更新图书馆书本，我们将创建页面来创建，编辑和删除书本和作者（复制上面显示的表格的基本版本，以便编辑书本）。

HTML 表单
-------

首先简要概述HTML 表单。考虑一个简单的 HTML 表单，其中包含一个文本字段，用于输入某些“团队”的名称及其相关标签：

![Simple name field example in HTML form](/zh-CN/docs/Learn/Server-side/Django/Forms/form_example_name_field.png)

表单在 HTML 中定义为`<form>...</form>` 标记内的元素集合，包含至少一个`type="submit"`的`input` 输入元素。

```html
<form action="/team\_name\_url/" method="post">
  <label for="team\_name">Enter name: </label>
  <input
 id="team\_name"
 type="text"
 name="name\_field"
 value="Default name for team." />
  <input type="submit" value="OK" />
</form>
```

虽然在这里，我们只有一个文本字段，用于输入团队名称，但表单可能包含任意数量的其他输入元素，及其相关标签。字段的`type` 属性，定义将显示哪种窗口小部件。该字段的名称`name` 和 `id`，用于标识 JavaScript / CSS / HTML 中的字段，而`value` 定义字段首次显示时的初始值。匹配团队标签使用`label` 标签指定（请参阅上面的“输入名称”Enter name），其中`for` 字段包含相关`input`输入的`id` 值。

提交输入`submit` 将显示为一个按钮（默认情况下），用户可以按下该按钮，将表单中所有其他输入元素中的数据，上传到服务器（在本例中，只有`team_name`）。表单属性定义用于发送数据的 HTTP `method` 方法，和服务器上数据的目标（`action`）：

* `action`: 提交表单时，要发送数据以进行处理的资源 /URL。如果未设置（或设置为空字符串），则表单将提交回当前页面 URL。
* `method`: 用于发送数据的 HTTP 方法：post 或 get。
    + 如果数据将导致服务器数据库的更改，则应始终使用`POST` 方法，因为这可以更加抵抗跨站点伪造请求攻击。
    + `GET` 方法，只应用于不更改用户数据的表单（例如搜索表单）。当你希望能够为 URL 添加书签、或共享时，建议使用此选项。

服务器的角色，首先是呈现初始表单状态 - 包含空白字段或预先填充初始值。在用户按下提交按钮之后，服务器将从 Web 浏览器，接收具有值的表单数据，并且必须验证该信息。如果表单包含无效数据，则服务器应再次显示表单，这次使用用户输入的数据在“有效”字段中，并使用消息来描述无效字段的问题。一旦服务器获得具有所有有效表单数据的请求，它就可以执行适当的操作（例如，保存数据，返回搜索结果，上载文件等），然后通知用户。

可以想象，创建 HTML，验证返回的数据，根据需要重新显示输入的数据，和错误报告，以及对有效数据执行所需的操作，都需要花费很多精力才能“正确”。通过删除一些繁重的重复代码，Django 使这变得更容易！

Django 表单处理流程
-------------

Django 的表单处理，使用了我们在之前的教程中，学到的所有相同技术（用于显示有关模型的信息）：视图获取请求，执行所需的任何操作，包括从模型中读取数据，然后生成并返回 HTML 页面（从模板中，我们传递一个包含要显示的数据的上下文。使事情变得更复杂的是，服务器还需要能够处理用户提供的数据，并在出现任何错误时，重新显示页面。

下面显示了 Django 如何处理表单请求的流程图，从对包含表单的页面的请求开始（以绿色显示）。

![Updated form handling process doc.](/zh-CN/docs/Learn/Server-side/Django/Forms/form_handling_-_standard.png)

基于上图，Django 表单处理的主要内容是：

1. 在用户第一次请求时，显示默认表单。
    * 表单可能包含空白字段（例如，如果你正在创建新记录），或者可能预先填充了初始值（例如，如果你要更改记录，或者具有有用的默认初始值）。
    * 此时表单被称为未绑定，因为它与任何用户输入的数据无关（尽管它可能具有初始值）。
2. 从提交请求接收数据，并将其绑定到表单。
    * 将数据绑定到表单，意味着当我们需要重新显示表单时，用户输入的数据和任何错误都可取用。
3. 清理并验证数据。
    * 清理数据会对输入执行清理（例如，删除可能用于向服务器发送恶意内容的无效字符）并将其转换为一致的 Python 类型。
    * 验证检查值是否适合该字段（例如，在正确的日期范围内，不是太短或太长等）
4. 如果任何数据无效，请重新显示表单，这次使用任何用户填充的值，和问题字段的错误消息。
5. 如果所有数据都有效，请执行必要的操作（例如保存数据，发送表单和发送电子邮件，返回搜索结果，上传文件等）
6. 完成所有操作后，将用户重定向到另一个页面。

Django 提供了许多工具和方法，来帮助你完成上述任务。最基本的是 `Form` 类，它简化了表单 HTML 和数据清理/验证的生成。在下一节中，我们将描述表单如何使用页面的实际示例，来允许图书馆员更新书本籍。

**备注：** 在我们讨论 Django 更“高级”的表单框架类时，了解 `Form` 的使用方式，将对你有所帮助。

续借表单 - 使用表单和功能视图
----------------

接下来，我们将添加一个页面，以允许图书馆员，为被借用的书本办理续借。为此，我们将创建一个允许用户输入日期值的表单。我们将从当前日期（正常借用期）起 3 周内，为该字段设定初始值，并添加一些验证，以确保图书管理员无法输入过去的日期、或未来的日期。输入有效日期后，我们会将其写入当前记录的 `BookInstance.due_back`字段。

该示例将使用基于函数的视图和`Form` 类。以下部分，说明了表单的工作方式，以及你需要对正在进行的 LocalLibrary 项目所做的更改。

### 表单

`Form` 类是 Django 表单处理系统的核心。它指定表单中的字段、其布局、显示窗口小部件、标签、初始值、有效值，以及（一旦验证）与无效字段关联的错误消息。该类还提供了使用预定义格式（表，列表等）在模板中呈现自身的方法，或者用于获取任何元素的值（启用细粒度手动呈现）的方法。

#### 声明表单

`Form` 的声明语法，与声明`Model`非常相似，并且共享相同的字段类型（以及一些类似的参数）。这是有道理的，因为在这两种情况下，我们都需要确保每个字段处理正确类型的数据，受限于有效数据，并具有显示/文档的描述。

要创建表单，我们导入表单库，从`Form` 类派生，并声明表单的字段。我们的图书馆图书续借表单的一个非常基本的表单类如下所示：

```python
from django import forms

class RenewBookForm(forms.Form):
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")
```

#### 表单字段

在这种情况下，我们有一个 `DateField` 用于输入续借日期，该日期将使用空白值在 HTML 中呈现，默认标签为“续借日期：”，以及一些有用的用法文本：“输入从现在到 4 周之间的日期（默认为 3）周）。”由于没有指定其他可选参数，该字段将使用 input\_formats 接受日期：YYYY-MM-DD（2016-11-06）、MM/DD/YYYY（02/26/2016）、MM/DD/YY（10/25/16），并且将使用默认小部件呈现：DateInput。

还有许多其他类型的表单字段，你可以从它们与等效模型字段类的相似性中大致认识到：

`BooleanField`, `CharField`, `ChoiceField`, `TypedChoiceField`, `DateField`, `DateTimeField`, `DecimalField`, `DurationField`, `EmailField`, `FileField`, `FilePathField`, `FloatField`, `ImageField`, `IntegerField`, `GenericIPAddressField`, `MultipleChoiceField`, `TypedMultipleChoiceField`, `NullBooleanField`, `RegexField`, `SlugField`, `TimeField`, `URLField`, `UUIDField`, `ComboField`, `MultiValueField`, `SplitDateTimeField`, `ModelMultipleChoiceField`, `ModelChoiceField`.

下面列出了大多数字段共有的参数（这些参数具有合理的默认值）：

* required: 如果为`True`，则该字段不能留空或给出`None`值。默认情况下需要字段，因此你可以设置`required=False`以允许表单中的空白值。
* label: 在 HTML 中呈现字段时使用的标签。如果未指定label，则 Django 将通过大写第一个字母、并用空格替换下划线（例如续订日期）的方式，从字段名称创建一个。
* label\_suffix: 默认情况下，标签后面会显示冒号（例如续借日期:)。此参数允许你指定包含其他字符的不同后缀。
* initial: 显示表单时，字段的初始值。
* widget: 要使用的显示小部件。
* help\_text （如上例所示）：可以在表单中显示的附加文本，用于说明如何使用该字段。
* error\_messages: 字段的错误消息列表。如果需要，你可以使用自己的消息，覆盖这些消息。
* validators: 验证时将在字段上调用的函数列表。
* localize: 启用表单数据输入的本地化（有关详细信息，请参阅链接）。
* disabled: 如果为`True`，该字段会被显示，但无法编辑其值。默认值为`False`。

#### 验证

Django 提供了许多可以验证数据的地方。验证单个字段的最简单方法，是覆盖要检查的字段的方法`clean_<fieldname>()` 。因此，例如，我们可以通过实现`clean_renewal_date()`，验证输入的`renewal_date` 值是从现在到 4 周之间，如下所示。

```python
from django import forms

from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
import datetime #for checking renewal date range.

class RenewBookForm(forms.Form):
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

    def clean\_renewal\_date(self):
        data = self.cleaned_data['renewal\_date']

        #Check date is not in past.
        if data < datetime.date.today():
            raise ValidationError(_('Invalid date - renewal in past'))

        #Check date is in range librarian allowed to change (+4 weeks).
        if data > datetime.date.today() + datetime.timedelta(weeks=4):
            raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

        # Remember to always return the cleaned data.
        return data
```

有两件重要的事情需要注意。首先，我们使用`self.cleaned_data['renewal_date']` 获取数据，并且无论是否在函数末尾更改数据，我们都会返回此数据。此步骤使用默认验证器，将数据“清理”、并清除可能不安全的输入，并转换为数据的正确标准类型（在本例中为 Python `datetime.datetime`对象）。

第二点是，如果某个值超出了我们的范围，我们会引发`ValidationError`，指定在输入无效值时，我们要在表单中显示的错误文本。上面的例子，也将这个文本包含在 Django 的翻译函数`ugettext_lazy()`中（导入为 `_()`），如果你想在稍后翻译你的网站，这是一个很好的做法。

**备注：** 在表单和字段验证（Django docs）中验证表单还有其他很多方法和示例。例如，如果你有多个相互依赖的字段，则可以覆盖Form.clean() 函数并再次引发`ValidationError`。

这就是我们在这个例子中，对表单所需要了解的全部内容！

#### 复制表单

创建并打开文件 **locallibrary/catalog/forms.py**，并将前一个块中的整个代码清单，复制到其中。

### URL 配置

在创建视图之前，让我们为续借页面添加 URL 配置。将以下配置，复制到**locallibrary/catalog/urls.py** 的底部。

```python
urlpatterns += [
    path('book/<uuid:pk>/renew/', views.renew_book_librarian, name='renew-book-librarian'),
]
```

URL 配置会将格式为 **/catalog/book/<bookinstance id>/renew/** 的 URL，重定向到 **views.py** 中，名为`renew_book_librarian()` 的函数，并将`BookInstance` id 作为名为 `pk`的参数发送。只有 `pk`是正确格式化的 `uuid`，该模式才会匹配。

**备注：** 我们可以将捕获的 URL 数据，命名为“`pk`”，因为我们可以完全控制视图函数（我们不使用需要具有特定名称的参数的通用详细视图类）。然而，`pk`，“主键”primary key 的缩写，是一个合理的惯例！

### 视图

正如上面的 Django 表单处理过程中，所讨论的那样，视图必须在首次调用时呈现默认表单，然后在数据无效时，重新呈现它，并显示错误消息，或者数据有效时，处理数据，并重定向到新页面。为了执行这些不同的操作，视图必须能够知道，它是第一次被调用以呈现默认表单，还是后续处理以验证数据。

对于使用`POST` 请求向服务器提交信息的表单，最常见的模式，是视图针对`POST` 请求类型进行测试（`if request.method == 'POST':`) 以识别表单验证请求和`GET` （使用一个`else` 条件）来识别初始表单创建请求。如果要使用`GET` 请求提交数据，则识别这是第一个、还是后续视图调用的典型方法，是读取表单数据（例如，读取表单中的隐藏值）。

书本续借过程将写入我们的数据库，因此按照惯例，我们使用 `POST` 请求方法。下面的代码片段，显示了这种函数视图的（非常标准）模式。

```python
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
import datetime

from .forms import RenewBookForm

def renew\_book\_librarian(request, pk):
    book_inst=get_object_or_404(BookInstance, pk = pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned\_data as required (here we just write it to the model due\_back field)
            book_inst.due_back = form.cleaned_data['renewal\_date']
            book_inst.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    # If this is a GET (or any other method) create the default form.
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal\_date': proposed_renewal_date,})

    return render(request, 'catalog/book\_renew\_librarian.html', {'form': form, 'bookinst':book_inst})
```

首先，我们导入我们的表单（`RenewBookForm`）和视图函数中使用的许多其他有用的对象/方法：

* `get_object_or_404()`: 根据模型的主键值，从模型返回指定的对象，如果记录不存在，则引发`Http404` 异常（未找到）。
* `HttpResponseRedirect`: 这将创建指向指定 URL 的重定向（HTTP 状态代码 302）。
* `reverse()`: 这将从 URL 配置名称和一组参数生成 URL。它是我们在模板中使用的 `url` 标记的 Python 等价物。
* `datetime`: 用于操作日期和时间的 Python 库。

在视图中，我们首先使用 `get_object_or_404()`中的 `pk` 参数，来获取当前的 `BookInstance` （如果这不存在，视图将立即退出，页面将显示“未找到”错误）。如果这不是 `POST` 请求（由 `else` 子句处理），那么我们创建默认表单，传递 `renewal_date` 字段的`initial` 初始值（如下面的**粗体**所示，这是从当前日期起的 3 周）。

```python
    book_inst=get_object_or_404(BookInstance, pk = pk)

    # If this is a GET (or any other method) create the default form
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal\_date': proposed_renewal_date,})

    return render(request, 'catalog/book\_renew\_librarian.html', {'form': form, 'bookinst':book_inst})
```

创建表单后，我们调用 `render()` 来创建 HTML 页面，指定模板和包含表单的上下文。在这种情况下，上下文还包含我们的 `BookInstance`，我们将在模板中使用它，来提供有关我们正在续借的书本信息。

但是，如果这是一个`POST` 请求，那么我们创建表单对象，并使用请求中的数据填充它。此过程称为“绑定”，并且允许我们验证表单。然后我们检查表单是否有效，它运行所有字段上的所有验证代码 - 包括用于检查我们的日期字段，实际上是有效日期的通用代码，以及用于检查日期的特定表单的`clean_renewal_date()`函数在合适的范围内。

```python
    book_inst=get_object_or_404(BookInstance, pk = pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned\_data as required (here we just write it to the model due\_back field)
            book_inst.due_back = form.cleaned_data['renewal\_date']
            book_inst.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    return render(request, 'catalog/book\_renew\_librarian.html', {'form': form, 'bookinst':book_inst})
```

如果表单无效，我们再次调用`render()` ，但这次在上下文中传递的表单值将包含错误消息。

如果表单有效，那么我们可以开始使用数据，通过 `form.cleaned_data`属性访问它（例如 `data = form.cleaned_data['renewal_date']`）。这里我们只将数据保存到关联的`BookInstance` 对象的`due_back` 值中。

**警告：** 虽然你也可以通过请求直接访问表单数据（例如`request.POST['renewal_date']` 或 `request.GET['renewal_date']`（如果使用 GET 请求），但不建议这样做。清理后的数据是无害的、验证过的、并转换为 Python 友好类型。

视图的表单处理部分的最后一步，是重定向到另一个页面，通常是“成功”页面。在这种情况下，我们使用 `HttpResponseRedirect` 和 `reverse()` ，重定向到名为'`all-borrowed`'的视图（这是在 Django 教程第 8 部分中创建的“挑战”：用户身份验证和权限）。如果你没有创建该页面，请考虑重定向到 URL'/'处的主页。

这就是表单处理本身所需的一切，但我们仍然需要将视图，限制为图书馆员可以访问。我们应该在 `BookInstance` （“`can_renew`”）中创建一个新的权限，但为了简单起见，我们只需使用`@permission_required`函数装饰器，和我们现有的 `can_mark_returned` 权限。

因此，最终视图如下所示。请将其复制到 **locallibrary/catalog/views.py** 的底部。

```python
from django.contrib.auth.decorators import permission_required

from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
import datetime

from .forms import RenewBookForm

@permission\_required('catalog.can\_mark\_returned')
def renew\_book\_librarian(request, pk):
    """
 View function for renewing a specific BookInstance by librarian
 """
    book_inst=get_object_or_404(BookInstance, pk = pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned\_data as required (here we just write it to the model due\_back field)
            book_inst.due_back = form.cleaned_data['renewal\_date']
            book_inst.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    # If this is a GET (or any other method) create the default form.
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal\_date': proposed_renewal_date,})

    return render(request, 'catalog/book\_renew\_librarian.html', {'form': form, 'bookinst':book_inst})
```

### 模板

创建视图中引用的模板（**/catalog/templates/catalog/book\_renew\_librarian.html**），并将下面的代码，复制到其中：

```django
{% extends "base\_generic.html" %}

{% block content %}
  <h1>Renew: {{bookinst.book.title}}</h1>
  <p>Borrower: {{bookinst.borrower}}</p>
  <p{% if bookinst.is\_overdue %} class="text-danger"{% endif %}>Due date: {{bookinst.due\_back}}</p>

  <form action="" method="post">
    {% csrf\_token %}
    <table>
      {{ form }}
    </table>
    <input type="submit" value="Submit" />
  </form>
{% endblock %}
```

这里大部分内容，和以前的教程都是完全类似的。我们扩展基本模板，然后重新定义内容块。我们能够引用 `{{bookinst}}`（及其变量），因为它被传递到 `render()`函数中的上下文对象中，我们使用这些来列出书名，借阅者和原始截止日期。

表单代码相对简单。首先，我们声明表单标签，指定表单的提交位置（`action`）和提交数据的方法（在本例中为“HTTP POST”） - 如果你回想一下页面顶部的 HTML 表单概述，如图所示的空`action` ，意味着表单数据将被发布回页面的当前 URL（这是我们想要的！）。在标签内部，我们定义了`submit` 提交输入，用户可以按这个输入来提交数据。在表单标签内添加的`{% csrf_token %}` ，是 Django 跨站点伪造保护的一部分。

**备注：** 将`{% csrf_token %}` 添加到你创建的每个使用 `POST` 提交数据的 Django 模板中。这将减少恶意用户劫持表单的可能性。

剩下的就是 `{{form}}`模板变量，我们将其传递给上下文字典中的模板。也许不出所料，当如图所示使用时，它提供了所有表单字段的默认呈现，包括它们的标签、小部件、和帮助文本 - 呈现如下所示：

```html
<tr>
  <th><label for="id\_renewal\_date">Renewal date:</label></th>
  <td>
    <input
 id="id\_renewal\_date"
 name="renewal\_date"
 type="text"
 value="2016-11-08"
 required />
    <br />
    <span class="helptext"
 >Enter date between now and 4 weeks (default 3 weeks).</span
 >
  </td>
</tr>
```

**备注：** 它可能并不明显，因为我们只有一个字段，但默认情况下，每个字段都在其自己的表格行中定义（这就是变量在上面的`table` 表格标记内部的原因）。如果你引用模板变量`{{ form.as_table }}`，会提供相同的渲染。

如果你输入无效日期，你还会获得页面中呈现的错误列表（下面以**粗体**显示）。

```html
<tr>
  <th><label for="id\_renewal\_date">Renewal date:</label></th>
  <td>
    <ul class="errorlist">
      <li>Invalid date - renewal in past</li>
    </ul>
    <input
 id="id\_renewal\_date"
 name="renewal\_date"
 type="text"
 value="2015-11-08"
 required />
    <br />
    <span class="helptext"
 >Enter date between now and 4 weeks (default 3 weeks).</span
 >
  </td>
</tr>
```

#### 使用表单模板变量的其他方法

如上所示使用`{{form}}`，每个字段都呈现为表格行。你还可以将每个字段呈现为列表项（使用`{{form.as_ul}}` ）或作为段落（使用`{{form.as_p}}`）。

更酷的是，你可以通过使用点表示法，索引其属性，来完全控制表单每个部分的呈现。例如，我们可以为`renewal_date` 字段访问许多单独的项目：

* `{{form.renewal_date}}`: 整个领域。
* `{{form.renewal_date.errors}}`: 错误列表。
* `{{form.renewal_date.id_for_label}}`: 标签的 id。
* `{{form.renewal_date.help_text}}`: 字段帮助文本。
* 其他等等！

有关如何在模板中，手动呈现表单，并动态循环模板字段的更多示例，请参阅使用表单>手动呈现字段（Django 文档）。

### 测试页面

如果你接受了Django 教程第 8 部分中的“挑战”：用户身份验证和权限，你将获得图书馆中借出的所有书本的列表，这只有图书馆工作人员才能看到。我们可以使用下面的模板代码，为每个项目旁边的续借页面，添加链接。

```django
{% if perms.catalog.can\_mark\_returned %}-
  <a href="{% url 'renew-book-librarian' bookinst.id %}">Renew</a>
{% endif %}
```

**备注：** 请记住，你的测试登录需要具有“`catalog.can_mark_returned`”权限，才能访问续借书本页面（可能使用你的超级用户帐户）。

你也可以手动构建这样的测试 URL - `http://127.0.0.1:8000/catalog/book/<bookinstance_id>/renew/` （可以通过导航到图书馆中的书本详细信息页面，获取有效的 bookinstance id，并复制`id` 字段）。

### 它看起来是什么样子？

如果你成功，默认表单将如下所示：

![](/zh-CN/docs/Learn/Server-side/Django/Forms/forms_example_renew_default.png)

输入无效值的表单将如下所示：

![](/zh-CN/docs/Learn/Server-side/Django/Forms/forms_example_renew_invalid.png)

所有包含续借链接的图书清单如下所示：

![](/zh-CN/docs/Learn/Server-side/Django/Forms/forms_example_renew_allbooks.png)

模型表单
----

使用上述方法创建`Form` 类非常灵活，允许你创建任何类型的表单页面，并将其与任何单一模型、或多个模型相关联。

但是，如果你只需要一个表单，来映射单个模型的字段，那么你的模型，将已经定义了表单中所需的大部分信息：字段、标签、帮助文本等。而不是在表单中重新创建模型定义，使用 ModelForm 帮助程序类从模型创建表单更容易。然后，可以在视图中使用此`ModelForm` ，其方式与普通`Form`完全相同。

包含与原始`RenewBookForm` 相同的字段的基本 `ModelForm` 如下所示。创建表单所需要做的，就是添加带有相关模型（`BookInstance`）的`class Meta`、和要包含在表单中的模型字段列表（你可以使用 `fields = '__all__'`，以包含所有字段，或者你可以使用 `exclude` （而不是字段），指定不包含在模型中的字段）。

```python
from django.forms import ModelForm
from .models import BookInstance

class RenewBookModelForm(ModelForm):
    class Meta:
        model = BookInstance
        fields = ['due\_back',]
```

**备注：** 这可能看起来不像使用`Form` 那么简单（在这种情况下不是这样，因为我们只有一个字段）。但是，如果你有很多字段，它可以显着减少代码量！

其余信息来自模型字段的定义（例如标签、小部件、帮助文本、错误消息）。如果这些不太正确，那么我们可以在 `Meta`类中覆盖它们，指定包含要更改的字段、及其新值的字典。例如，在这种形式中，我们可能需要“更新日期”*Renewal date* 字段的标签（而不是基于字段名称的默认值：截止日期 *Due date*），并且我们还希望我们的帮助文本，特定于此用例。下面的`Meta` 显示了如何覆盖这些字段，如果默认值不够，你可以类似地方式设置`widgets` 窗口小部件和`error_messages` 。

```python
class Meta:
    model = BookInstance
    fields = ['due\_back',]
    labels = { 'due\_back': _('Renewal date'), }
    help_texts = { 'due\_back': _('Enter a date between now and 4 weeks (default 3).'), }
```

要添加验证，你可以使用与普通表单相同的方法 - 定义名为 `clean_field_name()`的函数，并为无效值引发`ValidationError` 异常。与我们原始形式的唯一区别，是模型字段名为`due_back` 而不是“`renewal_date`”。

```python
from django.forms import ModelForm
from .models import BookInstance

class RenewBookModelForm(ModelForm):
    def clean\_due\_back(self):
       data = self.cleaned_data['due\_back']

       #Check date is not in past.
       if data < datetime.date.today():
           raise ValidationError(_('Invalid date - renewal in past'))

       #Check date is in range librarian allowed to change (+4 weeks)
       if data > datetime.date.today() + datetime.timedelta(weeks=4):
           raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

       # Remember to always return the cleaned data.
       return data

    class Meta:
        model = BookInstance
        fields = ['due\_back',]
        labels = { 'due\_back': _('Renewal date'), }
        help_texts = { 'due\_back': _('Enter a date between now and 4 weeks (default 3).'), }
```

下面的 `RenewBookModelForm` 类现在在功能上等同于我们原来的 `RenewBookForm`。你可以在当前使用`RenewBookForm` 的任何地方导入和使用它。

通用编辑视图
------

我们在上面的函数视图示例中，使用的表单处理算法，表示表单编辑视图中非常常见的模式。Django 通过创建基于模型创建、编辑和删除视图的通用编辑视图，为你抽象出大部分“样板”。这些不仅处理“视图”行为，而且它们会自动从模型中为你创建表单类（`ModelForm`）。

**备注：** 除了这里描述的编辑视图之外，还有一个 FormView类，它位于我们的函数视图，和其他通用视图之间的“灵活性”与“编码工作”之间。使用 `FormView` ，你仍然需要创建表单，但不必实现所有标准表单处理模式。相反，你只需提供一个函数的实现，一旦知道提交有效，就会调用该函数。

在本节中，我们将使用通用编辑视图，来创建页面，以添加从我们的库中创建、编辑和删除`Author` 作者记录的功能 - 有效地提供管理站点一部分的基本重新实现（这可能很有用，如果你需要比管理站点能提供的、更加灵活的管理功能）。

### 视图

打开视图文件（**locallibrary/catalog/views.py**），并将以下代码块，附加到其底部：

```python
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Author

class AuthorCreate(CreateView):
    model = Author
    fields = '\_\_all\_\_'
    initial={'date\_of\_death':'05/01/2018',}

class AuthorUpdate(UpdateView):
    model = Author
    fields = ['first\_name','last\_name','date\_of\_birth','date\_of\_death']

class AuthorDelete(DeleteView):
    model = Author
    success_url = reverse_lazy('authors')
```

如你所见，要创建视图，你需要从`CreateView`, `UpdateView`, 和 `DeleteView`（分别）派生，然后定义关联的模型。

对于“创建”和“更新”的情况，你还需要指定要在表单中显示的字段（使用与`ModelForm`相同的语法）。在这种情况下，我们将说明两者的语法，如何显示“所有”字段，以及如何单独列出它们。你还可以使用 field\_name / value 对的字典，为每个字段指定初始值（此处我们为了演示目的，而任意设置死亡日期 - 你可能希望删除它！）。默认情况下，这些视图会在成功时，重定向到显示新创建/编辑的模型项的页面，在我们的示例中，这将是我们在上一个教程中，创建的作者详细信息视图。你可以通过显式声明参数`success_url` ，指定备用重定向位置（与`AuthorDelete` 类一样）。

`AuthorDelete` 类不需要显示任何字段，因此不需要指定这些字段。但是你需要指定`success_url`，因为 Django 没有明显的默认值。在这种情况下，我们使用`reverse_lazy()`函数，在删除作者后，重定向到我们的作者列表 - `reverse_lazy()`是一个延迟执行的`reverse()`版本，在这里使用，是因为我们提供了一个基于类的 URL 查看属性。

### 模板

“创建”和“更新”视图默认使用相同的模板，它将以你的模型命名：**model\_name\_form.html**（你可以使用视图中的`template_name_suffix` 字段，将后缀更改为 **form** 以外的其他内容，例如，`template_name_suffix = '_other_suffix'`）

创建模板文件 **locallibrary/catalog/templates/catalog/author\_form.html**，并复制到下面的文本中。

```django
{% extends "base\_generic.html" %}

{% block content %}
  <form action="" method="post">
    {% csrf\_token %}
    <table>
    {{ form.as\_table }}
    </table>
    <input type="submit" value="Submit" />
  </form>
{% endblock %}
```

这与我们之前的表单类似，并使用表单呈现字段。另请注意我们如何声明`{% csrf_token %}`，以确保我们的表单能够抵抗 CSRF 攻击。

“删除”视图需要查找以 **model\_name\_confirm\_delete.html** 格式命名的模板（同样，你可以在视图中，使用`template_name_suffix` 更改后缀）。创建模板文件 **locallibrary/catalog/templates/catalog/author\_confirm\_delete.html** ，并复制到下面的文本中。

```django
{% extends "base\_generic.html" %}

{% block content %}
  <h1>Delete Author</h1>

  <p>Are you sure you want to delete the author: {{ author }}?</p>

  <form action="" method="POST">
    {% csrf\_token %}
    <input type="submit" action="" value="Yes, delete." />
  </form>
{% endblock %}
```

### URL 配置

打开 URL 配置文件（**locallibrary/catalog/urls.py**），并将以下配置，添加到文件的底部：

```python
urlpatterns += [
    path('author/create/', views.AuthorCreate.as_view(), name='author\_create'),
    path('author/<int:pk>/update/', views.AuthorUpdate.as_view(), name='author\_update'),
    path('author/<int:pk>/delete/', views.AuthorDelete.as_view(), name='author\_delete'),
]
```

这里没有什么特别的新东西！你可以看到视图是类，因此必须通过`.as_view()`调用，并且你应该能够识别每种情况下的 URL 模式。我们必须使用 `pk` 作为捕获的主键值的名称，因为这是视图类所期望的参数名称。

作者的创建，更新和删除页面，现在已准备好进行测试（在这种情况下，我们不会将它们连接到站点侧栏，尽管如果你愿意，也可以这样做）。

**备注：** 敏锐的用户会注意到，我们没有采取任何措施，来防止未经授权的用户访问这些页面！我们将其作为练习留给你（提示：你可以使用`PermissionRequiredMixin` ，并创建新权限，或重用我们的`can_mark_returned`权限）。

### 测试页面

首先，使用具有访问作者编辑页面权限的帐户（由你决定），登录该站点。

然后导航到作者创建页面： http://127.0.0.1:8000/catalog/author/create/，它应该如下面的截图。

![Form Example: Create Author](/zh-CN/docs/Learn/Server-side/Django/Forms/forms_example_create_author.png)

输入字段的值，然后按“提交” **Submit** ，保存作者记录。现在，你应该进入新作者的详细视图，其 URL 为 `http://127.0.0.1:8000/catalog/author/10`。

你可以通过将 /update/ ，附加到详细视图 URL 的末尾，来测试编辑记录（例如 `http://127.0.0.1:8000/catalog/author/10/update/`） - 我们不显示截图，因为它看起来就像“创建”页面！

最后，我们可以删除页面，方法是将删除，附加到作者详细信息视图 URL 的末尾（例如 `http://127.0.0.1:8000/catalog/author/10/delete/`）。Django 应该显示如下所示的删除页面。按 "是，删除" **(Yes, delete)**。删除记录，并将其带到所有作者的列表中。

![](/zh-CN/docs/Learn/Server-side/Django/Forms/forms_example_delete_author.png)

挑战自己
----

创建一些表单，来创建、编辑和删除书本记录`Book`。你可以使用与作者`Authors`完全相同的结构。如果你的 **book\_form.html** 模板只是 **author\_form.html** 模板的复制重命名版本，则新的“创建图书”页面，将如下所示：

![](/zh-CN/docs/Learn/Server-side/Django/Forms/forms_example_create_book.png)

总结
--

创建和处理表单可能是一个复杂的过程！Django 通过提供声明、呈现和验证表单的编程机制，使其变得更加容易。此外，Django 提供了通用的表单编辑视图，几乎可以完成所有工作，以定义可以创建，编辑和删除与单个模型实例关联的记录的页面。

表单可以完成更多工作（请参阅下面的“请参阅”列表），但你现在应该了解，如何将基本表单和表单处理代码，添加到你自己的网站。

也可以参考
-----

* Working with forms (Django docs)
* Writing your first Django app, part 4 > Writing a simple form (Django docs)
* The Forms API (Django docs)
* Form fields (Django docs)
* Form and field validation (Django docs)
* Form handling with class-based views (Django docs)
* Creating forms from models (Django docs)
* Generic editing views (Django docs)
* 上一页  (en-US)
* Overview: Django
* 下一页


设置 Django 开发环境 - 学习 Web 开发

设置 Django 开发环境
==============

* 上一页
* Overview: Django
* 下一页

 现在，你知道什么是 Django。
 那么我们将向你展示如何在 Windows，Linux（Ubuntu）和 Mac OSX 上设置和测试 Django 开发环境—无论你常用哪种操作系统，本文能给你开发 Django 应用所需的一切。

|  |  |
| --- | --- |
| 前提： | 知道如何在你开发所用的计算机操作系统中，打开终端/命令行和安装软件包。 |
| 目标： | 在你的计算机中运行 Django（1.10）开发环境。 |

Django 开发环境概述
-------------

Django 使你可以轻松配置自己的电脑，以便开始开发网络应用。本节解释你可以从开发环境中获得什么，并提供一些设置和配置选项的概述。本文的其余部分介绍了在**Ubuntu**，**Mac** OSX 和**Windows**上安装 Django 开发环境的 **推荐方法**，以及如何测试。

### 什么是 Django 开发环境？

开发环境是本地计算机上的 Django 安装，在将 Django 应用程序部署到生产环境之前，你可以使用它来开发和测试 Django 应用程序。

Django 本身提供的主要工具是一组用于创建和使用 Django 项目的 Python 脚本，以及可在你电脑的 web 浏览器中测试本地 Django web 应用（在你的计算机，而不是在外部的 web 服务器）。

还有其他外部工具，它们构成了开发环境的一部分，我们将不再赘述。这些包括 文本编辑器 或编辑代码的 IDE，以及像 Git 这样的源代码控制管理工具，用于安全地管理不同版本的代码。我们假设你已经安装了一个文本编辑器。

### 什么是 Django 设置选项？

Django 在安装和配置方面非常灵活。Django 可以：

* 安装在不同的操作系统上。
* 通过源代码、Python 包索引（PyPi）进行安装，而大多数情况下，是通过主机的包管理应用程序安装的。
* 配置为使用几个数据库之一，可能还需要单独安装和配置。
* 在主系统的 Python 环境或在单独的 Python 虚拟环境中运行。

每个选项都需要略微不同的配置和设置。以下小节解释了你的一些选择。在本文的其余部分中，我们将介绍 Django 在几个操作系统上的设置，并且在本教程的剩余模块中将假设你已进行该设置。

**备注：** 其他可能的安装选项在官方 Django 文档中介绍。相应文件 点击这里.

#### 支持哪些操作系统？

Django web 应用程序能运行在几乎任何可以运行 Python3 的计算机上：Windows，Mac OSX，Linux/Unix，Solaris，仅举几例。几乎任何计算机都具备在开发期间运行 Django 所需的性能。

在本文中。我们将提供 Windows，macOS 和 Linux/Unix 的说明。

#### 你应该使用什么版本的 Python?

我们建议你使用最近发行的版本，在本文档写作的时候是 Python 3.8.2。

事实上，Python 3.5 以及更新的版本都可以用来开发，不过对 Python 3.5 的支持可能会在未来的版本更新中被移除。

我们建议你使用最新版本的 Python 3，除非该站点依赖于仅适用于 Python 2 的第三方库。本文将介绍如何为 Python 3 安装环境（Python 2 的等效设置将非常相似）。

**备注：** Python 2.7 无法用于当前的 Django 发行版本（Django 1.11.x 系列是最后支持 Python 2.7 的版本）。

#### 我们在哪里下载 Django?

有三个地方可以下载 Django:

* Python 包资源库 (PyPi)。并用 **pip** 工具进行安装，这是获取 Django 最新稳定版本的最佳方式。
* 计算机软件包管理器。与操作系统捆绑在一起的 Django 发行版是一种常见的安装途径。请注意，打包的版本可能很老，且只能安装到系统 Python 环境中（而这可能不是你想要的）。
* 源代码。你可以从源代码获得并安装最新版本的 Django。这并不推荐给初学者，但是当你准备好开始贡献给 Django 项目本身的时候，它是必需的。

本文介绍如何从 PyPi 安装 Django 的最新稳定版本。

#### 哪个数据库？

Django 支持四个主要数据库（PostgreSQL，MySQL，Oracle 和 SQLite），还有一些社区库可以为其他流行的 SQL 和 NOSQL 数据库提供不同级别的支持。我们建议你为生产和开发选择相同的数据库（尽管 Django 使用其对象关系映射器（ORM）抽象了许多数据库之间的差异，但是仍然存在本可以避免的潜在问题 ).

对于本文（和本模块的大部分），我们将使用将数据存储在文件中的 SQLite 数据库。SQLite 旨在用作轻量级数据库，不能支持高并发。然而，这确实是只读的应用程序的绝佳选择。

**备注：** 当你使用标准工具（django-admin）启动你的网站项目时，Django 将默认使用 SQLite。用来入门时，这是一个很好的选择，因为它不需要额外的配置和设置。

#### 安装本机系统还是 Python 虚拟环境中？

当你安装 Python3 时，将获得一个由所有 Python3 代码共享的全局环境。虽然你可以在该环境中安装任何你喜欢的 Python 包，但是每次只能安装每个包的一个特定版本。

**备注：** 安装到全局环境的 Python 应用程序可能会相互冲突（例如如果它们依赖于同一包的不同版本）。

如果你把 Django 安装到默认/全局环境中，那么在该计算机上将只能定位到 Django 的一个版本。如果你想创建新的网站 (使用最新版本的 Django)，同时仍然维护依赖旧版本的网站，这可能是个问题。

因此，经验丰富的 Python/Django 开发人员通常在独立 Python 虚拟环境中运行 Python 应用程序。这样就可以在一台计算机上实现多个不同的 Django 环境。Django 开发团队同样建议你使用 Python 虚拟环境。

本模块假设已经将 Django 安装到虚拟环境中，下面我们会演示如何进行。

安装 Python 3
-----------

为了使用 Django，你需要在你的操作系统中安装 Python。如果你使用 Python3，那么你同样需要Python 包管理工具 — *pip3* — 用来管理 (安装，更新和删除) 被 Django 和其他 Python 应用程序使用的 Python 软件包／库。

本节简要介绍了如何检查有哪些版本的 Python，并根据需要安装适用于 **Ubuntu Linux 16.04,macOS, and Windows 10**的新版本。

**备注：** 根据你的平台，你还可以从操作系统自己的软件包管理器或其他机制安装 Python / pip。对于大多数平台，你可以从https://www.python.org/downloads/下载所需的安装文件，并使用该平台特定的方法进行安装。

### Ubuntu 18.04

Ubuntu Linux 18.04 LTS 默认包含 Python 3.6.6。你可以通过在 Bash 终端中运行以下命令来确认这一点：

```bash
python3 -V
    Python 3.6.6
```

然而，在默认情况下，为 Python 3（包括 Django）安装软件包的 Python 包管理工具**不可用。你**可以在**bash**终端中使用以下命令安装 **pip3**：

```bash
sudo apt-get install python3-pip
```

### macOS

macOS 的"El Capitan" 及其他最新版本不包含 Python 3。你可以通过在 bash 终端中运行一下命令来确认：

```bash
python3 -V
 -bash: python3: command not found
```

你可以轻松地从python.org 安装 Python 3（以及 pip3 工具）:

1. 下载所需的安装程序：
    1. 点击 https://www.python.org/downloads/
    2. 选择 **Download Python 3.8.2** (具体的版本号可能不同)。
2. 使用 Finder 找到安装包，然后双击运行，并按照提示进行安装。

之后可以通过检查 Python3 版本确认是否安装成功，如下所示：

```bash
python3 -V
 Python 3.8.2
```

你也可以通过列出可用的包来检查 pip3 是否安装了：

```bash
pip3 list
```

### Windows 10

windows 默认不包含 Python, 但你可以从python.org 轻松地安装它（以及 pip3 工具）：

1. 下载所需版本：
    1. 点击 https://www.python.org/downloads/
    2. 选择 **Download Python 3.8.2** (具体的版本号可能不同)。
2. 双击现在的文件并按照提示安装 Python。
3. 确保勾选了"Add Python to PATH"选项。

你可以在命令提示符中输入以下内容来验证是否安装了 Python:

```bash
python -V
    Python 3.8.2
```

Windows 安装程序默认包含 pip3 (Python 包管理器)。同样在命令提示符中输入以下内容来列出已安装的包：

```bash
pip3 list
```

**备注：** 安装包应该已把运行上述命令所需的一切设置完成。但如果你得到的消息是找不到 Python，那么你可能忘记将 Python 添加到系统路径中了。你可以通过再次运行安装包，选择"Modify"，并在下一页面中勾选 "Add Python to environment variables"来修复这个问题。

在 Python 虚拟环境中使用 Django
-----------------------

我们使用virtualenvwrapper（Linux 及 macOS）和 virtualenvwrapper-win（WIndows）来创建 Python 虚拟环境，而它们又使用了virtualenv (en-US)。封装工具创建了一个一致的接口来管理各个平台上的接口。

### 安装虚拟环境软件

#### Ubuntu 虚拟环境设置

安装了 Python 和 pip 之后，你就可以安装 virtualenvwrapper（包括了 virtualenv）。可以在这里找到正式的安装指南，或按照以下指导操作。

使用 pip3 安装该工具：

```bash
sudo pip3 install virtualenvwrapper
```

然后将以下代码行添加到 shell 启动文件的末尾 (这是主目录中的一个隐藏文件，名字是.bashrc)。这些文件设置了虚拟环境应该存在的位置、开发项目目录的位置以及与这个包一起安装的脚本的位置。

```bash
export WORKON\_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER\_PYTHON=/usr/bin/python3
export VIRTUALENVWRAPPER\_VIRTUALENV\_ARGS=' -p /usr/bin/python3 '
export PROJECT\_HOME=$HOME/Devel
source /usr/local/bin/virtualenvwrapper.sh
```

**备注：** `VIRTUALENVWRAPPER_PYTHON` 和 `VIRTUALENVWRAPPER_VIRTUALENV_ARGS`变量指向 Python3 的常规安装位置，`source /usr/local/bin/virtualenvwrapper.sh`指向`virtualenvwrapper.sh`脚本的一般安装位置。如果你在测试时发现*virtualenv*无法正常工作，则要检查的一件事是 Python 和该脚本是否在预期的位置（然后适当更改启动文件）。

你可以使用`which virtualenvwrapper.sh` 和 `which python3`命令为你的系统找到正确的安装位置。

然后通过在终端中运行以下命令重载启动文件：

```bash
source ~/.bashrc
```

此时，你应该能看到一些脚本正在运行，如下所示：
```

virtualenvwrapper.user_scripts creating /home/ubuntu/.virtualenvs/premkproject
virtualenvwrapper.user_scripts creating /home/ubuntu/.virtualenvs/postmkproject
...
virtualenvwrapper.user_scripts creating /home/ubuntu/.virtualenvs/preactivate
virtualenvwrapper.user_scripts creating /home/ubuntu/.virtualenvs/postactivate
virtualenvwrapper.user_scripts creating /home/ubuntu/.virtualenvs/get_env_details
```

然后你就可以使用 `mkvirtualenv`命令创建一个新的虚拟环境。

#### macOS 虚拟环境设置

在 macOS 上设置*virtualenvwrapper* 几乎和在 Ubuntu 上是一样的（你同样可以按照以下指导操作，或在这里找到正式的安装指南）。

使用*pip*安装*virtualenvwrapper*(并绑定*virtualenv*)，如下所示。

```bash
sudo pip3 install virtualenvwrapper
```

然后将以下代码行添加到 shell 启动文件的末尾：

```bash
export WORKON\_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER\_PYTHON=/usr/bin/python3
export PROJECT\_HOME=$HOME/Devel
source /usr/local/bin/virtualenvwrapper.sh
```

**备注：** `VIRTUALENVWRAPPER_PYTHON` 和 `VIRTUALENVWRAPPER_VIRTUALENV_ARGS`变量指向 Python3 的常规安装位置，`source /usr/local/bin/virtualenvwrapper.sh`指向`virtualenvwrapper.sh`脚本的一般安装位置。如果你在测试时发现*virtualenv*无法正常工作，则要检查的一件事是 Python 和该脚本是否在预期的位置（然后适当更改启动文件）。

例如，在 macOS 上的一个安装测试中，启动文件中必须有以下几行代码：

```bash
export WORKON\_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER\_PYTHON=/Library/Frameworks/Python.framework/Versions/3.7/bin/python3
export PROJECT\_HOME=$HOME/Devel
source /Library/Frameworks/Python.framework/Versions/3.7/bin/virtualenvwrapper.sh
```

你可以使用`which virtualenvwrapper.sh` 和 `which python3`命令为你的系统找到正确的安装位置。

此处使用和 Ubuntu 相同的代码行，但是启动文件是主目录中叫做 **.bash\_profile** 的隐藏文件。

**备注：** 如果找不到 **.bash\_profile** 进行编辑，也可以使用 nano 在终端中打开它，命令看起来类似于：

```bash
cd ~  # Navigate to my home directory
ls -la #List the content of the directory. YOu should see .bash\_profile
nano .bash_profile # Open the file in the nano text editor, within the terminal
# Scroll to the end of the file, and copy in the lines above
# Use Ctrl+X to exit nano, Choose Y to save the file.
```

然后通过在终端中运行以下命令重载启动文件：

```bash
source ~/.bashrc
```

此时，你应该能看到一些脚本正在运行（和 Ubuntu 中同样的脚本）。然后你就可以使用 `mkvirtualenv`命令创建一个新的虚拟环境。

#### Windows 10 虚拟环境设置

安装 virtualenvwrapper-win 甚至比设置*virtualenvwrapper* 更简单，因为你无需配置工具用来存储虚拟环境信息的位置（有一个默认值）。你需要做的只是在命令提示符中运行以下命令：

```bash
pip3 install virtualenvwrapper-win
```

然后你就可以使用 `mkvirtualenv`命令创建一个新的虚拟环境。

### 创建虚拟环境

一旦你成功安装了 *virtualenvwrapper* 或 *virtualenvwrapper-win*，那么在所有平台中使用虚拟环境的方法是非常相似的。

现在你可以使用 `mkvirtualenv`命令创建一个新的虚拟环境。在运行此命令时，你将看到正在设置的环境（你所看到的只略微与平台相关）。命令完成后，新的虚拟环境将被激活——你能看到提示符的开头就是括号中的环境名称（以下我们展示的是 Ubuntu 的，但是在 Windows/macOS 上，末行时相似的|）

```bash
$ mkvirtualenv my_django_environment

Running virtualenv with interpreter /usr/bin/python3
...
virtualenvwrapper.user_scripts creating /home/ubuntu/.virtualenvs/t_env7/bin/get_env_details
(my_django_environment) ubuntu@ubuntu:~$
```

现在你已进入虚拟环境，可以进行 Django 安装并开始开发。

**备注：** 从现在开始，在本文 (实际上是该模块) 中，请假定所有命令都在类似于我们上面设置的 Python 虚拟环境中运行。

### 使用一个虚拟环境

你应该知道一些其他有用的命令 (在工具的文档中还有更多，但这些是你将经常使用的命令)：

* `deactivate` —退出当前的 Python 虚拟环境
* `workon` — 列出可用的所有虚拟环境
* `workon name_of_environment` —激活特定的 Python 虚拟环境
* `rmvirtualenv name_of_environment` — 移除特定的虚拟环境
安装 Django
---------

一旦你创建了一个虚拟环境，并且使用`workon` 进入了它，就可以使用 pip3 来安装 Django。

```bash
pip3 install django
```

你可以通过运行以下命令来测试 Django 是否安装（这只是用来测试 Python 是否可以找到 Django 模块）：

```bash
# Linux/macOS
python3 -m django --version
    1.10.10

# Windows
py -3 -m django --version
    1.10.10
```

**备注：** 如果上面的 Windows 命令没有显示 django 模块，请尝试：

```bash
python -m django --version
```

在 Windows 中，Python 3 脚本是通过在命令前面加上`py -3`来启动的，尽管该脚本可能会因你的特定安装而有所不同。如果遇到命令问题，请尝试省略`-3`修饰符。在 Linux /macOS 中，命令是 python3。

**警告：** 本模块的其余部分使用 Linux 命令来调用 Python 3（`python3`）。如果你在 Windows 上工作，只需将此前缀替换为： `py -3`

测试你的安装
------

上面的测试工作并不是很有趣。一个更有趣的测试是创建一个框架项目并查看它的工作情况。要做到这一点，先在你的命令提示符/终端导航到你想存储你**Django**应用程序的位置。为你的测试站点创建一个文件夹并进入其中。

```bash
mkdir django_test
cd django_test
```

然后，你可以像所展示的一样使用 django-admin 工具创建一个名为“*mytestsite*”的新框架站点。创建网站后，你可以 CD 到此文件夹，并将在其中找到管理项目的主要脚本，名为**manage.py**。

```bash
django-admin startproject mytestsite
cd mytestsite
```

我们可以在这个文件夹中使用**manager.py**和`runserver`命令运行*开发 web 服务器*，如下所示。

```bash
$ python3 manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

December 16, 2018 - 07:06:30
Django version 2.2.12, using settings 'mytestsite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

**备注：** 上面的命令显示了 Linux /macOS 命令。你可以忽略关于“15 unapplied migration(s)”的警告！

一旦服务器运行，你可以通过本地 Web 浏览器打开`http://127.0.0.1:8000/`来查看该站点。你应该看到一个如下所示的网站：

![The home page of the skeleton Django app.](/zh-CN/docs/Learn/Server-side/Django/development_environment/django_skeleton_app_homepage_django_4_0.png)

概要
--

现在，你的计算机中已经启动并运行了一个 Django 开发环境。

在测试部分，你还简要地了解了如何使用`django-admin startproject`创建一个新的 Django 网站，并使用开发 web 服务器 (`python3 manager .py runserver`) 在浏览器中运行它。在下一篇文章中，我们将对此过程进行扩展，构建一个简单但完整的 web 应用程序。

看看瞧瞧
----

* 快速安装指南(Django docs)
* 如何安装 Django — 完整指南 (Django docs) - 包含有关如何删除 Django 的信息
* 如何安装 Django 在 Windows (Django docs)
* 上一页
* Overview: Django
* 下一页


Django Tutorial Part 3: 使用模型 - 学习 Web 开发

Django Tutorial Part 3: 使用模型
============================

* 上一页
* Overview: Django
* 下一页

这篇文章展示了如何为我们的 LocalLibray（本地图书馆）网站定义 models。它解释了一个模型是什么，它是怎么被声明的，和其中的一些主要域类型。

|  |  |
| --- | --- |
| Prerequisites: | Django Tutorial Part 2: Creating a skeleton website. |
| Objective: | 能够设计并创建你自己的数据模型，并为其合适地选择域。 |

概要
--

Django 网络应用通过作为模型被参照的 Python 对象访问并管理数据。模型定义了储存数据的结构，包括域类型和可能的最大值，默认值，可选择的列表，帮助理解文档的文本，表格内的标签文本，等等。模型的定义是独立于数据库的——你可以为你自己的项目设置选择一种。一旦你已经选择了你想用的数据库，你不需要直接谈论它——你只是写出你的模型结构和其他代码，然后 Django 会为你处理所有繁琐的和数据库打交道的工作。

这个教程展示了如何定义并访问 LocalLibrary website 的模型。

设计 LocalLibaray 模型
------------------

在你继续开始为模型写代码前，用几分钟考虑一下我们需要储存什么数据和不同对象之间的关系是很有价值的。

我们知道我们需要存储书籍（书名，简介，作者，写作语言，类别，ISBN 编号）和我们可能有的可获取的副本数量（全局独立 ID，可获取状态，等等）。我们可能需要存储更多的关于作者的信息而不仅仅是她们的名字。我们希望能够将信息根据书名，作者，写作语言和类别分类。

当设计你的模型时，给予每个“对象”（一组关联信息）独立的模型似乎挺说得通的。当前情况下，最为明显的对象就是书籍和作者。

你可能在想相比硬编码所有的选项到网站上，用模型来呈现选择列表（例如包括了许多选项的下拉菜单）——我们推荐这样，尤其是当选项未知或者可能改变时。显然，目前模型的候选者包括了书的流派（例如科幻小说，法国诗歌，等等）和语言（英语，法语，日语）。

一旦我们决定了模型和域，我们需要考虑他们的关系。Django 允许你定义一对一 (`OneToOneField`)，一对多 (`ForeignKey`) 和多对多 (`ManyToManyField`) 的关系。（译者注：此处我们以关系型数据库为基准，如果采用 NoSQL，如 MangoDB 则无法如此考虑）

思考着以上内容，以下的 UML 关系表显示了我们在该例子里定义的模型。如上所述哦，我们已经为书籍（大概的细节），书籍实例（物理副本是否可获取状态）和作者创建了模型。我们也决定了为流派而创建的模型，所以变量值可以通过管理界面获取。此外，我们决定了不创建 `BookInstance:status`的模型——我们已经硬编码了这个值 (`LOAN_STATUS`) 因为我们不期望这些被改变。通过每个方框你可以看到模型名字，值域名和类型，还有方法和返回的类型。

这个图表也显示了模型之间的关系，including their *multiplicities*. 这些在图表里每个模型边上的数字（最大和最小）显示了他们的关系。例如，链接 Book 和 Genre 两个盒子的线表示它们是关联的。靠近 Book 模型的数字显示一本书必须有一个或多个 Genre（要多少有多少），然而另一端靠近 Genre 的数字显示了它可以有零或无数本相关的书籍。

![LocalLibrary Model UML - v3](/zh-CN/docs/Learn/Server-side/Django/Models/local_library_model_uml_v0_1.png)

**备注：** 下一部分提供了基本的关于模型如何被定义和使用的解释。边阅读，边考虑以下我们是如何根据以上的图标构建数据库内的模型的。

Model primer
------------

This section provides a brief overview of how a model is defined and some of the more important fields and field arguments.

### Model definition

Models are usually defined in an application's **models.py** file. They are implemented as subclasses of `django.db.models.Model`, and can include fields, methods and metadata. The code fragment below shows a "typical" model, named `MyModelName`:

```python
from django.db import models

class MyModelName(models.Model):
    """
 A typical class defining a model, derived from the Model class.
 """

    # Fields
    my_field_name = models.CharField(max_length=20, help_text="Enter field documentation")
    ...

    # Metadata
    class Meta:
        ordering = ["-my\_field\_name"]

    # Methods
    def get\_absolute\_url(self):
            """
 Returns the url to access a particular instance of MyModelName.
 """
            return reverse('model-detail-view', args=[str(self.id)])

    def \_\_str\_\_(self):
        """
 String for representing the MyModelName object (in Admin site etc.)
 """
        return self.field_name
```

In the below sections we'll explore each of the features inside the model in detail:

#### 域

一个模型可以有任意数量的域，或任意的类型——每个用一行呈现我们想存储进数据库的数据。让我们看一下以下的例子吧 o(≧v≦)o：

```python
my_field_name = models.CharField(max_length=20, help_text="Enter field documentation")
```

上面的例子有一个单域，叫做 my\_field\_name，类型是 models.CharField——这意味着此域会包含着由字母组成的字符串们。域类型被特殊的 class 赋值，这确认了记录的类型是用来存入数据库的，以及当用户从 HTML 表格里提交值后，我们用来验证提交的值是否有效的条件。

字段类型还可以获取参数，进一步指定字段如何存放或如何被使用。在这里的情况下，我们给了字段两个参数：

* `max_length=20` — 表示此字段中值的最大长度为 20 个字符的状态。
* `help_text="Enter field documentation"` — 提供一个帮助用户的文本标签，让用户知道当前透过 HTML 表单输入时要提供什么值。

字段名称用于在视图和模版中引用它。字段还有一个标签，它被指定一个参数（`verbose_name`），或者通过大写字段的变量名的第一个字母，并用空格替换下划线（例如`my_field_name`的默认标签为 My field name）。

如果模型在表单中呈现（例如：在管理站点中），则声明该字段的顺序，将影响其默认顺序，但可能会被覆盖。

##### Common field arguments

当声明很多／大多数不同的字段类型时，可以使用以下常用参数：

* help\_text :提供 HTML 表单文本标签 (eg i 在管理站点中),如上所述。
* verbose\_name :字段标签中的可读性名称，如果没有被指定，Django 将从字段名称推断默认的详细名称。
* default :该字段的默认值。这可以是值或可呼叫物件 (callable object)，在这种情况下，每次创建新纪录时都将呼叫该物件。
* null：如为`True`，即允许 Django 于资料库该栏位写入`NULL`（但栏位型态如为`CharField`则会写入空字串）。预设值是`False`。
* blank :如为 `True`，表单中的字段被允许为空白。默认是`False`，这意味着 Django 的表单验证将强制你输入一个值。这通常搭配 `NULL=True` 使用，因为如果要允许空值，你还希望数据库能够适当地表示它们。
* choices :这是给此字段的一组选项。如果提供这一项，预设对应的表单部件是「该组选项的列表」，而不是原先的标准文本字段。
* primary\_key :如果是 True，将当前字段设置为模型的主键（主键是被指定用来唯一辨识所有不同表记录的特殊数据库栏位 (column)）。如果没有指定字段作为主键，则 Django 将自动为此添加一个字段。

还有许多其他选项—你可以在这里看到完整的字段选项。

##### Common field types

以下列表描述了一些更常用的字段类型。

* CharField 是用来定义短到中等长度的字段字符串。你必须指定`max_length`要存储的数据。
* TextField 用于大型任意长度的字符串。你可以`max_length`为该字段指定一个字段，但仅当该字段以表单显示时才会使用（不会在数据库级别强制执行）。
* IntegerField 是一个用于存储整数（整数）值的字段，用于在表单中验证输入的值为整数。
* DateField 和DateTimeField 用于存储／表示日期和日期／时间信息（分别是`Python.datetime.date` 和 `datetime.datetime` 对象）。这些字段可以另外表明（互斥）参数 `auto_now=Ture`（在每次保存模型时将该字段设置为当前日期），`auto_now_add`（仅设置模型首次创建时的日期）和 `default`（设置默认日期，可以被用户覆盖）。
* EmailField 用于存储和验证电子邮件地址。
* FileField 和ImageField 分别用于上传文件和图像（`ImageField` 只需添加上传的文件是图像的附加验证）。这些参数用于定义上传文件的存储方式和位置。
* AutoField 是一种 **IntegerField** 自动递增的特殊类型。如果你没有明确指定一个主键，则此类型的主键将自动添加到模型中。
* ForeignKey 用于指定与另一个数据库模型的一对多关系（例如，汽车有一个制造商，但制造商可以制作许多汽车）。关系的“一”侧是包含密钥的模型。
* ManyToManyField 用于指定多对多关系（例如，一本书可以有几种类型，每种类型可以包含几本书）。在我们的图书馆应用程序中，我们将非常类似地使用它们 ForeignKeys，但是可以用更复杂的方式来描述组之间的关系。这些具有参数 `on_delete` 来定义关联记录被删除时会发生什么（例如，值 `models.SET_NULL` 将简单地设置为值 NULL）。

还有许多其他类型的字段，包括不同类型数字的字段（大整数，小整数，浮点数），布林值，URLs，唯一 ids 和其他“时间相关”的信息（持续时间，时间等）。你可以查阅完整列表 .

#### 元数据 (Metadata)

你可以通过宣告 class Meta 来宣告模型级别的元数据，如图所示：

```python
class Meta:
    ordering = ['-my\_field\_name']
```

此元数据最有用的功能之一是控制在查询模型类型时返回之记录的默认排序。你可以透过在`ordering`属性的字段名称列表中指定匹配顺序来执行此操作，如上所示。排序将依赖字段的类型（字符串字段按字母顺序排序，而日期字段按时间顺序排序）。如上所示，你可以使用减号（-）对字段名称进行前缀，以反转排序顺序。

例如，如果我们选择依照此预设来排列书单：

```python
ordering = ['title', '-pubdate']
```

书单通过标题依据--字母排序--排列，从 A 到 Z，然后再依每个标题的出版日期，从最新到最旧排列。

另一个常见的属性是 `verbose_name`,一个 `verbose_name`说明单数和复数形式的类别。

```python
verbose_name = 'BetterName'
```

其他有用的属性允许你为模型创建和应用新的“访问权限”（预设权限会被自动套用），允许基于其他的字段排序，或声明该类是”抽象的“（你无法创建的记录基类，并将由其他型号派生）。

许多其他元数据选项控制模型中必须使用哪些数据库以及数据的存储方式。（如果你需要模型映射一个现有数据库，这会有用）。

完整有用的元数据选项在这里Model metadata options (Django docs).

#### 方法

一个模型也可以有方法。

最起码，在每个模型中，你应该定义标准的 Python 类方法 `__str__()`，**来为每个物件返回一个人类可读的字符串**。此字符用于表示管理站点的各个记录（以及你需要引用模型实例的任何其他位置）。通常这将返回模型中的标题或名称字段。

```python
def \_\_str\_\_(self):
    return self.field_name
```

Django 方法中另一个常用方法是 `get_absolute_url()` ，这函数返回一个在网站上显示个人模型记录的 URL（如果你定义了该方法，那么 Django 将自动在“管理站点”中添加“在站点中查看“按钮在模型的记录编辑栏）。`get_absolute_url()`的典型示例如下：

```python
def get\_absolute\_url(self):
    """Returns the url to access a particular instance of the model."""
    return reverse('model-detail-view', args=[str(self.id)])
```

**注意** :假设你将使用 URL `/myapplication/mymodelname/2` 来显示模型的单个记录（其中“2”是 id 特定记录），则需要创建一个 URL 映射器来将响应和 id 传递给“模型详细视图” （这将做出显示记录所需的工作）。以上示例中，`reverse()`函数可以“反转”你的 url 映射器（在上诉命名为“model-detail-view”的案例中，以创建正确格式的 URL。

当然要做这个工作，你还是要写 URL 映射，视图和模版！

你可以定义一些你喜欢的其他方法，并从你的代码或模版调用它们（只要它们不带任何参数）。

### Model management

一旦你定义了模型类，你可以使用它们来创建，更新或删除记录，并运行查询获取所有记录或特定的记录子集。当我们定义我们的视图，我们将展示给你在这个教程如何去做。

#### 创建和修改记录

要创建一个记录，你可以定义一个模型实例，然后呼叫 `save()`。

```python
# Create a new record using the model's constructor.
a_record = MyModelName(my_field_name="Instance #1")

# Save the object into the database.
a_record.save()
```

**备注：** 如果没有任何的栏位被宣告为`主鍵`，这笔新的纪录会被自动的赋予一个主键并将主键栏命名为`id`。上例的那笔资料被储存后，试着查询这笔纪录会看到它被自动赋予 1 的编号。

你可以透过「点 (dot) 的语法」取得或变更这笔新资料的栏位 (字段)。你需要呼叫`save()`将变更过的资料存进资料库：

```python
# Access model field values using Python attributes.
print(a_record.id) #should return 1 for the first record.
print(a_record.my_field_name) # should print 'Instance #1'

# Change record by modifying the fields, then calling save().
a_record.my_field_name="New Instance Name"
a_record.save()
```

#### 搜寻纪录

你可以使用模型的 `objects` 属性 (由 base class 提供) 搜寻符合某个条件的纪录

**备注：** 要用"抽象的"模型还有栏位说明怎么搜寻纪录可能会有点令人困惑。我们会以一个 Book 模型，其包含`title`与`genre`字段，而 genre 也是一个仅有`name`一个字段的模型。

我们可以取得一个模型的所有纪录，为一个 `QuerySet` 使用`objects.all()`。`QuerySet` 是一个可迭代的物件，表示他含有多个物件，而我们可以藉由迭代/回圈取得每个物件。

```python
all_books = Book.objects.all()
```

Django 的 `filter()`方法让我们可以透过符合特定文字或数值的字段筛选回传的`QuerySet`。例如筛选书名里有"wild"的书并且计算总数，如下面所示。

```python
wild_books = Book.objects.filter(title__contains='wild')
number_wild_books = Book.objects.filter(title__contains='wild').count()
```

要比对的字段与比对方法都要被定义在筛选的参数名称里，并且使用这个格式：`比對字段__比對方法` (请注意上方范例中的 `title` 与 `contains` 中间隔了两个底线唷)。在上面我们使用大小写区分的方式比对`title`。还有很多比对方式可以使用： `icontains`(不区分大小写), `iexact`(不大小写区分且完全符合), `exact`(区分大小写但完全符合) 还有 `in`, `gt`(大于), `startswith`,之类的。全部的用法在这里。

有时候你会须要透过某个一对多的字段来筛选 (例如一个 `外鍵`)。这样的状况下，你可以使用两个底线来指定相关模型的字段。例如透过某个特定的 genre 名称筛选书籍，如下所示：

```python
books_containing_genre = Book.objects.filter(genre__name__icontains='fiction')  # Will match on: Fiction, Science fiction, non-fiction etc.
```

**备注：** 你可以用下划线来表示不同关系 (`ForeignKey`/`ManyToManyField`) .例如，一本书有不同的类型，用“cover“关系可能会帮助起一个参数名字 `type__cover__name__exact='hard'.`

还有很多是你可以用索引 (queries) 来做的，包含从相关的模型做向后查询 (backwards searches)、连锁过滤器 (chaining filters)、回传「值的小集合」等。更多资讯可以到 Making queries (Django Docs) 查询。

Defining the LocalLibrary Models
--------------------------------

In this section we will start defining the models for the library. Open *models.py (in /locallibrary/catalog/)*. The boilerplate at the top of the page imports the *models* module, which contains the model base class `models.Model` that our models will inherit from.

```python
from django.db import models

# Create your models here.
```

### Genre model

Copy the Genre model code shown below and paste it into the bottom of your `models.py` file. This model is used to store information about the book category — for example whether it is fiction or non-fiction, romance or military history, etc. As mentioned above, we've created the Genre as a model rather than as free text or a selection list so that the possible values can be managed through the database rather than being hard coded.

```python
class Genre(models.Model):
    """
 Model representing a book genre (e.g. Science Fiction, Non Fiction).
 """
    name = models.CharField(max_length=200, help_text="Enter a book genre (e.g. Science Fiction, French Poetry etc.)")

    def \_\_str\_\_(self):
        """
 String for representing the Model object (in Admin site etc.)
 """
        return self.name
```

The model has a single `CharField` field (`name`), which is used to describe the genre (this is limited to 200 characters and has some `help_text`. At the end of the model we declare a `__str__()` method, which simply returns the name of the genre defined by a particular record. No verbose name has been defined, so the field will be called `Name` in forms.

### Book model

Copy the Book model below and again paste it into the bottom of your file. The book model represents all information about an available book in a general sense, but not a particular physical "instance" or "copy" available for loan. The model uses a `CharField` to represent the book's `title` and `isbn` (note how the `isbn` specifies its label as "ISBN" using the first unnamed parameter because the default label would otherwise be "Isbn"). The model uses `TextField` for the `summary`, because this text may need to be quite long.

```python
from django.urls import reverse #Used to generate URLs by reversing the URL patterns

class Book(models.Model):
    """
 Model representing a book (but not a specific copy of a book).
 """
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    # Foreign Key used because book can only have one author, but authors can have multiple books
    # Author as a string rather than object because it hasn't been declared yet in the file.
    summary = models.TextField(max_length=1000, help_text="Enter a brief description of the book")
    isbn = models.CharField('ISBN',max_length=13, help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
    genre = models.ManyToManyField(Genre, help_text="Select a genre for this book")
    # ManyToManyField used because genre can contain many books. Books can cover many genres.
    # Genre class has already been defined so we can specify the object above.

    def \_\_str\_\_(self):
        """
 String for representing the Model object.
 """
        return self.title

    def get\_absolute\_url(self):
        """
 Returns the url to access a particular book instance.
 """
        return reverse('book-detail', args=[str(self.id)])
```

The genre is a `ManyToManyField`, so that a book can have multiple genres and a genre can have many books. The author is declared as `ForeignKey`, so each book will only have one author, but an author may have many books (in practice a book might have multiple authors, but not in this implementation!)

In both field types the related model class is declared as the first unnamed parameter using either the model class or a string containing the name of the related model. You must use the name of the model as a string if the associated class has not yet been defined in this file before it is referenced! The other parameters of interest in the `author` field are `null=True`, which allows the database to store a `Null` value if no author is selected, and `on_delete=models.SET_NULL`, which will set the value of the author to `Null` if the associated author record is deleted.

The model also defines `__str__()` , using the book's `title` field to represent a `Book` record. The final method, `get_absolute_url()` returns a URL that can be used to access a detail record for this model (for this to work we will have to define a URL mapping that has the name `book-detail`, and define an associated view and template).

### BookInstance model

Next, copy the `BookInstance` model (shown below) under the other models. The `BookInstance` represents a specific copy of a book that someone might borrow, and includes information about whether the copy is available or on what date it is expected back, "imprint" or version details, and a unique id for the book in the library.

Some of the fields and methods will now be familiar. The model uses

* `ForeignKey` to identify the associated Book (each book can have many copies, but a copy can only have one `Book`).
* `CharField` to represent the imprint (specific release) of the book.

```python
import uuid # Required for unique book instances

class BookInstance(models.Model):
    """
 Model representing a specific copy of a book (i.e. that can be borrowed from the library).
 """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text="Unique ID for this particular book across whole library")
    book = models.ForeignKey('Book', on_delete=models.SET_NULL, null=True)
    imprint = models.CharField(max_length=200)
    due_back = models.DateField(null=True, blank=True)

    LOAN_STATUS = (
        ('m', 'Maintenance'),
        ('o', 'On loan'),
        ('a', 'Available'),
        ('r', 'Reserved'),
    )

    status = models.CharField(max_length=1, choices=LOAN_STATUS, blank=True, default='m', help_text='Book availability')

    class Meta:
        ordering = ["due\_back"]

    def \_\_str\_\_(self):
        """
 String for representing the Model object
 """
        return '%s (%s)' % (self.id,self.book.title)
```

We additionally declare a few new types of field:

* `UUIDField` is used for the `id` field to set it as the `primary_key` for this model. This type of field allocates a globally unique value for each instance (one for every book you can find in the library).
* `DateField` is used for the `due_back` date (at which the book is expected to come available after being borrowed or in maintenance). This value can be `blank` or `null` (needed for when the book is available). The model metadata (`Class Meta`) uses this field to order records when they are returned in a query.
* `status` is a `CharField` that defines a choice/selection list. As you can see, we define a tuple containing tuples of key-value pairs and pass it to the choices argument. The value in a key/value pair is a display value that a user can select, while the keys are the values that are actually saved if the option is selected. We've also set a default value of 'm' (maintenance) as books will initially be created unavailable before they are stocked on the shelves.

The model `__str__()` represents the `BookInstance` object using a combination of its unique id and the associated `Book`'s title.

**备注：** A little Python:

* The value returned by `__str__()` is a *formatted string*. Within the string we use `%s` to declare "placeholders'. After the string we specify `%` and then a tuple containing the values to be inserted in the placeholders. If you just have one placeholder then you can omit the tuple — e.g. `'My value: %s' % variable.`
 Note also that although this approach is perfectly valid, please be aware that it is no longer prefered. Since Python 3 you should instead use the format method, eg. '{0} ({1})'.format(self.id,self.book.title). You can read more about it here.

### Author model

Copy the `Author` model (shown below) underneath the existing code in **models.py**.

All of the fields/methods should now be familiar. The model defines an author as having a first name, last name, date of birth, and (optional) date of death. It specifies that by default the `__str__()` returns the name in *last name*, *firstname* order. The `get_absolute_url()` method reverses the `author-detail` URL mapping to get the URL for displaying an individual author.

```python
class Author(models.Model):
    """
 Model representing an author.
 """
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get\_absolute\_url(self):
        """
 Returns the url to access a particular author instance.
 """
        return reverse('author-detail', args=[str(self.id)])

    def \_\_str\_\_(self):
        """
 String for representing the Model object.
 """
        return '%s, %s' % (self.last_name, self.first_name)
```

Re-run the database migrations
------------------------------

All your models have now been created. Now re-run your database migrations to add them to your database.

```bash
python3 manage.py makemigrations
python3 manage.py migrate
```

Language model — challenge
--------------------------

Imagine a local benefactor donates a number of new books written in another language (say, Farsi). The challenge is to work out how these would be best represented in our library website, and then to add them to the models.

Some things to consider:

* Should "language" be associated with a `Book`, `BookInstance`, or some other object?
* Should the different languages be represented using model, a free text field, or a hard-coded selection list?

After you've decided, add the field. You can see what we decided on Github here.

Summary
-------

In this article we've learned how models are defined, and then used this information to design and implement appropriate models for the *LocalLibrary* website.

At this point we'll divert briefly from creating the site, and check out the *Django Administration site*. This site will allow us to add some data to the library, which we can then display using our (yet to be created) views and templates.

See also
--------

* Writing your first Django app, part 2 (Django docs)
* Making queries (Django Docs)
* QuerySet API Reference (Django Docs)
* 上一页
* Overview: Django
* 下一页


评估：DIY Django 微博客 - 学习 Web 开发

评估：DIY Django 微博客
=================

* 上一页
* Overview: Django

在这个评估中，你将使用你在 Django Web Framework (Python) 模块中获得的知识，来创建一个非常基本的博客。

|  |  |
| --- | --- |
| 前提： | 在尝试此评估之前，你应该已经完成了本模块中的所有文章。 |
| 目标： | 测试对 Django 基础知识的理解，包括 URL 配置，模型，视图，表单和模板。 |

项目概要
----

下面列出了需要显示的页面、URL 和其他要求：

| 页面 | URL | 要求 |
| --- | --- | --- |
| 主页 | `/` 和 `/blog/` | 描述站点的索引页面。 |
| 所有博客文章列表 | `/blog/blogs/` | 所有博客文章列表：* 可通过侧边栏链接访问所有用户。
* 列表的排序按发布日期（从最新到最旧）。
* 列表分页为每 5 篇文章 1 个分页。
* 列表项显示博客标题，发布日期和作者。
* 博客帖子名称，链接到博客详细信息页面。
* 博主（作者姓名）链接到博客作者详细信息页面。
 |
| 博客作者（博主）详细信息页面 | `/blog/blogger/*<author-id>*` | 指定作者的信息（按 ID）和他们的博客文章列表：* 可以从博客文章等的作者链接访问所有用户
* 包含有关博主/作者的一些简要经历信息。
* 按发布日期排序的列表（从最新到最旧）。
* 没有分页。
* 列表项仅显示博客帖子名称和发布日期。
* 博客帖子名称，链接到博客详细信息页面。
 |
| 博客帖子详细信息页面 | `/blog/*<blog-id>*` | 博客帖子详情。* 可从博客帖子列表访问所有用户。
* 页面包含博客文章：姓名，作者，发布日期和内容。
* 博客文章的评论应显示在底部。
* 评论应按顺序排序：从最旧到最近。
* 包含为登录用户添加注释的链接（请参阅注释表单页面）
* 博客帖子和评论，只需显示纯文本。不需要支持任何类型的 HTML
 标记（例如链接，图像，粗体/斜体等）。
 |
| 所有博主的名单 | `/blog/bloggers/` | 系统上的博主列表：* 可从站点侧栏访问所有用户
* 博主名称链接到博客作者详细信息页面。
 |
| 评论表单页面 | `/blog/*<blog-id>*/create` | 为博客帖子创建评论：* 只能从博客帖子详细信息页面底部的链接，访问登录用户（仅限）。
* 显示表单以及用于输入注释的描述（发布日期和博客不可编辑）。
* 发布评论后，该页面将重定向回相关的博客帖子页面。
* 用户无法编辑或删除其帖子。
* 注销用户将被引导至登录页面进行登录，然后才能添加评论。登录后，他们将被重定向，回到他们想要评论的博客页面。
* 评论页面应包含被评论的博客帖子的名称/链接。
 |
| 用户认证页面 | `/accounts/*<standard urls>*` | 用于登录，注销和设置密码的标准 Django 身份验证页面：* 应该可以通过侧边栏链接，访问登录/退出页面。
 |
| 管理站点 | `/admin/*<standard urls>*` | 应启用管理站点，以允许创建/编辑/删除博客帖子、博客作者、和博客评论（这是博客创建新博客帖子的机制）：* 管理站点的博客帖子记录，应显示内联的相关评论列表（在每篇博客文章下方）。
* 管理站点中的注释名称，是通过将注释说明，截断为 75 个字符来创建的。
* 其他类型的记录，可以使用基本注册。
 |

此外，你应该编写一些基本测试来验证：

* 所有模型字段都具有正确的标签和长度。
* 所有模型都具有预期的对象名称（例如 `__str__()` 返回预期值）。
* 模型具有单个博客和评论记录的预期 URL（例如，`get_absolute_url()`返回预期的 URL）。
* BlogListView（所有博客页面）可在预期位置访问（例如 /blog/blogs）
* BlogListView（所有博客页面）可通过预期的命名网址访问（例如 'blogs'）
* BlogListView（所有博客页面）使用预期的模板（例如默认模板）
* BlogListView 以 5 个记录为 1 个分页（至少在第一页上）

**备注：** 当然还有许多其他测试可以运行。请谨慎使用，但我们希望你至少进行上述测试。

以下部分，显示了实现上述要求的站点的屏幕截图。

截图
--

以下屏幕截图，提供了完成的程序应输出的示例。

### 所有博客文章列表

这将显示所有博客帖子的列表（可从侧栏中的“所有博客”All blogs 链接访问）。注意事项：

* 侧栏还列出了登录用户。
* 个人博客帖子和博主可以作为页面中的链接访问。
* 启用分页（以 5 个为一组）
* 次序是从最新到最旧。

![List of all blogs](/zh-CN/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_allblogs.png)

### 所有博主的列表

这提供了到所有博客的链接，如同来自侧栏中的“所有博客”All bloggers 链接。在这种情况下，我们可以从侧边栏看到，并没有用户登录。

![List of all bloggers](/zh-CN/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blog_allbloggers.png)

### 博客详情页面

这显示了指定博客的详细信息页面。

![Blog detail with add comment link](/zh-CN/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blog_detail_add_comment.png)

请注意，注释具有日期和时间，并且从最旧到最新排序（与博客次序相反）。最后，我们有一个链接，用于访问表单，以添加新评论。如果用户未登录，我们会看到登录的建议。

![Comment link when not logged in](/zh-CN/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blog_detail_not_logged_in.png)

### 添加评论表单

这是添加评论的表单。请注意，我们已登录。如果成功，我们应该返回相关的博客帖子页面。

![Add comment form](/zh-CN/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_comment_form.png)

### 作者简介

这会显示博主的个人信息及其博客帖子列表。

![Blogger detail page](/zh-CN/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blogger_detail.png)

完成的步骤
-----

以下部分，描述了你需要执行的操作。

1. 为站点创建骨架项目和 Web 应用程序（如Django 教程 2：创建骨架网站中所述）。你可以使用 'diyblog' 作为项目名称，使用 'blog' 作为应用程序名称。
2. 为博客帖子，评论和所需的任何其他对象创建模型。在考虑你的设计时，请记住：
    * 每个评论只有一个博客，但博客可能有很多评论。
    * 博客帖子和评论，必须按发布日期排序。
    * 并非每个用户都必须是博客作者，尽管任何用户都可能是评论者。
    * 博客作者还必须包含个人信息。
3. 为新模型运行迁移，并创建超级用户。
4. 使用管理站点，创建一些示例博客帖子，和博客评论。
5. 为博客帖子、和博客列表页面，创建视图、模板、和 URL 配置。
6. 为博客帖子、和博客详细信息页面，创建视图、模板、和 URL 配置。
7. 创建一个页面，其中包含用于添加新评论的表单（请记住，这仅适用于已登录的用户！）
提示和技巧
-----

该项目与 LocalLibrary 教程非常相似。你将能够使用几乎所有相同的方法，包含设置框架，用户登录/注销行为，对静态文件，视图，URL，表单，基本模板和管理站点配置的支持。

一些一般提示：

1. 索引页面可以实现为基本功能视图和模板（就像 locallibrary 一样）。
2. T 可以使用通用列表和详细信息视图，以创建博客帖子和博主的列表视图，以及博客帖子的详细信息视图。
3. 可以使用通用列表的博客列表视图，并对指定作者匹配的博客对象进行过滤，来创建特定作者的博客帖子列表。
    * 你将必须实现`get_queryset(self)`来进行过滤（很像我们的图书馆类`LoanedBooksAllListView`），并从 URL 获取作者信息。
    * 你还需要将作者的名称，传递给上下文中的页面。要在基于类的视图中执行此操作，你需要实现`get_context_data()`（在下面讨论）。
4. 可以使用基于函数的视图（以及关联的模型和表单），或使用通用`CreateView`，以创建添加注释表单。如果你使用`CreateView`（推荐），那么：
    * 你还需要将博客文章的名称，传递到上下文中的评论页面（实现`get_context_data()` ，如下所述）。
    * 表单应仅显示用户输入的注释“description”（日期和相关的博客文章，不应该是可编辑的）。由于它们本身不在表单中，因此你的代码，需要在`form_valid()` 函数中，设置注释的作者，以便将其保存到模型中（如此处所述 - Django 文档）。在同一个功能中，我们设置了相关的博客。可能的实现如下所示（`pk`是从 URL / URL 配置传入的博客 ID）。
     python
    ```python
        def form\_valid(self, form):
            """
     Add author and associated blog to form data before setting it as valid (so it is saved to model)
     """
            #Add logged-in user as author of comment
            form.instance.author = self.request.user
            #Associate comment with blog based on passed id
            form.instance.blog=get_object_or_404(Blog, pk = self.kwargs['pk'])
            # Call super-class form validation behaviour
            return super(BlogCommentCreate, self).form_valid(form)

    ```
    * 在表单验证后，你需要提供成功的 URL，以进行重新定向；这应该是原来的博客。为此，你需要覆盖 `get_success_url()`，并为原来的博客“反转”URL。你可以使用`self.kwargs`属性，获取所需的博客 ID，如上面的 `form_valid()` 方法所示。

我们简要地讨论了在Django 教程 6：通用列表和详细信息视图主题中，在基于类的视图中，将上下文传递给模板。要执行此操作，你需要覆盖`get_context_data()`（首先，获取现有上下文，使用要传递给模板的任何其他变量，更新它，然后返回更新的上下文）。例如，下面的代码片段，显示了如何根据`BlogAuthor` id，将 blogger 对象添加到上下文中。

```python
class SomeView(generic.ListView):
    ...

    def get\_context\_data(self, \*\*kwargs):
        # Call the base implementation first to get a context
        context = super(SomeView, self).get_context_data(\*\*kwargs)
        # Get the blogger object from the "pk" URL parameter and add it to the context
        context['blogger'] = get_object_or_404(BlogAuthor, pk = self.kwargs['pk'])
        return context
```

评估
--

这个任务的评估，可以在Github上找到。此评估主要基于你的应用程序，满足上面列出要求的程度，尽管评估的某些部分，会检查你的代码是否使用了适当的模型，并且你至少编写了一些测试代码。完成后，你可以查看我们完成的示例，该示例项目的表现是“满分”。

完成本单元后，表示你还完成了所有 MDN 用于学习《基本 Django 服务器端网站编程》的内容！我们希望你喜欢这个模块，并感觉你已经掌握了基础知识！

* 上一页
* Overview: Django


Django 教程 5：主页构建 - 学习 Web 开发

Django 教程 5：主页构建
================

* 上一页
* Overview: Django
* 下一页

我们现在可以添加代码来显示我们的第一个完整页面 - LocalLibrary 网站的主页，显示每个模型类型有多少条记录，并提供我们其他页面的侧边栏导航链接。一路上，我们将获得编写基本 URL 地图和视图，从数据库获取记录以及使用模板的实践经验。

|  |  |
| --- | --- |
| 前提： |
 读 the
 Django Introduction. 完成上章节 (including
 Django Tutorial Part 4: Django admin site).
  |
| 目标： |
 了解如何创建简单的 URL 映射和视图（没有数据编码在 URL
 中）以及如何从模型中获取数据并创建模版。
  |

概要
--

现在我们已经定义了我们的模型，并创建了一些初始库记录来处理，现在是编写代码以向用户呈现该信息的时候了。我们需要做的第一件事是确定我们希望能够在我们的页面中显示哪些信息，然后为返回这些资源定义适当的 URL。那么我们将需要创建一个 url 映射器，视图和模板来显示这些页面。

以下图表提供了处理 HTTP 请求/响应时需要实现的数据和事情的主要流程。我们已经创建了这个模型，我们需要创建的主要内容是：

* URL 映射 - 根据 - 支持的 URL（以及任何编码在 URL 里的信息）跳转到相应的**View**功能函数。
* **View** 函数从模型中获取请求的数据，创建一个显示数据的 HTML 页面，并将其返回给用户在浏览器查看。
* **Templates** 在 View 视图中进行数据渲染的时候使用。

![](/zh-CN/docs/Learn/Server-side/Django/Home_page/basic-django.png)

正如你将在下一节中看到的，我们将要显示 5 个页面，这在一篇文章中是很重要的。因此，本文的大部分内容将重点介绍如何实现主页（我们将在随后的文章中介绍其他页面）。这应该让你对 URL 映射器，视图和模型在实践中如何工作有一个很好的端到端的了解。

定义资源 URL
--------

由于本版本的 LocalLibrary 对于最终用户本质上是只读的，所以我们只需要为该网站（主页）提供一个着陆页，以及显示书籍和作者的列表和详细视图的页面。

下面这些 URL 是我们页面需要的：

* `catalog/` — 主页
* `catalog/books/` — 书单页
* `catalog/authors/` — 作者页
* `catalog/book/<id>` — 主键字段 ID 的具体书（默认） —详细视图。如下例子 `/catalog/book/3`，第三本书。
* `catalog/author/<id>` — 主键字段 ID 的具体作者（默认） —详细视图。如下例子 `/catalog/author/11`，第 11 个作者。

前三个 URL 用于列出索引，书籍和作者。这些不会对任何附加信息进行编码，而返回的结果将取决于数据库中的内容，运行获取信息的查询将始终保持一致。

相比之下，最后两个 URL 用于显示有关特定书籍或作者的详细信息 - 这些 URL 将编码要显示在 URL 中的项目的标识（如上所示<id>）。URL 映射器可以提取编码信息并将其传递给视图，然后将动态地确定从数据库获取哪些信息。通过对我们的 URL 中的信息进行编码，我们只需要一个 URL 映射，视图和模板来处理每本书（或作者）。

**备注：** Django 允许你以任何你喜欢的方式构建你的 URL - 你可以如上所示编码 URL 正文中的信息，或使用 URL `GET`参数（例如 `/book/?id=6`）。无论你使用哪种方法，URL 都应保持清洁，逻辑和可读性 (check out the W3C advice here).

Django 文档倾向于在 URL 的主体中推荐编码信息，这是他们觉得鼓励更好的 URL 设计的实践。

如概述，本文其余部分介绍如何构建索引页

创建索引页
-----

我们创建的第一个页面将会是索引页（catalog/)。这会显示一些静态 HTML，以及数据库中不同记录的一些计算的“计数“。为了使其工作，我们必须创建一个 URL 映射，视图和模版。

**备注：** 本节应该特别注意。一些”材料“在所有页面都通用。

### URL 映射

在我们创建的基础网站上，更新 **/locallibrary/urls.py** 文件。以确保每当收到以 **`catalog/`** 开头的 URL 时，URLConf 模块中的 **catalog.urls** 会处理剩余的字符串。

打开 catalog/**urls.py** ，复制下面代码

```python
urlpatterns = [
    path('', views.index, name='index'),
]
```

如果检测到 URL 模式''，(views.index——在 view.py 中函数命名 index() ）将被调用。URL 模式是Python 正则表达式 (RE)。我们将在本教程中进一步介绍 RE。

**备注：** 在 **/locallibrary/locallibrary/urls.py**

```python
urlpatterns += [
    path('catalog/', include('catalog.urls')),
]
```

每当 Django 使用 include() （`django.conf.urls.include()`），它排除与该点匹配 URL 的任何部分，并将剩余的字符串发送到随附的 URLconf 进行一步处理。

匹配的 URL 实际上是 `catalog/`+<空字符串> （`/catalog/` 假定是因为 `include()` 是使用的方法）。如果我们收到一个 URL 的 HTTP 请求，我们的第一个视图函数将被调用`/catalog/`。

此函数还说明了一个`name`参数，此唯一标识指定 URL 映射。你可以使用 "reverse" 映射—去动态创建指定映射设计处理的资源的一个 URL。例如，我们现在可以通过在我们的模版中创建以下链接到我们的主页：

```html
<a href="{% url 'index' %}">Home</a>.
```

**备注：** 我们当然可以硬编码上面的链接（如：`<a href="/catalog/">Home</a>`），但是如果我们改变了主页的模式，模版将不再正确链接，使用反向网址映射会更灵活和强大。

### View (基于功能)

视图是处理 HTTP 请求的功能，根据需要从数据库获取数据，通过使用 HTML 模板呈现此数据生成 HTML 页面，然后以 HTTP 响应返回 HTML 以显示给用户。索引视图遵循此模型 - 它提取有关数据库中有多少`Book`，`BookInstance` 可用 `BookInstance` 和 `Author` 记录的信息，并将其传递给模板以进行显示。

打开 catalog / views.py，并注意该文件已经导入了 使用模板和数据生成 HTML 文件的 render() 快捷方式函数。

```python
from django.shortcuts import render

# Create your views here.
```

复制文件底部的以下代码。第一行导入我们将用于访问所有视图中数据的模型类。

```python
from .models import Book, Author, BookInstance, Genre

def index(request):
    """
 View function for home page of site.
 """
    # Generate counts of some of the main objects
    num_books=Book.objects.all().count()
    num_instances=BookInstance.objects.all().count()
    # Available books (status = 'a')
    num_instances_available=BookInstance.objects.filter(status__exact='a').count()
    num_authors=Author.objects.count()  # The 'all()' is implied by default.

    # Render the HTML template index.html with the data in the context variable
    return render(
        request,
        'index.html',
        context={'num\_books':num_books,'num\_instances':num_instances,'num\_instances\_available':num_instances_available,'num\_authors':num_authors},
    )
```

视图函数的第一部分使用`objects.all()`模型类的属性来获取记录计数。它还会获取一个`BookInstance`状态字段值为“a”（可用）的对象列表。你可以在前面的教程 (Django Tutorial Part 3: Using models > Searching for records) 中找到更多关于如何访问模型的信息。

在函数结束时，我们将该函数称为`render()`创建和返回 HTML 页面作为响应（此快捷方式函数包含许多其他函数，简化了这种非常常见的用例）。它将原始`request`对象（an HttpRequest）作为参数，具有数据占位符的 HTML 模板以及`context`变量（包含要插入到这些占位符中的数据的 Python 字典）。

我们将在下一节中详细介绍模板和上下文变量; 让我们创建我们的模板，以便我们可以向用户显示一些内容

### 模版

模版是定义一个文件（例如 HTML 页面）的结构与布局的文本文件，其中占位符用于表示实际内容。Django 将自动在应用程序“templates”目录查找模版。所以例如，在我们刚刚加的索引页，`render()` 函数会期望能够找到 **/locallibrary/catalog/templates/index.html** 这个文件，如何找不到该文件，则会引发错误。如果保存以前的更改并返回到浏览器，你可以看到访问 `127.0.0.1:8000` 现在将提供你一个相当直观的错误信息“**TemplateDoesNotExist at /catalog/**”以及其他详细信息。

**备注：** Django 将根据你的项目的设置文件，来查看模版的许多位置 (在已安装的应用程序中进行搜索是默认设置). 你可以查阅更多关于 Django 如何找到模版以及它支持的模版格式在（Templates ）。

#### 扩展模版

索引模版将需要标准的 HTML 标记头部和正文，以及用于导航的部分（去我们尚为创建的网站其他的页面）以及显示一些介绍文本和我们书籍数据。我们网站上的每一页，大部分文字（HTML 和导航结构）都是一样的。Django 模版语言不是强制开发人员在每个页面中复制这个“样板”，而是让你声明一个基本模版，然后再扩展它，仅替换每个特定页面不同的位置。

例如，基本模版 `base_generic.html` 可能看起来像下面的文本。正如你所见的，它包含一些“常见“HTML”和标题，侧边栏和使用命名 `block` 和 `endblock` 模版标记（粗体显示）标记的内容部分。块可以是空的，或者包含将被派生页“默认使用”的内容。

**备注：** 模版标签就像你可以在模版中使用的函数循环列表，基于变量的值执行条件操作等。除了模版标签，模版语法允许你引用模版变量（通过从视图进入模版），并使用模版过滤器，其中重新格式化变量（例如，将字符串设置为小写）。

```django
<!doctype html>
<html lang="en">
  <head>
    {% block title %}<title>Local Library</title>{% endblock %}
  </head>

  <body>
    {% block sidebar %}<!-- insert default navigation text for every page -->{% endblock %}
    {% block content %}<!-- default content text (typically empty) -->{% endblock %}
  </body>
</html>
```

当我们要为特定视图定义一个模版时，我们首先指定基本模版（使用 `extends` 模版标签—查看下一个代码片段）。如果我们想要在模版中替换的章节，会使用相同的 `block/endblock` 部分在基本模版表明。

例如，下面我们使用 `extends` 模版标签，并覆盖 `content` 块。生成的最终 HTML 页面将具有基本模版中定义的所以 HTML 和结构（包括你在`title`块中定义的默认内容），但你新的 `content` 块插入到了默认的那块。

`base_generic.html` 详细会在下文中，请耐心往下看。

```django
{% extends "base\_generic.html" %}

{% block content %}
  <h1>Local Library Home</h1>
  <p>Welcome to <em>LocalLibrary</em>, a very basic Django website developed as a tutorial example on the Mozilla Developer Network.</p>
{% endblock %}
```

#### 本地图书馆 - 基本模版

下面就是我们计划的基本模版用于本地图书馆网站。正如所看到的，内容包括一些 HTML 和定义块 `title` ，`sidebar` 和 `content`。我们有默认的 `title`（当然我们可以改）和默认的所以书籍和作者的链接列表 `sidebar` （我们可能并不会怎么改，但需要时，我们通过把想法放入块`block`中，比如想法是—允许范围）。

**备注：** 我们再介绍两个额外的模版标签： `url` 和 `load static`。下文中我们会详细介绍。

创建一个新的文件 — **/locallibrary/catalog/templates/*base\_generic.html*** — 写入如下代码

```django
<!doctype html>
<html lang="en">
  <head>
    {% block title %}<title>Local Library</title>{% endblock %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
 rel="stylesheet"
 href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <!-- Add additional CSS in static file -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'css/styles.css' %}" />
  </head>

  <body>
    <div class="container-fluid">
      <div class="row">
        <div class="col-sm-2">
          {% block sidebar %}
            <ul class="sidebar-nav">
              <li><a href="{% url 'index' %}">Home</a></li>
              <li><a href="">All books</a></li>
              <li><a href="">All authors</a></li>
            </ul>
          {% endblock %}
        </div>
        <div class="col-sm-10">{% block content %}{% endblock %}</div>
      </div>
    </div>
  </body>
</html>
```

该模版使用（并包含）JavaScript 和 Bootstrap（css 框架）来改进 HTML 页面的布局和显示，这个框架或者另一个客户端网络框架，这是快速创建一个可用页面来适应在不同浏览器尺寸和允许我们处理页面呈现且不用一点细节—我们只需要专注在服务器端。

基本模版还引用了一个本地 css 文件 (**styles.css**) ，它提供了一些额外的样式。新建 **/locallibrary/catalog/static/css/styles.css** 如下：

```css
.sidebar-nav {
  margin-top: 20px;
  padding: 0;
  list-style: none;
}
```

#### 索引模版

新建 HTML 文件 **/locallibrary/catalog/templates/*index.html*** 写入下面代码。第一行我们扩展了我们的基本模版，使用 `content`替换默认块。

```django
{% extends "base\_generic.html" %}

{% block content %}
  <h1>Local Library Home</h1>

  <p>Welcome to <em>LocalLibrary</em>, a very basic Django website developed as a tutorial example on the Mozilla Developer Network.</p>

  <h2>Dynamic content</h2>

  <p>The library has the following record counts:</p>
  <ul>
    <li><strong>Books:</strong> {{ num\_books }}</li>
    <li><strong>Copies:</strong> {{ num\_instances }}</li>
    <li><strong>Copies available:</strong> {{ num\_instances\_available }}</li>
    <li><strong>Authors:</strong> {{ num\_authors }}</li>
  </ul>
{% endblock %}
```

在动态内容部分，我们的占位符（模版变量），是给我们想要视图的信息声明。变量使用“双大括号“或者“句柄“语法进行标记。

**备注：** 你可以轻松地识别是否使用变量或模版标签（函数），因为变量具有双括号 (`{{ num_books }}`) 而标记被包含在带有百分比符号 (`{% extends "base_generic.html" %}`) 的单个大括号中。

这里要注意的重要事情是这些变量用我们视图函数`render`中的字典—注入 `context` （下面）；当渲染模版时，这些将替换为相关联的值。

```python
return render(
    request,
    'index.html',
     context={'num\_books':num_books,'num\_instances':num_instances,'num\_instances\_available':num_instances_available,'num\_authors':num_authors},
)
```

#### 在模版中引用静态文件

你的项目可能会使用静态资源，包括**javascript**，**css** 和图像。由于这些文件的位置可能不知道（或者可能会发生变化），则 Django 允许你指定你的模版相对于这些文件的位置 **`STATIC_URL`** 全局设置（默认基本网站设置的值 **`STATIC_URL`**，以“**`／static／`**”，但你可能选择在 CDN 和其他地方托管内容）。

在模版中，你首先调用 `load` 指定“ `static`”去添加此模版库（如下）。静态加载后，你可以使用 `static` 模版标签，指定感兴趣的文件相对`URL`

```django
<!-- Add additional CSS in static file -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}" />
```

你可以用同样的方式将图像添加到页面中：

```django
{% load static %}
<img
 src="{% static 'catalog/images/local\_library\_model\_uml.png' %}"
 alt="My image"
 style="width:555px;height:540px;" />
```

**备注：** 上面的更改指定文件所在的位置，但 Django 默认不提供它们。当我们created the website skeleton,我们在全局 URL 映射器 r (**/locallibrary/locallibrary/urls.py**) 中开发 Web 服务器提供服务，你仍然需要安排它们在生产中投放。我们接下来看一看

更多内容—Managing static files (Django docs).

#### 链接 URL

基本的模版引入 `url` 模版标签

```python
<li><a href="{% url 'index' %}">Home</a></li>
```

此标记`url()`使用你的**urls.py**中调用的函数的名称 和相关视图将从该函数接收的任何参数的值，并返回可用于链接到该资源的 URL。

它看起来什么样？
--------

运行 (`python3 manage.py runserver`) 和在浏览器中打开 http://127.0.0.1:8000/. I 如果一切都正确设置，当当当当。

![Index page for LocalLibrary website](/zh-CN/docs/Learn/Server-side/Django/Home_page/index_page_ok.png)

**备注：** 由于尚未定义这些网页的网址，视图和模板，因此你将无法使用“**所有图书**和**所有作者**”链接（目前我们刚刚在 `base_generic.html` 模板中插入了这些链接的占位符）

挑战自己
----

以下是一些测试你熟悉模型查询，视图和模板的任务。

1. 在索引模板中声明一个新的标题块，并更改页面标题以匹配此特定页面。
2. 修改视图以生成包含特定单词（不区分大小写）的类型计数和书数，然后将这些字段添加到模板。
总结
--

我们现在已经为我们的网站创建了主页——一个 HTML 页面，显示数据库中的一些记录数，并且链接到我们其他尚待创建的页面。一路上，我们已经学到了很多有关 url 映射器，视图，使用我们的模型查询数据库的基本信息，如何从你的视图传递信息到模板，以及如何创建和扩展模板。

在我们的下一篇文章中，我们将基于我们的知识来创建其他四个页面。

参见
--

* Writing your first Django app, part 3: Views and Templates (Django docs)
* URL 调度程序 (Django docs)
* 视图函数 (DJango docs)
* 模版 (Django docs)
* 管理静态文件(Django docs)
* Django 快捷功能(Django docs)
* 上一页
* Overview: Django
* 下一页