Django 开发连载 - 开篇


文本将讲述从头开发一个 Django 项目所遇到的问题和解决方案,以及记录一些自己的思考。自己也是在摸索阶段,难免有错误,但我会及时修正。所以文章将持续更新,直到最终标上完结。

-2018.08.04


  1. Python 容器环境, Virtualenv

很早就看到这个概念了,在 pycharm 需要制定 python interpreter 的时候,有 virtualenv, glob

pip : Python Installs Packages, 管理以 Python 写的包,负责下载并安装这些包,重要的是解决万恶的包依赖关系。

pip 的原理,引用了 PyPI 的库,这个库将所有 Python 包作者开发的包都放在了同一个地址,并给这些包打上 PyPI 的索引标记。一旦在 pip 客户端搜索的时候,就要发出命令到存放这些包的地址中搜索类似的 Python 包。

pip 也会有自己的缓存,所以包的更新需要清缓存,之后再重新和包仓库中的最新包做对比,安装新包。

PipEnv :
Pipenv is a dependency manager for Python projects. If you’re familiar with Node.js’ npm or Ruby’s bundler, it is similar in spirit to those tools. While pip can install Python packages, Pipenv is recommended as it’s a higher-level tool that simplifies dependency management for common use cases.
Use pip to install Pipenv:
$ pip install –user pipenv

为什么要使用 PipEnv ?

Pipenv manages dependencies on a per-project basis. To install packages, change into your project’s directory (or just an empty directory for this tutorial) and run:

$ cd myproject

$ pipenv install requests

Pipenv will install the excellent Requests library and create a Pipfile for you in your project’s directory. The Pipfile is used to track which dependencies your project needs in case you need to re-install them, such as when you share your project with others.

1 PipEnv 可以为每一个项目单独管理其包的应用

2 PipEnv 会创建一个 Pipfile, 这个 PipFile 包含了项目所依赖的包信息。当与其他人合作开发的时候,这个文件可以告知同伴们此项目的依赖

3 这里的问题在于,如果用 pipenv 安装了每个项目各自的包,那么用 pycharm 如何识别包配置?

Using installed packages

Now that Requests is installed you can create a simple main.py file to use it:

import requests

response = requests.get('https://httpbin.org/ip')

print('Your IP is {0}'.format(response.json()['origin']))

Then you can run this script using pipenv run:
$ pipenv run python main.py

You should get output similar to this:
Your IP is 8.8.8.8

但是在安装 pipenv 的时候,出现了错误:

InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. You can upgrade to a newer version of Python to solve this.

尝试用 Python3 来解决这个问题

但是 Python 3.6 默认的 pip 版本 是 10 , 而通过 Python install pipenv 的时候,需要 pip 18. 重新安装 pip 18


PipEnv 安装
-2018.08.05


今早很神奇的一次性安装 pipenv 成功。
看来只要多试个几次,安装可以顺利完成。

按照昨天的笔记,只要在某个 Python 项目文件下面运行 pipenv 安装想要的包,就可以单独为那个项目配置包运行:

$cd Demo

$pipenv install request

Successfully built request get post query-string public

Installing collected packages: public, query-string, get, post, request
Successfully installed get-1.0.3 post-1.0.2 public-1.0.3 query-string-1.0.2 request-1.0.2

Adding request to Pipfile’s [packages]…
Pipfile.lock not found, creating…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (c4925f)!
Installing dependencies from Pipfile.lock (c4925f)…
================================ 5/5 - 00:00:08
To activate this project’s virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.

PipEnv 的安装话题到此就结束了,但他的应用以及原理才刚刚开始。

可以看出来,PipEnv 本身是一个管理程序,用来为每个项目配置包文件

1.既然是管理程序,那么应该可以有命令可以看到 PipEnv 当前管理了那些项目以及项目的包依赖

2.它与 virtualenv 有着深刻的渊源,它是来取代 virtualenv

https://robots.thoughtbot.com/how-to-manage-your-python-projects-with-pipenv

