分类 软件开发 下的文章

R 还是 Python ? Python 脚本模仿易使用的 R 风格函数,使得数据统计变得简单易行。

“Python vs. R” 是数据科学和机器学习的现代战争之一。毫无疑问,近年来这两者发展迅猛,成为数据科学、预测分析和机器学习领域的顶级编程语言。事实上,根据 IEEE 最近的一篇文章,Python 已在 最受欢迎编程语言排行榜 中超越 C++ 成为排名第一的语言,并且 R 语言也稳居前 10 位。

但是,这两者之间存在一些根本区别。R 语言设计的初衷主要是作为统计分析和数据分析问题的快速原型设计的工具,另一方面,Python 是作为一种通用的、现代的面向对象语言而开发的,类似 C++ 或 Java,但具有更简单的学习曲线和更灵活的语言风格。因此,R 仍在统计学家、定量生物学家、物理学家和经济学家中备受青睐,而 Python 已逐渐成为日常脚本、自动化、后端 Web 开发、分析和通用机器学习框架的顶级语言,拥有广泛的支持基础和开源开发社区。

在 Python 环境中模仿函数式编程

R 作为函数式编程语言的本质为用户提供了一个极其简洁的用于快速计算概率的接口,还为数据分析问题提供了必不可少的描述统计和推论统计方法(LCTT 译注:统计学从功能上分为描述统计学和推论统计学)。例如,只用一个简洁的函数调用来解决以下问题难道不是很好吗?

  • 如何计算数据向量的平均数 / 中位数 / 众数。
  • 如何计算某些服从正态分布的事件的累积概率。如果服 从泊松分布 Poisson distribution 又该怎样计算呢?
  • 如何计算一系列数据点的四分位距。
  • 如何生成服从学生 t 分布的一些随机数(LCTT 译注: 在概率论和统计学中,学生 t-分布(Student’s t-distribution)可简称为 t 分布,用于根据小样本来估计呈正态分布且方差未知的总体的均值)。

R 编程环境可以完成所有这些工作。

另一方面,Python 的脚本编写能力使分析师能够在各种分析流程中使用这些统计数据,具有无限的复杂性和创造力。

要结合二者的优势,你只需要一个简单的 Python 封装的库,其中包含与 R 风格定义的概率分布和描述性统计相关的最常用函数。 这使你可以非常快速地调用这些函数,而无需转到正确的 Python 统计库并理解整个方法和参数列表。

便于调用 R 函数的 Python 包装脚本

我编写了一个 Python 脚本 ,用 Python 简单统计分析定义了最简洁和最常用的 R 函数。导入此脚本后,你将能够原生地使用这些 R 函数,就像在 R 编程环境中一样。

此脚本的目标是提供简单的 Python 函数,模仿 R 风格的统计函数,以快速计算密度估计和点估计、累积分布和分位数,并生成重要概率分布的随机变量。

为了延续 R 风格,脚本不使用类结构,并且只在文件中定义原始函数。因此,用户可以导入这个 Python 脚本,并在需要单个名称调用时使用所有功能。

请注意,我使用 mimic 这个词。 在任何情况下,我都声称要模仿 R 的真正的函数式编程范式,该范式包括深层环境设置以及这些环境和对象之间的复杂关系。 这个脚本允许我(我希望无数其他的 Python 用户)快速启动 Python 程序或 Jupyter 笔记本程序、导入脚本,并立即开始进行简单的描述性统计。这就是目标,仅此而已。

如果你已经写过 R 代码(可能在研究生院)并且刚刚开始学习并使用 Python 进行数据分析,那么你将很高兴看到并在 Jupyter 笔记本中以类似在 R 环境中一样使用一些相同的知名函数。

无论出于何种原因,使用这个脚本很有趣。

简单的例子

首先,只需导入脚本并开始处理数字列表,就好像它们是 R 中的数据向量一样。

from R_functions import *
lst=[20,12,16,32,27,65,44,45,22,18]
<more code, more statistics...>

假设你想从数据向量计算 Tuckey 五数摘要。 你只需要调用一个简单的函数 fivenum,然后将向量传进去。 它将返回五数摘要,存在 NumPy 数组中。

lst=[20,12,16,32,27,65,44,45,22,18]
fivenum(lst)
> array([12. , 18.5, 24.5, 41. , 65. ])

或许你想要知道下面问题的答案:

假设一台机器平均每小时输出 10 件成品,标准偏差为 2。输出模式遵循接近正态的分布。 机器在下一个小时内输出至少 7 个但不超过 12 个单位的概率是多少?

答案基本上是这样的:

使用 pnorm ,你可以只用一行代码就能获得答案:

pnorm(12,10,2)-pnorm(7,10,2)
> 0.7745375447996848

或者你可能需要回答以下问题:

假设你有一个不公平硬币,每次投它时有 60% 可能正面朝上。 你正在玩 10 次投掷游戏。 你如何绘制并给出这枚硬币所有可能的胜利数(从 0 到 10)的概率?

只需使用一个函数 dbinom 就可以获得一个只有几行代码的美观条形图:

probs=[]
import matplotlib.pyplot as plt
for i in range(11):
    probs.append(dbinom(i,10,0.6))
plt.bar(range(11),height=probs)
plt.grid(True)
plt.show()

简单的概率计算接口

R 提供了一个非常简单直观的接口,可以从基本概率分布中快速计算。 接口如下:

  • d 分布:给出点 x 处的密度函数值
  • p 分布:给出 x 点的累积值
  • q 分布:以概率 p 给出分位数函数值
  • r 分布:生成一个或多个随机变量

在我们的实现中,我们坚持使用此接口及其关联的参数列表,以便你可以像在 R 环境中一样执行这些函数。

目前已实现的函数

脚本中实现了以下 R 风格函数,以便快速调用。

  • 平均数、中位数、方差、标准差
  • Tuckey 五数摘要、 四分位距 interquartile range (IQR)
  • 矩阵的协方差或两个向量之间的协方差
  • 以下分布的密度、累积概率、分位数函数和随机变量生成:正态、均匀、二项式、 泊松 Poisson 、F、 学生 t Student’s t 卡方 Chi-square 贝塔 beta 伽玛 gamma

进行中的工作

显然,这是一项正在进行的工作,我计划在此脚本中添加一些其他方便的R函数。 例如,在 R 中,单行命令 lm 可以为数字数据集提供一个简单的最小二乘拟合模型,其中包含所有必要的推理统计(P 值,标准误差等)。 这非常简洁! 另一方面,Python 中的标准线性回归问题经常使用 Scikit-learn 库来处理,此用途需要更多的脚本,所以我打算使用 Python 的 statsmodels 库合并这个单函数线性模型来拟合功能。

如果你喜欢这个脚本,并且愿意在工作中使用,请 GitHub 仓库点个 star 或者 fork 帮助其他人找到它。 另外,你可以查看我其他的 GitHub 仓库,了解 Python、R 或 MATLAB 中的有趣代码片段以及一些机器学习资源。

如果你有任何问题或想法要分享,请通过 [tirthajyoti [AT] gmail.com](mailto:[email protected]) 与我联系。 如果你像我一样热衷于机器学习和数据科学,请 在 LinkedIn 上加我为好友或者在 Twitter 上关注我

本篇文章最初发表于走向数据科学。 请在 CC BY-SA 4.0 协议下转载。


via: https://opensource.com/article/18/10/write-favorite-r-functions-python

作者:Tirthajyoti Sarkar 选题:lujun9972 译者:yongshouzhang 校对:Flowsnow

本文由 LCTT 原创编译,Linux中国 荣誉推出

在一个美好的夜晚,你的肚子拒绝消化你在晚餐吃的大块披萨,所以你不得不在睡梦中冲进洗手间。

在浴室里,当你在思考为什么会发生这种情况时,你听到一个来自通风口的低沉声音:“嘿,我是蝙蝠侠。”

这时,你会怎么做呢?

在你恐慌并处于关键时刻之前,蝙蝠侠说:“我需要你的帮助。我是一个超级极客,但我不懂 HTML。我需要用 HTML 写一封情书,你愿意帮助我吗?”

谁会拒绝蝙蝠侠的请求呢,对吧?所以让我们用 HTML 来写一封蝙蝠侠的情书。

你的第一个 HTML 文件

HTML 网页与你电脑上的其它文件一样。就同一个 .doc 文件以 MS Word 打开,.jpg 文件在图像查看器中打开一样,一个 .html 文件在浏览器中打开。

那么,让我们来创建一个 .html 文件。你可以在 Notepad 或其它任何编辑器中完成此任务,但我建议使用 VS Code。在这里下载并安装 VS Code。它是免费的,也是我唯一喜欢的微软产品。

在系统中创建一个目录,将其命名为 “HTML Practice”(不带引号)。在这个目录中,再创建一个名为 “Batman’s Love Letter”(不带引号)的目录,这将是我们的项目根目录。这意味着我们所有与这个项目相关的文件都会在这里。

打开 VS Code,按下 ctrl+n 创建一个新文件,按下 ctrl+s 保存文件。切换到 “Batman’s Love Letter” 文件夹并将其命名为 “loveletter.html”,然后单击保存。

现在,如果你在文件资源管理器中双击它,它将在你的默认浏览器中打开。我建议使用 Firefox 来进行 web 开发,但 Chrome 也可以。

让我们将这个过程与我们已经熟悉的东西联系起来。还记得你第一次拿到电脑吗?我做的第一件事是打开 MS Paint 并绘制一些东西。你在 Paint 中绘制一些东西并将其另存为图像,然后你可以在图像查看器中查看该图像。之后,如果要再次编辑该图像,你在 Paint 中重新打开它,编辑并保存它。

我们目前的流程非常相似。正如我们使用 Paint 创建和编辑图像一样,我们使用 VS Code 来创建和编辑 HTML 文件。就像我们使用图像查看器查看图像一样,我们使用浏览器来查看我们的 HTML 页面。

HTML 中的段落

我们有一个空的 HTML 文件,以下是蝙蝠侠想在他的情书中写的第一段。

“After all the battles we fought together, after all the difficult times we saw together, and after all the good and bad moments we’ve been through, I think it’s time I let you know how I feel about you.”

复制这些到 VS Code 中的 loveletter.html。单击 “View -> Toggle Word Wrap (alt+z)” 自动换行。

保存并在浏览器中打开它。如果它已经打开,单击浏览器中的刷新按钮。

瞧!那是你的第一个网页!

我们的第一段已准备就绪,但这不是在 HTML 中编写段落的推荐方法。我们有一种特定的方法让浏览器知道一个文本是一个段落。

如果你用 <p></p> 来包裹文本,那么浏览器将识别 <p></p> 中的文本是一个段落。我们这样做:

<p>After all the battles we fought together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.</p>

通过在 <p></p>中编写段落,你创建了一个 HTML 元素。一个网页就是 HTML 元素的集合。

让我们首先来认识一些术语:<p> 是开始标签,</p> 是结束标签,“p” 是标签名称。元素开始和结束标签之间的文本是元素的内容。

“style” 属性

在上面,你将看到文本覆盖屏幕的整个宽度。

我们不希望这样。没有人想要阅读这么长的行。让我们设定段落宽度为 550px。

我们可以通过使用元素的 style 属性来实现。你可以在其 style 属性中定义元素的样式(例如,在我们的示例中为宽度)。以下行将在 p 元素上创建一个空样式属性:

<p style="">...</p>

你看到那个空的 "" 了吗?这就是我们定义元素外观的地方。现在我们要将宽度设置为 550px。我们这样做:

<p style="width:550px;">
    After all the battles we fought together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
</p>

我们将 width 属性设置为 550px,用冒号 : 分隔,以分号 ; 结束。

另外,注意我们如何将 <p></p> 放在单独的行中,文本内容用一个制表符缩进。像这样设置代码使其更具可读性。

HTML 中的列表

接下来,蝙蝠侠希望列出他所钦佩的人的一些优点,例如:

You complete my darkness with your light. I love:
- the way you see good in the worst things
- the way you handle emotionally difficult situations
- the way you look at Justice
I have learned a lot from you. You have occupied a special place in my heart over time.

这看起来很简单。

让我们继续,在 </p> 下面复制所需的文本:

<p style="width:550px;">
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
</p>
<p style="width:550px;">
    You complete my darkness with your light. I love:
    - the way you see good in the worse
    - the way you handle emotionally difficult situations
    - the way you look at Justice
    I have learned a lot from you. You have occupied a special place in my heart over the time.
</p>

保存并刷新浏览器。

哇!这里发生了什么,我们的列表在哪里?

如果你仔细观察,你会发现没有显示换行符。在代码中我们在新的一行中编写列表项,但这些项在浏览器中显示在一行中。