这篇文章写得好,以防文章链接失效,全文摘录下来,放到 Python 目录下面

  1. Virtualenv:

    假设有这么一个应用场景:我们针对应用开发了第一个版本,用了第三方库的第一个版本。第三方库经过一段时间后,又开发了一个新版本,而这个新版本与老版本不兼容。如果我们修改程序使得其调用了第三方库的 2.0 版本,那么我们所有的应用程序都要升级,但有部分老客户是不想升级的,硬性升级将破坏老客户正在使用的系统。

    那么我们只能在新版本的程序中调用第三方 2.0 版本的库,而在老版本中依旧使用第三方 1.0 版本的库。

    这样在设定这份第三方库的声明的时候,就不能用全局声明了,而只能在某一个版本中声明。一般的开发中,对于新旧版本的替换,要么你重命名,要么直接用新版本替换。但是在 Python 中用了聪明的方法,就是 virtualenv, 即一个 virtualenv 就是当前软件版本的所有安装包的汇总。这一套 virtualenv 管理下的包都跟着当前版本的应用程序一起部署。

    知道了 Virtualenv 的好处,我们就要开始使用 virtualenv.

    假设我们需要创建一个 Spider 的项目:

    3.1 创建一个文件夹 Spider 用来容纳所有我们的项目文件;

    3.2 运行 virtualenv spider, 来建立一个虚拟环境

    3.3 在新的虚拟环境中,virtualenv 已经帮我们配置好了 Lib, Include, Scripts 等文件夹,用来盛放各种源文件,比如引用库源文件,使用的 Python 解释器和 Pip 等

    3.4 建立起新的虚拟环境之后,我们需要激活虚拟环境。不同的平台激活手段大同小异,就是运行 activate.bat/activate.sh 等,目的就是将虚拟环境下的 Python 解释器等放在运行程序账户下的 PATH 变量中,以便下次调用的时候,不会和其他混乱。这并不改变全局的 PATH 设定。


正式用 Django 来开发一个网站
-2018.08.05


参考这篇文章 :

https://docs.djangoproject.com/en/2.0/intro/tutorial01/

文章讲述了如何开发一个简单的投票网站,该网站实现了两个功能:

1 用户可以投票,并且浏览票数

2 管理员可以发布新的投票

首先,需要安装 Django , 用 Python -m Django –version 可以查看当前安装的 Django 版本。


安装 Django


两种方案:

  • 通过 Pip 安装系统级别的 Django. 作用是所有的 Python 环境都可以使用
  • 通过 Virtualenv, pipEnv 来安装,作用是每一份 Python 环境都有自己独有的 Django 开发环境

选择 virtualenv 来开始第一个项目

关于 virtualenv 如何构建一个项目的虚拟环境,可以参考这篇文章:
https://virtualenv.pypa.io/en/stable/userguide/

大致步骤就是:

创建项目虚拟环境;

$virtualenv DjangoPoll

建立一个专属的项目文件,并为其配置 Python 运行虚拟环境

激活虚拟环境的使用:

$ cd DjangoPoll\Scripts
$ activate.bat

这时候运行命令, path 可以看到最靠前的 python 环境已经指向 DjangoPoll\Scripts 下的 Python 了。

此时我们打开另一个 command line console,输入 path, 其实 python 的全局指向没有变。

所以接下来的开发全部是在这一个 python 虚拟环境中开发,当然可以同时运行很多个已经开发完毕的 Django, 因为每一个都基于不同的 virtualenv 来运行

Pip 安装 Django:

在刚才运行了 activate.bat 的窗口中,我们继续使用 pip 来装 Django:

pip install Django

建立一个 Django 项目:

django-admin.py startproject DjangoPoll

到目前为止,我们一直在激活的 virtualenv 中执行命令,因此 Django 模块也是安装在了 $virtualenv\scripts 下面,仔细看会找到 Django-admin.exe 和 Django-admin.py.同目录下还有 Python3.6 ,因此 Django-admin.py 使用的是同目录下的 Python3.6 来执行的。

执行完毕之后,同目录下多出一个 DjangoPoll 的文件夹。

在这个文件夹下面有一个 manage.py 文件,该文件可以用来运行开发环境的 web server, 而无需部署一个类似 apache 这么正式的 web server.

运行:

manage.py runserver

通过访问 127.0.0.1:8000 就可以看到 Django 为开发背景的网站了