如果你想在 HTML(新行)中插入换行符,你必须使用 <br>。让我们来使用 <br>,看看它长什么样:

<p style="width:550px;">
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
</p>
<p style="width:550px;">
    You complete my darkness with your light. I love: <br>
    - the way you see good in the worse <br>
    - the way you handle emotionally difficult situations <br>
    - the way you look at Justice <br>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
</p>

保存并刷新:

好的,现在它看起来就像我们想要的那样!

另外,注意我们没有写一个 </br>。有些标签不需要结束标签(它们被称为自闭合标签)。

还有一件事:我们没有在两个段落之间使用 <br>,但第二个段落仍然是从一个新行开始,这是因为 <p> 元素会自动插入换行符。

我们使用纯文本编写列表,但是有两个标签可以供我们使用来达到相同的目的:<ul> and <li>

让我们解释一下名字的意思:ul 代表 无序列表 Unordered List ,li 代表 列表项目 List Item 。让我们使用它们来展示我们的列表:

<p style="width:550px;">
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
</p>
<p style="width:550px;">
  You complete my darkness with your light. I love:
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
</p>

在复制代码之前,注意差异部分:

  • 我们删除了所有的 <br>,因为每个 <li> 会自动显示在新行中
  • 我们将每个列表项包含在 <li></li> 之间
  • 我们将所有列表项的集合包裹在 <ul></ul> 之间
  • 我们没有像 <p> 元素那样定义 <ul> 元素的宽度。这是因为 <ul><p> 的子节点,<p> 已经被约束到 550px,所以 <ul> 不会超出这个范围。

让我们保存文件并刷新浏览器以查看结果:

你会立即注意到在每个列表项之前显示了重点标志。我们现在不需要在每个列表项之前写 “-”。

经过仔细检查,你会注意到最后一行超出 550px 宽度。这是为什么?因为 HTML 不允许 <ul> 元素出现在 <p> 元素中。让我们将第一行和最后一行放在单独的 <p> 元素中:

<p style="width:550px;">
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
</p>
<p style="width:550px;">
    You complete my darkness with your light. I love:
</p>
<ul style="width:550px;">
  <li>the way you see good in the worse</li>
  <li>the way you handle emotionally difficult situations</li>
  <li>the way you look at Justice</li>
</ul>
<p style="width:550px;">
    I have learned a lot from you. You have occupied a special place in my heart over the time.
</p>

保存并刷新。

注意,这次我们还定义了 <ul> 元素的宽度。那是因为我们现在已经将 <ul> 元素放在了 <p> 元素之外。

定义情书中所有元素的宽度会变得很麻烦。我们有一个特定的元素用于此目的:<div> 元素。一个 <div> 元素就是一个通用容器,用于对内容进行分组,以便轻松设置样式。

让我们用 <div> 元素包装整个情书,并为其赋予宽度:550px 。

<div style="width:550px;">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
</div>

棒极了,我们的代码现在看起来简洁多了。

HTML 中的标题

到目前为止,蝙蝠侠对结果很高兴,他希望在情书上标题。他想写一个标题: “Bat Letter”。当然,你已经看到这个名字了,不是吗?:D

你可以使用 <h1><h2><h3><h4><h5><h6> 标签来添加标题,<h1> 是最大的标题和最主要的标题,<h6> 是最小的标题。

让我们在第二段之前使用 <h1> 做主标题和一个副标题:

<div style="width:550px;">
  <h1>Bat Letter</h1>
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
</div>

保存,刷新。

HTML 中的图像

我们的情书尚未完成,但在继续之前,缺少一件大事:蝙蝠侠标志。你见过是蝙蝠侠的东西但没有蝙蝠侠的标志吗?

并没有。

所以,让我们在情书中添加一个蝙蝠侠标志。

在 HTML 中包含图像就像在一个 Word 文件中包含图像一样。在 MS Word 中,你到 “菜单 -> 插入 -> 图像 -> 然后导航到图像位置为止 -> 选择图像 -> 单击插入”。

在 HTML 中,我们使用 <img> 标签让浏览器知道我们需要加载的图像,而不是单击菜单。我们在 src 属性中写入文件的位置和名称。如果图像在项目根目录中,我们可以简单地在 src 属性中写入图像文件的名称。

在我们深入编码之前,从这里下载蝙蝠侠标志。你可能希望裁剪图像中的额外空白区域。复制项目根目录中的图像并将其重命名为 “bat-logo.jpeg”。

<div style="width:550px;">
  <h1>Bat Letter</h1>
  <img src="bat-logo.jpeg">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
<h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
</div>

我们在第 3 行包含了 <img> 标签。这个标签也是一个自闭合的标签,所以我们不需要写 </img>。在 src 属性中,我们给出了图像文件的名称。这个名称应与图像名称完全相同,包括扩展名(.jpeg)及其大小写。

保存并刷新,查看结果。

该死的!刚刚发生了什么?

当使用 <img> 标签包含图像时,默认情况下,图像将以其原始分辨率显示。在我们的例子中,图像比 550px 宽得多。让我们使用 style 属性定义它的宽度:

<div style="width:550px;">
  <h1>Bat Letter</h1>
  <img src="bat-logo.jpeg" style="width:100%">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
<h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
</div>

你会注意到,这次我们定义宽度使用了 “%” 而不是 “px”。当我们在 “%” 中定义宽度时,它将占据父元素宽度的百分比。因此,100% 的 550px 将为我们提供 550px。

保存并刷新,查看结果。

太棒了!这让蝙蝠侠的脸露出了羞涩的微笑 :)。

HTML 中的粗体和斜体

现在蝙蝠侠想在最后几段中承认他的爱。他有以下文本供你用 HTML 编写:

“I have a confession to make

It feels like my chest does have a heart. You make my heart beat. Your smile brings a smile to my face, your pain brings pain to my heart.

I don’t show my emotions, but I think this man behind the mask is falling for you.”

当阅读到这里时,你会问蝙蝠侠:“等等,这是给谁的?”蝙蝠侠说:

“这是给超人的。”

你说:哦!我还以为是给神奇女侠的呢。

蝙蝠侠说:不,这是给超人的,请在最后写上 “I love you Superman.”。

好的,我们来写:

<div style="width:550px;">
  <h1>Bat Letter</h1>
  <img src="bat-logo.jpeg" style="width:100%">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
<h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest does have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p>I love you Superman.</p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>

这封信差不多完成了,蝙蝠侠另外想再做两次改变。蝙蝠侠希望在最后段落的第一句中的 “does” 一词是斜体,而 “I love you Superman” 这句话是粗体的。

我们使用 <em><strong> 以斜体和粗体显示文本。让我们来更新这些更改:

<div style="width:550px;">
  <h1>Bat Letter</h1>
  <img src="bat-logo.jpeg" style="width:100%">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest <em>does</em> have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p><strong>I love you Superman.</strong></p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>

HTML 中的样式

你可以通过三种方式设置样式或定义 HTML 元素的外观:

  • 内联样式:我们使用元素的 style 属性来编写样式。这是我们迄今为止使用的,但这不是一个好的实践。
  • 嵌入式样式:我们在由 <style></style> 包裹的 “style” 元素中编写所有样式。
  • 链接样式表:我们在具有 .css 扩展名的单独文件中编写所有元素的样式。此文件称为样式表。

让我们来看看如何定义 <div> 的内联样式:

<div style="width:550px;">

我们可以在 <style></style> 里面写同样的样式:

div{
  width:550px;
}

在嵌入式样式中,我们编写的样式是与元素分开的。所以我们需要一种方法来关联元素及其样式。第一个单词 “div” 就做了这样的活。它让浏览器知道花括号 {...} 里面的所有样式都属于 “div” 元素。由于这种语法确定要应用样式的元素,因此它称为一个选择器。

我们编写样式的方式保持不变:属性(width)和值(550px)用冒号(:)分隔,以分号(;)结束。

让我们从 <div><img> 元素中删除内联样式,将其写入 <style> 元素:

<style>
  div{
    width:550px;
  }
  img{
    width:100%;
  }
</style>
<div>
  <h1>Bat Letter</h1>
  <img src="bat-logo.jpeg">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest <em>does</em> have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p><strong>I love you Superman.</strong></p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>

保存并刷新,结果应保持不变。

但是有一个大问题,如果我们的 HTML 文件中有多个 <div><img> 元素该怎么办?这样我们在 <style> 元素中为 div 和 img 定义的样式就会应用于页面上的每个 div 和 img。

如果你在以后的代码中添加另一个 div,那么该 div 也将变为 550px 宽。我们并不希望这样。

我们想要将我们的样式应用于现在正在使用的特定 div 和 img。为此,我们需要为 div 和 img 元素提供唯一的 id。以下是使用 id 属性为元素赋予 id 的方法:

<div id="letter-container">

以下是如何在嵌入式样式中将此 id 用作选择器:

#letter-container{
  ...
}

注意 # 符号。它表示它是一个 id,{...} 中的样式应该只应用于具有该特定 id 的元素。

让我们来应用它:

<style>
  #letter-container{
    width:550px;
  }
  #header-bat-logo{
    width:100%;
  }
</style>
<div id="letter-container">
  <h1>Bat Letter</h1>
  <img id="header-bat-logo" src="bat-logo.jpeg">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest <em>does</em> have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p><strong>I love you Superman.</strong></p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>

HTML 已经准备好了嵌入式样式。

但是,你可以看到,随着我们包含越来越多的样式,<style></style> 将变得很大。这可能很快会混乱我们的主 HTML 文件。

因此,让我们更进一步,通过将 <style> 标签内的内容复制到一个新文件来使用链接样式。

在项目根目录中创建一个新文件,将其另存为 “style.css”:

#letter-container{
  width:550px;
}
#header-bat-logo{
  width:100%;
}

我们不需要在 CSS 文件中写 <style></style>

我们需要使用 HTML 文件中的 <link> 标签来将新创建的 CSS 文件链接到 HTML 文件。以下是我们如何做到这一点:

<link rel="stylesheet" type="text/css" href="style.css">

我们使用 <link> 元素在 HTML 文档中包含外部资源,它主要用于链接样式表。我们使用的三个属性是:

  • rel:关系。链接文件与文档的关系。具有 .css 扩展名的文件称为样式表,因此我们保留 rel=“stylesheet”。
  • type:链接文件的类型;对于一个 CSS 文件来说它是 “text/css”。
  • href:超文本参考。链接文件的位置。

link 元素的结尾没有 </link>。因此,<link> 也是一个自闭合的标签。

<link rel="gf" type="cute" href="girl.next.door">

如果只是得到一个女朋友,那么很容易:D

可惜没有那么简单,让我们继续前进。

这是我们 “loveletter.html” 的内容:

<link rel="stylesheet" type="text/css" href="style.css">
<div id="letter-container">
  <h1>Bat Letter</h1>
  <img id="header-bat-logo" src="bat-logo.jpeg">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest <em>does</em> have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p><strong>I love you Superman.</strong></p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>

“style.css” 内容:

#letter-container{
  width:550px;
}
#header-bat-logo{
  width:100%;
}

保存文件并刷新,浏览器中的输出应保持不变。

一些手续

我们的情书已经准备好给蝙蝠侠,但还有一些正式的片段。

与其他任何编程语言一样,HTML 自出生以来(1990 年)经历过许多版本,当前版本是 HTML5。

那么,浏览器如何知道你使用哪个版本的 HTML 来编写页面呢?要告诉浏览器你正在使用 HTML5,你需要在页面顶部包含 <!DOCTYPE html>。对于旧版本的 HTML,这行不同,但你不需要了解它们,因为我们不再使用它们了。

此外,在之前的 HTML 版本中,我们曾经将整个文档封装在 <html></html> 标签内。整个文件分为两个主要部分:头部在 <head></head> 里面,主体在 <body></body> 里面。这在 HTML5 中不是必须的,但由于兼容性原因,我们仍然这样做。让我们用 <Doctype>, <html><head><body> 更新我们的代码:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="letter-container">
  <h1>Bat Letter</h1>
  <img id="header-bat-logo" src="bat-logo.jpeg">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest <em>does</em> have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p><strong>I love you Superman.</strong></p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>
</body>
</html>

主要内容在 <body> 里面,元信息在 <head> 里面。所以我们把 <div> 保存在 <body> 里面并加载 <head> 里面的样式表。

保存并刷新,你的 HTML 页面应显示与之前相同的内容。

HTML 的标题

我发誓,这是最后一次改变。

你可能已经注意到选项卡的标题正在显示 HTML 文件的路径:

我们可以使用 <title> 标签来定义 HTML 文件的标题。标题标签也像链接标签一样在 <head> 内部。让我们我们在标题中加上 “Bat Letter”:

<!DOCTYPE html>
<html>
<head>
  <title>Bat Letter</title>
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="letter-container">
  <h1>Bat Letter</h1>
  <img id="header-bat-logo" src="bat-logo.jpeg">
  <p>
    After all the battles we faught together, after all the difficult times we saw together, after all the good and bad moments we've been through, I think it's time I let you know how I feel about you.
  </p>
  <h2>You are the light of my life</h2>
  <p>
    You complete my darkness with your light. I love:
  </p>
  <ul>
    <li>the way you see good in the worse</li>
    <li>the way you handle emotionally difficult situations</li>
    <li>the way you look at Justice</li>
  </ul>
  <p>
    I have learned a lot from you. You have occupied a special place in my heart over the time.
  </p>
  <h2>I have a confession to make</h2>
  <p>
    It feels like my chest <em>does</em> have a heart. You make my heart beat. Your smile brings smile on my face, your pain brings pain to my heart.
  </p>
  <p>
    I don't show my emotions, but I think this man behind the mask is falling for you.
  </p>
  <p><strong>I love you Superman.</strong></p>
  <p>
    Your not-so-secret-lover, <br>
    Batman
  </p>
</div>
</body>
</html>

保存并刷新,你将看到在选项卡上显示的是 “Bat Letter” 而不是文件路径。

蝙蝠侠的情书现在已经完成。

恭喜!你用 HTML 制作了蝙蝠侠的情书。

我们学到了什么

我们学习了以下新概念:

  • 一个 HTML 文档的结构
  • 在 HTML 中如何写元素(<p></p>
  • 如何使用 style 属性在元素内编写样式(这称为内联样式,尽可能避免这种情况)
  • 如何在 <style>...</style> 中编写元素的样式(这称为嵌入式样式)
  • 在 HTML 中如何使用 <link> 在单独的文件中编写样式并链接它(这称为链接样式表)
  • 什么是标签名称,属性,开始标签和结束标签
  • 如何使用 id 属性为一个元素赋予 id
  • CSS 中的标签选择器和 id 选择器

我们学习了以下 HTML 标签:

  • <p>:用于段落
  • <br>:用于换行
  • <ul><li>:显示列表
  • <div>:用于分组我们信件的元素
  • <h1><h2>:用于标题和子标题
  • <img>:用于插入图像
  • <strong><em>:用于粗体和斜体文字样式
  • <style>:用于嵌入式样式
  • <link>:用于包含外部样式表
  • <html>:用于包裹整个 HTML 文档
  • <!DOCTYPE html>:让浏览器知道我们正在使用 HTML5
  • <head>:包裹元信息,如 <link><title>
  • <body>:用于实际显示的 HTML 页面的主体
  • <title>:用于 HTML 页面的标题

我们学习了以下 CSS 属性:

  • width:用于定义元素的宽度
  • CSS 单位:“px” 和 “%”

朋友们,这就是今天的全部了,下一个教程中见。


作者简介:开发者 + 作者 | supersarkar.com | twitter.com/supersarkar


via: https://medium.freecodecamp.org/for-your-first-html-code-lets-help-batman-write-a-love-letter-64c203b9360b

作者:Kunal Sarkar 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Selenium 是浏览器自动化的绝佳工具。使用 Selenium IDE,你可以录制命令序列(如单击、拖动和输入),验证结果并最终存储此自动化测试供日后使用。这非常适合在浏览器中进行活跃开发。但是当你想要将这些测试与 CI/CD 流集成时,是时候使用 Selenium WebDriver 了。

WebDriver 公开了一个绑定了许多编程语言的 API,它允许你将浏览器测试与其他测试集成。这篇文章向你展示了如何在容器中运行 WebDriver 并将其与 Python 程序一起使用。

使用 Podman 运行 Selenium

Podman 是下面例子的容器运行时。有关如何开始使用 Podman 的信息,请参见此前文章

此例使用了 Selenium 的独立容器,其中包含 WebDriver 服务器和浏览器本身。要在后台启动服务器容器,请运行以下命令:

$ podman run -d --network host --privileged --name server docker.io/selenium/standalone-firefox

当你使用特权标志和主机网络运行容器时,你可以稍后从在 Python 中连接到此容器。你不需要使用 sudo

在 Python 中使用 Selenium

现在你可以提供一个使用此服务器的简单程序。这个程序很小,但应该会让你知道可以做什么:

from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

server ="http://127.0.0.1:4444/wd/hub"

driver = webdriver.Remote(command_executor=server,
    desired_capabilities=DesiredCapabilities.FIREFOX)

print("Loading page...")
driver.get("https://fedoramagazine.org/")
print("Loaded")
assert "Fedora" in driver.title

driver.quit()
print("Done.")

首先,程序连接到你已经启动的容器。然后它加载 Fedora Magazine 网页并判断 “Fedora” 是页面标题的一部分。最后,它退出会话。

需要 Python 绑定才能运行此程序。既然你已经在使用容器了,为什么不在容器中这样做呢?将以下内容保存到 Dockerfile 中:

FROM fedora:29
RUN dnf -y install python3
RUN pip3 install selenium

然后使用 Podman 在与 Dockerfile 相同的文件夹中构建容器镜像:

$ podman build -t selenium-python .

要在容器中运行程序,在运行容器时将包含 Python 代码的文件作为卷挂载:

$ podman run -t --rm --network host \
 -v $(pwd)/browser-test.py:/browser-test.py:z \
 selenium-python python3 browser-test.py

输出看上去像这样:

Loading page...
Loaded
Done.

接下来做什么

上面的示例程序是最小的,也许没那么有用。但这仅仅是最表面的东西!查看 SeleniumPython 绑定 的文档。在那里,你将找到有关如何在页面中查找元素、处理弹出窗口或填写表单的示例。拖放也是可能的,当然还有等待事件。

在实现一些不错的测试后,你可能希望将它们包含在 CI/CD 流程中。幸运的是,这是相当直接的,因为一切都是容器化的。

你可能也有兴趣设置 grid 来并行运行测试。这不仅有助于加快速度,还允许你同时测试多个不同的浏览器。

清理

当你容器使用完后,可以使用以下命令停止并删除独立容器:

$ podman stop server
$ podman rm server

如果你还想释放磁盘空间,请运行以下命令删除镜像:

$ podman rmi docker.io/selenium/standalone-firefox
$ podman rmi selenium-python fedora:29

总结

在本篇中,你已经看到使用容器技术开始使用 Selenium 是多么容易。它允许你自动化与网站的交互,以及测试交互。Podman 允许你在没有超级用户权限或 Docker 守护程序的情况下运行所需的容器。最后,Python 绑定允许你使用普通的 Python 代码与浏览器进行交互。


via: https://fedoramagazine.org/automate-web-browser-selenium/

作者:Lennart Jern 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在这个比较 Python 框架的最后一篇中,让我们看看 Django。

在本系列(由四部分组成)的前三篇文章中,我们讨论了 PyramidFlaskTornado 这 3 个 Web 框架。我们已经构建了三次相同的应用程序,最终我们遇到了 Django。总的来说,Django 是目前 Python 开发人员使用的主要 Web 框架,并且原因显而易见。它擅长隐藏大量的配置逻辑,让你专注于能够快速构建大型应用程序。

也就是说,当涉及到小型项目时,比如我们的待办事项列表应用程序,Django 可能有点像用消防水管来进行水枪大战。让我们来看看它们是如何结合在一起的。

关于 Django

Django 将自己定位为“一个鼓励快速开发和整洁、实用的设计的高级 Python Web 框架。它由经验丰富的开发人员构建,解决了 Web 开发的很多麻烦,因此你可以专注于编写应用程序而无需重新发明轮子”。而且它确实做到了!这个庞大的 Web 框架附带了非常多的工具,以至于在开发过程中,如何将所有内容组合在一起协同工作可能是个谜。

除了框架本身很大,Django 社区也是非常庞大的。事实上,它非常庞大和活跃,以至于有一个网站专门用于为人们收集第三方包,这些第三方包可集成进 Django 来做一大堆事情。包括从身份验证和授权到完全基于 Django 的内容管理系统,电子商务附加组件以及与 Stripe(LCTT 译注:美版“支付宝”)集成的所有内容。至于不要重新发明轮子:如果你想用 Django 完成一些事情,有人可能已经做过了,你只需将它集成进你的项目就行。

为此,我们希望使用 Django 构建 REST API,因此我们将使用流行的 Django REST 框架。它的工作是将 Django 框架(Django 使用自己的模板引擎构建 HTML 页面)转换为专门用于有效地处理 REST 交互的系统。让我们开始吧。

Django 启动和配置

$ mkdir django_todo
$ cd django_todo
$ pipenv install --python 3.6
$ pipenv shell
(django-someHash) $ pipenv install django djangorestframework

作为参考,我们使用的是 django-2.0.7djangorestframework-3.8.2

与 Flask, Tornado 和 Pyramid 不同,我们不需要自己编写 setup.py 文件,我们并不是在做一个可安装的 Python 发布版。像很多事情一样,Django 以自己的方式处理这个问题。我们仍然需要一个 requirements.txt 文件来跟踪我们在其它地方部署的所有必要安装。但是,就 Django 项目中的目标模块而言,Django 会让我们列出我们想要访问的子目录,然后允许我们从这些目录中导入,就像它们是已安装的包一样。

首先,我们必须创建一个 Django 项目。

当我们安装了 Django 后,我们还安装了命令行脚本 django-admin。它的工作是管理所有与 Django 相关的命令,这些命令有助于我们将项目整合在一起,并在我们继续开发的过程中对其进行维护。django-admin 并不是让我们从头开始构建整个 Django 生态系统,而是让我们从标准 Django 项目所需的所有必要文件(以及更多)的基础上开始。

调用 django-adminstart-project 命令的语法是 django-admin startproject <项目名称> <存放目录>。我们希望文件存于当前的工作目录中,所以:

(django-someHash) $ django-admin startproject django_todo .

输入 ls 将显示一个新文件和一个新目录。

(django-someHash) $ ls
manage.py   django_todo

manage.py 是一个可执行命令行 Python 文件,它最终成为 django-admin 的封装。因此,它的工作与 django-admin 是一样的:帮助我们管理项目。因此得名 manage.py

它在 django_todo 目录里创建了一个新目录 django_todo,其代表了我们项目的配置根目录。现在让我们深入研究一下。

配置 Django

可以将 django_todo 目录称为“配置根目录”,我们的意思是这个目录包含了通常配置 Django 项目所需的文件。几乎所有这个目录之外的内容都只关注与项目模型、视图、路由等相关的“业务逻辑”。所有连接项目的点都将在这里出现。

django_todo 目录中调用 ls 会显示以下四个文件:

(django-someHash) $ cd django_todo
(django-someHash) $ ls
__init__.py settings.py urls.py     wsgi.py
  • __init__.py 文件为空,之所以存在是为了将此目录转换为可导入的 Python 包。
  • settings.py 是设置大多数配置项的地方。例如项目是否处于 DEBUG 模式,正在使用哪些数据库,Django 应该定位文件的位置等等。它是配置根目录的“主要配置”部分,我们将在一会深入研究。
  • urls.py 顾名思义就是设置 URL 的地方。虽然我们不必在此文件中显式写入项目的每个 URL,但我们需要让此文件知道在其他任何地方已声明的 URL。如果此文件未指向其它 URL,则那些 URL 就不存在。
  • wsgi.py 用于在生产环境中提供应用程序。就像 Pyramid、 Tornado 和 Flask 暴露了一些 “app” 对象一样,它们用来提供配置好的应用程序,Django 也必须暴露一个,就是在这里完成的。它可以和 GunicornWaitress 或者 uWSGI 一起配合来提供服务。

设置 settings

看一看 settings.py,它里面有大量的配置项,那些只是默认值!这甚至不包括数据库、静态文件、媒体文件、任何集成的钩子,或者可以配置 Django 项目的任何其它几种方式。让我们从上到下看看有什么:

  • BASE_DIR 设置目录的绝对路径,或者是 manage.py 所在的目录。这对于定位文件非常有用。
  • SECRET_KEY 是用于 Django 项目中加密签名的密钥。在实际中,它用于会话、cookie、CSRF 保护和身份验证令牌等。最好在第一次提交之前,尽快应该更改 SECRET_KEY 的值并将其放置到环境变量中。
  • DEBUG 告诉 Django 是以开发模式还是生产模式运行项目。这是一个非常关键的区别。

    • 在开发模式下,当弹出一个错误时,Django 将显示导致错误的完整堆栈跟踪,以及运行项目所涉及的所有设置和配置。如果在生产环境中将 DEBUG 设置为 True,这可能成为一个巨大的安全问题。
    • 在生产模式下,当出现问题时,Django 会显示一个简单的错误页面,即除错误代码外不提供任何信息。
    • 保护我们项目的一个简单方法是将 DEBUG 设置为环境变量,如 bool(os.environ.get('DEBUG', ''))
  • ALLOWED_HOSTS 是应用程序提供服务的主机名的列表。在开发模式中,这可能是空的;但是在生产环境中,如果为项目提供服务的主机不在 ALLOWED_HOSTS 列表中,Django 项目将无法运行。这是设置为环境变量的另一种情况。
  • INSTALLED_APPS 是我们的 Django 项目可以访问的 Django “apps” 列表(将它们视为子目录,稍后会详细介绍)。默认情况下,它将提供:

    • 内置的 Django 管理网站
    • Django 的内置认证系统
    • Django 的数据模型通用管理器
    • 会话管理
    • Cookie 和基于会话的消息传递
    • 站点固有的静态文件的用法,比如 css 文件、js 文件、任何属于我们网站设计的图片等。
  • MIDDLEWARE 顾名思义:帮助 Django 项目运行的中间件。其中很大一部分用于处理各种类型的安全,尽管我们可以根据需要添加其它中间件。
  • ROOT_URLCONF 设置基本 URL 配置文件的导入路径。还记得我们之前见过的那个 urls.py 吗?默认情况下,Django 指向该文件以此来收集所有的 URL。如果我们想让 Django 在其它地方寻找,我们将在这里设置 URL 位置的导入路径。
  • TEMPLATES 是 Django 用于我们网站前端的模板引擎列表,假如我们依靠 Django 来构建我们的 HTML。我们在这里不需要,那就无关紧要了。
  • WSGI_APPLICATION 设置我们的 WSGI 应用程序的导入路径 —— 在生产环境下使用的东西。默认情况下,它指向 wsgi.py 中的 application 对象。这很少(如果有的话)需要修改。
  • DATABASES 设置 Django 项目将访问那些数据库。必须设置 default 数据库。我们可以通过名称设置别的数据库,只要我们提供 HOSTUSERPASSWORDPORT、数据库名称 NAME 和合适的 ENGINE。可以想象,这些都是敏感的信息,因此最好将它们隐藏在环境变量中。查看 Django 文档了解更多详情。

    • 注意:如果不是提供数据库的每个单个部分,而是提供完整的数据库 URL,请查看 djdatabaseurl
  • AUTH_PASSWORD_VALIDATORS 实际上是运行以检查输入密码的函数列表。默认情况下我们有一些,但是如果我们有其它更复杂的验证需求:不仅仅是检查密码是否与用户的属性匹配,是否超过最小长度,是否是 1000 个最常用的密码之一,或者密码完全是数字,我们可以在这里列出它们。
  • LANGUAGE_CODE 设置网站的语言。默认情况下它是美国英语,但我们可以将其切换为其它语言。
  • TIME_ZONE 是我们 Django 项目后中自动生成的时间戳的时区。我强调坚持使用 UTC 并在其它地方执行任何特定于时区的处理,而不是尝试重新配置此设置。正如这篇文章 所述,UTC 是所有时区的共同点,因为不需要担心偏移。如果偏移很重要,我们可以根据需要使用与 UTC 的适当偏移来计算它们。
  • USE_I18N 将让 Django 使用自己的翻译服务来为前端翻译字符串。I18N = 国际化(internationalization,“i” 和 “n” 之间共 18 个字符)。
  • USE_L10N L10N = 本地化(localization,在 l 和 n 之间共 10 个字符) 。如果设置为 True,那么将使用数据的公共本地格式。一个很好的例子是日期:在美国它是 MM-DD-YYYY。在欧洲,日期往往写成 DD-MM-YYYY。
  • STATIC_URL 是用于提供静态文件的主体部分。我们将构建一个 REST API,因此我们不需要考虑静态文件。通常,这会为每个静态文件的域名设置根路径。所以,如果我们有一个 Logo 图像,那就是 http://<domainname>/<STATIC_URL>/logo.gif

默认情况下,这些设置已准备就绪。我们必须改变的一个选项是 DATABASES 设置。首先,我们创建将要使用的数据库:

(django-someHash) $ createdb django_todo

我们想要像使用 Flask、Pyramid 和 Tornado 一样使用 PostgreSQL 数据库,这意味着我们必须更改 DATABASES 设置以允许我们的服务器访问 PostgreSQL 数据库。首先是引擎。默认情况下,数据库引擎是 django.db.backends.sqlite3,我们把它改成 django.db.backends.postgresql

有关 Django 可用引擎的更多信息,请查看文档。请注意,尽管技术上可以将 NoSQL 解决方案整合到 Django 项目中,但为了开箱即用,Django 强烈偏向于 SQL 解决方案。

接下来,我们必须为连接参数的不同部分指定键值对。

  • NAME 是我们刚刚创建的数据库的名称。
  • USER 是 Postgres 数据库用户名。
  • PASSWORD 是访问数据库所需的密码。
  • HOST 是数据库的主机。当我们在本地开发时,localhost127.0.0.1 都将起作用。
  • PORT 是我们为 Postgres 开放的端口,它通常是 5432

settings.py 希望我们为每个键提供字符串值。但是,这是高度敏感的信息。任何负责任的开发人员都不应该这样做。有几种方法可以解决这个问题,一种是我们需要设置环境变量。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', ''),
        'USER': os.environ.get('DB_USER', ''),
        'PASSWORD': os.environ.get('DB_PASS', ''),
        'HOST': os.environ.get('DB_HOST', ''),
        'PORT': os.environ.get('DB_PORT', ''),
    }
}

在继续之前,请确保设置环境变量,否则 Django 将无法工作。此外,我们需要在此环境中安装 psycopg2,以便我们可以与数据库通信。

Django 路由和视图

让我们在这个项目中实现一些函数。我们将使用 Django REST 框架来构建 REST API,所以我们必须确保在 settings.py 中将 rest_framework 添加到 INSTALLED_APPS 的末尾。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework'
]

虽然 Django REST 框架并不专门需要基于类的视图(如 Tornado)来处理传入的请求,但类是编写视图的首选方法。让我们来定义一个类视图。

让我们在 django_todo 创建一个名为 views.py 的文件。在 views.py 中,我们将创建 “Hello, world!” 视图。

# in django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

class HelloWorld(APIView):
    def get(self, request, format=None):
        """Print 'Hello, world!' as the response body."""
        return JsonResponse("Hello, world!")

每个 Django REST 框架基于类的视图都直接或间接地继承自 APIViewAPIView 处理大量的东西,但针对我们的用途,它做了以下特定的事情:

* 根据 HTTP 方法(例如 GET、POST、PUT、DELETE)来设置引导对应请求所需的方法
* 用我们需要的所有数据和属性来填充 `request` 对象,以便解析和处理传入的请求 
* 采用 `Response` 或 `JsonResponse`,每个调度方法(即名为 `get`、`post`、`put`、`delete` 的方法)返回并构造格式正确的 HTTP 响应。

终于,我们有一个视图了!它本身没有任何作用,我们需要将它连接到路由。

如果我们跳转到 django_todo/urls.py,我们会到达默认的 URL 配置文件。如前所述:如果 Django 项目中的路由不包含在此处,则它不存在。

我们在给定的 urlpatterns 列表中添加所需的 URL。默认情况下,我们有一整套 URL 用于 Django 的内置管理后端系统。我们会完全删除它。

我们还得到一些非常有用的文档字符串,它告诉我们如何向 Django 项目添加路由。我们需要调用 path(),伴随三个参数:

  • 所需的路由,作为字符串(没有前导斜线)
  • 处理该路由的视图函数(只能有一个函数!)
  • 在 Django 项目中路由的名称

让我们导入 HelloWorld 视图并将其附加到主路径 / 。我们可以从 urlpatterns 中删除 admin 的路径,因为我们不会使用它。

# django_todo/urls.py, after the big doc string
from django.urls import path
from django_todo.views import HelloWorld

urlpatterns = [
    path('', HelloWorld.as_view(), name="hello"),
]

好吧,这里有一点不同。我们指定的路由只是一个空白字符串,为什么它会工作?Django 假设我们声明的每个路由都以一个前导斜杠开头,我们只是在初始域名后指定资源路由。如果一条路由没有去往一个特定的资源,而只是一个主页,那么该路由是 '',实际上是“没有资源”。

HelloWorld 视图是从我们刚刚创建的 views.py 文件导入的。为了执行此导入,我们需要更新 settings.py 中的 INSTALLED_APPS 列表使其包含 django_todo。是的,这有点奇怪。以下是一种理解方式。

INSTALLED_APPS 指的是 Django 认为可导入的目录或包的列表。它是 Django 处理项目的各个组件的方式,比如安装了一个包,而不需要经过 setup.py 的方式。我们希望将 django_todo 目录视为可导入的包,因此我们将该目录包含在 INSTALLED_APPS 中。现在,在该目录中的任何模块也是可导入的。所以我们得到了我们的视图。

path 函数只将视图函数作为第二个参数,而不仅仅是基于类的视图。幸运的是,所有有效的基于 Django 类的视图都包含 .as_view() 方法。它的工作是将基于类的视图的所有优点汇总到一个视图函数中并返回该视图函数。所以,我们永远不必担心转换的工作。相反,我们只需要考虑业务逻辑,让 Django 和 Django REST 框架处理剩下的事情。

让我们在浏览器中打开它!

Django 提供了自己的本地开发服务器,可通过 manage.py 访问。让我们切换到包含 manage.py 的目录并输入:

(django-someHash) $ ./manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
August 01, 2018 - 16:47:24
Django version 2.0.7, using settings 'django_todo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

runserver 执行时,Django 会检查以确保项目(或多或少)正确连接在一起。这不是万无一失的,但确实会发现一些明显的问题。如果我们的数据库与代码不同步,它会通知我们。毫无疑问,因为我们没有将任何应用程序的东西提交到我们的数据库,但现在这样做还是可以的。让我们访问 http://127.0.0.1:8000 来查看 HelloWorld 视图的输出。

咦?这不是我们在 Pyramid、Flask 和 Tornado 中看到的明文数据。当使用 Django REST 框架时,HTTP 响应(在浏览器中查看时)是这样呈现的 HTML,以红色显示我们的实际 JSON 响应。

但不要担心!如果我们在命令行中使用 curl 快速访问 http://127.0.0.1:8000,我们就不会得到任何花哨的 HTML,只有内容。

# 注意:在不同的终端口窗口中执行此操作,在虚拟环境之外
$ curl http://127.0.0.1:8000
"Hello, world!"

棒极了!

Django REST 框架希望我们在使用浏览器浏览时拥有一个人性化的界面。这是有道理的,如果在浏览器中查看 JSON,通常是因为人们想要检查它是否正确,或者在设计一些消费者 API 时想要了解 JSON 响应。这很像你从 Postman 中获得的东西。

无论哪种方式,我们都知道我们的视图工作了!酷!让我们概括一下我们做过的事情:

  1. 使用 django-admin startproject <项目名称> 开始一个项目
  2. 使用环境变量来更新 django_todo/settings.py 中的 DEBUGSECRET_KEY,还有 DATABASES 字典
  3. 安装 Django REST 框架,并将它添加到 INSTALLED_APPS
  4. 创建 django_todo/views.py 来包含我们的第一个类视图,它返回响应 “Hello, world!”
  5. 更新 django_todo/urls.py,其中包含我们的根路由
  6. django_todo/settings.py 中更新 INSTALLED_APPS 以包含 django_todo

创建模型

现在让我们来创建数据模型吧。

Django 项目的整个基础架构都是围绕数据模型构建的,它是这样编写的,每个数据模型够可以拥有自己的小天地,拥有自己的视图,自己与其资源相关的 URL 集合,甚至是自己的测试(如果我们想要的话)。

如果我们想构建一个简单的 Django 项目,我们可以通过在 django_todo 目录中编写我们自己的 models.py 文件并将其导入我们的视图来避免这种情况。但是,我们想以“正确”的方式编写 Django 项目,因此我们应该尽可能地将模型拆分成符合 Django Way™(Django 风格)的包。

Django Way 涉及创建所谓的 Django “应用程序”,它本身并不是单独的应用程序,它们没有自己的设置和诸如此类的东西(虽然它们也可以)。但是,它们可以拥有一个人们可能认为属于独立应用程序的东西:

  • 一组自建的 URL
  • 一组自建的 HTML 模板(如果我们想要提供 HTML)
  • 一个或多个数据模型
  • 一套自建的视图
  • 一套自建的测试

它们是独立的,因此可以像独立应用程序一样轻松共享。实际上,Django REST 框架是 Django 应用程序的一个例子。它包含自己的视图和 HTML 模板,用于提供我们的 JSON。我们只是利用这个 Django 应用程序将我们的项目变成一个全面的 RESTful API 而不用那么麻烦。