可能在正常访问服务器之前,还需要执行 :

manage.py migrate

这是为了更好的服务于运行 Django 网站,同步保存一些数据模型和服务器配置

建立一个 Django 应用

配置

刚才我们建立了一个 Django 项目,

django-admin.py startproject DjangoPoll

在这项目下,我们接着建立一个应用

manage.py startapp blogsearch

仔细体会,项目和应用还有些不一样。项目可以包含丰富多彩的应用,分别用来完成不同的功能。这些功能就是所要建立的应用。

在项目文件夹顶层 DjangoPoll 新建了一个应用专属文件夹 blogsearch.

为了使得 Django 项目可以识别新建的 blogsearch, 需要在 DjangoPoll 下面的 settings.py 中添加一个 blogsearch 的条目:

INSTALLED_APPS={

    ...,
    'blogsearch',

}

这里让我费解的是,为什么manage.py startapp blogsearch 不直接把新建的应用直接添加到 settings.py 里面去呢。

似乎条件也够啊:

1 在项目文件夹下面用本项目的 manage.py 进行新建的

2 blogsearch 新建的应用就在项目文件夹的顶层目录中

这里值得拿来说道的是应用的模型。在新建的应用 blogsearch 下面,我们会看到 models.py 文件。该文件中可以自定义我们需要的数据结构,这些数据结构对应的便是数据库中数据表中部分字段甚至全部字段的映射。web 程序员肯定不陌生 ORM 的概念,这里便是。

举个简单的例子:

# models.py
from django.db import models 

class blogpost(models.Model):
    title = models.CharField(max_length=150)
    body = models.TextField()
    timestamp = models.DateTimeField()

完整的模型映射可以参考官方文档:

http://docs.djangoproject.com/en/dev/ref/models/fields/#field-types

==一个疑问:==

针对不同的数据库,比如 MySQL, Oracle, SQL Server 等,数据类型肯定不会是一样的,那么 models.charField 会对应到这些数据库的哪些数据类型上去呢?

或者说,我定义的上述模型映射,怎么才能适配所有的数据库供应商,而不用重新定义数据结构映射呢?

如果不用 SQLite , 而改用 MySQL, Oracle 等,可以修改 DjangoPoll 项目文件夹下的 Settings.py 中有关 DATABASE 那一部分。

** 批评下: 人民邮电出版社似乎很放任错别字的作者,在 Django 这章节已经出现很多次写错模块名字和类名字的情况了。

数据库

一切的应用都离不开存储数据,也就离不开数据库。Django 支持很多种数据库,最简单的非 SQLite 莫属。

刚在 models.py 中建立的数据结构,通过同步功能 syncdb 可以部署到 SQLite 里面。

这里大家肯定会想到的问题是:

  • 新建的模型每次都要这么同步,那么数据会不会被清除;
  • 可不可以通过数据库的客户端或者 ERwin 等建模工具,建立数据结构;

这两个问题先留着,之后再来解答!

现在要做的就是执行命令 syncdb:

manage.py syncdb

** 这里要说明的问题是, syncdb 已经被 migrate 给取代了。 migrate 的步骤大致是:

manage.py makemigrations 
manage.py migrate

第一步先生成需要修改的模型脚本,第二步就是部署到数据库

在这中间,我们可以随时查看模型生成的增量更新脚本:

manage.py showmigrations

参考文档:
https://docs.djangoproject.com/en/2.1/topics/migrations/

Admin

在应用中,数据库占据着很大一部分工作。但仅仅有数据库及其模型还远远不够可以支撑一个应用。在正式的使用 MVC 模式开发之前,使用 Admin 来快速建立交互接口程序,是非常有利于模型与视图并行开发的。

设置启用 Admin 管理单元

在 settings.py 中,添加对 Admin 管理单元启用的声明:

INSTALLED_APPS=(

    'django.contrib.admin',
    'blogsearch',

)

每一次对项目的修改,都需要将改动保存到数据库中。

保存的流程可以查看上面关于 migrate 的用法 :

manage.py makemigrations
manage.py migrate

==留下一个问题==:

如果我没有将一些 Django 项目中的更改提交到数据库里面,会发生什么情况?

Admin 管理单元已经启用了,但是在 web 页面上并没有他的入口地址,因此需要配置一个 :

修改 urls.py :

from django.contrib import admin
admin.autodiscover()

(r'^admin/',include(admin.site.urls)),

通过访问 127.0.0.1:8000/admin 就可以访问 admin 单元了。

但是此时的 admin 单元并没有绑定任何的数据模型,因此没有互动可以执行。为了测试我们建立的 blogpost 表的交互性,我们需要在 admin 上绑定 blogpost.

在 blogsearch 文件目录下,查找是否有 admin.py 文件,如果没有,则新建一个 :

#admin.py 

from django.contrib import admin 
from blogsearch import models

admin.site.register(models.blogpost)

==留下一个问题==:

这些地方为什么就不需要 migrate 到数据库了呢,
仅仅是因为这些修改是应用级别而非项目级别

值得说明的是,我用了 Python 3.6 和 Django 2.1 发现 settings.py, urls.py, admin.py 都已经帮我配置好了,仅仅剩下指定需要 admin 绑定的类即可。

我们使用了 Migrate 来替代 Syncdb, 因此重要的一步 - 创建超级用户并没有完成。所以登录不了 admin 界面。

在 Django 2.1 中, 创建超级用户是这样的:

manage.py createsuperuser

接下来按照提示输入邮箱和密码即可:

admin/[email protected]/l**w*****6.

当然,不要忘记重启服务

manage.py runserver

总结:如果安装的是 Django 2.1 ,那么配置 Admin 界面的时候,只需要做两件事情:

  • 创建超级用户
  • 为 Admin 指定需要绑定的模型类,即修改 admin.py 注册数据模型类
美化 Admin 列表的输出

更改应用下面 Admin.py 可以美化 Admin 列表的输出

#admin.py 

from django.contrib import admin

# Register your models here.

from blogsearch import models

class blogsearchAdmin(admin.ModelAdmin):
    list_display = ('title','timestamp')

admin.site.register(models.blogpost, blogsearchAdmin)

创建应用的用户界面

在真正使用应用的时候,往往不是用 Admin 界面或者 Python Shell 做交互,而是创建一些真正可用的网页,提交用户申请。

时下流行的构建网页的方法是 MVC - Model, View , Controller

Django 与 .net MVC 差不多,是由模板,视图,URL 模式架构而成。

模板: 用于显示通过模型(Python 类字典对象)传入的信息;

试图函数:从数据库中获得信息,并格式化结果,执行请求的核心逻辑便是封装在此;

URL 模式:将请求映射到某一个视图函数中去。

在建一个交互网页的过程中,往往是从底层往上执行,即:

  1. 解析 URL 模式
  2. 执行视图函数
  3. 视图函数调用模板与数据库通信,拉回数据展现

模板样例:

模板必须保存在应用(应用必须与项目严格区分开来:项目是一个顶级的范畴,它可以包含多个应用,每个应用负责一块业务逻辑)的 templates 文件夹下面:

所以在我们新建的 blogsearch 应用下面,必须新建一个 templates 文件夹

模板的目的是为了显示 Python 字典对象传进来的信息,所以在真实反馈给用户的时候,除了用于可视化的 HTML元素外,还需要将数据对象(在这个案例中,我们用的是 blogpost 对象,该对象已经在 blogsearch 应用下面的 Models.py 重定义完毕)映射到 Html 文件中来,拆解成各种 html 空间可以绑定的对象或值。

难题是,Html 本身是静态的文件,如何才能动态的展现数据(当有多条记录被视图返回时),并且保证格式美观?

在这里,Django 引入了一个重要的概念,就是模板语言

<p>{{ post.title }}</p>

...

{% for post in posts %}
    <p>{{ post.title }} </p>
    <p>{{ post.body }} </p>
    <p>{{ post.timestamp }} </p>
{% endfor %}

上面的代码就是最基本的 Django 模板语言,关键问题在于如何将 post/posts 传进这段代码里来,这是视图函数的工作。

URL 模式

整个项目占据了一级 URL, 各个应用则分别会占据顶级 URL 下的二级 URL。