要为我们的待办事项列表项创建 Django 应用程序,我们将要使用 manage.pystartapp 命令。

(django-someHash) $ ./manage.py startapp todo

startapp 命令成功执行后没有输出。我们可以通过使用 ls 来检查它是否完成它应该做的事情。

(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    todo

看看:我们有一个全新的 todo 目录。让我们看看里面!

(django-someHash) $ ls todo
__init__.py admin.py    apps.py     migrations  models.py   tests.py    views.py

以下是 manage.py startapp 创建的文件:

  • __init__.py 是空文件。它之所以存在是因为此目录可看作是模型、视图等的有效导入路径。
  • admin.py 不是空文件。它用于在 Django admin 中规范化这个应用程序的模型,我们在本文中没有涉及到它。
  • apps.py 这里基本不起作用。它有助于规范化 Django admin 的模型。
  • migrations 是一个包含我们数据模型快照的目录。它用于更新数据库。这是少数几个内置了数据库管理的框架之一,其中一部分允许我们更新数据库,而不必拆除它并重建它以更改 Schema。
  • models.py 是数据模型所在。
  • tests.py 是测试所在的地方,如果我们需要写测试。
  • views.py 用于我们编写的与此应用程序中的模型相关的视图。它们不是一定得写在这里。例如,我们可以在 django_todo/views.py 中写下我们所有的视图。但是,它在这个应用程序中更容易将我们的概念理清。在覆盖了许多概念的扩展应用程序的关系之间会变得更加密切。

它并没有为这个应用程序创建 urls.py 文件,但我们可以自己创建。

(django-someHash) $ touch todo/urls.py

在继续之前,我们应该帮自己一个忙,将这个新 Django 应用程序添加到 django_todo/settings.py 中的 INSTALLED_APPS 列表中。

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'django_todo',
    'todo' # <--- 添加了这行
]

检查 todo/models.py 发现 manage.py 已经为我们编写了一些代码。不同于在 Flask、Tornado 和 Pyramid 实现中创建模型的方式,Django 不利用第三方来管理数据库会话或构建其对象实例。它全部归入 Django 的 django.db.models 子模块。

然而,建立模型的方式或多或少是相同的。要在 Django 中创建模型,我们需要构建一个继承自 models.Modelclass,将应用于该模型实例的所有字段都应视为类属性。我们不像过去那样从 SQLAlchemy 导入列和字段类型,而是直接从 django.db.models 导入。

# todo/models.py
from django.db import models

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)

虽然 Django 的需求和基于 SQLAlchemy 的系统之间存在一些明显的差异,但总体内容和结构或多或少相同。让我们来指出这些差异。

我们不再需要为对象实例声明自动递增 ID 的单独字段。除非我们指定一个不同的字段作为主键,否则 Django 会为我们构建一个。

我们只是直接引用数据类型作为列本身,而不是实例化传递数据类型对象的 Column 对象。

Unicode 字段变为 models.CharFieldmodels.TextFieldCharField 用于特定最大长度的小文本字段,而 TextField 用于任何数量的文本。

TextField 应该是空白的,我们以两种方式指定它。blank = True 表示当构建此模型的实例,并且正在验证附加到该字段的数据时,该数据是可以为空的。这与 null = True 不同,后者表示当构造此模型类的表时,对应于 note 的列将允许空白或为 NULL。因此,总而言之,blank = True 控制如何将数据添加到模型实例,而 null = True 控制如何构建保存该数据的数据库表。

DateTime 字段增加了一些属性,并且能够为我们做一些工作,使得我们不必修改类的 __init__ 方法。对于 creation_date 字段,我们指定 auto_now_add = True。在实际意义上意味着,当创建一个新模型实例时,Django 将自动记录现在的日期和时间作为该字段的值。这非常方便!

auto_now_add 及其类似属性 auto_now 都没被设置为 True 时,DateTimeField 会像其它字段一样需要预期的数据。它需要提供一个适当的 datetime 对象才能生效。due_date 列的 blanknull 属性都设置为 True,这样待办事项列表中的项目就可以成为将来某个时间点完成,没有确定的日期或时间。

BooleanField 最终可以取两个值:TrueFalse。这里,默认值设置为 False

管理数据库

如前所述,Django 有自己的数据库管理方式。我们可以利用 Django 提供的 manage.py 脚本,而不必编写任何关于数据库的代码。它不仅可以管理我们数据库的表的构建,还可以管理我们希望对这些表进行的任何更新,而不必将整个事情搞砸!

因为我们构建了一个新模型,所以我们需要让数据库知道它。首先,我们需要将与此模型对应的模式放入代码中。manage.pymakemigrations 命令对我们构建的模型类及其所有字段进行快照。它将获取该信息并将其打包成一个 Python 脚本,该脚本将存在于特定 Django 应用程序的 migrations 目录中。永远没有理由直接运行这个迁移脚本。它的存在只是为了让 Django 可以使用它作为更新数据库表的基础,或者在我们更新模型类时继承信息。

(django-someHash) $ ./manage.py makemigrations
Migrations for 'todo':
  todo/migrations/0001_initial.py
    - Create model Task

这将查找 INSTALLED_APPS 中列出的每个应用程序,并检查这些应用程序中存在的模型。然后,它将检查相应的 migrations 目录中的迁移文件,并将它们与每个 INSTALLED_APPS 中的模型进行比较。如果模型已经升级超出最新迁移所应存在的范围,则将创建一个继承自最新迁移文件的新迁移文件,它将自动命名,并且还会显示一条消息,说明自上次迁移以来发生了哪些更改。

如果你上次处理 Django 项目已经有一段时间了,并且不记得模型是否与迁移同步,那么你无需担心。makemigrations 是一个幂等操作。无论你运行 makemigrations 一次还是 20 次,migrations 目录只有一个与当前模型配置的副本。更棒的是,当我们运行 ./manage.py runserver 时,Django 检测到我们的模型与迁移不同步,它会用彩色文本告诉我们以便我们可以做出适当的选择。

下一个要点是至少让每个人访问一次:创建一个迁移文件不会立即影响我们的数据库。当我们运行 makemigrations 时,我们布置我们的 Django 项目定义了给定的表应该如何创建和最终查找。我们仍要将这些更改应用于数据库。这就是 migrate 命令的用途。

(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK

当我们应用这些迁移时,Django 首先检查其他 INSTALLED_APPS 是否有需要应用的迁移,它大致按照列出的顺序检查它们。我们希望我们的应用程序最后列出,因为我们希望确保,如果我们的模型依赖于任何 Django 的内置模型,我们所做的数据库更新不会受到依赖性问题的影响。

我们还有另一个要构建的模型:User 模型。但是,因为我们正在使用 Django,事情有一些变化。许多应用程序需要某种类型的用户模型,Django 的 django.contrib.auth 包构建了自己的用户模型供我们使用。如果无需用户所需要的身份验证令牌,我们可以继续使用它而不是重新发明轮子。

但是,我们需要那个令牌。我们可以通过两种方式来处理这个问题。

  • 继承 Django 的 User 对象,我们自己的对象通过添加 token 字段来扩展它
  • 创建一个与 Django 的 User 对象一对一关系的新对象,其唯一目的是持有一个令牌

我习惯于建立对象关系,所以让我们选择第二种选择。我们称之为 Owner,因为它基本上具有与 User 类似的内涵,这就是我们想要的。

出于纯粹的懒惰,我们可以在 todo/models.py 中包含这个新的 Owner 对象,但是不要这样做。Owner 没有明确地与任务列表上的项目的创建或维护有关。从概念上讲,Owner 只是任务的所有者。甚至有时候我们想要扩展这个 Owner 以包含与任务完全无关的其他数据。

为了安全起见,让我们创建一个 owner 应用程序,其工作是容纳和处理这个 Owner 对象。

(django-someHash) $ ./manage.py startapp owner

不要忘记在 settings.py 文件中的 INSTALLED_APPS 中添加它。 `INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_todo',
'todo',
'owner'
]`

如果我们查看 Django 项目的根目录,我们现在有两个 Django 应用程序:

(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    owner        todo

owner/models.py 中,让我们构建这个 Owner 模型。如前所述,它与 Django 的内置 User 对象有一对一的关系。我们可以用 Django 的 models.OneToOneField 强制实现这种关系。

# owner/models.py
from django.db import models
from django.contrib.auth.models import User
import secrets

class Owner(models.Model):
    """The object that owns tasks."""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256)
    def __init__(self, *args, **kwargs):
        """On construction, set token."""
        self.token = secrets.token_urlsafe(64)
        super().__init__(*args, **kwargs)

这表示 Owner 对象对应到 User 对象,每个 user 实例有一个 owner 实例。on_delete = models.CASCADE 表示如果相应的 User 被删除,它所对应的 Owner 实例也将被删除。让我们运行 makemigrationsmigrate 来将这个新模型放入到我们的数据库中。

(django-someHash) $ ./manage.py makemigrations
Migrations for 'owner':
  owner/migrations/0001_initial.py
    - Create model Owner
(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, owner, sessions, todo
Running migrations:
  Applying owner.0001_initial... OK

现在我们的 Owner 需要拥有一些 Task 对象。它与上面看到的 OneToOneField 非常相似,只不过我们会在 Task 对象上贴一个 ForeignKey 字段指向 Owner

# todo/models.py
from django.db import models
from owner.models import Owner

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE)

每个待办事项列表任务只有一个可以拥有多个任务的所有者。删除该所有者后,他们拥有的任务都会随之删除。

现在让我们运行 makemigrations 来获取我们的数据模型设置的新快照,然后运行 migrate 将这些更改应用到我们的数据库。

(django-someHash) django $ ./manage.py makemigrations
You are trying to add a non-nullable field 'owner' to task without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

不好了!出现了问题!发生了什么?其实,当我们创建 Owner 对象并将其作为 ForeignKey 添加到 Task 时,要求每个 Task 都需要一个 Owner。但是,我们为 Task 对象进行的第一次迁移不包括该要求。因此,即使我们的数据库表中没有数据,Django 也会对我们的迁移进行预先检查,以确保它们兼容,而我们提议的这种新迁移不是。

有几种方法可以解决这类问题:

  1. 退出当前迁移并构建一个包含当前模型配置的新迁移
  2. 将一个默认值添加到 Task 对象的 owner 字段
  3. 允许任务为 owner 字段设置 NULL

方案 2 在这里没有多大意义。我们建议,默认情况下,任何创建的 Task 都会对应到某个默认所有者,尽管默认所有者不一定存在。 方案 1 要求我们销毁和重建我们的迁移,而我们应该把它们留下。

让我们考虑选项 3。在这种情况下,如果我们允许 Task 表为所有者提供空值,它不会很糟糕。从这一点开始创建的任何任务都必然拥有一个所有者。如果你的数据库表并非不能接受重新架构,请删除该迁移、删除表并重建迁移。

# todo/models.py
from django.db import models
from owner.models import Owner

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE, null=True)
(django-someHash) $ ./manage.py makemigrations
Migrations for 'todo':
  todo/migrations/0002_task_owner.py
    - Add field owner to task
(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, owner, sessions, todo
Running migrations:
  Applying todo.0002_task_owner... OK

酷!我们有模型了!欢迎使用 Django 声明对象的方式。

出于更好的权衡,让我们确保无论何时制作 User,它都会自动与新的 Owner 对象对应。我们可以使用 Django 的 signals 系统来做到这一点。基本上,我们确切地表达了意图:“当我们得到一个新的 User 被构造的信号时,构造一个新的 Owner 并将新的 User 设置为 Owneruser 字段。”在实践中看起来像这样:

# owner/models.py
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

import secrets


class Owner(models.Model):
    """The object that owns tasks."""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256)

    def __init__(self, *args, **kwargs):
        """On construction, set token."""
        self.token = secrets.token_urlsafe(64)
        super().__init__(*args, **kwargs)


@receiver(post_save, sender=User)
def link_user_to_owner(sender, **kwargs):
    """If a new User is saved, create a corresponding Owner."""
    if kwargs['created']:
        owner = Owner(user=kwargs['instance'])
        owner.save()

我们设置了一个函数,用于监听从 Django 中内置的 User 对象发送的信号。它正在等待 User 对象被保存之后的情况。这可以来自新的 User 或对现有 User 的更新。我们在监听功能中辨别出两种情况。

如果发送信号的东西是新创建的实例,kwargs ['created'] 将具有值 True。如果是 True 的话,我们想做点事情。如果它是一个新实例,我们创建一个新的 Owner,将其 user 字段设置为创建的新 User 实例。之后,我们 save() 新的 Owner。如果一切正常,这将提交更改到数据库。如果数据没通过我们声明的字段的验证,它将失败。