项目占用的是 127.0.0.1, 那么我们新建的 blogsearch, 就可以占据 127.0.0.1/blogsearch. 这是最理想的 URL 解析规则。当然硬要将 127.0.0.1/search 分配给 blogsearch 应用也是可以的。只要在 URL 模式中设置好。

URL 模式存在于两个地方,项目文件夹下面,和各自应用的文件夹下面。文件名叫做 urls.py .

URL 模式通过正则表达式作为匹配规则,一旦请求的 URL 符合了某条规则,则要么执行相应的视图函数,要么转到相应的应用下 URL 解析规则去解析。所以大部分人都已经猜出来了,项目文件的 URL 模式都会转移到各个应用的 URL 模式解析规则去做进一步的解析,而应用的 URL 模式规则解析,则是执行视图解析函数,来显示结果。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blogsearch/',include('blogsearch.urls'))
]

任何匹配 blogsearch 的 URL 请求,都会将 127.0.0./blogsearch/xxx/yyy 截断成 xxx/yyy 传给 blogsearch.urls 处理。前提是必须使用 include.
否则容易爆这么个错误:

TypeError: view must be a callable or a list/tuple in the case of include().

# urls.py


from django.urls import path, include
import blogsearch.views

urlpatterns = [        path(r'',blogsearch.views.archive),

               ]

以上是 应用的 Urls.py 处理程序。当请求访问 127.0.0.1/blogsearch 的时候,直接调用 blogsearch 应用下面的 archive 视图函数。

视图函数:

# views.py

from django.shortcuts import render_to_response
from blogsearch.models import blogpost

def archive(request):
    posts = blogpost.objects.all()
    return render_to_response('archive.html',{'posts':posts})

最传神的一笔,使用 posts 获取所有 Blog 之后,传给 archive.html 中 posts 变量!

==接下来要思考的问题==:

  • 如何让视图函数接收参数:如果访问带额外的节点,比如 127.0.0.1/blogsearch/django, archive 函数如何接收 django 这个多余的文本,会做出什么样的反应?假如函数 archive 需要 2 个及以上的参数,URL 应该做出什么调整?

回答第一个小问:请求127.0.0.1/blogsearch/django, archive 函数如何接收 django 这个多余的文本。

blogsearch 本身已经被 URL 模式解析为调用 archive 视图函数了。那么后面再跟一个文本节点,要么是当做参数,要么是另一个视图函数的开始。

如果是 archive 的视图参数,作为 http post/get 方法,肯定被封装在了 request 这个对象中,可以通过访问 request 对象属性来传值。

# views.py

from django.shortcuts import render_to_response
from blogsearch.models import blogpost

def archive(request):
    posts = blogpost.objects.all()
    if request.method == "POST":
        keyword = request.POST.get("keyword")

    return render_to_response('archive.html',{'posts':posts})

事实上是,通过 http post/get 方法传递参数,是不可能以 blogsearch/django 这样的方式显式传递的,而是由表单封装之后传递的:

交互表单:

<!--  archive.html -->

<form action="/blogsearch/create" method = "post">

    Title:<input type = text name = title><br>

    Body:<textarea type = text name = body rows = 3 cols = 60></textarea><br>

    <input type = submit>

</form>


{% for post in posts %}
    <p> {{ post.title }} </p>
    <p> {{post.body  }}</p>
    <p> {{post.timestamp  }}</p>
{% endfor %}

为表单的提交绑定一个视图函数,代码中我们指定了新建一个 blog 的提交节点是 /create, 因此在 urls.py 中配置这节点:

# urls.py


from django.urls import path, include
import blogsearch.views

urlpatterns = [        path(r'',blogsearch.views.archive),
                       path(r'Django', blogsearch.views.archive),
                       path(r'create',blogsearch.views.create)
               ]

定义视图函数来处理表单请求

# views.py

from django.shortcuts import render_to_response
from blogsearch.models import blogpost

def archive(request):
    posts = blogpost.objects.all()
    return render_to_response('archive.html',{'posts':posts})

def create(request):
    if request.method =="POST":
        blogpost(title=request.POST.get("title"),
                 body=request.POST.get("body"),
                 timestamp = datetime.now()
                ).save()
    return HttpResponseRedirect('/blogsearch')