现在让我们谈谈我们将如何访问数据。

访问模型数据

在 Flask、Pyramid 和 Tornado 框架中,我们通过对某些数据库会话运行查询来访问模型数据。也许它被附加到 request 对象,也许它是一个独立的 session 对象。无论如何,我们必须建立与数据库的实时连接并在该连接上进行查询。

这不是 Django 的工作方式。默认情况下,Django 不利用任何第三方对象关系映射(ORM)与数据库进行通信。相反,Django 允许模型类维护自己与数据库的对话。

django.db.models.Model 继承的每个模型类都会附加一个 objects 对象。这将取代我们熟悉的 sessiondbsession。让我们打开 Django 给我们的特殊 shell,并研究这个 objects 对象是如何工作的。

(django-someHash) $ ./manage.py shell
Python 3.7.0 (default, Jun 29 2018, 20:13:13)
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

Django shell 与普通的 Python shell 不同,因为它知道我们正在构建的 Django 项目,可以轻松导入我们的模型、视图、设置等,而不必担心安装包。我们可以通过简单的 import 访问我们的模型。

>>> from owner.models import Owner
>>> Owner
<class 'owner.models.Owner'>

目前,我们没有 Owner 实例。我们可以通过 Owner.objects.all() 查询它们。

>>> Owner.objects.all()
<QuerySet []>

无论何时我们在 <Model> .objects 对象上运行查询方法,我们都会得到 QuerySet。为了我们的目的,它实际上是一个列表,这个列表向我们显示它是空的。让我们通过创建一个 User 来创建一个 Owner

>>> from django.contrib.auth.models import User
>>> new_user = User(username='kenyattamurphy', email='[email protected]')
>>> new_user.set_password('wakandaforever')
>>> new_user.save()

如果我们现在查询所有的 Owner,我们应该会找到 Kenyatta。

>>> Owner.objects.all()
<QuerySet [<Owner: Owner object (1)>]>

棒极了!我们得到了数据!

序列化模型

我们将在 “Hello World” 之外来回传递数据。因此,我们希望看到某种类似于 JSON 类型的输出,它可以很好地表示数据。获取该对象的数据并将其转换为 JSON 对象以通过 HTTP 提交是数据序列化的一种方式。在序列化数据时,我们正在获取我们目前拥有的数据并重新格式化以适应一些标准的、更易于理解的形式。

如果我用 Flask、Pyramid 和 Tornado 这样做,我会在每个模型上创建一个新方法,让用户可以直接调用 to_json()to_json() 的唯一工作是返回一个 JSON 可序列化的(即数字、字符串、列表、字典)字典,其中包含我想要为所讨论的对象显示的任何字段。

对于 Task 对象,它可能看起来像这样:

class Task(Base):
    ...all the fields...

    def to_json(self):
        """Convert task attributes to a JSON-serializable dict."""
        return {
            'id': self.id,
            'name': self.name,
            'note': self.note,
            'creation_date': self.creation_date.strftime('%m/%d/%Y %H:%M:%S'),
            'due_date': self.due_date.strftime('%m/%d/%Y %H:%M:%S'),
            'completed': self.completed,
            'user': self.user_id
        }

这不花哨,但它确实起到了作用。

然而,Django REST 框架为我们提供了一个对象,它不仅可以为我们这样做,还可以在我们想要创建新对象实例或更新现有实例时验证输入,它被称为 ModelSerializer

Django REST 框架的 ModelSerializer 是我们模型的有效文档。如果没有附加模型,它们就没有自己的生命(因为那里有 Serializer 类)。它们的主要工作是准确地表示我们的模型,并在我们的模型数据需要序列化并通过线路发送时,将其转换为 JSON。

Django REST 框架的 ModelSerializer 最适合简单对象。举个例子,假设我们在 Task 对象上没有 ForeignKey。我们可以为 Task 创建一个序列化器,它将根据需要将其字段值转换为 JSON,声明如下:

# todo/serializers.py
from rest_framework import serializers
from todo.models import Task

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    class Meta:
        model = Task
        fields = ('id', 'name', 'note', 'creation_date', 'due_date', 'completed')

在我们新的 TaskSerializer 中,我们创建了一个 Meta 类。Meta 的工作就是保存关于我们试图序列化的东西的信息(或元数据)。然后,我们会注意到要显示的特定字段。如果我们想要显示所有字段,我们可以简化过程并使用 __all __。或者,我们可以使用 exclude 关键字而不是 fields 来告诉 Django REST 框架我们想要除了少数几个字段以外的每个字段。我们可以拥有尽可能多的序列化器,所以也许我们想要一个用于一小部分字段,而另一个用于所有字段?在这里都可以。

在我们的例子中,每个 Task 和它的所有者 Owner 之间都有一个关系,必须在这里反映出来。因此,我们需要借用 serializers.PrimaryKeyRelatedField 对象来指定每个 Task 都有一个 Owner,并且该关系是一对一的。它的所有者将从已有的全部所有者的集合中找到。我们通过对这些所有者进行查询并返回我们想要与此序列化程序关联的结果来获得该集合:Owner.objects.all()。我们还需要在字段列表中包含 owner,因为我们总是需要一个与 Task 相关联的 Owner

# todo/serializers.py
from rest_framework import serializers
from todo.models import Task
from owner.models import Owner

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    owner = serializers.PrimaryKeyRelatedField(queryset=Owner.objects.all())

    class Meta:
        model = Task
        fields = ('id', 'name', 'note', 'creation_date', 'due_date', 'completed', 'owner')

现在构建了这个序列化器,我们可以将它用于我们想要为我们的对象做的所有 CRUD 操作:

  • 如果我们想要 GET 一个特定的 Task 的 JSON 类型版本,我们可以做 TaskSerializer((some_task).data
  • 如果我们想接受带有适当数据的 POST 来创建一个新的 Task,我们可以使用 TaskSerializer(data = new_data).save()
  • 如果我们想用 PUT 更新一些现有数据,我们可以用 TaskSerializer(existing_task, data = data).save()

我们没有包括 delete,因为我们不需要对 delete 操作做任何事情。如果你可以删除一个对象,只需使用 object_instance.delete()

以下是一些序列化数据的示例:

>>> from todo.models import Task
>>> from todo.serializers import TaskSerializer
>>> from owner.models import Owner
>>> from django.contrib.auth.models import User
>>> new_user = User(username='kenyatta', email='[email protected]')
>>> new_user.save_password('wakandaforever')
>>> new_user.save() # creating the User that builds the Owner
>>> kenyatta = Owner.objects.first() # 找到 kenyatta 的所有者
>>> new_task = Task(name="Buy roast beef for the Sunday potluck", owner=kenyatta)
>>> new_task.save()
>>> TaskSerializer(new_task).data
{'id': 1, 'name': 'Go to the supermarket', 'note': None, 'creation_date': '2018-07-31T06:00:25.165013Z', 'due_date': None, 'completed': False, 'owner': 1}

使用 ModelSerializer 对象可以做更多的事情,我建议查看文档以获得更强大的功能。否则,这就是我们所需要的。现在是时候深入视图了。

查看视图

我们已经构建了模型和序列化器,现在我们需要为我们的应用程序设置视图和 URL。毕竟,对于没有视图的应用程序,我们无法做任何事情。我们已经看到了上面的 HelloWorld 视图的示例。然而,这总是一个人为的、概念验证的例子,并没有真正展示 Django REST 框架的视图可以做些什么。让我们清除 HelloWorld 视图和 URL,这样我们就可以从我们的视图重新开始。

我们要构建的第一个视图是 InfoView。与之前的框架一样,我们只想打包并发送一个我们用到的路由的字典。视图本身可以存在于 django_todo.views 中,因为它与特定模型无关(因此在概念上不属于特定应用程序)。

# django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

class InfoView(APIView):
    """List of routes for this API."""
    def get(self, request):
        output = {
            'info': 'GET /api/v1',
            'register': 'POST /api/v1/accounts',
            'single profile detail': 'GET /api/v1/accounts/<username>',
            'edit profile': 'PUT /api/v1/accounts/<username>',
            'delete profile': 'DELETE /api/v1/accounts/<username>',
            'login': 'POST /api/v1/accounts/login',
            'logout': 'GET /api/v1/accounts/logout',
            "user's tasks": 'GET /api/v1/accounts/<username>/tasks',
            "create task": 'POST /api/v1/accounts/<username>/tasks',
            "task detail": 'GET /api/v1/accounts/<username>/tasks/<id>',
            "task update": 'PUT /api/v1/accounts/<username>/tasks/<id>',
            "delete task": 'DELETE /api/v1/accounts/<username>/tasks/<id>'
        }
        return JsonResponse(output)

这与我们在 Tornado 中所拥有的完全相同。让我们将它放置到合适的路由并继续。为了更好的测试,我们还将删除 admin/ 路由,因为我们不会在这里使用 Django 管理后端。

# in django_todo/urls.py
from django_todo.views import InfoView
from django.urls import path

urlpatterns = [
    path('api/v1', InfoView.as_view(), name="info"),
]

连接模型与视图

让我们弄清楚下一个 URL,它将是创建新的 Task 或列出用户现有任务的入口。这应该存在于 todo 应用程序的 urls.py 中,因为它必须专门处理 Task对象而不是整个项目的一部分。

# in todo/urls.py
from django.urls import path
from todo.views import TaskListView

urlpatterns = [
    path('', TaskListView.as_view(), name="list_tasks")
]

这个路由处理的是什么?我们根本没有指定特定用户或路径。由于会有一些路由需要基本路径 /api/v1/accounts/<username>/tasks,为什么我们只需写一次就能一次又一次地写它?

Django 允许我们用一整套 URL 并将它们导入 django_todo/urls.py 文件。然后,我们可以为这些导入的 URL 中的每一个提供相同的基本路径,只关心可变部分,你知道它们是不同的。

# in django_todo/urls.py
from django.urls import include, path
from django_todo.views import InfoView

urlpatterns = [
    path('api/v1', InfoView.as_view(), name="info"),
    path('api/v1/accounts/<str:username>/tasks', include('todo.urls'))
]

现在,来自 todo/urls.py 的每个 URL 都将以路径 api/v1/accounts/<str:username>/tasks 为前缀。

让我们在 todo/views.py 中构建视图。

# todo/views.py
from django.shortcuts import get_object_or_404
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

from owner.models import Owner
from todo.models import Task
from todo.serializers import TaskSerializer


class TaskListView(APIView):
    def get(self, request, username, format=None):
        """Get all of the tasks for a given user."""
        owner = get_object_or_404(Owner, user__username=username)
        tasks = Task.objects.filter(owner=owner).all()
        serialized = TaskSerializer(tasks, many=True)
        return JsonResponse({
            'username': username,
            'tasks': serialized.data
        })

这里一点代码里面有许多要说明的,让我们来看看吧。

我们从与我们一直使用的 APIView 的继承开始,为我们的视图奠定基础。我们覆盖了之前覆盖的相同 get 方法,添加了一个参数,允许我们的视图从传入的请求中接收 username

然后我们的 get 方法将使用 username 来获取与该用户关联的 Owner。这个 get_object_or_404 函数允许我们这样做,添加一些特殊的东西以方便使用。

如果无法找到指定的用户,那么查找任务是没有意义的。实际上,我们想要返回 404 错误。get_object_or_404 根据我们传入的任何条件获取单个对象,并返回该对象或引发 Http 404 异常。我们可以根据对象的属性设置该条件。Owner 对象都通过 user 属性附加到 User。但是,我们没有要搜索的 User 对象,我们只有一个 username。所以,当你寻找一个 Owner 时,我们对 get_object_or_404 说:通过指定 user__username(这是两个下划线)来检查附加到它的 User 是否具有我想要的 username。通过 QuerySet 过滤时,这两个下划线表示 “此嵌套对象的属性”。这些属性可以根据需要进行深度嵌套。

我们现在拥有与给定用户名相对应的 Owner。我们使用 Owner 来过滤所有任务,只用 Task.objects.filter 检索它拥有的任务。我们可以使用与 get_object_or_404 相同的嵌套属性模式来钻入连接到 TasksOwnerUsertasks = Task.objects.filter(owner__user__username = username)).all()),但是没有必要那么宽松。

Task.objects.filter(owner = owner).all() 将为我们提供与我们的查询匹配的所有 Task 对象的QuerySet。很棒。然后,TaskSerializer 将获取 QuerySet 及其所有数据以及 many = True 标志,以通知其为项目集合而不是仅仅一个项目,并返回一系列序列化结果。实际上是一个词典列表。最后,我们使用 JSON 序列化数据和用于查询的用户名提供传出响应。

处理 POST 请求

post 方法看起来与我们之前看到的有些不同。

# still in todo/views.py
# ...other imports...
from rest_framework.parsers import JSONParser
from datetime import datetime

class TaskListView(APIView):
    def get(self, request, username, format=None):
        ...

    def post(self, request, username, format=None):
        """Create a new Task."""
        owner = get_object_or_404(Owner, user__username=username)
        data = JSONParser().parse(request)
        data['owner'] = owner.id
        if data['due_date']:
            data['due_date'] = datetime.strptime(data['due_date'], '%d/%m/%Y %H:%M:%S')

        new_task = TaskSerializer(data=data)
        if new_task.is_valid():
            new_task.save()
            return JsonResponse({'msg': 'posted'}, status=201)

        return JsonResponse(new_task.errors, status=400)

当我们从客户端接收数据时,我们使用 JSONParser().parse(request) 将其解析为字典。我们将所有者添加到数据中并格式化任务的 due_date(如果存在)。

我们的 TaskSerializer 完成了繁重的任务。它首先接收传入的数据并将其转换为我们在模型上指定的字段。然后验证该数据以确保它适合指定的字段。如果附加到新 Task 的数据有效,它将使用该数据构造一个新的 Task 对象并将其提交给数据库。然后我们发回适当的“耶!我们做了一件新东西!”响应。如果没有,我们收集 TaskSerializer 生成的错误,并将这些错误发送回客户端,并返回 400 Bad Request 状态代码。

如果我们要构建 put 视图来更新 Task,它看起来会非常相似。主要区别在于,当我们实例化 TaskSerializer 时,我们将传递旧对象和该对象的新数据,如 TaskSerializer(existing_task,data = data)。我们仍然会进行有效性检查并发回我们想要发回的响应。

总结

Django 作为一个框架是高度可定制的,每个人都有自己打造 Django 项目的方式。我在这里写出来的方式不一定是 Django 建立项目的确切方式。它只是 a) 我熟悉的方式,以及 b) 利用 Django 的管理系统。当你将概念切分到不同的小块时,Django 项目的复杂性会增加。这样做是为了让多个人更容易为整个项目做出贡献,而不会麻烦彼此。

然而,作为 Django 项目的大量文件映射并不能使其更高效或自然地偏向于微服务架构。相反,它很容易成为一个令人困惑的独石应用,这可能对你的项目仍然有用,它也可能使你的项目难以管理,尤其是随着项目的增长。

仔细考虑你的需求并使用合适的工具来完成正确的工作。对于像这样的简单项目,Django 可能不是合适的工具。

Django 旨在处理多种模型,这些模型涵盖了不同的项目领域,但它们可能有一些共同点。这个项目是一个小型的双模型项目,有一些路由。即便我们把它构建更复杂,也只有七条路由,而仍然只是相同的两个模型。这还不足以证明一个完整的 Django 项目。

如果我们期望这个项目能够拓展,那么将会是一个很好的选择。这不是那种项目。这就是使用火焰喷射器来点燃蜡烛,绝对是大材小用了。

尽管如此,Web 框架就是一个 Web 框架,无论你使用哪个框架。它都可以接收请求并做出任何响应,因此你可以按照自己的意愿进行操作。只需要注意你选择的框架所带来的开销。

就是这样!我们已经到了这个系列的最后!我希望这是一次启发性的冒险。当你在考虑如何构建你的下一个项目时,它将帮助你做出的不仅仅是最熟悉的选择。请务必阅读每个框架的文档,以扩展本系列中涉及的任何内容(因为它没有那么全面)。每个人都有一个广阔的世界。愉快地写代码吧!


via: https://opensource.com/article/18/8/django-framework

作者:Nicholas Hunt-Walker 选题:lujun9972 译者:MjSeven 校对:Bestony, wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文介绍如何构建一个基于 Grails 的数据浏览器来可视化复杂的表格数据。

我是 Grails 的忠实粉丝。当然,我主要是热衷于利用命令行工具来探索和分析数据的数据从业人员。数据从业人员经常需要查看数据,这也意味着他们通常拥有优秀的数据浏览器。利用 Grails、jQuery,以及 DataTables jQuery 插件,我们可以制作出非常友好的表格数据浏览器。

DataTables 网站提供了许多“食谱式”的教程文档,展示了如何组合一些优秀的示例应用程序,这些程序包含了完成一些非常漂亮的东西所必要的 JavaScript、HTML,以及偶尔出现的 PHP。但对于那些宁愿使用 Grails 作为后端的人来说,有必要进行一些说明示教。此外,样本程序中使用的数据是一个虚构公司的员工的单个平面表格数据,因此处理这些复杂的表关系可以作为读者的一个练习项目。

本文中,我们将创建具有略微复杂的数据结构和 DataTables 浏览器的 Grails 应用程序。我们将介绍 Grails 标准,它是 Groovy 式的 Java Hibernate 标准。我已将代码托管在 GitHub 上方便大家访问,因此本文主要是对代码细节的解读。

首先,你需要配置 Java、Groovy、Grails 的使用环境。对于 Grails,我倾向于使用终端窗口和 Vim,本文也使用它们。为获得现代的 Java 环境,建议下载并安装 Linux 发行版提供的 Open Java Development Kit (OpenJDK)(应该是 Java 8、9、10 或 11 之一,撰写本文时,我正在使用 Java 8)。从我的角度来看,获取最新的 Groovy 和 Grails 的最佳方法是使用 SDKMAN!

从未尝试过 Grails 的读者可能需要做一些背景资料阅读。作为初学者,推荐文章 创建你的第一个 Grails 应用程序

获取员工信息浏览器应用程序

正如上文所提,我将本文中员工信息浏览器的源代码托管在 GitHub上。进一步讲,应用程序 embrow 是在 Linux 终端中用如下命令构建的:

cd Projects
grails create-app com.nuevaconsulting.embrow

域类和单元测试创建如下:

grails create-domain-class com.nuevaconsulting.embrow.Position
grails create-domain-class com.nuevaconsulting.embrow.Office
grails create-domain-class com.nuevaconsulting.embrow.Employeecd embrowgrails createdomaincom.grails createdomaincom.grails createdomaincom.

这种方式构建的域类没有属性,因此必须按如下方式编辑它们:

Position 域类:

package com.nuevaconsulting.embrow
 
class Position {

    String name
    int starting

    static constraints = {
        name nullable: false, blank: false
        starting nullable: false
    }
}com.Stringint startingstatic constraintsnullableblankstarting nullable

Office 域类:

package com.nuevaconsulting.embrow
 
class Office {

    String name
    String address
    String city
    String country

    static constraints = {
        name nullable: false, blank: false
        address nullable: false, blank: false
        city nullable: false, blank: false
        country nullable: false, blank: false
    }
}

Enployee 域类:

package com.nuevaconsulting.embrow
 
class Employee {

    String surname
    String givenNames
    Position position
    Office office
    int extension
    Date hired
    int salary
    static constraints = {
        surname nullable: false, blank: false
        givenNames nullable: false, blank: false
        : false
        office nullable: false
        extension nullable: false
        hired nullable: false
        salary nullable: false
    }
}

请注意,虽然 PositionOffice 域类使用了预定义的 Groovy 类型 String 以及 int,但 Employee 域类定义了 PositionOffice 字段(以及预定义的 Date)。这会导致创建数据库表,其中存储的 Employee 实例中包含了指向存储 PositionOffice 实例表的引用或者外键。

现在你可以生成控制器,视图,以及其他各种测试组件:

-all com.nuevaconsulting.embrow.Position
grails generate-all com.nuevaconsulting.embrow.Office
grails generate-all com.nuevaconsulting.embrow.Employeegrails generateall com.grails generateall com.grails generateall com.

此时,你已经准备好了一个基本的增删改查(CRUD)应用程序。我在 grails-app/init/com/nuevaconsulting/BootStrap.groovy 中包含了一些基础数据来填充表格。

如果你用如下命令来启动应用程序:

grails run-app

在浏览器输入 http://localhost:8080/,你将会看到如下界面:

Embrow home screen

Embrow 应用程序主界面。

单击 “OfficeController” 链接,会跳转到如下界面:

Office list

Office 列表

注意,此表由 OfficeControllerindex 方式生成,并由视图 office/index.gsp 显示。

同样,单击 “EmployeeController” 链接 跳转到如下界面:

Employee controller

employee 控制器

好吧,这很丑陋: Position 和 Office 链接是什么?

上面的命令 generate-all 生成的视图创建了一个叫 index.gsp 的文件,它使用 Grails <f:table/> 标签,该标签默认会显示类名(com.nuevaconsulting.embrow.Position)和持久化示例标识符(30)。这个操作可以自定义用来产生更好看的东西,并且自动生成链接,自动生成分页以及自动生成可排序列的一些非常简洁直观的东西。

但该员工信息浏览器功能也是有限的。例如,如果想查找 “position” 信息中包含 “dev” 的员工该怎么办?如果要组合排序,以姓氏为主排序关键字,“office” 为辅助排序关键字,该怎么办?或者,你需要将已排序的数据导出到电子表格或 PDF 文档以便通过电子邮件发送给无法访问浏览器的人,该怎么办?

jQuery DataTables 插件提供了这些所需的功能。允许你创建一个完成的表格数据浏览器。

创建员工信息浏览器视图和控制器的方法

要基于 jQuery DataTables 创建员工信息浏览器,你必须先完成以下两个任务:

  1. 创建 Grails 视图,其中包含启用 DataTable 所需的 HTML 和 JavaScript
  2. 给 Grails 控制器增加一个方法来控制新视图。

员工信息浏览器视图

在目录 embrow/grails-app/views/employee 中,首先复制 index.gsp 文件,重命名为 browser.gsp

cd Projects
cd embrow/grails-app/views/employee
cp gsp browser.gsp

此刻,你自定义新的 browser.gsp 文件来添加相关的 jQuery DataTables 代码。

通常,在可能的时候,我喜欢从内容提供商处获得 JavaScript 和 CSS;在下面这行后面:

<title><g:message code="default.list.label" args="[entityName]" /></title>

插入如下代码:

<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/scroller/1.4.4/css/scroller.dataTables.min.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/scroller/1.4.4/js/dataTables.scroller.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.flash.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.html5.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.print.min.js "></script>

然后删除 index.gsp 中提供数据分页的代码:

<div id="list-employee" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<f:table collection="${employeeList}" />

<div class="pagination">
<g:paginate total="${employeeCount ?: 0}" />
</div>
</div>

并插入实现 jQuery DataTables 的代码。

要插入的第一部分是 HTML,它将创建浏览器的基本表格结构。DataTables 与后端通信的应用程序来说,它们只提供表格页眉和页脚;DataTables JavaScript 则负责表中内容。

<div id="employee-browser" class="content" role="main">
<h1>Employee Browser</h1>
<table id="employee_dt" class="display compact" style="width:99%;">
<thead>
<tr>
<th>Surname</th>
<th>Given name(s)</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Hired</th>
<th>Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Surname</th>
<th>Given name(s)</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Hired</th>
<th>Salary</th>
</tr>
</tfoot>
</table>
</div>

接下来,插入一个 JavaScript 块,它主要提供三个功能:它设置页脚中显示的文本框的大小,以进行列过滤,建立 DataTables 表模型,并创建一个处理程序来进行列过滤。

<g:javascript>
$('#employee_dt tfoot th').each( function() {javascript

下面的代码处理表格列底部的过滤器框的大小:

var title = $(this).text();
if (title == 'Extension' || title == 'Hired')
$(this).html('<input type="text" size="5" placeholder="' + title + '?" />');
else
$(this).html('<input type="text" size="15" placeholder="' + title + '?" />');
});titletitletitletitletitle

接下来,定义表模型。这是提供所有表选项的地方,包括界面的滚动,而不是分页,根据 DOM 字符串提供的装饰,将数据导出为 CSV 和其他格式的能力,以及建立与服务器的 AJAX 连接。 请注意,使用 Groovy GString 调用 Grails createLink() 的方法创建 URL,在 EmployeeController 中指向 browserLister 操作。同样有趣的是表格列的定义。此信息将发送到后端,后端查询数据库并返回相应的记录。

var table = $('#employee_dt').DataTable( {
"scrollY": 500,
"deferRender": true,
"scroller": true,
"dom": "Brtip",
"buttons": [ 'copy', 'csv', 'excel', 'pdf', 'print' ],
"processing": true,
"serverSide": true,
"ajax": {
"url": "${createLink(controller: 'employee', action: 'browserLister')}",
"type": "POST",
},
"columns": [
{ "data": "surname" },
{ "data": "givenNames" },
{ "data": "position" },
{ "data": "office" },
{ "data": "extension" },
{ "data": "hired" },
{ "data": "salary" }
]
});

最后,监视过滤器列以进行更改,并使用它们来应用过滤器。