在实际的运行过程中,遇到如下错误:

Forbidden (403)
CSRF verification failed. Request aborted.

Help
Reason given for failure:

CSRF token missing or incorrect.

In general, this can occur when there is a genuine Cross Site Request Forgery, or when Django’s CSRF mechanism has not been used correctly. For POST forms, you need to ensure:

Your browser is accepting cookies.
The view function passes a request to the template’s render method.
In the template, there is a {% csrf_token %} template tag inside each POST form that targets an internal URL.
If you are not using CsrfViewMiddleware, then you must use csrf_protect on any views that use the csrf_token template tag, as well as those that accept the POST data.
The form has a valid CSRF token. After logging in in another browser tab or hitting the back button after a login, you may need to reload the page with the form, because the token is rotated after a login.
You’re seeing the help section of this page because you have DEBUG = True in your Django settings file. Change that to False, and only the initial error message will be displayed.

You can customize this page using the CSRF_FAILURE_VIEW setting.

具体如何解决 CSRF 的问题,可以参考下面两个文章:

http://docs.djangoproject.com/en/dev/intro/tutorial04/#write-a-simple-form

http://docs.djangoproject.com/en/dev/ref/contrib/csrf

简单来说,就是在 archive.html 文件中添加 csrf token:

<!--  archive.html -->

<form action="/blogsearch/create" method = "post">
{% csrf_token %}
    Title:<input type = text name = title><br>

    Body:<textarea type = text name = body rows = 3 cols = 60></textarea><br>

    <input type = submit>

</form>


{% for post in posts %}
    <p> {{ post.title }} </p>
    <p> {{post.body  }}</p>
    <p> {{post.timestamp  }}</p>
{% endfor %}

并且需要修改 view.py 增加 RequestContext 的参数:

# views.py

from django.shortcuts import render_to_response
from blogsearch.models import blogpost
from django.template import RequestContext


def archive(request):
    posts = blogpost.objects.all()
    return render_to_response('archive.html',{'posts':posts},RequestContext(request))

def create(request):
    if request.method =="POST":
        blogpost(title=request.POST.get("title"),
                 body=request.POST.get("body"),
                 timestamp = datetime.now()
                ).save()
    return HttpResponseRedirect('/blogsearch')

CSRF 的原理还有待开发,这只是暂时的解决之道!

但是修改完之后,还是有 CSRF 的问题,似乎 RequestContext(request) 是比较老的用法,而 render_to_response 也是一种陈旧的函数。在参考了 Django 项目的tutorial 文档之后,发现其实已经广泛的使用 render 来替代 render_to_response 了,所以修改了以上 views.py 中的 archive 函数:

def archive(request):
    posts = blogpost.objects.all()
    curtime = datetime.datetime.now()
    context = {"posts":posts,"curtime":curtime}

    '''
    return render_to_response('archive.html',context,  RequestContext(request))


    '''
    return render(request, 'archive.html', context)

如果是另一个视图函数的开始,那么需要在 urls.py 中配置对应的函数声明:

# urls.py


from django.urls import path, include
import blogsearch.views

urlpatterns = [        path(r'',blogsearch.views.archive),
                        path(r'Django',blogsearch.views.showQueried),
               ]

因此在这里, blogsearch/django 只能代表的是新绑定 django 到另一个视图函数执行,即 showQueried.

  • posts 和数据表之间是直联(对象属性与字段一一对应)的关系,所以在我们的例子里,直接用 objects.all() 来获取所有字段值没有问题。假如 posts 是经过查询 SQL 的出来的结果,那么怎么处理。若要返回的是存储过程的结果集,又该如何处理? SQLite 的调用方式在 settings.py 中设置好了,换成其他的 数据库,该如何处理,如果是要从 ElasticSearch 数据库中取数呢

  • 例子中采用的是单一的 Python 对象 posts 来传递信息,那么如果在 html 文件中,我们使用了多个变量来承载 Python 传来的对象,这该如何传递进去多个 Python 数据结构对象?

猜你喜欢

转载自blog.csdn.net/wujiandao/article/details/81556619