table.columns().every(function() {
var that = this;
$('input', this.footer()).on('keyup change', function(e) {
if (that.search() != this.value && 8 < e.keyCode && e.keyCode < 32)
that.search(this.value).draw();
});

这就是 JavaScript,这样就完成了对视图代码的更改。

});
</g:javascript>

以下是此视图创建的UI的屏幕截图:

这是另一个屏幕截图,显示了过滤和多列排序(寻找 “position” 包括字符 “dev” 的员工,先按 “office” 排序,然后按姓氏排序):

这是另一个屏幕截图,显示单击 CSV 按钮时会发生什么:

最后,这是一个截图,显示在 LibreOffice 中打开的 CSV 数据:

好的,视图部分看起来非常简单;因此,控制器必须做所有繁重的工作,对吧? 让我们来看看……

控制器 browserLister 操作

回想一下,我们看到过这个字符串:

"${createLink(controller: 'employee', action: 'browserLister')}"

对于从 DataTables 模型中调用 AJAX 的 URL,是在 Grails 服务器上动态创建 HTML 链接,其 Grails 标记背后通过调用 createLink() 的方法实现的。这会最终产生一个指向 EmployeeController 的链接,位于:

embrow/grails-app/controllers/com/nuevaconsulting/embrow/EmployeeController.groovy

特别是控制器方法 browserLister()。我在代码中留了一些 print 语句,以便在运行时能够在终端看到中间结果。

    def browserLister() {
        // Applies filters and sorting to return a list of desired employees

首先,打印出传递给 browserLister() 的参数。我通常使用此代码开始构建控制器方法,以便我完全清楚我的控制器正在接收什么。

      println "employee browserLister params $params"
        println()

接下来,处理这些参数以使它们更加有用。首先,jQuery DataTables 参数,一个名为 jqdtParams 的 Groovy 映射:

def jqdtParams = [:]
params.each { key, value ->
    def keyFields = key.replace(']','').split(/\[/)
    def table = jqdtParams
    for (int f = 0; f < keyFields.size() - 1; f++) {
        def keyField = keyFields[f]
        if (!table.containsKey(keyField))
            table[keyField] = [:]
        table = table[keyField]
    }
    table[keyFields[-1]] = value
}
println "employee dataTableParams $jqdtParams"
println()

接下来,列数据,一个名为 columnMap 的 Groovy 映射:

def columnMap = jqdtParams.columns.collectEntries { k, v ->
    def whereTerm = null
    switch (v.data) {
    case 'extension':
    case 'hired':
    case 'salary':
        if (v.search.value ==~ /\d+(,\d+)*/)
            whereTerm = v.search.value.split(',').collect { it as Integer }
        break
    default:
        if (v.search.value ==~ /[A-Za-z0-9 ]+/)
            whereTerm = "%${v.search.value}%" as String
        break
    }
    [(v.data): [where: whereTerm]]
}
println "employee columnMap $columnMap"
println()

接下来,从 columnMap 中检索的所有列表,以及在视图中应如何排序这些列表,Groovy 列表分别称为 allColumnListorderList

def allColumnList = columnMap.keySet() as List
println "employee allColumnList $allColumnList"
def orderList = jqdtParams.order.collect { k, v -> [allColumnList[v.column as Integer], v.dir] }
println "employee orderList $orderList"

我们将使用 Grails 的 Hibernate 标准实现来实际选择要显示的元素以及它们的排序和分页。标准要求过滤器关闭;在大多数示例中,这是作为标准实例本身的创建的一部分给出的,但是在这里我们预先定义过滤器闭包。请注意,在这种情况下,“date hired” 过滤器的相对复杂的解释被视为一年并应用于建立日期范围,并使用 createAlias 以允许我们进入相关类别 PositionOffice

def filterer = {
    createAlias 'position',        'p'
    createAlias 'office',          'o'

    if (columnMap.surname.where)    ilike  'surname',     columnMap.surname.where
    if (columnMap.givenNames.where) ilike  'givenNames',  columnMap.givenNames.where
    if (columnMap.position.where)   ilike  'p.name',      columnMap.position.where
    if (columnMap.office.where)     ilike  'o.name',      columnMap.office.where
    if (columnMap.extension.where)  inList 'extension',   columnMap.extension.where
    if (columnMap.salary.where)     inList 'salary',      columnMap.salary.where
    if (columnMap.hired.where) {
        if (columnMap.hired.where.size() > 1) {
            or {
                columnMap.hired.where.each {
                    between 'hired', Date.parse('yyyy/MM/dd',"${it}/01/01" as String),
                        Date.parse('yyyy/MM/dd',"${it}/12/31" as String)
                }
            }
        } else {
            between 'hired', Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/01/01" as String),
                Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/12/31" as String)
        }
    }
}

是时候应用上述内容了。第一步是获取分页代码所需的所有 Employee 实例的总数:

        def recordsTotal = Employee.count()
        println "employee recordsTotal $recordsTotal"

接下来,将过滤器应用于 Employee 实例以获取过滤结果的计数,该结果将始终小于或等于总数(同样,这是针对分页代码):

        def c = Employee.createCriteria()
        def recordsFiltered = c.count {
            filterer.delegate = delegate
            filterer()
        }
        println "employee recordsFiltered $recordsFiltered"

获得这两个计数后,你还可以使用分页和排序信息获取实际过滤的实例。

      def orderer = Employee.withCriteria {
            filterer.delegate = delegate
            filterer()
            orderList.each { oi ->
                switch (oi[0]) {
                case 'surname':    order 'surname',    oi[1]; break
                case 'givenNames': order 'givenNames', oi[1]; break
                case 'position':   order 'p.name',     oi[1]; break
                case 'office':     order 'o.name',     oi[1]; break
                case 'extension':  order 'extension',  oi[1]; break
                case 'hired':      order 'hired',      oi[1]; break
                case 'salary':     order 'salary',     oi[1]; break
                }
            }
            maxResults (jqdtParams.length as Integer)
            firstResult (jqdtParams.start as Integer)
        }

要完全清楚,JTable 中的分页代码管理三个计数:数据集中的记录总数,应用过滤器后得到的数字,以及要在页面上显示的数字(显示是滚动还是分页)。 排序应用于所有过滤的记录,并且分页应用于那些过滤的记录的块以用于显示目的。

接下来,处理命令返回的结果,在每行中创建指向 EmployeePositionOffice 实例的链接,以便用户可以单击这些链接以获取相关实例的所有详细信息:

        def dollarFormatter = new DecimalFormat('$##,###.##')
        def employees = orderer.collect { employee ->
            ['surname': "<a href='${createLink(controller: 'employee', action: 'show', id: employee.id)}'>${employee.surname}</a>",
                'givenNames': employee.givenNames,
                'position': "<a href='${createLink(controller: 'position', action: 'show', id: employee.position?.id)}'>${employee.position?.name}</a>",
                'office': "<a href='${createLink(controller: 'office', action: 'show', id: employee.office?.id)}'>${employee.office?.name}</a>",
                'extension': employee.extension,
                'hired': employee.hired.format('yyyy/MM/dd'),
                'salary': dollarFormatter.format(employee.salary)]
        }

最后,创建要返回的结果并将其作为 JSON 返回,这是 jQuery DataTables 所需要的。

        def result = [draw: jqdtParams.draw, recordsTotal: recordsTotal, recordsFiltered: recordsFiltered, data: employees]
        render(result as JSON)
    }

大功告成。

如果你熟悉 Grails,这可能看起来比你原先想象的要多,但这里没有火箭式的一步到位方法,只是很多分散的操作步骤。但是,如果你没有太多接触 Grails(或 Groovy),那么需要了解很多新东西 - 闭包,代理和构建器等等。

在那种情况下,从哪里开始? 最好的地方是了解 Groovy 本身,尤其是 Groovy closuresGroovy delegates and builders。然后再去阅读上面关于 Grails 和 Hibernate 条件查询的建议阅读文章。

结语

jQuery DataTables 为 Grails 制作了很棒的表格数据浏览器。对视图进行编码并不是太棘手,但 DataTables 文档中提供的 PHP 示例提供的功能仅到此位置。特别是,它们不是用 Grails 程序员编写的,也不包含探索使用引用其他类(实质上是查找表)的元素的更精细的细节。

我使用这种方法制作了几个数据浏览器,允许用户选择要查看和累积记录计数的列,或者只是浏览数据。即使在相对适度的 VPS 上的百万行表中,性能也很好。

一个警告:我偶然发现了 Grails 中暴露的各种 Hibernate 标准机制的一些问题(请参阅我的其他 GitHub 代码库),因此需要谨慎和实验。如果所有其他方法都失败了,另一种方法是动态构建 SQL 字符串并执行它们。在撰写本文时,我更喜欢使用 Grails 标准,除非我遇到杂乱的子查询,但这可能只反映了我在 Hibernate 中对子查询的相对缺乏经验。

我希望 Grails 程序员发现本文的有趣性。请随时在下面留下评论或建议。


via: https://opensource.com/article/18/9/using-grails-jquery-and-datatables

作者:Chris Hermansen 选题:lujun9972 译者:jrg 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Python 测试工具最好的一方面是其强大的生态系统。这里列出了八个最好的插件。

我们是 pytest 的忠实粉丝,并将其作为工作和开源项目的默认 Python 测试工具。在本月的 Python 专栏中,我们分享了为什么我们喜欢 pytest 以及一些让 pytest 测试工作更有趣的插件。

什么是 pytest?

正如该工具的网站所说,“pytest 框架可以轻松地编写小型测试,也能进行扩展以支持应用和库的复杂功能测试。”

pytest 允许你在任何名为 test_*.py 的文件中定义测试,并将其定义为以 test_* 开头的函数。然后,pytest 将在整个项目中查找所有测试,并在控制台中运行 pytest 时自动运行这些测试。pytest 接受标志和参数,它们可以在测试运行器停止时更改,这些包含如何输出结果,运行哪些测试以及输出中包含哪些信息。它还包括一个 set_trace() 函数,它可以进入到你的测试中。它会暂停您的测试, 并允许你与变量进行交互,不然你只能在终端中“四处翻弄”来调试你的项目。

pytest 最好的一方面是其强大的插件生态系统。因为 pytest 是一个非常流行的测试库,所以多年来创建了许多插件来扩展、定制和增强其功能。这八个插件是我们的最爱。

8 个很棒的插件

1、pytest-sugar

pytest-sugar 改变了 pytest 的默认外观,添加了一个进度条,并立即显示失败的测试。它不需要配置,只需 pip install pytest-sugar,用 pytest 运行测试,来享受更漂亮、更有用的输出。

2、pytest-cov

pytest-covpytest 中增加了覆盖率支持,来显示哪些代码行已经测试过,哪些还没有。它还将包括项目的测试覆盖率。

3、pytest-picked

pytest-picked 对你已经修改但尚未提交 git 的代码运行测试。安装库并运行 pytest --picked 来仅测试自上次提交后已更改的文件。

4、pytest-instafail

pytest-instafail 修改 pytest 的默认行为来立即显示失败和错误,而不是等到 pytest 完成所有测试。

5、pytest-tldr

一个全新的 pytest 插件,可以将输出限制为你需要的东西。pytest-tldrtldr 代表 “too long, didn’t read” —— 太长,不想读),就像 pytest-sugar 一样,除基本安装外不需要配置。不像 pytest 的默认输出那么详细,pytest-tldr 将默认输出限制为失败测试的回溯信息,并忽略了一些令人讨厌的颜色编码。添加 -v 标志会为喜欢它的人返回更详细的输出。

6、pytest-xdist

pytest-xdist 允许你通过 -n 标志并行运行多个测试:例如,pytest -n 2 将在两个 CPU 上运行你的测试。这可以显著加快你的测试速度。它还包括 --looponfail 标志,它将自动重新运行你的失败测试。

7、pytest-django

pytest-django 为 Django 应用和项目添加了 pytest 支持。具体来说,pytest-django 引入了使用 pytest fixture 测试 Django 项目的能力,而省略了导入 unittest 和复制/粘贴其他样板测试代码的需要,并且比标准的 Django 测试套件运行得更快。

8、django-test-plus

django-test-plus 并不是专门为 pytest 开发,但它现在支持 pytest。它包含自己的 TestCase 类,你的测试可以继承该类,并使你能够使用较少的按键来输出频繁的测试案例,例如检查特定的 HTTP 错误代码。

我们上面提到的库绝不是你扩展 pytest 的唯一选择。有用的 pytest 插件的前景是广阔的。查看 pytest 插件兼容性页面来自行探索。你最喜欢哪些插件?


via: https://opensource.com/article/18/6/pytest-plugins

作者:Jeff Triplett, Lacery Williams Henschel 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出