├── code ├── chapter2 │ └── section2 │ │ ├── wsgi_example │ │ ├── __init__.py │ │ ├── app.py │ │ └── gateway.py │ │ ├── socket_server.py │ │ ├── fork_server.py │ │ └── thread_socketserver.py └── student_house │ └── student_sys │ ├── student │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_student_homephone.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── forms.py │ ├── templates │ │ └── index.html │ ├── admin.py │ ├── middlewares.py │ ├── models.py │ ├── views.py │ └── tests.py │ ├── student_sys │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py │ └── manage.py ├── images ├── course.png ├── django-layers.png └── middleware.svg ├── chapter2 ├── section6.md ├── README.md ├── section5.md ├── section4.md ├── section3.md ├── section1.md └── section2.md ├── chapter3 ├── README.md ├── section1.md ├── section2.md ├── section3.md └── section4.md ├── chapter6 ├── README.MD └── section2.md ├── .gitignore ├── book.json ├── chapter1 ├── README.md ├── section1.md ├── section4.md ├── section2.md └── section3.md ├── SUMMARY.md ├── course.md └── README.md /code/chapter2/section2/wsgi_example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student_sys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/django-practice-book/HEAD/images/course.png -------------------------------------------------------------------------------- /images/django-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/django-practice-book/HEAD/images/django-layers.png -------------------------------------------------------------------------------- /code/student_house/student_sys/student/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class StudentConfig(AppConfig): 8 | name = 'student' 9 | -------------------------------------------------------------------------------- /chapter2/section6.md: -------------------------------------------------------------------------------- 1 | # 技术选型总结 2 | 3 | 对于选择什么样的框架,技术栈来支撑我们的业务,首先要想到的是,整套技术是否能够匹配我们的业务需求。上面我提到的几个框架都有各自的特点和所针对的领域。 我们要做的还是带着需求去找框架,找技术栈,而不是拿着框架来做需求。有时我们需要的就是做一个简单的页面,这种场景下,拿Django过来可以做,但是会显得有点束手束脚。 4 | 5 | 所以拿去需求去找框架,选择合适的框架,而不是功能最全的框架或者最先进的框架。 6 | -------------------------------------------------------------------------------- /chapter3/README.md: -------------------------------------------------------------------------------- 1 | # Django小试牛刀 2 | 3 | 前面的课程中,我们经过了需求分析,经过了技术选型,选择了我们匹配我们需求的框架。 4 | 5 | 在这一章,用我们选出来的框架简单做一个系统出来。在正式开发流程中,我们有可能会在选型的阶段来完成这部分内容。这是在我们对所有要用的框架都不熟悉的情况下。我们需要分别做一个简单的系统,找找感觉,或者说看看实际使用中的匹配程度。 6 | 7 | 在这一章,我们一起来熟悉下Django概括,它所能提供的具体的功能点。另外,最重要的一个事情是如何查文档。对于一个文档如此丰富的框架,即便是Django的文档写的再好,也会让人“迷路”。所以我会带大家一起读一下文档。 8 | 9 | 之后我们会快速的把Django文档中涉及到的各个模块都用一下,最终做出来一个简单的系统。这一章跟Django文档上提供的[新手指导](https://docs.djangoproject.com/en/1.11/intro/)类似,你可以选择一个来做。但是一定要跟着做一遍。 10 | 11 | 好了,让我们开始吧。 12 | -------------------------------------------------------------------------------- /chapter2/README.md: -------------------------------------------------------------------------------- 1 | # 框架基础和技术选型 2 | 3 | 上一章中我们对需求进行了评审和分析,最终得到了具体要开发的功能点,以及对模块进行了划分。现在我们需要做的是根据要开发的功能进行框架的选择。 4 | 5 | 针对不同的场景,选择不同的技术架构,所产生的开发成本和维护成本都不一样。特定场景下合适的技术架构能够让开发人员更快速的开发系统,并且后期的维护成本也大大降低。相反,一个不合适的技术架构,会导致开发和维护成本都大大增加。 6 | 7 | 我尝试总结下在做技术选型时应该考虑的因素。 8 | * 所选语言或者框架或者数据库是否应用广泛,有比较好的社区支持,以及大量的用户反馈。 9 | * 语言/框架/数据库所提供的能力(功能)是否能够契合业务的需要,从而减少重复的造轮子的工作量。 10 | * 自己团队的成员是否熟悉该框架和数据库,是否有人能够掌控开始使用框架之后遇到的所有问题。 11 | 12 | 这一章,我们会对讲下Python2和Python3的选择,以及比下flask、tornado、django这三个web框架。了解下它们的特点和应用场景。 13 | -------------------------------------------------------------------------------- /chapter6/README.MD: -------------------------------------------------------------------------------- 1 | # 开发管理后台 2 | 3 | 这一章我们主要使用Django自带的admin来完成管理后台的开发。 4 | 5 | admin属于Django的杀手锏了,对于内容管理系统来说,当你有了表,有了Model,就自动有了一套管理后台,还包括权限控制,这简直是不要太爽的操作。当然这得益于Django的诞生环境,也依赖Django的Model层。 6 | 7 | 我们在上一节有说过,Django是一个重Model的框架,Model定义好了字段类型,上层可以根据这些字段类型,定义form中需要呈现以及编辑的字段类型,这样就形成了表单。有了表单之后,基本上就有了增删改的页面。而基于Queryset这个数据集合,以及它所提供的查询操作,就有了列表的数据,以及列表页的操作。 8 | 9 | 其实我们可以想一下,对于一个内容管理系统来说,需要哪些页面来完成数据的增删改查。其实也就是我们上面说到的那些。有了Model层的支持,上面的业务逻辑很容易实现,当然这也带来另外一个问题,就是上层的实现跟Model层耦合的比较紧。 10 | 11 | 有了大概的认识之后,我们来看admin的使用。 12 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student_sys/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for student_sys project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "student_sys.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/forms.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django import forms 5 | 6 | from .models import Student 7 | 8 | 9 | class StudentForm(forms.ModelForm): 10 | def clean_qq(self): 11 | cleaned_data = self.cleaned_data['qq'] 12 | if not cleaned_data.isdigit(): 13 | raise forms.ValidationError('必须是数字!') 14 | 15 | return int(cleaned_data) 16 | 17 | class Meta: 18 | model = Student 19 | fields = ( 20 | 'name', 'sex', 'profession', 21 | 'email', 'qq', 'phone' 22 | ) 23 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/migrations/0002_student_homephone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.2 on 2017-07-02 00:34 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('student', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='student', 17 | name='homephone', 18 | field=models.CharField(max_length=128, null=True, verbose_name='\u5bb6\u5ead\u7535\u8bdd'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # https://github.com/github/gitignore/blob/master/Python.gitignore 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | __pycache__ 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | nosetests.xml 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | 41 | *.log 42 | *.sqlite3 43 | 44 | local.py 45 | fabfile.py 46 | _book* 47 | node_modules/ 48 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 学员管理系统-by the5fire 5 | 6 | 11 | 12 | Admin 13 | 18 |
19 |
20 | {% csrf_token %} 21 | {{ form }} 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | from .models import Student 7 | 8 | 9 | class StudentAdmin(admin.ModelAdmin): 10 | list_display = ('id', 'name', 'sex', 'profession', 'email', 'qq', 'phone', 'status', 'created_time') 11 | list_filter = ('sex', 'status', 'created_time') 12 | search_fields = ('name', 'profession') 13 | # fieldsets = ( 14 | # (None, { 15 | # 'fields': ( 16 | # 'name', 17 | # ('sex', 'profession'), 18 | # ('email', 'qq', 'phone'), 19 | # 'status', 20 | # ) 21 | # }), 22 | # ) 23 | 24 | 25 | admin.site.register(Student, StudentAdmin) 26 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Django企业开发实战教程", 3 | "description": "Django是一个脱胎于新闻系统中的用于生产环境开发的框架,是开发者不可或缺的Web项目开发利器。", 4 | "language": "zh-cn", 5 | "gitbook": "3.x.x", 6 | "plugins": [ 7 | "github", 8 | "3-ba", 9 | "ad" 10 | ], 11 | "pluginsConfig": { 12 | "github": { 13 | "url": "https://github.com/the5fire/django-practice-book" 14 | }, 15 | "3-ba": { 16 | "token": "2d07b6b466a61c6c12550d39c53f4ad0" 17 | }, 18 | "ad": { 19 | "contentTop": "
Created by the5fire
", 20 | "contentBottom": "《Django企业开发实战》购买纸质版: https://item.jd.com/12537842.html" 21 | }, 22 | "sitemap": { 23 | "hostname": "http://www.the5fire.com" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/middlewares.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import time 3 | 4 | from django.utils.deprecation import MiddlewareMixin 5 | from django.urls import reverse 6 | 7 | 8 | class TimeItMiddleware(MiddlewareMixin): 9 | def process_request(self, request): 10 | return 11 | 12 | def process_view(self, request, func, *args, **kwargs): 13 | if request.path != reverse('index'): 14 | return None 15 | 16 | start = time.time() 17 | response = func(request) 18 | costed = time.time() - start 19 | # print('{:.2f}s'.format(costed)) 20 | return response 21 | 22 | def process_exception(self, request, exception): 23 | pass 24 | 25 | def process_template_response(self, request, response): 26 | return response 27 | 28 | def process_response(self, request, response): 29 | return response 30 | -------------------------------------------------------------------------------- /code/student_house/student_sys/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "student_sys.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student_sys/urls.py: -------------------------------------------------------------------------------- 1 | """student_sys URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | 19 | from student.views import IndexView 20 | 21 | 22 | urlpatterns = [ 23 | url(r'^$', IndexView.as_view(), name='index'), 24 | url(r'^admin/', admin.site.urls), 25 | ] 26 | -------------------------------------------------------------------------------- /chapter1/README.md: -------------------------------------------------------------------------------- 1 | # 第一章 需求 2 | 3 | 凡是得有个来由,就像物理中的能量守恒定律一样,各个模块(部门、组)之前相关作用、推动,让这整个公司的业务运作起来。不可能凭空产生能量。做项目开发也是一样,总得有一个需求过来,启动一个项目,或者推动整个项目的进展。这个需求可能是老板提出来的,也可能是产品提出来的,最终到开发组这里都需要转化为功能点。 4 | 5 | 做功能分析这个事,往往需要有独立开发项目能力的人来做,而刚刚入行的初级工程师,或者是没独立承担过项目的工程师是不会委以重任的。毕竟做项目是要盈利或者达到KPI的,是实打实的现实操作,跟写几个Demo截然不同。在作者以往的经历中,也常常会推荐一些自己觉得比较靠谱的工程师(比如小A)去承担一些重任。但结果往往会被拒绝,理由很简单,在公司以往的项目中,没有什么证据能够证明小A有独立承担项目的能力。对于这个反馈作者也是认可的,毕竟让新人(刚开始独立承担项目)去做重要的项目,是存在风险的。 6 | 7 | 在现实社会或者说商业社会就是如此,用事实说话。而对于想要独立承担项目的人来说,能够分析清楚需求是至关重要的,否则,就是“差之毫厘谬以千里”。 8 | 9 | 有些开发人员会在社交圈吐槽说,又要跟PM(产品经理)开撕了。这其实是一个正常的情况,工种不同,责任不同。PM梳理用户需求,开发人员来实现。PM在做需求时不会考虑系统实现的问题,满足用户需求为第一位,因此开发人员在接到需求的第一件事就是考虑各个需求点能否实现,以及实现起来的复杂度(工期),然后反馈给产品经理。这个时候开发人员经常犯的一个错误是,以系统好实现好维护优先。这是一个错误的认知,系统的实现应该以满足用户需求为第一要务,试想下,不满足用户需求的产品怎么会留住用户,没有用户的系统,维护它作甚。所以在工期紧、任务重的情况下,往往需要排出个一二三期来,分段完成。 10 | 11 | 所以一个优秀的工程师,应该是在尽量满足用户需求的前提下,构建一个稳定、易维护、易扩展的系统。 12 | 13 | 但,这并不意味着要一味满足产品的需求,有时产品构想出来的需求,可能是脱离了技术实现的。比方说在移动端的网页中,拿到所有用户的网络状况。所以开发人员,或者称项目的技术负责人要及时的告诉产品,哪些东西是目前经验上已经明确无法实现的。避免在后期进行无谓的返工。 14 | 15 | 对于一个优秀的工程师来说,把握住需求很重要,你需要像产品那样去思考用户的需求,也需要像工程师那样去考虑功能的实现。你需要不断的权衡,尝试,总结,最终你才能到达轻松的hold住一个项目的境界。 16 | -------------------------------------------------------------------------------- /code/chapter2/section2/wsgi_example/app.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | def simple_app(environ, start_response): 4 | """Simplest possible application object""" 5 | print(environ, start_response) 6 | print('\n\r') 7 | status = '200 OK' 8 | response_headers = [('Content-type', 'text/plain')] 9 | start_response(status, response_headers) 10 | return ['Hello simple_app world!\n'] 11 | 12 | 13 | class AppClass(object): 14 | status = '200 OK' 15 | response_headers = [('Content-type', 'text/plain')] 16 | 17 | def __call__(self, environ, start_response): 18 | print(environ, start_response) 19 | start_response(self.status, self.response_headers) 20 | return ['Hello AppClass.__call__\n'] 21 | 22 | application = AppClass() 23 | 24 | 25 | class AppClassIter(object): 26 | status = '200 OK' 27 | response_headers = [('Content-type', 'text/plain')] 28 | 29 | def __init__(self, environ, start_response): 30 | self.environ = environ 31 | self.start_response = start_response 32 | 33 | def __iter__(self): 34 | print(self.environ, self.start_response) 35 | self.start_response(self.status, self.response_headers) 36 | yield 'Hello AppClassIter\n' 37 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | 7 | class Student(models.Model): 8 | SEX_ITEMS = [ 9 | (1, '男'), 10 | (2, '女'), 11 | (0, '未知'), 12 | ] 13 | STATUS_ITEMS = [ 14 | (0, '申请'), 15 | (1, '通过'), 16 | (2, '拒绝'), 17 | ] 18 | name = models.CharField(max_length=128, verbose_name="姓名") 19 | sex = models.IntegerField(choices=SEX_ITEMS, verbose_name="性别") 20 | profession = models.CharField(max_length=128, verbose_name="职业") 21 | email = models.EmailField(verbose_name="Email") 22 | qq = models.CharField(max_length=128, verbose_name="QQ") 23 | phone = models.CharField(max_length=128, verbose_name="电话") 24 | homephone = models.CharField(max_length=128, null=True, verbose_name="家庭电话") 25 | 26 | status = models.IntegerField(choices=STATUS_ITEMS, default=0, verbose_name="审核状态") 27 | 28 | created_time = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="创建时间") 29 | 30 | def __unicode__(self): 31 | return ''.format(self.name) 32 | 33 | class Meta: 34 | verbose_name = verbose_name_plural = "学员信息" 35 | -------------------------------------------------------------------------------- /chapter1/section1.md: -------------------------------------------------------------------------------- 1 | # 需求文档 2 | 3 | 这是产品经理跟开发人员交流的必不可少的东西,很多东西如果不落实到文档上,出了问题很难追溯。另外交流基本靠吼的方式也很容易丢失信息。所以无论是什么需求,能落实到文档上的一定要落实。即便是临时需求也要通过邮件的方式沟通。 4 | 5 | 接下来说博客开发的需求。 6 | 7 | ## 博客的需求说明 8 | 9 | ### 介绍 10 | 11 | > 博客(英语:Blog,为Web Log的混成词),意指log on the web意即在网络上纪录,是一种由个人管理、张贴新的文章、图片或视频的网站或在线日记,用来纪录、抒发情感或分享信息。博客上的文章通常根据张贴时间(Chronological Order),以倒序方式由新到旧排列。 12 | 许多博客作者专注评论特定的课题或新闻,其他则作为个人日记。一个典型的博客结合了文字、图像、其他博客或网站的超链接、及其它与主题相关的媒体。能够让读者以互动的方式留下意见,是许多博客的重要要素。大部分的博客内容以文字为主,也有一些博客专注艺术、摄影、视频、音乐、播客等各种主题。博客是社会媒体网络的一部分。 13 | > —— 摘自维基百科 14 | 15 | 博客也是一个与他人分享和交流的平台,通过书写自己的想法、学习技巧,工作经验,来结识不同领域的读者,进行技术/思想/文化/公司等话题上的交流和探讨。 16 | 17 | 18 | ### 需求描述 19 | 20 | 要开发的博客系统,简单来说分为两部分,读者可访问的部分和作者进行创作的部分。 21 | 22 | 读者访问部分的需求如下: 23 | 24 | * 需要能够通过搜索引擎搜索到博客内容,进而来到博客 25 | * 可在博客中进行关键词搜索,然后展示出文章列表 26 | * 能够根据某个分类查看所有关于这一分类的文章 27 | * 访问首页需要能看到有新到旧的文章列表,以便于查看最新的文章 28 | * 需要能够通过RSS阅读器订阅博客的文章 29 | * 要能够对某一个文章进行评论 30 | * 能够配置友链,方便与网友进行链接 31 | 32 | 33 | 创作者的需求如下: 34 | 35 | * 博客后台需要登录后方可进入 36 | * 能够创建分类和标签 37 | * 能够编写文章,以Markdown格式编写 38 | * 能够配置导航,以便引导读者 39 | * 作者更新后,读者能够收到通知 40 | 41 | 42 | ## 总结 43 | 44 | 这就是一个简单的需求描述,整理出用户(作者)的需求。从这个需求描述上来看,无法确定需要做出什么样的东西,因为很多细节没有说到,这时,如果技术人员尝试以自己的理解去开发一个博客系统,可能会导致跟产品或者用户想要的结果不一样,从而进行无谓的返工。 45 | 46 | 下一节,我们进入需求评审和分析,帮助产品整理清楚需求,让技术在开发时能够明确具体的需求点是什么,需要开发哪些功能,需要如何设计系统,建立模型等。 47 | -------------------------------------------------------------------------------- /code/chapter2/section2/socket_server.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | """ 3 | 代码需要在 Python 3 下执行 4 | """ 5 | 6 | import socket 7 | 8 | EOL1 = b'\n\n' 9 | EOL2 = b'\n\r\n' 10 | body = '''Hello, world!

from the5fire 《Django企业开发实战》

''' 11 | response_params = [ 12 | 'HTTP/1.0 200 OK', 13 | 'Date: Sat, 10 jun 2017 01:01:01 GMT', 14 | 'Content-Type: text/plain; charset=utf-8', 15 | 'Content-Length: {}\r\n'.format(len(body)), 16 | body, 17 | ] 18 | response = '\r\n'.join(response_params) 19 | 20 | 21 | def handle_connection(conn, addr): 22 | request = b"" 23 | while EOL1 not in request and EOL2 not in request: 24 | request += conn.recv(1024) 25 | print(request) 26 | conn.send(response.encode()) 27 | conn.close() 28 | 29 | 30 | def main(): 31 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 33 | serversocket.bind(('127.0.0.1', 8080)) 34 | serversocket.listen(1) 35 | print('http://127.0.0.1:8080') 36 | 37 | try: 38 | while True: 39 | conn, address = serversocket.accept() 40 | handle_connection(conn, address) 41 | finally: 42 | serversocket.close() 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [课程介绍](README.md) 4 | * [视频课程](course.md) 5 | * [第一章 - 需求](chapter1/README.md) 6 | * [第一节 - 需求文档](chapter1/section1.md) 7 | * [第二节 - 需求评审/分析](chapter1/section2.md) 8 | * [第三节 - 功能分析](chapter1/section3.md) 9 | * [第四节 - 模块划分](chapter1/section4.md) 10 | * [第二章 - 技术选型](chapter2/README.md) 11 | * [第一节 - Python2.x vs Python3.x](chapter2/section1.md) 12 | * [第二节 - WSGI](chapter2/section2.md) 13 | * [第三节 - Flask](chapter2/section3.md) 14 | * [第四节 - Tornado](chapter2/section4.md) 15 | * [第五节 - Django](chapter2/section5.md) 16 | * [第六节 - 总结](chapter2/section6.md) 17 | * [第三章 - Django小试牛刀](chapter3/README.md) 18 | * [第一节 - 如何读Django文档](chapter3/section1.md) 19 | * [第二节 - Model和Admin](chapter3/section2.md) 20 | * [第三节 - 前台开发](chapter3/section3.md) 21 | * [第四节 - 进阶部分:CBV、Middleware、TestCase](chapter3/section4.md) 22 | 23 | ## 第二部分 正式开发 24 | * [概括] 25 | * [第四章 - 进入开发] 26 | * [第一节 编码规范] 27 | * [第二节 虚拟环境] 28 | * [第三节 项目结构] 29 | * [第四节 版本管理与协作(Git)] 30 | * [第五章 - 奠定项目基石-Model] 31 | * [第一节 创建项目配置settings] 32 | * [第二节 编写Model] 33 | * [第三节 Model层Fields总结] 34 | * [第四节 Model层-QuerySet总结] 35 | * [第六章 - 开发管理后台](chapter6/README.md) 36 | * [第一节 配置Admin] 37 | * [第二节 根据需求进行定制] 38 | * 定制site 39 | * 定义list页面 40 | * 定制编辑页面 41 | * 自定义展示字段 42 | * 配置adminform 43 | * 自定义admin方法 44 | 45 | -------------------------------------------------------------------------------- /course.md: -------------------------------------------------------------------------------- 1 | # 《Django企业开发实战-视频版》 2 | 更新期优惠价499/元;(大纲全部完成,后面会陆续补充遗漏的知识点,以及同学们反馈的技术问题) 3 | 4 | 购买课程一定要加VIP QQ群(见课程大纲),课程咨询可以加:111054501(群),然后私聊群主。 5 | 6 | _课程内容包括:录制的视频课程(PC端可播放),每章课程后定期直播答疑,VIP QQ群永久答疑。_ 7 | 8 | ![Django企业开发实战视频](images/course.png) 9 | 10 | 11 | ## 课程由来 12 | 首先来说,我是一个热衷于分享的人,从我博客上[https://www.the5fire.com/](https://the5fire.com)以及之前的多次年度总结[阶段总结](https://www.the5fire.com/category/%E9%98%B6%E6%AE%B5%E6%80%BB%E7%BB%93/)中也能看到。 13 | 14 | 2015年跟培训机构合作过一次,讲的是《Python基础》,侧重点在把学员带到Python来的的门里,认真学完课程,并且完成所有作业的在之后的工作中应该能熟练运用python,课程最后包含了一部分web开发的内容。以帮助大家更好的往web方向深入。 15 | 16 | 课程的学员大部分是运维工程师,从之后几个常沟通的学员那里得到的反馈还不错,掌握Python对运维工作/运维开发很有帮助。 17 | 前几个月知道小鹅通后,觉得这是一个不错的知识传播的平台,自己把握课程节奏,不受约束。当然还有最最重要的是,在同等内容的情况下,价格折半。之前报过我那套课程的同学应该知道价格。 18 | 19 | ## 课程介绍 20 | 21 | *本课程主要针对Python初学者,初步掌握Python之后想要了解Python在Web开发上的应用以及在公司正式环境下一个完整项目的开发和构建流程* 22 | 23 | 基于上述目的,再加上the5fire自己在使用Python/Django工作5年多,也希望做一些教程来帮助初学者更快速的了解PythonWeb开发上的内容,以及公司中是如何使用Python做线上项目的。让初学者所掌握的技能能尽快的匹配企业的需求。 24 | 25 | 对企业来说,越多的迈过初学者门槛的Python工程师,对企业招聘来说越是有益处。 26 | 27 | 课程从零开始基于Django构建线上博客,博客虽小,但是会涉及到网站开发的方方面面,包括:Git,Django,Fabric,MySQL,Redis,Cache,自动化部署等方面。 28 | 29 | 暂定包括内容: 30 | 31 | * Django的使用 32 | - 涉及Django框架部分知识点 33 | * Bootstrap框架的使用 34 | * Redis的使用 35 | * MySQL的使用 36 | * 自动化部署流程的开发 37 | * 工作中Git协作流程的开发 38 | 39 | 我估计最终写完,内容会远多于这个,目前只是大纲。 40 | 41 | 42 | 所有购买课程的同学均可以加入到 the5fire vip交流群,the5fire随时为你答疑解惑,助你成长。开课后会邀请入群。QQ群见《课程大纲》 43 | 44 | 45 | ## 课程安排 46 | 目前大纲基本整理完毕,大概有14章内容,每一章会分若干小节,每一节计划在30分钟内讲完。每一章讲完后会准备一个语音直播答疑(类似于知乎Live),当然前提是大部分人学完了这一章。 47 | 48 | ## 课程目标 49 | 目标很简单,把我这几年工作的经验融入到课程中,分享出去。涉及的知识点非常多,工作中也经常用到。对学员来说,学完且学懂课程内容后,可以独立去开发Django项目,或者其他类似项目。 50 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.2 on 2017-07-02 00:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Student', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=128, verbose_name='\u59d3\u540d')), 21 | ('sex', models.IntegerField(choices=[(1, '\u7537'), (2, '\u5973'), (0, '\u672a\u77e5')], verbose_name='\u6027\u522b')), 22 | ('profession', models.CharField(max_length=128, verbose_name='\u804c\u4e1a')), 23 | ('email', models.EmailField(max_length=254, verbose_name='Email')), 24 | ('qq', models.CharField(max_length=128, verbose_name='QQ')), 25 | ('phone', models.CharField(max_length=128, verbose_name='\u7535\u8bdd')), 26 | ('status', models.IntegerField(choices=[(0, '\u7533\u8bf7'), (1, '\u901a\u8fc7'), (2, '\u62d2\u7edd')], verbose_name='\u5ba1\u6838\u72b6\u6001')), 27 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 28 | ], 29 | options={ 30 | 'verbose_name': '\u5b66\u5458\u4fe1\u606f', 31 | 'verbose_name_plural': '\u5b66\u5458\u4fe1\u606f', 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /chapter1/section4.md: -------------------------------------------------------------------------------- 1 | # 功能模块划分和技术点 2 | 3 | 从前面的几篇文章,我们已经知道了一个需求经过怎么样的产品跟技术之间的沟通或者PK之后,才能确定下来。当我们有了完整的需求之后,再接下来的一步就是做功能分析和技术选型,以及架构设计。 4 | 5 | 但是,很重要的一点是,需要再次提醒一下。一定要问清楚后期产品上的计划,因为这影响到技术的选型,以及架构的设计。 6 | 7 | 好了,让我们开始来抽取实体和划分模块。 8 | 9 | 10 | ## 实体及关系 11 | 12 | 文章: 13 | - id 14 | - 标题 15 | - 作者 16 | - 分类(多对一) 17 | - 标签(多对多) 18 | - 摘要 19 | - 正文 20 | - 状态 21 | - 发布时间 22 | 23 | 分类: 24 | - id 25 | - 名称 26 | - 状态 27 | - 作者 28 | - 创建时间 29 | - 是否置顶导航 30 | 31 | 标签: 32 | - id 33 | - 名称 34 | - 状态 35 | - 作者 36 | - 创建时间 37 | 38 | 友链: 39 | - id 40 | - 网站名称 41 | - 链接 42 | - 作者 43 | - 状态 44 | - 创建时间 45 | - 权重 46 | 47 | 评论: 48 | - id 49 | - 文章(多对一) 50 | - 用户名 51 | - 邮箱 52 | - 网站地址 53 | - 内容 54 | - 创建时间 55 | - 作者 56 | 57 | 侧栏: 58 | - id 59 | - 标题 60 | - 类型(最新文章/最热文章/最近评论/内容) 61 | - 内容 62 | - 创建时间 63 | - 作者 64 | 65 | 到此,实体及关系就梳理清楚了,可以看到文章是所有实体的中心。我们可以通过在线的ER图工具,把结构画出来。 66 | 67 | 在线ER图工具:[https://editor.ponyorm.com/user/pony/PhotoSharing](https://editor.ponyorm.com/user/pony/PhotoSharing) 68 | 69 | 70 | ## 模块划分 71 | 72 | 上面我们已经建立好实体了,接着就需要多功能进行模块划分,划分的好处是让系统结构更加清晰,模块和模块之前相互解耦。同时对于多人协作的项目来说,可以独立分配一个模块进行开发。 73 | 74 | 首先网站功能整体来说,分为用户端和管理后台。这算是一个大的分类。 75 | 76 | 用户端的功能又可以分为:内容模块,评论模块,侧栏模块,功能模块。内容模块是指首页,分类列表页,标签列表页,友链页;评论模块是指用户添加评论,展示评论的部分;侧栏模块是指博客侧边栏展示的内容;功能页是指sitemap页面,rss页面。 77 | 78 | 管理后台可以纵向和横向分割,横向的话就是按照模型层,业务层,操作页面。纵向的话就是指比如文章的模型、业务层,操作页面(展示层)作为一个部分,另外的模型,业务,展示划分为另一部分。 79 | 80 | 我们通过思维导图来看下最终的结果。 81 | 82 | [博客需求分析思维导图](http://naotu.baidu.com/file/3f7f8238a936155341cac37aaac40a66?token=96dd1b636b4bc558) 密码: GTDL 83 | 84 | 85 | ## 总结 86 | 87 | 到此为止,我们通过对需求的评审和整理,最终得到了明确要开发的功能。然后对功能进行了实体抽取以及模块划分。后面我们需要做的就是,在已经清楚知道要开发什么功能之后,如何进行技术选型。一个好的技术选型不仅能够提高开发效率,也能降低维护成本。 88 | 89 | 下节课我们将会选几个常见的Python Web开发框架进行分析对比。 90 | -------------------------------------------------------------------------------- /code/chapter2/section2/fork_server.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | import errno 4 | import os 5 | import socket 6 | 7 | EOL1 = b'\n\n' 8 | EOL2 = b'\n\r\n' 9 | body = '''Hello, world!

from the5fire 《Django企业开发实战》

''' 10 | response_params = [ 11 | 'HTTP/1.0 200 OK', 12 | 'Date: Sat, 10 jun 2017 01:01:01 GMT', 13 | 'Content-Type: text/plain; charset=utf-8', 14 | 'Content-Length: {}\r\n'.format(len(body.encode())), 15 | body, 16 | ] 17 | response = '\r\n'.join(response_params) 18 | 19 | 20 | def handle_connection(conn, addr): 21 | pid = os.fork() # 产生一个新的子进程 22 | if pid: # 是否为父进程 23 | return 24 | 25 | # 子进程继续执行 26 | print(conn, addr) 27 | import time 28 | time.sleep(10) 29 | request = b"" 30 | while EOL1 not in request and EOL2 not in request: 31 | request += conn.recv(1024) 32 | print('request handle by pid:', os.getpid()) 33 | print(request) 34 | conn.send(response.encode()) 35 | conn.close() 36 | 37 | 38 | def main(): 39 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 40 | serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 41 | serversocket.bind(('127.0.0.1', 8000)) 42 | serversocket.listen(5) 43 | serversocket.setblocking(0) 44 | print('http://127.0.0.1:8000') 45 | 46 | try: 47 | while True: 48 | try: 49 | conn, address = serversocket.accept() 50 | except socket.error as e: 51 | if e.args[0] != errno.EAGAIN: 52 | raise 53 | continue 54 | handle_connection(conn, address) 55 | finally: 56 | serversocket.close() 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.http import HttpResponseRedirect 5 | from django.urls import reverse 6 | from django.shortcuts import render 7 | from django.views import View 8 | 9 | from .models import Student 10 | from .forms import StudentForm 11 | 12 | 13 | class IndexView(View): 14 | template_name = 'index.html' 15 | 16 | def get_context(self): 17 | students = Student.objects.all() 18 | context = { 19 | 'students': students, 20 | } 21 | return context 22 | 23 | def get(self, request): 24 | context = self.get_context() 25 | form = StudentForm() 26 | context.update({ 27 | 'form': form 28 | }) 29 | raise Exception('hhh') 30 | response = render(request, self.template_name, context=context) 31 | return response 32 | 33 | def post(self, request): 34 | form = StudentForm(request.POST) 35 | if form.is_valid(): 36 | cleaned_data = form.cleaned_data 37 | student = Student() 38 | student.name = cleaned_data['name'] 39 | student.sex = cleaned_data['sex'] 40 | student.email = cleaned_data['email'] 41 | student.profession = cleaned_data['profession'] 42 | student.qq = cleaned_data['qq'] 43 | student.phone = cleaned_data['phone'] 44 | student.save() 45 | return HttpResponseRedirect(reverse('index')) 46 | context = self.get_context() 47 | context.update({ 48 | 'form': form 49 | }) 50 | return render(request, self.template_name, context=context) 51 | 52 | 53 | class PrettyView(IndexView): 54 | template_name = 'pretty.html' 55 | -------------------------------------------------------------------------------- /chapter1/section2.md: -------------------------------------------------------------------------------- 1 | # 需求分析/评审 2 | 3 | 对于有经验的产品经理来说,在做任何需求的时候,都会计划的足够细致,落实到一个功能点。更好的是能够出原型稿。之后可以通过原型来对每一个功能点进行逐一核对。 4 | 5 | 对技术来说评审的目的有三个 6 | * 一、明确所有的需求点,避免返工; 7 | * 二、确认技术可行性,避免延期或者后面再修改需求; 8 | * 三、确认工期,是否需要分期开发; 9 | 10 | 11 | ## 博客需求评审 12 | 13 | 针对产品提出的每个需求,我们都需要仔细核对,尽量避免歧义或者沟通不畅。下面我们逐条来分析。 14 | 15 | ### 用户端部分 16 | 17 | > * 需要能够通过搜索引擎搜索到博客内容,进而来到博客 18 | 19 | 技术上来说这个属于SEO的部分,只需要提供Sitemap到搜索引擎即可。同时页面需要对爬虫友好。需要跟产品明确的事情是,技术上无法保证一定能够通过搜索引擎搜索到博客,这最终取决于搜索引擎。 20 | 21 | > * 可在博客中进行关键词搜索,然后展示出文章列表 22 | 23 | 需要明确搜索哪些字段,比如title,标签,分类等。如果需要全文搜索,就要考虑数据量的问题,如果数据量大,就不能直接使用MySQL的LIKE语句,需要增加Elasticsearch之类的技术栈进来。 24 | 25 | > * 能够根据某个分类查看所有关于这一分类的文章 26 | 27 | 对于分类,要明确的是有没有子分类这样的需求,如果有子分类,那子分类的文章要不要在父分类下展示。 28 | 29 | > * 访问首页需要能看到有新到旧的文章列表,以便于查看最新的文章 30 | 31 | 首页排序从新到旧没问题,是否有置顶的需求,另外是通过分页的方式展示列表还是,一个页面可以不断加载的方式。每个页面/每次加载多少条数据。 32 | 33 | > * 需要能够通过RSS阅读器订阅博客的文章 34 | 35 | 需要提供rss格式数据的页面 36 | 37 | > * 要能够对某一个文章进行评论 38 | 39 | 是否需要前台(用户端)有查看所有评论的页面。 40 | 41 | > * 能够配置友链,方便与网友进行链接 42 | 43 | 友链在前台如何展示,只是一个页面还是一个列表页 44 | 45 | 46 | ### 作者端需求 47 | 48 | > * 博客后台需要登录后方可进入 49 | 50 | 是否有多用户登录的需求?如果有,那么用户之间权限如何划分? 51 | 52 | > * 能够创建分类和标签 53 | 54 | 跟上面问题一样,是否有多级分类和标签的情况,如果有,需要明确,父级分类或者标签是否包含子级所关联的内容。 55 | 56 | > * 能够编写文章,以Markdown格式编写 57 | 58 | 作者编写文章时,有哪些是必填的,在网页上编写是否需要实时保存 59 | 60 | > * 能够配置导航,以便引导读者 61 | 62 | 导航是否是指分类?是否包含标签?需要明确的需求。 63 | 64 | > * 作者更新后,读者能够收到通知 65 | 66 | 博客的整个需求中并没有读者的用户系统,无法对读者进行实时通知。但是可以考虑增加邮件订阅功能。通过邮件的方式通知读者。需要产品上明确邮件的内容格式,以及作者是否需要控制发送邮件的开关。 67 | 68 | ## 评审之后 69 | 70 | 其实在实际的需求评审中,不需要每个需求点都抛出问题来确认的,因为大部分都是专业的产品经理,知道用户想要什么的同时也知道技术能实现什么,主要是基于过往的经验。所以这类产品经理会给出很明确的需求,配合起来和比较默契。不过也不能太相信产品经理,毕竟术业有专攻。 71 | 72 | 对于不太懂技术,又没有太多经验的产品经理来说,上面的阶段必不可少。 73 | 74 | 经过这么一轮的问答,产品经理也会在产品文档上更加明确自己的需求点,最终的描述应该是包含了技术所提问题的解答。 75 | 76 | 另外有一点需要注意到的是,对于PM自己也不是特别明确的功能点,比如涉及到技术方面的,开发人员应该能够根据以往的开发经验以及技术积累,给出合适的建议,在满足同等功能的情况下,让技术实现上更加容易。但是,记住一点,用户需求是第一位的,技术复杂度是第二位的。 77 | 78 | 在这之后,我们应该能得到一份详细的需求列表。下一节我们开始对需求进行拆分,把需求转为技术上需要实现的功能点/技术点。 79 | -------------------------------------------------------------------------------- /chapter2/section5.md: -------------------------------------------------------------------------------- 1 | # Django框架 2 | 3 | https://www.djangoproject.com/ 4 | 5 | the5fire使用Django的时间比Tornado还久,在我从Java开发转到Python开发时直接是从Java的SSH(Struts、Spring、Hibernate)框架逃离到了Django上。一开始使用Django的感觉就是,这玩意太轻便了,比SSH轻太多了。但是没想到的是在Python社区Django也算是比较重的框架了。 6 | 7 | 对于Django框架,我的评价是这个是一个全功能的Web开发框架。Web开发所需要的一切它都包含了,你不需要去选择,只需要去熟悉,然后使用。 8 | 9 | ## 新手友好程度 10 | 11 | 前面介绍的两个框架:Flask和Tornado,你从文档上直接把代码copy到server.py文件中,然后直接`Python server.py`就能看到界面。但是到Django中,你发现新手指导需要写好多代码才能看到界面。所以大部分人觉得Django对新手并不友好,或者说它有一定的门槛。 12 | 13 | 其实我们换个角度来看,你在写完Flask和Tornado的第一个Python文件之后,接下来应该怎么做呢?就拿开发一个Blog来说吧,你要怎么组织你的代码,组织你的项目结构?这些搞定了之后,接下来你要怎么选择一个适合你的ORM,然后把它配置到项目中。你的配置文件要怎么共享给其他模块?你要怎么来处理用户登录?如果要放到外网访问的话,你怎么保证系统安全? 14 | 15 | 这些都是接下来要面对的问题。所以我的看法是,微框架让你能够快速的做些小的应用,比如就是几个页面,整个项目只需要三四个Python文件(模块)就搞定了。稍微大一些的项目,那就是考验Python能力了。这对于初学者来说,并不那么友好。 16 | 17 | 而Django提供了更完善的新手指导。你一开始可能无法写一个文件就让代码run起来,但是这一套新手招数打完之后,你可以基于此来完成一个稍微大点的项目。并且Django也会帮你处理好我上面提到的那些问题。 18 | 19 | 20 | ## 内置功能 21 | 22 | 一开始我也说到了,Django是作为全功能的Web开发框架出现的。这意味着它提供的可能远多于你想要的。我们简单列下常用的功能。 23 | 24 | * HTTP的封装-request和response 25 | * ORM 26 | * Admin 27 | * form 28 | * template 29 | * session和cookie 30 | * 权限 31 | * 安全 32 | * Cache 33 | * Logging 34 | * Sitemap 35 | * RSS 36 | 37 | 上面列出了常用的部分,也是我们这次需求需要的部分,都能满足我们的需求。Django再次之外还提供了更多的功能,比如i18n(多语言的支持),gis的支持等。 38 | 39 | 我的观点是,如果你掌握了Django,那么你就是掌握了Web开发中的大部分知识。因为这个框架涉及到了Web开发的所有层面。 40 | 41 | 42 | ## 总结 43 | 44 | 对于Web开发来说,尤其是基于内容驱动的项目,我推荐用Django来做,因为即便你选择了Flask或者其他的微框架,然后把插件拼装起来,最终也是做了一个类Django的框架,基于松散的配置。还不如Django在整体上的整合。 45 | 46 | Django作为一个从新闻系统生成环境中诞生的框架,是直接面向企业级开发的。无论是从社区的发展,还是整体的生态(比如Django大会,Django基金会)来看Django都是十分成熟的框架,并有有十分完善的周边生态。 47 | 48 | 另外我们也可以看看基于它开发的那些我们耳熟能详的产品,如Instagram, Disqus,Sentry,Open Stack等,这些都证明了Django在企业开发中的地位。 49 | 50 | 51 | ## 参考 52 | * https://www.djangoproject.com/start/overview/ 53 | * https://docs.djangoproject.com/en/1.11/ 54 | * [Django第三方插件](https://djangopackages.org/) 55 | * [Django Sites](https://www.djangosites.org/) 56 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import Client 5 | from django.test import TestCase 6 | 7 | from .models import Student 8 | 9 | 10 | class StudentTestCase(TestCase): 11 | def setUp(self): 12 | Student.objects.create( 13 | name='test', 14 | sex=1, 15 | email='333@dd.com', 16 | profession='程序员', 17 | qq='3333', 18 | phone='32222', 19 | ) 20 | 21 | def test_create_and_unicode(self): 22 | student = Student.objects.create( 23 | name='test', 24 | sex=1, 25 | email='333@dd.com', 26 | profession='程序员', 27 | qq='3333', 28 | phone='32222', 29 | ) 30 | student_name = '' 31 | self.assertEqual(unicode(student), student_name, 'student __unicode__ must be {}'.format(student_name)) 32 | 33 | def test_filter(self): 34 | students = Student.objects.filter(name='test') 35 | self.assertEqual(students.count(), 1, 'only one is right') 36 | 37 | def test_get_index(self): 38 | client = Client() 39 | response = client.get('/') 40 | self.assertEqual(response.status_code, 200, 'status code must be 200!') 41 | 42 | def test_post_student(self): 43 | client = Client() 44 | data = dict( 45 | name='test_for_post', 46 | sex=1, 47 | email='333@dd.com', 48 | profession='程序员', 49 | qq='3333', 50 | phone='32222', 51 | ) 52 | response = client.post('/', data) 53 | self.assertEqual(response.status_code, 302, 'status code must be 302!') 54 | 55 | response = client.get('/') 56 | self.assertTrue(b'test_for_post' in response.content, 'response content must contain `test_for_post`') 57 | -------------------------------------------------------------------------------- /chapter2/section4.md: -------------------------------------------------------------------------------- 1 | # Tornado 框架 2 | 3 | https://github.com/tornadoweb/tornado 4 | 5 | 在工作中使用Tornado到现在也有5年了。相对于上一节的Flask,我对Tornado非常熟悉。但是如果要总结Tornado的特性的话,那也只是`高性能`。除此之外没有什么可以介绍的。 6 | 7 | 不同于Flask或者其他的基于WSGI的框架,Tornado并不是基于WSGI协议的框架。虽然它提供了WSGI协议的支持,但是为了能够用到它的特性(异步、非阻塞),官方建议还是直接通过自带的HTTP Server进行部署,而不是WSGI。 8 | 9 | 因为WSGI协议是同步,Application端只需要处理上游的发送过来的environ(第二节我们有讲到)。当然现在的WSGIServer或者叫WSGI容器都支持多种启动方式,比如Gunicorn可以通过gevent/greenlet/gthread等来实现协程或者异步IO,但是这些都是WSGIServer中的,对于Application没有太多影响。所以对Tornado中的WSGI协议的适配也没太多作用,无法利用Tornado自身的特性,所以官方也不推荐使用WSGI的方式部署。 10 | 11 | 12 | ## 内置功能 13 | 14 | 对比flask来说,Tornado的特点十分明显,除了基本的Request和Response封装之外,就是基于io loop的特性。我们只来看下Web相关的功能。 15 | 16 | * tornado.web — RequestHandler and Application classes (基础的Request的封装) 17 | * tornado.template — Flexible output generation(简单的模板系统) 18 | * tornado.routing — Basic routing implementation(基础的路由配置) 19 | * tornado.escape — Escaping and string manipulation (转码和字符串的操作) 20 | * tornado.locale — Internationalization support (国际化的支持) 21 | * tornado.websocket — Bidirectional communication to the browser(Websocket的支持) 22 | 23 | 从整体上看,它并不如上一节我们介绍的Flask丰富,比如session的实现,比如文档友好程度,比如第三方插件的丰富程度。但是,这个差异其实是两个框架定位的不同,Flask更多的是对业务需求的满足,而Tornado针对的是高性能web系统。至于业务的部分,自己实现吧。 24 | 25 | 除了上述列出来的基础功能,tornado最大的卖点还是基于io loop(或者说基于event loop)的异步非阻塞的实现。就像文档中声称的: 26 | 27 | > By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user. 28 | 29 | > 翻译一下就是:通过非阻塞的网络I/O,Tornado能够支撑成千上万的连接,这使它很适合对每个用户都需要建立长连接的需求,无论是是通过long polling(长轮询)Websockets,或者其他应用。 30 | 31 | 这也是我们选择它的原因,虽然我们的业务场景并非长连接,但能够承担更多的并发量正是我们需要的。 32 | 33 | ## 总结 34 | 35 | 在Python2.x的环境中,基于event loop模型的Tornado确实很有卖点。但是在Python3.x中,语言内部支持了event loop,这导致更多的框架可以很容易的开发出了异步非阻塞的模型。这对于Tornado确实是一个挑战。 36 | 37 | 但是,新兴的框架必然还要经受生成环境的考验,产生大量经验之后,其他人才可能放心使用。而Tornado基于多年的发展已经在生产环境中得到了证明,并且有大量的企业会分享出他们的最佳实践。 38 | 39 | 未来而言,哪种异步非阻塞的框架更加流行不好断言,但是从技术知识上来讲,都差不太多。 40 | -------------------------------------------------------------------------------- /chapter1/section3.md: -------------------------------------------------------------------------------- 1 | # 功能分析 2 | 3 | 上一节我们对需求进行了评审,经过对细节的沟通之后,产品对需求进行了修改和明确。 4 | 5 | ## 需求列表 6 | ### 用户端部分 7 | 8 | * 网站需要对SEO友好,具体可参考搜索引擎站长白皮书,另外需要给搜索引擎提供xml格式的sitemap文件。 9 | 10 | * 博客需要提供搜索功能,搜索范围限定在标题,分类,标签上。博客每天的增量数据为10篇文章。 11 | 12 | * 能够根据某个分类查看所有关于这一分类的文章,分类没有层级的关系,只有一级分类。一篇文章只能属于一个分类。 13 | 14 | * 访问首页需要能看到有新到旧的文章列表,以便于查看最新的文章,作者可以通过设置置顶某篇文章,可以同时置顶多篇文章,多篇文章置顶时,排序规则为从新到旧。 15 | 16 | * 列表分页需求,针对首页,频道页,标签页,都需要提供分页需求,每页展示10条文章。列表页展示文章是,需要展示摘要,默认为文章的前140个文字。 17 | 18 | * 需要能够通过RSS阅读器订阅博客的文章, 可参考RSS规范 19 | 20 | * 要能够对某一个文章进行评论,评论不需要支持盖楼的模式,只需要在文章页面展示评论。页面侧边栏也需要能展示最新评论。 21 | 22 | * 能够配置友链,方便与网友进行链接,一个页面中展示即可,不需要分类,但是需要能够制定某个友链的权重,权重高者在前面展示。 23 | 24 | 25 | ### 作者端需求 26 | 27 | * 博客后台需要登录后方可进入,目前没有多用户需求,以后可能会有,要考虑扩展。 28 | 29 | * 能够创建分类和标签, 一篇文章只能属于一个分类,但是可以属于多个标签。标签和分类都没有层级关系。 30 | 31 | * 作者在后台需要设置文章标题,摘要(如果为空则展示文章前140个文字),正文,分类,标签。不需要实时保存。文章格式默认为Markdown,开发周期够的话,增加可视化编辑器。 32 | 33 | * 导航只是分类,默认展示在顶部。同时每篇文章都需要有面包屑,以告知读者目前所处问题,面包屑组成如下:首页>文章所属分类> 正文。导航的顺序作者可以进行设置权重,权重高者在前。顶部最多展示6个分类,多余的分类展示到底部。 34 | 35 | * 作者更新后,读者能够收到通知(暂时不开发) 36 | 37 | 38 | ## 功能分析 39 | 功能分析的目的是从产品经理所提的需求中提炼出这个系统有哪些功能点,最终落实为功能列表/清单,可以按照模块,或者按照相关功能来划分,进而再进行任务分配。 40 | 41 | 从上面最终已经确定过的需求列表中,我们可以逐条的列出,博客系统所需要的功能点有哪些。 42 | 43 | * 后端渲染页面,对SEO友好; 44 | * 提供sitemap.xml文件,输出所有文章; 45 | * 搜索功能,能够针对标题,分类,标签进行搜索; 46 | * 根据分类、标签、查看文章列表; 47 | * 文章可以设置置顶,可以同时多篇文章置顶; 48 | * 首页(列表页)需要展示文章摘要,140以内,可以作者填写,或者自动展示文章前140个字; 49 | * 首页(列表页)需要分页展示,每页10条; 50 | * 提供rss页面,根据RSS2.0规范,输出内容; 51 | * 文章页面支持评论,不需要盖楼,侧边栏能够展示最近评论; 52 | * 评论模块需要增加验证码功能,避免被刷; 53 | * 后台能够配置友链,所有友链在一个页面展示; 54 | * 用户可以通过用户名密码登录后台,之后才能够创建文章; 55 | * 需要考虑多用户的扩展情况,多用户时需要多分类,标签,文章,友链进行隔离; 56 | * 分类增删改查——需要字段id,名称,创建日期,创建人,是否置顶导航,权重; 57 | * 标签增删改查——需要字段id,名称,创建日期,创建人; 58 | * 文章增删改查——需要字段id,标题,摘要,正文,所属分类,所属标签,状态(发布,草稿,删除),创建日期,创建人; 59 | * 侧栏模块用来展示侧边栏需要的数据,需要字段id,类型,标题,内容,创建日期,创建人; 60 | 61 | 62 | ## 模块划分 63 | 64 | 经过上面的分析,我们得到了足够细节的功能列表,有了这些细节的描述,技术人员就能够确定要做什么样的功能出来。不过这个时候,需要有人来做一个整体的梳理,把相关的功能整理为一个模块,同时需要抽象出实体。 65 | 66 | 在早些时候做软件开发,都是需要先画出ER图(Entity Relationship 对象关系图),理清楚每个模块之间的关系。然后再做系统设计,画出时序图,理清每个模块之间的交互逻辑。这些都是通过UML工具来做,当然还有最初的用例图。 67 | 68 | 现在理论上来讲产品经理会整理出所有的用户需求,输出PRD(产品需求文档)给开发人员。开发人员需要从中提取出实体,以及各模块之间的交互关系。 69 | 70 | 我们可以通过思维导图来梳理下我们的功能点。 71 | 72 | 下一节我们来具体操作。 73 | -------------------------------------------------------------------------------- /code/chapter2/section2/thread_socketserver.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import errno 3 | import socket 4 | import threading 5 | import time 6 | 7 | EOL1 = b'\n\n' 8 | EOL2 = b'\n\r\n' 9 | body = '''Hello, world!

from the5fire 《Django企业开发实战》

- from {thread_name}''' 10 | response_params = [ 11 | 'HTTP/1.0 200 OK', 12 | 'Date: Sun, 27 may 2018 01:01:01 GMT', 13 | 'Content-Type: text/plain; charset=utf-8', 14 | 'Content-Length: {length}\r\n', 15 | body, 16 | ] 17 | response = '\r\n'.join(response_params) 18 | 19 | 20 | def handle_connection(conn, addr): 21 | print(conn, addr) 22 | # time.sleep(60) 23 | request = b"" 24 | while EOL1 not in request and EOL2 not in request: 25 | request += conn.recv(1024) # 注意在设置为非阻塞模式时这里会有报错,建议自己探索一下问题来源。 26 | 27 | print(request) 28 | current_thread = threading.currentThread() 29 | content_length = len(body.format(thread_name=current_thread.name).encode()) 30 | print(current_thread.name) 31 | conn.send(response.format(thread_name=current_thread.name, length=content_length).encode()) 32 | conn.close() 33 | 34 | 35 | def main(): 36 | # socket.AF_INET 用于服务器与服务器之间的网络通信 37 | # socket.SOCK_STREAM 基于TCP的流式socket通信 38 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | # 设置端口可复用,保证我们每次Ctrl C之后,快速再次重启 40 | serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 41 | serversocket.bind(('127.0.0.1', 8000)) 42 | # 可参考:https://stackoverflow.com/questions/2444459/python-sock-listen 43 | serversocket.listen(10) 44 | print('http://127.0.0.1:8000') 45 | serversocket.setblocking(0) # 设置socket为非阻塞模式 46 | 47 | try: 48 | i = 0 49 | while True: 50 | try: 51 | conn, address = serversocket.accept() 52 | except socket.error as e: 53 | if e.args[0] != errno.EAGAIN: 54 | raise 55 | continue 56 | i += 1 57 | print(i) 58 | t = threading.Thread(target=handle_connection, args=(conn, address), name='thread-%s' % i) 59 | t.start() 60 | finally: 61 | serversocket.close() 62 | 63 | 64 | if __name__ == '__main__': 65 | main() 66 | -------------------------------------------------------------------------------- /chapter2/section3.md: -------------------------------------------------------------------------------- 1 | # Flask框架 2 | 3 | https://github.com/pallets/flask 4 | 5 | 上节我们讲了两种提供Web服务的方式,分别是一:直接通过socket来处理http请求; 二:通过实现WSGI Application部分的协议。 6 | 7 | 基于这两种方式我们完全可以自己写一个框架,或者抛开框架来实现自己的Web服务。从实现的角度来说,没有任何问题。但是考虑到我们已经进入现代化阶段,通过原始的方式除了增加我们的开发成本之外没有任何益处。我们需要更完善的脚手架帮我们把项目结构搭出来、封装好HTTP协议、处理session和cookie等等内容,让我们能够更加专注在项目有本身的业务。 8 | 9 | ## 入门推荐 10 | 11 | 在Python中微型框架的选择有很多,比如web.py,bottle。但是如果让我给新手推荐一个易上手的框架的话,我会建议他先用下Flask。原因很简单,所有微型框架的特点就是小,只提供核心能力,这意味着很容易上手,很容易掌握。但是在此之后呢?无论是从文档还是第三方插件的发展来看,Flask都要优于其他微框架。这也意味着上手Flask之后,除了写一个入门的Demo页,你可以学习/实践更多的东西。 12 | 13 | 不过对于正经的做业务开发,我们不能只考虑入门难度,毕竟大家都是专业的程序员。我们考虑更多的还是我第一节中说到的: 14 | 15 | * 所选语言或者框架或者数据库是否应用广泛,有比较好的社区支持,以及大量的用户反馈。 16 | * 语言/框架/数据库所提供的能力(功能)是否能够契合业务的需要,从而减少重复的造轮子的工作量。 17 | * 自己团队的成员是否熟悉该框架和数据库,是否有人能够掌控开始使用框架之后遇到的所有问题。 18 | 19 | 作为微型框架来说, Flask是很受关注的一个,这点从github的star数也可窥得一斑。另外也可以在Github上看Flask的更新频率,issues和Pull Request的数量,以及对issues和Pull Request的处理速度。这些都能够看出这个框架的受欢迎程度以及活跃程度。 20 | 21 | ## Flask内置功能 22 | 23 | Flask定位是微型框架,这意味着它的目标就是给你提供一个Web开发的核心支持。如果你需要其他的功能,你可以使用第三方插件,甚至可以自己写一个插件。 24 | 25 | 我们先看下Flask本身所提供的功能。 26 | 27 | * built-in development server and debugger(内置的开发server和debug模块) 28 | * integrated unit testing support(集成的单元测试的支持) 29 | * RESTful request dispatching(RESTful风格的请求分发机制) 30 | * uses Jinja2 templating(默认使用Jinja2模板) 31 | * support for secure cookies (client side sessions)(安全cookie的支持,用作客户端会话) 32 | * 100% WSGI 1.0 compliant(100%兼容WSGI 1.0协议) 33 | * Unicode based(基于Unicode) 34 | * extensively documented(良好的文档) 35 | 36 | 基本的Web开发能力已经都有了,这符合它的定位。因此并没有提供数据库相关的功能,比如ORM,比如权限控制。这么做的好处就是你可以选择自己熟悉的ORM工具,但是要求你有足够的能力来掌握其他工具。 37 | 38 | 39 | ## 匹配需求 40 | 41 | 有了上面的了解之后,我们可以再来回顾下我们的需求,如果用flask来发开的话需我们自己在框架之外做些什么? 42 | 43 | * ORM工具,你可以选择SQLALchemy,或者PonyORM或者Peewee 44 | * ORM跟Flask集成到一起的插件,当然你可以自己写 45 | * admin界面开发,可以选择第三方的flask-admin 46 | * 用第三方的flask-admin之后,你需要自己控制后台权限 47 | * 等等 48 | 49 | 对于微型框架来说,我们的诉求并不多。但是对于我们需要实现的业务来说,如果我们选择微型框架,意味着我们需要写更多的代码,去攒更多的插件,这其实是对Python能力的考验。 50 | 51 | 从另外一个层面来说,微框架给开发者提供了很好的灵活性,没有太多的约束,这导致的一个问题是“一千个开发者,就有一千种使用微框架的方式”。 52 | 53 | ## 总结 54 | 55 | 关于flask我没有太多的实践经验,上面的那些仅供参考。 56 | 57 | 但是对于微框架,我个人的看法是,如果开发者能力足够强,微框架很适合,不会约束你。但是你需要考虑团队协作,需要定好统一的规范。 58 | 59 | 如果能力比较弱,微框架就无法给你提供更多的帮助。比方说,你可以选择任意一个ORM框架来跟Flask结合。那么对于一个初学者来说,选择哪个ORM框架?确实有多个选择,但是有句话叫:Too Choices Means No Choices。这种场景下,新手可能不知道应该怎么做。 60 | 61 | 不过总的来说,对于简单的需求,或者不是很复杂的项目,可以使用Flask,利用它的轻量的优势。 62 | -------------------------------------------------------------------------------- /chapter2/section1.md: -------------------------------------------------------------------------------- 1 | # Python2.7 vs Python3.x 2 | 3 | 选择2和选择3是近几年来比较流行的一个“话题”,当然这个只是在网络某些论坛或者社区里。在真实的环境下没有这么多的纠结。选择目前应用最广泛的,周围人都在用,并且自己团队能够hold住的,就是最合适的选择。对于2和3的差异,其实写起代码来,没那么大的差别,最关键的一点还是环境。 4 | 5 | ## 历史演进 6 | 7 | 随着3.x版本的成熟,越来越的新项目在开始时都会考量是否要选用3来做,相对于几年前来说,这种倾向性更加明显。 8 | 9 | 我们在新开发项目时也会做这样的考虑。早在前几年我们就已经在考虑这样的事情了。只是碍于相关周边的依赖,有些库还是没有Python3的支持,所以还是在Python2上开发。当然,另外一个因素也不得不说,那就是成本,时间成本。我们往往会选择团队擅长的技术栈来开发项目,这样能够在可控的时间内,完成交付/上线。选择一个不熟悉的或者没有经受过大规模线上实践的技术栈是有风险的。 10 | 11 | > 哪些包已经支持Python3,哪些还不支持可以通过这里查看:[https://python3wos.appspot.com/](https://python3wos.appspot.com/) (需翻墙) 12 | 13 | 今天看来,大部分的包已经对Python3做了支持。 14 | 15 | 在刚结束的PyCon 2017上,Instagram宣布作为基于Django的大规模的应用,目前已经全量切到Python3.6上,并且得到了不错的性能提升。有兴趣的可以到youtube上观看、学习: [Lisa Guo, Hui Ding Keynote PyCon 2017](https://www.youtube.com/watch?v=66XoCk79kjM&index=19&list=WL)。 16 | 17 | 这是个不错的信号,有企业带了个好头,并且也有一些踩坑的分享。后面会有越来越多的基于Python3的项目,以及对于重点项目,也会考虑迁移到Python3上。这其实也是一个环境问题。今年大家还在谈论2还是3,2上踩过哪些坑,基于2的生产环境的经验分享。但是过几年大家都在讨论的可能就是基于3的经验分享了,如果你不跟上,你也会脱离环境。这个问题就跟早些年有人孤独的使用Python3来做项目一样。 18 | 19 | 整体来说,3是趋势,并且从目前的周边环境和配套上来说,可以开始尝试Python3上的开发了。Python3成为主流已经很快了。 20 | 21 | 22 | ## 现实场景 23 | 24 | 回到现实,就像开头所说,从写代码上来说,Python2和Python3的差异没那么大。并不是说那些在企业中工作的人,为了偷懒,而不去把代码改为Python3。而是工作或者说企业中,考虑更多的还是成本和回报。从开发项目的角度来说,没有哪些业务是只有在Python3中能实现,而在2中无法实现的。所以第一优先考虑的还是让项目如期上线,尽量避免线上的bug给用户造成影响。 25 | 26 | 虽说对于语法、基础库上的变化,写起代码来,不会有太大差别,但是对于运行中的问题,在没有大量经验的前提下,直接在生产环境下跑还是很有风险的。这个风险是我们要尽量避免的。就像我在知乎上的回答一样,产品和用户不会关心你用的是2还是3,他们只关心你的程序会不会挂,数据会不会丢。[如何看待 Instagram 将所有 Web 后端迁移到 Python 3.6 ?](https://www.zhihu.com/question/60333140/answer/175130694) 27 | 28 | 因此即便你现在(2017.06)直接学习的Python3,等你到公司之后会发现,有些老的项目依然是跑在Python2的上面。你可能需要接手这些项目。但是不用担心,还是上面那句话,语言和库上的差别不需要花太多时间就能熟悉。主要还是经验,你是是否有Python2生产环境下的开发和解决问题的经验,能够帮助企业快速的解决老项目中的线上问题。 29 | 30 | ## 为未来做准备 31 | 32 | 上面虽然说到现实场景中我们应该拿我们擅长的工具来做项目。但是这样下去会不可避免的进入一个死循环,导致工作中的技术环境跟不上社区主流环境的发展。对技术人员来说,这是一个可怕的事情。好像是你并没有做错什么事情,却被企业淘汰了。 33 | 34 | 因此我们需要专注当下的同时,研究下新的技术,无论是通过写一个Demo还是像the5fire这样,写一个线上的blog,把新的技术用进去。等你熟练掌握了新技术之后,你可以推动项目升级或者团队的技术栈升级。 35 | 36 | 在这个教程中,虽然我们是基于Python2.7来开发项目,但是我们会做一些兼容的处理,以便于我们在最终可以轻松的把项目跑在Python3.6上。基于Python2.7的原因依然是现在大部分的项目依然是跑在它上面,最终要迁移到Python3.6,也是因为我们要迎合社区的发展。避免我们成为孤独的开发者。 37 | 38 | 39 | ## 参考 40 | * [Python2和Python3的差别](http://blog.jobbole.com/80006/) 41 | * [Python2 3 key diff](http://sebastianraschka.com/Articles/2014_python_2_3_key_diff.html) 42 | * [如何看待 Instagram 将所有 Web 后端迁移到 Python 3.6 ?](https://www.zhihu.com/question/60333140/answer/175130694) 43 | -------------------------------------------------------------------------------- /code/chapter2/section2/wsgi_example/gateway.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | import os 4 | import sys 5 | 6 | from app import AppClass, simple_app 7 | 8 | 9 | def run_with_cgi(application): 10 | environ = dict(os.environ.items()) 11 | environ['wsgi.input'] = sys.stdin 12 | environ['wsgi.errors'] = sys.stderr 13 | environ['wsgi.version'] = (1, 0) 14 | environ['wsgi.multithread'] = False 15 | environ['wsgi.multiprocess'] = True 16 | environ['wsgi.run_once'] = True 17 | 18 | if environ.get('HTTPS', 'off') in ('on', '1'): 19 | environ['wsgi.url_scheme'] = 'https' 20 | else: 21 | environ['wsgi.url_scheme'] = 'http' 22 | 23 | headers_set = [] 24 | headers_sent = [] 25 | 26 | def write(data): 27 | if not headers_set: 28 | raise AssertionError("write() before start_response()") 29 | 30 | elif not headers_sent: 31 | # Before the first output, send the stored headers 32 | status, response_headers = headers_sent[:] = headers_set 33 | sys.stdout.write('Status: %s\r\n' % status) 34 | for header in response_headers: 35 | sys.stdout.write('%s: %s\r\n' % header) 36 | sys.stdout.write('\r\n') 37 | 38 | sys.stdout.write(data) 39 | sys.stdout.flush() 40 | 41 | def start_response(status, response_headers, exc_info=None): 42 | if exc_info: 43 | try: 44 | if headers_sent: 45 | # Re-raise original exception if headers sent 46 | raise (exc_info[0], exc_info[1], exc_info[2]) 47 | finally: 48 | exc_info = None # avoid dangling circular ref 49 | elif headers_set: 50 | raise AssertionError("Headers already set!") 51 | 52 | headers_set[:] = [status, response_headers] 53 | return write 54 | 55 | result = application(environ, start_response) 56 | try: 57 | for data in result: 58 | if data: # don't send headers until body appears 59 | write(data) 60 | if not headers_sent: 61 | write('') # send headers now if body was empty 62 | finally: 63 | if hasattr(result, 'close'): 64 | result.close() 65 | 66 | application = AppClass() 67 | 68 | if __name__ == '__main__': 69 | run_with_cgi(simple_app) 70 | # run_with_cgi(application) 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《Django企业开发实战》初版草稿 -- by the5fire 2 | 3 | * 博客: https://www.the5fire.com 4 | * github: https://github.com/the5fire/django-practice-book 5 | * 配套视频: [视频版](course.md) 6 | * 进阶视频: [Django源码分析视频](https://www.the5fire.com/django-source-inside-catalog-by-the5fire.html) 7 | 8 | ## 纸质版已上市 9 | 10 | * [图灵社区-目录和试读](http://www.ituring.com.cn/book/2663) 11 | 12 | * [![Django企业开发实战](http://file.ituring.com.cn/SmallCover/19014dfb7e0e331be8db)](https://book.douban.com/subject/30434690/) 13 | 14 | * [图灵社区购买](http://www.ituring.com.cn/book/2663) 15 | * [京东购买](https://item.jd.com/12537842.html) 16 | * [当当网购买](http://product.dangdang.com/26509799.html) 17 | * [亚马逊购买](https://www.amazon.cn/dp/B07N3PVGZK/ref=sr_1_1?ie=UTF8&qid=1550195346&sr=8-1&keywords=Django%E4%BC%81%E4%B8%9A%E5%BC%80%E5%8F%91) 18 | 19 | ## 疑惑解答 20 | 21 | * 问:视频和图书有什么差别 22 | * 答: 23 | * 图书是在视频之后产出的,基于Python3.6 和 Django 1.11(LTS版本),在书最后会升级到Django 2.0,内容上会更加细致。 24 | * 视频是基于Python 2.7和Django 1.11(LTS版本) 的版本,最终会升级到 Python3.6 和 Django2.0,内容上会更加动态,信息量会更大,毕竟书上不能带着你写代码,视频是可以非常直观的演示代码编写的。 25 | 26 | ## 提交勘误 27 | 28 | 如果你发现*纸质书籍*中存在问题,欢迎提交勘误,步骤如下: 29 | * 指明具体「章节」「页码」出错的部分代码或者相关信息 30 | * 点击[创建 Issues](https://github.com/the5fire/django-practice-book/issues/new) 来填些上面的信息。 31 | 32 | ## 随书源码 && 随视频源码 33 | 34 | 本书对应源码和相关视频对应源码都在: 35 | 36 | [https://github.com/the5fire/typeidea](https://github.com/the5fire/typeidea) 37 | 38 | 通过分支名来区分是视频还是图书,以及对应的章节,比如: 39 | 40 | 分支 ``book/05-initproject`` 就是对应的图书的第五章的代码,``book/06-admin`` 就是对应的第六章的代码。 41 | 而对应的 ``chapter7``、``chapter8``这样的是视频章节对应的代码。 42 | 43 | 44 | ## 前言 45 | 46 | 从JavaWeb开发转行到PythonWeb开发已经有6年多了,一开始就是从SSH框架转到Django,我觉得这玩意太轻便了,比SSH好用太多了。但是熟悉了Python社区之后,发现在Python中,Django确实算是一个比较重的框架。主要的原因在于它的定位:企业级开发框架,或者说全功能的Web开发框架。Web系统涉及到的方面它都有提供。这也是导致它学习成本看起来有点高的原因。 47 | 48 | 在用Django的几年中,我和我们的小伙伴们用它做了N多个系统,有对内的,也有对外的,都能够很好的满足我们的需求。所以今年我就在考虑,能不能总结出来一些东西,对大家有所帮助,主要是想学习Django,但是不得其门者。 49 | 50 | 于是就有了这个开始,包括这本教程,也包括配套的[视频教程](course.md)。 51 | 52 | 希望对你有所帮助。关于Django的问题可以加我的QQ群:111054501(Django企业开发实践)进行交流。 53 | 54 | 55 | ## 目标读者 56 | 57 | * 学完Python基础想要继续学习Web开发的同学 58 | * 想要学习Django的同学 59 | 60 | 61 | ## Power by Django 62 | 63 | https://www.djangoproject.com/start/overview/ 64 | 65 | 前几天比较火的Instagram就是基于Django开发的,从官网上也能看到其他我们耳熟能详的产品,比如:Disqus,Pinterest,Mozilla,Sentry。 66 | 67 | 68 | ## 生态 69 | 70 | Django之所有被广泛应用,除了本身是提供了完备的功能之外, 也得益于它的成熟生态,框架本身没有提供的功能,会有优秀的第三方插件来补足。比如Django-rest-framework,比如Django Debug toolbar。 71 | 72 | 73 | ## 学习曲线 74 | 75 | 相对于Flask、web.py、bottle这一类的微框架来说,Django的上手确实有点复杂,但是并不难。因为官网的新手指导写的很清晰。在众多框架中,Django的文档算是相当不错的了。 76 | 77 | 你需要花比学习微框架更多的时间来学习Django,是因为Django提供的内容远多于其他框架。刚开始可能会觉得很多地方不明白,但是等你熟悉了之后,会发现Django每个层所提供的功能都很清晰,什么样的需求,在哪一层来处理,会有清晰的认识。 78 | 79 | Django的学习曲线是先陡,然后平缓上升的。先陡主要是新手需要一下子接受很多东西,但是随着之后的不断使用,不断了解,你会发现,学习所耗费的时间完全值得。你可以更快的做出完善的系统,这会是一笔很划算的投资。 80 | 81 | 82 | ## 这本书的目的 83 | 84 | 把我知道的东西、开发项目中总结的经验,融到一个Blog系统中,写出来。让后来者可以参考我的经验快速成长。 85 | 86 | 87 | ## 勘误和提问 88 | 89 | 欢迎到github上给我提Issues: https://github.com/the5fire/django-practice-book 90 | -------------------------------------------------------------------------------- /chapter6/section2.md: -------------------------------------------------------------------------------- 1 | ## 定制admin 2 | 3 | 上一节我们完成了基础的admin代码编写,已经得到了一个基本可用的内容管理系统,这一节我们来说下常用的定制行的操作。让大家有一个初步的认识,后面在实现需求时还会做更多的讲解。 4 | 5 | 框架为了达到更高的通用性,只会抽象出通用的逻辑。因此有些特性的东西需要我们自己来做。不过一个好的框架,提供给我们定制的能力。比如这一节我们会看到如何定制admin的界面,来达到我们的需求。 6 | 7 | 8 | ## 定制site 9 | 10 | 重写Django自带的adminsite,实现自己的admin页面。 11 | 12 | 13 | ## 定义list页面 14 | 15 | 第一个我们需要定制的就是list页面,如果你确实把自己当做用户,去写几篇文章之后,你会发现,这个页面是会频繁操作到的页面,因此这个页面能提供的功能,对于用户的使用效率来说至关重要。 16 | 17 | 关于列表展示的几个配置: 18 | 19 | list_display = ['title', 'category', 'status', 'owner', 'created_time'] 20 | list_filter = ['category'] 21 | search_fields = ['title', 'category__name', 'owner__username'] 22 | show_full_result_count = True 23 | # 补充 24 | list_display_links = ['category', 'status'] 25 | actions_on_top = True 26 | actions_on_bottom = True 27 | date_hierarchy = 'created_time' 28 | list_editable = ('title', ) 29 | 30 | ## 刨源码来看上节课的问题 31 | 实际项目开发中经常会遇到与期望不符的结果,因此排查问题是必备技能,对于Python来说,看源码也是相对容易的,这一节我们来通过源码看下上节课的问题所在。 32 | 33 | 34 | ## 编辑页面配置 35 | 36 | save_on_top = True 37 | fields = ('title', 'category') 38 | fields = (('category', 'title'), 'content') # 布局 39 | exclude = ('owner',) 40 | 41 | fieldsets = ( # 跟fields互斥 42 | ('基础配置', { 43 | 'fields': (('category', 'title'), 'content') 44 | }), 45 | ('高级配置', { 46 | 'classes': ('collapse', 'addon'), 47 | 'fields': ('tags', ), 48 | }), 49 | ) 50 | filter_horizontal = ('tags', ) 51 | filter_vertical = ('tags', ) 52 | 53 | 54 | ## 自定义字段展示 55 | 56 | 增加编辑、删除操作: 57 | 58 | from django.utils.html import format_html 59 | 60 | def operator(self, obj): 61 | return format_html( 62 | '编辑', 63 | reverse('cus_admin:blog_post_change', args=(obj.id,)) 64 | ) 65 | # operator.allow_tags = True # 用format_html替代 66 | operator.show_description = '操作' 67 | operator.empty_value_display = '???' 68 | 69 | 70 | ## 自定义form 71 | 72 | 还是只针对postadmin来增加form, 在blog目录下增加文件(模块)adminforms.py 这里要命名为adminforms而不是forms,只为了跟前台针对用户输入进行处理的form区分开来。里面编写代码,定义form。关于form的作用,我们之前有讲到,form跟model其实是耦合在一起的,或者说form跟model的逻辑是一致的,model是对数据库中字段的抽象,form是对用户输入以及model中要展示数据的抽象。具体作用我们还是通过代码来看看。 73 | 74 | 我们通过form来定制下status这个字段的展示 75 | 76 | # adminforms.py 77 | from django import forms 78 | 79 | 80 | class PostAdminForm(forms.ModelForm): 81 | status = forms.BooleanField(label="是否删除", required=True) 82 | desc = forms.CharField(widget=forms.Textarea, label='摘要', required=False) 83 | 84 | 85 | # admin.py 86 | from .adminforms import PostAdminForm 87 | 88 | form = PostAdminForm 89 | 90 | 91 | ## 同时编辑外键和数据inline 92 | 93 | 我们有个需求,需要在分类页面直接编辑文章。当然这是个伪需求。因为这种内置(inline)的admin更适合的场景是针对字段较少的model,进行内联的操作。我们这里只是演示下它的用法。 94 | 95 | from django.contrib import admin 96 | 97 | class PostInline(admin.TabularInline): # StackedInline 样式不同 98 | fields = ('title', 'desc') 99 | extra = 1 # 控制额外多几个 100 | model = Post 101 | 102 | 103 | class CategoryAdmin(admin.ModelAdmin): 104 | inlines = [PostInline, ] 105 | 106 | ## 重写form的clean_status方法 107 | 108 | def clean_status(self): 109 | if self.cleaned_data['status']: 110 | return 1 111 | else: 112 | return 3 113 | 114 | 115 | 116 | ## 重写admin的save_model方法 117 | 118 | def save_model(self, request, obj, form, change): 119 | obj.owner = request.user 120 | super(PostAdmin, self).save_model(request, obj, form, change) 121 | 122 | 123 | ## 参考 124 | 1. https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#customizing-the-adminsite-class 125 | 2. https://docs.djangoproject.com/en/1.11/ref/utils/#django.utils.html.format_html 126 | -------------------------------------------------------------------------------- /chapter3/section1.md: -------------------------------------------------------------------------------- 1 | # 如何阅读Django文档 2 | 3 | 通过文字的方式来描述似乎不是件容易的事,我尽量通过文字表达清楚。 4 | 5 | ## 文档结构 6 | 7 | Django是基于MVC模式的框架,虽然也被称为“MTV”的模式,但是大同小异。对我们来说,需要了解的是无论是MVC模式还是MTV模式,甚至是其他的什么模式,都是为了解耦。把一个软件系统划分为一层一层的结构,让每一层的逻辑更加纯粹,便于开发人员维护。 8 | 9 | ![Django Layers](../images/django-layers.png) 10 | 11 | 基于这点儿认识,我们可以来看下Django的文档。 12 | 13 | 从大的划分上来说,Django的文档先是分出了这么几个模块:The model layer, The view layer, The template layer, Forms, 剩下的部分都是功能文档,比如Pagination,Caching等,可以贯穿所有层。 14 | 15 | 而每个模块或者说层,又分为了不同的模块,我们简要的把常用的模块介绍一下。 16 | 17 | ### Model部分 18 | 19 | Model在整个项目结构中是直接同数据库打交道的层,所以数据处理的部分都在这一层。在业务开发中,关于纯数据操作的部分,建议都放到这一层来做。 20 | 21 | * Models - 模型定义相关的使用说明,字段类型,Meta配置 22 | * QuerySets - 在Model的基础上,你要怎么通过Model来查数据,有哪些接口可以用,比如`all()`, `filter()`等,以及更进一步的定制,毕竟ORM在查询上会有一些限制,但是在这一部分你可以找到如何自定义查询。 23 | * Model instances - Model的实例,一个实例你可以理解为表中的一条记录,这个实例有哪些操作,如何修改表的数据,都在这了。 24 | * Migrations - 主要是在开发阶段,我们可能会不断的调整表的结构,这个就是用来做表结构调整的。理论上我们只需要知道两个命令:`makemigrations`和`migrate`就行了,但是如果你想做更多的了解,可以仔细看下这部分。 25 | * Advanced - 高级部分(别被`高级`这两字吓到),如何自定义Manager(也就是常用的Model.objects.all()中的objects),以及如果不爽ORM的查询限制,但是又想用到ORM对象的映射,你可以考虑的RAW_SQL。另外关于事务、聚合、搜索,以及多数据库支持等更多的关于Model层的需求都可以看着。 26 | * Other - 这一部分有两块,建议一定要看看,一个是`Legacy databases(遗留数据库)`,想象下,有人甩给你一个已有的CMS项目,要改成Django的,你拿到表,直接根据生成Model。之后你再花几分钟写写admin部分代码,CMS出来了,爽不爽。另外一部分就是`Optimize database access `,一定要看!避免你踩坑。如果不想看英文,可以到我博客看中文翻译,虽然版本较老,但是依然适用:[翻译了Django1.4数据库访问优化部分](https://www.the5fire.com/django-database-access-optimization.html) 27 | 28 | 29 | ### View部分 30 | 31 | 在View中,我们通过操作Model拿到数据,做一些业务上调整,然后把数据传递到模板中,最终渲染出来页面。 32 | 33 | 在Django的文档中,View部分包含了URL配置、http request、http response以及处理请求的View函数和类级的View等部分。下面我们一一列举。 34 | 35 | * The basics - URL配置,view方法,以及常用装饰器,比如想给这个接口增加缓存、或者要增加限制(只允许GET请求)等。 36 | * Reference - 一些参考,内置的view(比如静态文件处理,404页面处理等),Request和Response对象介绍,TemplateResponse对象介绍。 37 | * File uploads - 文件上传是Web开发中常遇到的问题,Django中可以通过这一节来看如何处理文件上传,它提供了一些内置的模块来帮你处理上传上来的文件,不过它也会告诉你如何来自定义后端存储。 38 | * Class-based views - 这部分可以理解为更复杂的View函数,只不过这儿是类。通过类可以提供更好的复用,从而避免自己要写很多代码。当你发现你的View中有太多的业务代码时,你可以考虑参考这一节把代码改造为ClassBase View(简称:CBV),如果你的代码中有很多类似的View函数,可以考虑这么做。这部分的文档就是告诉你Django中,如何来更好的构建你的View,以及复用你的View。 39 | * Advanced - 更高级的部分,就是告诉你如何把数据导出为CSV或者PDF,冠名为`更高级`可能是因为用的少。(瞧,`高级`没什么难的) 40 | * Middleware - 中间件(中间层),无论怎么翻译,你得理解它的作用,这一部分代码作用于WSGI(或者Socket连接)和View之间,还记得我们第二章讲的WSGI中间件的部分吗,一样的逻辑,还是对View函数做了一个包装,但是稍微复杂了一些。Django中安全的部分,Session的部分,整站缓存的部分,都在这一块了。 41 | 42 | 43 | ### Template部分 44 | 45 | 这是Django声称对设计师友好的部分,因为它提供的语法很简单,任何人都可以很快上手,即便是不同编程的人,也可以很容易学习和使用。 46 | 47 | * The basics - 这部分介绍了Django模板的基本配置,以及基本的模板语法,还有看起来可配置的如何替换为jinja2模板引擎的说明。 48 | * For designers - 说是给设计师看的,但是你也应该看一看,基础的控制语句、注释,还有内置的filter和tag,还有最重要的针对用户友好的数字的展示。 49 | * For programmers - 这个程序员更应该看看了,如何传递数据到模板中,如何配置模板,以至于能够在view中更好的渲染模板,还有就是如何对现有模板所提供的简单的功能最更多的定制。 50 | 51 | 52 | ### Forms部分 53 | 54 | 对于传统的,需要通过form来提交数据的页面,Form还是挺好用的。就像是ORM(关于ORM是什么不清楚的可以看:[什么是ORM?](https://www.the5fire.com/what-is-orm.html))一样。Form是对html中Form表单的抽象。在下面几节中我们会稍加演示。 55 | 56 | * The basics - 基础的API的介绍,里面有类似于Model的Field的部分,还有组件(Widgets)的部分。 57 | * Advanced - 更丰富的使用,如何把Form同Model结合(Model也有Field,Form也有Field,用一个不行?),以及如何把媒体资源渲染到页面上呢,如果form足够好用,其实我们不需要更多的操作模板了不是吗。还有如何布局你的字段,一行展示一个还是一行展示多个,还有更加细节、深入的部分就是如何定义字段级别的验证功能。比如页面上只允许输入数字的地方如何验证。 58 | 59 | 这部分在开发admin时很常用,因为admin跟Model结合紧密,我们如果需要去改模板的话成本会有点高,所以更好的做法是通过自定义Form以及自定义Widget来实现我们需要的功能。在前台(针对用户的界面)以为我们直接写的模板,所以更加灵活,并且我们也很少使用form表单来提交数据,所以用的较少。 60 | 61 | 62 | ### 剩余的部分 63 | 64 | 其他部分并没有太多的内容,都是单独的Topic,通过简单的单词,很好识别,比如admin,caching等。在文字中我就不再描述。 65 | 66 | 67 | ## 总结 68 | 69 | Django文档是个好东西,不过有一部分的文档Django始终没有补充上,那就是admin的部分。所以这就涉及到另外一个话题,看文档还是看代码,admin的部分,如果有较多的需求,建议看完文档,然后就去摸索代码。如果你熟悉了前面介绍的几个层,那admin的代码对你来说也不是什么难事儿。 70 | 71 | 除了admin的部分,其他的文档上基本都有。不过也没有必要像被字典一样的去看文档,有空搂一眼,遇到问题搂一眼,随着实践越来越多,对文档会越来越熟悉,但是依赖会越来越少。Django的代码结构跟文档一样划分清晰,所以the5fire现在大部分的问题都是靠读代码来解决。 72 | 73 | 最后需要提到一点的是,无论是Django文档,还是其他问题,甚至是这本书,都是仅供参考。正确的答案始终是在你电脑上的代码里。无论是因为,框架版本的原因,还是文档上的书写错误,或者是本书的书写错误,你电脑上的代码始终不会骗你。 74 | -------------------------------------------------------------------------------- /code/student_house/student_sys/student_sys/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for student_sys project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '%4ow+_cp^#s6n&&^5$_71om2onnr3!i^oim(ubs^zr0xh8lmk$' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['127.0.0.1', '*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'student', 35 | 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | 53 | 'student.middlewares.TimeItMiddleware', 54 | 55 | ] 56 | 57 | ROOT_URLCONF = 'student_sys.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'student_sys.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'zh-hans' 112 | 113 | TIME_ZONE = 'Asia/Shanghai' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | -------------------------------------------------------------------------------- /chapter3/section2.md: -------------------------------------------------------------------------------- 1 | # 学员管理系统 2 | 3 | 这一节让我们来快速的过一下Django的各个模块,在上一节内容中,你可能了解了Django所提供的功能,这一节我们来切实体会一下。你最好打开你熟悉的IDE,一起写起来。 4 | 5 | ## 需求 6 | 7 | 一句话就能描述清楚的需求:提供一个学员管理系统,一个前台页面,展示现有学员,并供新学员提交申请,一个后台,能够处理申请。over 8 | 9 | ## 初始化环境 10 | 11 | 首先,创建虚拟环境: ``mkvirtualenv student-env -p `which python2.7` `` (最后的-p是指明虚拟环境使用的python版本2.7)。不熟悉的可以看这里: [使用virtualenv创建虚拟python环境](https://www.the5fire.com/virtualenv-python-env.html) 12 | 13 | 然后激活虚拟环境: ``workon student-env``,接着我们安装django,目前的最新版1.11.2: ``pip install django==1.11.2``。 14 | 15 | 16 | ## 创建项目 17 | 18 | 虽然可以不创建虚拟环境就安装Django,但是我还是建议你在虚拟环境中安装,因为在实际的开发中,你可能需要维护不止一个项目。不同项目的所依赖库的版本也不同,如果你都安装到root下或者user下,会出现冲突的问题。 19 | 20 | 好了,cd到你喜欢的目录中,比如``/home/the5fire/workspace/``,创建项目根目录: ``mkdir student_house``,这是我们的项目目录,然后再我们创建项目结构: ``cd student_house && django-admin startproject student_sys``,我们能得到下面的结构: 21 | 22 | TODO: 23 | 24 | 25 | 26 | ## 创建APP 27 | 28 | 进入`student_house/student_sys`中,通过上一步创建好的manage.py创建一个app: ``python manage.py startapp student``。现在目录结构如下: 29 | 30 | 31 | TODO: 32 | 33 | 34 | 35 | ## 编写代码 36 | 37 | 我们可以在Model层开始写代码了,这是一个简单的需求,我们只需要一个Model就可以满足。 38 | 39 | student_house/student_sys/student/models.py: 40 | 41 | # -*- coding: utf-8 -*- 42 | from __future__ import unicode_literals 43 | 44 | from django.db import models 45 | 46 | 47 | class Student(models.Model): 48 | SEX_ITEMS = [ 49 | (1, '男'), 50 | (2, '女'), 51 | (0, '未知'), 52 | ] 53 | STATUS_ITEMS = [ 54 | (0, '申请'), 55 | (1, '通过'), 56 | (2, '拒绝'), 57 | ] 58 | name = models.CharField(max_length=128, verbose_name="姓名") 59 | sex = models.IntegerField(choices=SEX_ITEMS, verbose_name="性别") 60 | profession = models.CharField(max_length=128, verbose_name="职业") 61 | email = models.EmailField(verbose_name="Email") 62 | qq = models.CharField(max_length=128, verbose_name="QQ") 63 | phone = models.CharField(max_length=128, verbose_name="电话") 64 | 65 | status = models.IntegerField(choices=STATUS_ITEMS, verbose_name="审核状态") 66 | 67 | created_time = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="创建时间") 68 | 69 | def __unicode__(self): 70 | return ''.format(self.name) 71 | 72 | class Meta: 73 | verbose_name = verbose_name_plural = "学员信息" 74 | 75 | 再来写admin.py: 76 | 77 | # -*- coding: utf-8 -*- 78 | from __future__ import unicode_literals 79 | 80 | from django.contrib import admin 81 | 82 | from .models import Student 83 | 84 | 85 | class StudentAdmin(admin.ModelAdmin): 86 | list_display = ('id', 'name', 'sex', 'profession', 'email', 'qq', 'phone', 'status', 'created_time') 87 | list_filter = ('sex', 'status', 'created_time') 88 | search_fields = ('name', 'profession') 89 | fieldsets = ( 90 | (None, { 91 | 'fields': ( 92 | 'name', 93 | ('sex', 'profession'), 94 | ('email', 'qq', 'phone'), 95 | 'status', 96 | ) 97 | }), 98 | ) 99 | 100 | 101 | admin.site.register(Student, StudentAdmin) 102 | 103 | 写完这两个配置,model和admin的界面就ok了,接下来我们把这个``student``app放到settings.py中。 104 | 105 | 我们只需要在INSTALLED_APPS配置的最后,或者最前面增加'student'即可: 106 | 107 | settings.py文件: 108 | 109 | INSTALLED_APPS = [ 110 | 'student', 111 | 112 | 'django.contrib.admin', 113 | 'django.contrib.auth', 114 | 'django.contrib.contenttypes', 115 | 'django.contrib.sessions', 116 | 'django.contrib.messages', 117 | 'django.contrib.staticfiles', 118 | ] 119 | 120 | 好了,后台部分就完成了,我们来创建下表以及超级用户,通过下面命令: 121 | 122 | * ``cd student_house/student_sys/`` 123 | * ``python manage.py makemigrations`` 创建迁移文件 124 | * ``python manage.py migrate`` 创建表 125 | * ``python manage.py createsuperuser`` 根据提示,输出用户名,邮箱,密码 126 | 127 | 启动项目: ``python manage.py runserver``,访问: http://127.0.0.1:8000,看到一个提示页,这是因为我们还没开发首页。我们可以进入到admin的页面: http://127.0.0.1:8000/admin/。用你创建好的账户登录,就能看到一个完整的带有CURD的后台了。 128 | 129 | 130 | ## 基础配置(中文) 131 | 132 | 通过上面的配置,你看到的界面应该是英文的,并且时区也是UTC时区。所以我们需要进一步配置。 133 | 134 | 在settings中有如下配置: 135 | 136 | LANGUAGE_CODE = 'zh-hans' # 语言 137 | 138 | TIME_ZONE = 'Asia/Shanghai' # 时区 139 | 140 | USE_I18N = True # 语言 141 | 142 | USE_L10N = True # 数据和时间格式 143 | 144 | USE_TZ = True # 启用时区 145 | 146 | 修改完这些之后,刷新下试试。你可以尝试修改上面的配置,看看分别对应什么功能。 147 | 148 | 到这一部分我们基本上完成了admin的部分。下一节我们来完成页面提交数据的部分,看下如何使用Form。 149 | -------------------------------------------------------------------------------- /chapter3/section3.md: -------------------------------------------------------------------------------- 1 | # 学员管理系统- 前台 2 | 3 | ## 开发首页 4 | 5 | 有了上一节的Model和Admin的部分,我们接着来做一个简单的用户提交申请的表单页面。 6 | 7 | 首先在student/views.py文件中编写下面的代码: 8 | 9 | # -*- coding: utf-8 -*- 10 | from __future__ import unicode_literals 11 | 12 | from django.shortcuts import render 13 | 14 | 15 | def index(request): 16 | words = 'World!' 17 | return render(request, 'index.html', context={'words': words}) 18 | 19 | 上面的代码中,我们用了django提供的一个快捷的方法``render()``来渲染页面,使用模板``index.html``文件。我们需要在student目录下创建``templates``文件夹,这个文件夹是Django在渲染页面时会默认查找的。 20 | 21 | 这部分需要多说几句的是,Django查找会去每个App下,也就是我们的settings.py文件中配置的``INSTALLED_APPS``中的app下的``templates``文件夹中查找你在``render``上用到的模板,并且是顺序查找。这意味着,如果你有两个app,比如studentA, studentB,而这两个app中都存在``templates/index.html``,那么Django会加载位置在前的那个App的index.html文件。这个你可以自行尝试下。通过这种方式我们也可以重写admin的模板。 22 | 23 | 创建好``templates/index.html``之后,我们编写页面代码。简单来做: 24 | 25 | 26 | 27 | 28 | 学员管理系统-by the5fire 29 | 30 | 31 | Hello {{ words }}! 32 | 33 | 34 | 35 | 36 | 接着我们需要再配置下url,也就是提供一个url映射,可以让用户访问url时把数据发送到我们定义的``index`` 这个view上。 37 | 38 | 我们直接来修改student_sys目录下的urls.py文件: 39 | 40 | # coding:utf-8 41 | 42 | from django.conf.urls import url 43 | from django.contrib import admin 44 | 45 | from student.views import index 46 | 47 | urlpatterns = [ 48 | url(r'^$', index, name='index'), 49 | url(r'^admin/', admin.site.urls), 50 | ] 51 | 52 | 这样改完,我们再次启动项目: ``python manage.py runserver``,访问:http://127.0.0.1:8000就能看到我们输出的``Hello World!!' 了。 53 | 54 | 55 | ## 输出数据 56 | 57 | 接下来的工作就是把数据从表里面取出来,渲染到页面上了。你可以现在admin后台创建几条学员数据,以便于我们测试。 58 | 59 | 我们需要修改views.py中的代码: 60 | 61 | # -*- coding: utf-8 -*- 62 | from __future__ import unicode_literals 63 | 64 | from django.shortcuts import render 65 | 66 | from .models import Student 67 | 68 | 69 | def index(request): 70 | students = Student.objects.all() 71 | return render(request, 'index.html', context={'students': students}) 72 | 73 | 接着修改index.html中的代码: 74 | 75 | 76 | 77 | 78 | 学员管理系统-by the5fire 79 | 80 | 81 |
    82 | {% for student in students %} 83 |
  • {{ student.name }} - {{ student.get_status_display }}
  • 84 | {% endfor %} 85 |
86 | 87 | 88 | 89 | 这样我们就输出了一个简单的列表,展示学员名称和目前状态。这里有一个地方需要注意的是``{{ student.get_status_display }}``,在Model中我们只定义了``status``字段,并未定义这样的字段,为什么能通过这种方式取到数据呢。并且我们在Admin中,也没有使用这样的字段。 90 | 91 | 原因就是,对于设置了choices的字段,Django会帮我们提供一个方法(注意,是方法),用来获取这个字段对应的要展示的值。回头看下我们``status``的定义: 92 | 93 | ## 省略上下文代码 94 | 95 | STATUS_ITEMS = [ 96 | (0, '申请'), 97 | (1, '通过'), 98 | (2, '拒绝'), 99 | ] 100 | 101 | status = models.IntegerField(choices=STATUS_ITEMS, verbose_name="审核状态") 102 | 103 | ## 省略上下文代码 104 | 105 | 在admin中,展示带有choices属性的字段时,Django会自动帮我们调用``get_status_display``方法,所以我们不用配置。而在我们自己写的模板中,我们需要自己来写。并且在模板中不支持函数/方法调用,你只需要写方法名称即可,后面的括号不需要写。Django会自行帮你调用(如果是方法的话)。 106 | 107 | ## 提交数据 108 | 109 | 输出数据之后,我们再来开发提交数据的功能。这部分我们用一下Form。 110 | 111 | 首先我们先创建一个forms.py的文件,跟views.py同级。编写如下代码: 112 | 113 | # coding:utf-8 114 | from __future__ import unicode_literals 115 | 116 | from django import forms 117 | 118 | from .models import Student 119 | 120 | 121 | class StudentForm(forms.Form): 122 | name = forms.CharField(label='姓名', max_length=128) 123 | sex = forms.ChoiceField(label='性别', choices=Student.SEX_ITEMS) 124 | profession = forms.CharField(label='职业', max_length=128) 125 | email = forms.EmailField(label='邮箱', max_length=128) 126 | qq = forms.CharField(label='QQ', max_length=128) 127 | phone = forms.CharField(label='手机', max_length=128) 128 | 129 | 看这个``StudentForm``的定义是不是很熟悉,跟Model的定义类似,那么我们能不能复用Model的代码呢。答案是可以。还记得我们上节文档介绍的部分吗?有一个ModelForm可以用。我们来改下。 130 | 131 | # coding:utf-8 132 | from __future__ import unicode_literals 133 | 134 | from django import forms 135 | 136 | from .models import Student 137 | 138 | 139 | class StudentForm(forms.ModelForm): 140 | class Meta: 141 | model = Student 142 | fields = ( 143 | 'name', 'sex', 'profession', 144 | 'email', 'qq', 'phone' 145 | ) 146 | 147 | 只需要这么改就ok,不需要重复定义N多个字段。如果有修改对应字段类型的需求,比如把qq改成``IntegerField``用来做数字校验,也是可以声明出来。也可以通过定义``clean``方法的方式来做,我们来改下代码,增加QQ号必须为纯数字的校验: 148 | 149 | # coding:utf-8 150 | from __future__ import unicode_literals 151 | 152 | from django import forms 153 | 154 | from .models import Student 155 | 156 | 157 | class StudentForm(forms.ModelForm): 158 | def clean_qq(self): 159 | cleaned_data = self.cleaned_data['qq'] 160 | if not cleaned_data.isdigit(): 161 | raise forms.ValidationError('必须是数字!') 162 | 163 | return int(cleaned_data) 164 | 165 | class Meta: 166 | model = Student 167 | fields = ( 168 | 'name', 'sex', 'profession', 169 | 'email', 'qq', 'phone' 170 | ) 171 | 172 | 其中``clean_qq``就是Django的form会自动调用,来处理每个字段的方法,比如在这个form中你可以通过定义``clean_phone``来处理电话号码,可以定义``clean_email``来处理邮箱等等。如果验证失败,可以通过``raise forms.ValidationError('必须是数字!')``的方式返回错误信息,这个信息会存储在form中,最终会被我们渲染到页面上。 173 | 174 | 175 | 有了form,我们接下来需要做的就是在页面中展示form,让用户能够填写信息提交表单。同时对于提交的数据,我们需要先做校验,通过后可以保存到数据库中。来看下views.py中的文件最终的样子: 176 | 177 | # -*- coding: utf-8 -*- 178 | from __future__ import unicode_literals 179 | 180 | from django.http import HttpResponseRedirect 181 | from django.urls import reverse 182 | from django.shortcuts import render 183 | 184 | from .models import Student 185 | from .forms import StudentForm 186 | 187 | 188 | def index(request): 189 | students = Student.objects.all() 190 | if request.method == 'POST': 191 | form = StudentForm(request.POST) 192 | if form.is_valid(): 193 | cleaned_data = form.cleaned_data 194 | student = Student() 195 | student.name = cleaned_data['name'] 196 | student.sex = cleaned_data['sex'] 197 | student.email = cleaned_data['email'] 198 | student.profession = cleaned_data['profession'] 199 | student.qq = cleaned_data['qq'] 200 | student.phone = cleaned_data['phone'] 201 | student.save() 202 | return HttpResponseRedirect(reverse('index')) 203 | else: 204 | form = StudentForm() 205 | 206 | context = { 207 | 'students': students, 208 | 'form': form, 209 | } 210 | return render(request, 'index.html', context=context) 211 | 212 | 里面有一个``form.cleaned_data``,这个对象是Django的form对用户提交的数据根据字段类型做完转换之后的结果。另外还有``reverse``的使用,我们在urls.py中定义``index``的时候,声明了``name='index'``,所以我们这里可以通过``reverse``来拿到对应的url。这么做的好处是,不需要硬编码url到代码中,这意味着如果以后有修改url的需求,只要index的名称不变,这个地方的代码就不用改。 213 | 214 | 写完views.py中的代码之后,我们要把form传到模板中,这样用户才能最终看到一个可以填写数据的表单。要在模板中加form,是相当简单的一件事。最终模板(index.html)代码如下: 215 | 216 | 217 | 218 | 219 | 学员管理系统-by the5fire 220 | 221 | 222 |

Admin

223 |
    224 | {% for student in students %} 225 |
  • {{ student.name }} - {{ student.get_status_display }}
  • 226 | {% endfor %} 227 |
228 |
229 |
230 | {% csrf_token %} 231 | {{ form }} 232 | 233 |
234 | 235 | 236 | 237 | 其中 ``{% csrf_token %}``是Django对提交数据安全性做的校验,这意味着,如果没有这个token,提交过去的数据是无效的。这是用来防止跨站伪造请求攻击的一个手段。 238 | 239 | 只需要这么写,Django就会帮我们自动把所有字段列出来。当然,如果需要调整样式,那就要自己来增加css样式文件解决了。 240 | 241 | 不过到此为止,功能上已经完备。你可以再次通过命令: ``python manage.py runserver``,访问: http://localhost:8000测试下页面功能是否可用。 242 | -------------------------------------------------------------------------------- /chapter3/section4.md: -------------------------------------------------------------------------------- 1 | # 进阶部分 2 | 3 | 虽然是一个简单的Demo,但是有句老话叫:麻雀虽小五脏俱全,我们也得把常用的功能使用到。所以增加这一部分,包括:Class Base View, Middleware, TestCase这三个部分。 4 | 5 | *注意*,如果你前面的例子没有跑起来,可以先不看这一节,先把前面的代码跑起来再说。不然,你可以能越学越乱。 6 | 7 | 8 | ## Class Based View 9 | 10 | 在如何阅读文档的部分,我又讲到,如果你有很多类似的view方法,那么你可以考虑抽象出一个ClassBased View来。这样可以更好的复用你的代码。 11 | 12 | 不过对于我们的需求来说,用ClassBased View不是很必要,我们只是演示用法。用类的方式有一个好处就是我们可以分离``get``和``post``的处理逻辑。回头看下上节``views.py``中的代码,其中有一个关于``request.method``的判断。我们来通过类级的View去掉层控制语句。 13 | 14 | 来看完整的views.py代码: 15 | 16 | # -*- coding: utf-8 -*- 17 | from __future__ import unicode_literals 18 | 19 | from django.http import HttpResponseRedirect 20 | from django.urls import reverse 21 | from django.shortcuts import render 22 | from django.views import View 23 | 24 | from .models import Student 25 | from .forms import StudentForm 26 | 27 | 28 | class IndexView(View): 29 | template_name = 'index.html' 30 | 31 | def get_context(self): 32 | students = Student.objects.all() 33 | context = { 34 | 'students': students, 35 | } 36 | return context 37 | 38 | def get(self, request): 39 | context = self.get_context() 40 | form = StudentForm() 41 | context.update({ 42 | 'form': form 43 | }) 44 | return render(request, self.template_name, context=context) 45 | 46 | def post(self, request): 47 | form = StudentForm(request.POST) 48 | if form.is_valid(): 49 | cleaned_data = form.cleaned_data 50 | student = Student() 51 | student.name = cleaned_data['name'] 52 | student.sex = cleaned_data['sex'] 53 | student.email = cleaned_data['email'] 54 | student.profession = cleaned_data['profession'] 55 | student.qq = cleaned_data['qq'] 56 | student.phone = cleaned_data['phone'] 57 | student.save() 58 | return HttpResponseRedirect(reverse('index')) 59 | context = self.get_context() 60 | context.update({ 61 | 'form': form 62 | }) 63 | return render(request, self.template_name, context=context) 64 | 65 | 你可能已经发现了,代码量突然变多了。本来一个函数可以解决的问题,现在却有了一个类,和多一个方法。对,这么做的道理就是让每一部分变的跟明确,比如``get``就是来处理get请求,``post``就是来处理post请求。维护的时候不需要像之前那样,所有的需求都去改一个函数。 66 | 67 | 理解了这么做的原因,我们来改下urls.py的定义,完整的代码如下: 68 | 69 | # coding:utf-8 70 | 71 | from django.conf.urls import url 72 | from django.contrib import admin 73 | 74 | from student.views import IndexView 75 | 76 | urlpatterns = [ 77 | url(r'^$', IndexView.as_view(), name='index'), 78 | url(r'^admin/', admin.site.urls), 79 | ] 80 | 81 | 只是把之前的index改为了``IndexView.as_view()``,这个``as_view()``其实是对get和post方法的一个包装。里面做的事情,你可以简单的理解为我们上一节中自己写的判断``request.method``的逻辑。 82 | 83 | 84 | ## Middleware 85 | 86 | 这个需求中似乎没有需要用到Middleware的地方,不过我们可以生造一个,来练练手。 87 | 88 | 我们有这样一个需求,统计首页每次访问所消耗的时间,也就是wsgi接口或者socket接口接到请求,到最终返回的时间。先来创建一个middlewares.py的文件吧,在views.py的同级目录中。我们先来看下完整的代码: 89 | 90 | # coding:utf-8 91 | import time 92 | 93 | from django.utils.deprecation import MiddlewareMixin 94 | from django.urls import reverse 95 | 96 | 97 | class TimeItMiddleware(MiddlewareMixin): 98 | def process_request(self, request): 99 | return 100 | 101 | def process_view(self, request, func, *args, **kwargs): 102 | if request.path != reverse('index'): 103 | return None 104 | 105 | start = time.time() 106 | response = func(request) 107 | costed = time.time() - start 108 | print('{:.2f}s'.format(costed)) 109 | return response 110 | 111 | def process_exception(self, request, exception): 112 | pass 113 | 114 | def process_template_response(self, request, response): 115 | return response 116 | 117 | def process_response(self, request, response): 118 | return response 119 | 120 | 上面的代码中列出了一个Middleware的完整接口,虽然我们只用到了``process_view``。下面我们来逐个了解下: 121 | 122 | * ``process_request`` - 一个请求来到middelware层,进入的第一个方法。一般情况我们可以在这里做一些校验,比如用户登录,或者HTTP中是否有认证头之类的验证。这个方法需要两种返回值,HttpResponse或者None,如果返回HttpResponse,那么接下来的处理方法只会执行``process_response``,其他的方法将不会被执行。这里**需要注意的是**,如果你的middleware在settings配置的MIDDLEWARE_CLASS的第一个的话,那么剩下的middleware也不会被执行。另外一个返回值是None,如果返回None,那么Django会继续执行其他的方法。 123 | 124 | * ``process_view`` - 这个方法是在``process_request``之后执行的,参数如上面代码所示,其中的func就是我们将要执行的view方法,因此我们要统计一个view的执行时间,可以在这里来做。它的返回值跟``process_request``一样,HttpResponse/None,逻辑也是一样。如果返回None,那么Django会帮你执行view函数,从而得到最终的Response。 125 | 126 | * ``process_template_response`` - 执行完上面的方法,并且Django帮忙我们执行完view之后,拿到最终的response,如果是使用了模板的Response(是指通过``return render(request, 'index.html', context={})``的方式返回Response,就会来到这个方法中。这个方法中我们可以对response做一下操作,比如``Content-Type``设置,或者其他HEADER的修改/增加。 127 | 128 | * ``process_response`` - 当所有流程都处理完毕,就来到了这个方法,这个方法的逻辑跟``process_template_response``是完全一样的。只是``process_template_response``是针对带有模板的response的处理。 129 | 130 | * ``process_exception`` - 上面的所有处理方法是按顺序介绍的,而这个不太一样。只有在发生异常时,才会进入到这个方法。哪个阶段发生的异常呢?可以简单的理解为在将要调用的view中出现异常(就是在``process_view``的``func``函数中)或者返回的模板Response在render时发生的异常,会进入到这个方法中。但是**需要注意的是**,如果你在``process_view``中手动调用了``func``,就像我们上面做的那样,那就不会触发``process_exception``了。这个方法接收到异常之后,可以选择处理异常,然后返回一个含有异常信息的HttpResponse,或者直接返回None,不处理,这种情况Django会使用自己的异常模板。 131 | 132 | 133 | 这是一层Middleware中所有方法的执行顺序和说明,那么如果有多个Middleware配置,执行顺序应该是怎样的呢?我们可以通过下面的一个图来理解下。 134 | 135 | ![django-middleware](../images/middleware.svg) 136 | 137 | 138 | ## TestCase 139 | 140 | 单元测试是实际开发中,很重要,但是经常被忽视的部分。原因主要是编写对于Web功能的测试所耗费的时间会高于你开发此功能的时间。因此对于需要快速开发、上线的业务来说,这个项目中关于单页测试的部分很少。 141 | 142 | 单元测试的主要目的是为了让你的代码更健壮,尤其是在进行重构或者业务增加的时候。跑通单元测试,就意味着新加入的代码,或者你修改的代码没有问题。我们在实际开发中单元测试的覆盖率是比较低,原因主要也是上面说的,写单元测试的成本,尤其是对于很复杂的业务,另外一个就是团队成员的意识。但是为了保障在业务不断扩张的同时系统的稳定,对于负责的基础的逻辑,以及整体的功能会编写测试代码。 143 | 144 | 另外一个问题是公司有没有专门的测试人员,来保障每次上线的功能都可用,进行功能上的回归测试。如果没有专门的测试人员,那单元测试,或者集成测试,就是很有必要的。即便是有专门的测试,也可以通过自动化测试来加快项目进度。从我经历过的几次线上环境的事故来看,很多细小的问题,在人工测试阶段很难被发现。所以关于单元测试,我的建议是,关键部分的单元测试一定要有,集成测试一定要有。 145 | 146 | 对于Web项目来说,单元测试是一件很复杂的事,因为它的输入输出不像一个函数那样简单。好在Django给我们提供了相对好用的测试工具。单元测试本身是一个很大的话题,在这一小节我们只演示我们现在正在开发的这个项目``学员管理系统``中如何使用单元测试。 147 | 148 | ### TestCase中几个方法的说明 149 | 150 | 在Django中运行测试用例时,如果我们用的是sqlite数据库,Django会帮我们创建一个基于内存的测试数据库,用来测试。这意味着我们测试中所创建的数据,对我们的开发环境或者线上环境是没有影响的。 151 | 152 | 但是对于MySQL数据库,Django会直接用配置的数据库用户和密码创建一个``test_student_db``的数据库,用于测试,因此需要保证有建表和建库的权限。 153 | 154 | 你也可以定义测试用的数据库的名称,通过settings配置: 155 | 156 | DATABASES = { 157 | 'default': { 158 | 'ENGINE': 'django.db.backends.postgresql', 159 | 'USER': 'mydatabaseuser', 160 | 'NAME': 'mydatabase', 161 | 'TEST': { 162 | 'NAME': 'mytestdatabase', ## 这里配置 163 | }, 164 | }, 165 | } 166 | 167 | 下面对需要用到的几个方法做下说明: 168 | 169 | * ``def setUp(self)`` - 如其名,用来初始化环境,包括创建初始化的数据,或者做一些其他的准备的工作。 170 | * ``def test_xxxx(self)`` - 方法后面的xxxx可以是任意的东西,以``test_``开头的方法,会被认为是需要测试的方法,跑测试时会被执行。每个需要被测试的方法是相互独立的。 171 | * ``def tearDown(self)`` - 跟``setUp``相对,用来清理测试环境和测试数据。在Django中,我们可以不关心这个。 172 | 173 | 174 | ### Model层测试 175 | 176 | 这一层的测试,主要是来保证数据的写入和查询是可用的,同时也需要保证我们在Model层所提供的方法是符合预期的。比如我们的Model中实现了``__unicode__``方法,保证在Python2中运行时,直接print(或者直接在web界面展示) student对象时,能看到````这样的字样,而不是Python中的``object xxxxx``这样东西。 177 | 178 | 我们来看下代码: 179 | 180 | # -*- coding: utf-8 -*- 181 | from __future__ import unicode_literals 182 | 183 | from django.test import TestCase 184 | 185 | from .models import Student 186 | 187 | 188 | class StudentTestCase(TestCase): 189 | def setUp(self): 190 | Student.objects.create( 191 | name='test', 192 | sex=1, 193 | email='333@dd.com', 194 | profession='程序员', 195 | qq='3333', 196 | phone='32222', 197 | ) 198 | 199 | def test_create_and_unicode(self): 200 | student = Student.objects.create( 201 | name='test', 202 | sex=1, 203 | email='333@dd.com', 204 | profession='程序员', 205 | qq='3333', 206 | phone='32222', 207 | ) 208 | student_name = '' 209 | self.assertEqual(unicode(student), student_name, 'student __unicode__ must be {}'.format(student_name)) 210 | 211 | def test_filter(self): 212 | students = Student.objects.filter(name='test') 213 | self.assertEqual(students.count(), 1, 'only one is right') 214 | 215 | 在``setUp``我们创建了一条数据用于测试。``test_create_and_unicode``用来测试数据创建和自定义的``__unicode__``方法有效,``test_filter``测试查询可用。 216 | 217 | 218 | ### view层测试 219 | 220 | 这一层更多的是功能上的测试,也是我们一定要写的,功能上的可用是比什么都重要的事情。当然这事你可以通过手动浏览器访问来测试,但是如果你有几百个页面呢? 221 | 222 | 这部分的测试逻辑依赖Django提供的``Django.test.Client``对象。在上面的文件中``tests.py``中,我们增加下面两个函数: 223 | 224 | def test_get_index(self): 225 | client = Client() 226 | response = client.get('/') 227 | self.assertEqual(response.status_code, 200, 'status code must be 200!') 228 | 229 | def test_post_student(self): 230 | client = Client() 231 | data = dict( 232 | name='test_for_post', 233 | sex=1, 234 | email='333@dd.com', 235 | profession='程序员', 236 | qq='3333', 237 | phone='32222', 238 | ) 239 | response = client.post('/', data) 240 | self.assertEqual(response.status_code, 302, 'status code must be 302!') 241 | 242 | response = client.get('/') 243 | self.assertTrue(b'test_for_post' in response.content, 'response content must contain `test_for_post`') 244 | 245 | 246 | ``test_get_index``的作用是请求首页,并且得到正确的响应——status code = 200,``test_post_student``的作用是提交数据,然后请求首页,检查数据是否存在。 247 | 248 | ## 总结 249 | 这一部分中的三个技能点的使用,有助于你更好的理解Django,但是如果你需要更多的掌握着三个部分的内容,需要进一步的实践才行。这是我们之后要做的事了。不过关于测试部分,不仅仅是Django方面的只是,测试是一个单独的话题/领域,有兴趣的话可以找更专业的书籍来看。 250 | 251 | 小试牛刀部分就到这,其中的代码建议读者手敲一遍,在自己的Linux或者Mac上运行一下,改改代码,再次运行。别怕麻烦,也别赶进度。我经常说,所谓捷径就是一步一个脚印,每步都能前进/提高。 252 | 253 | 下一部分开始,我们将进入正式的开发阶段,请系好安全带,握紧键盘,跟上。 254 | -------------------------------------------------------------------------------- /images/middleware.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2012-12-09 19:48ZCanevas 1Calque 1CommonMiddlewareSessionMiddlewareCsrfViewMiddlewareAuthenticationMiddlewareMessageMiddlewareHttpRequestHttpResponseprocess_requestprocess_viewprocess_template_responseprocess_responseprocess_exceptionview function 4 | -------------------------------------------------------------------------------- /chapter2/section2.md: -------------------------------------------------------------------------------- 1 | # WSGI——Web框架基础 2 | 3 | ## 简介 4 | 5 | WSGI,全称是Web Server Gateway Interface(Web服务网关接口)。 6 | 7 | 这是Python中的定义的一个网关协议,规定了Web Server如何跟应用程序进行交互。Web server可以理解为一个Web应用的容器,可以通过Web server来启动应用,进而提供http服务。而应用程序是指我们基于框架所开发的系统。 8 | 9 | 这个协议最主要的目的就是保证在Python中,所有Web Server程序或者说Gateway程序,能够通过统一的协议跟web框架,或者Web应用进行交互。这对于部署Web程序来说很重要,你可以选择任何一个实现了WSGI协议的Web Server来跑你的程序。 10 | 11 | 如果没有这个协议,那可能每个程序,每个Web Server都会各自实现各自的接口。 12 | 13 | 这一节我们来简单了解下WSGI协议是如何运作的,理解这一协议非常重要,因为在Python中大部分的Web框架都实现了此协议,在部署时也使用WSGI容器来进行部署。 14 | 15 | 16 | ## 简单的Web Server 17 | 18 | 在看WSGI协议之前,我们先来看一个通过socket编程实现的Web服务的代码。逻辑很简单,就是通过监听本地8080端口,接受客户端发过来的数据,然后返回对应的HTTP的响应。 19 | 20 | # 文件位置:/code/chapter2/section2/socket_server.py 21 | # coding:utf-8 22 | 23 | import socket 24 | 25 | EOL1 = '\n\n' 26 | EOL2 = '\n\r\n' 27 | body = '''Hello, world!

from the5fire 《Django企业开发实战》

''' 28 | response_params = [ 29 | 'HTTP/1.0 200 OK', 30 | 'Date: Sat, 10 jun 2017 01:01:01 GMT', 31 | 'Content-Type: text/plain; charset=utf-8', 32 | 'Content-Length: {}\r\n'.format(len(body)), 33 | body, 34 | ] 35 | response = b'\r\n'.join(response_params) 36 | 37 | 38 | def handle_connection(conn, addr): 39 | request = "" 40 | while EOL1 not in request and EOL2 not in request: 41 | request += conn.recv(1024) 42 | print(request) 43 | conn.send(response) 44 | conn.close() 45 | 46 | 47 | def main(): 48 | # socket.AF_INET 用于服务器与服务器之间的网络通信 49 | # socket.SOCK_STREAM 基于TCP的流式socket通信 50 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 51 | # 设置端口可复用,保证我们每次Ctrl C之后,快速再次重启 52 | serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 53 | serversocket.bind(('127.0.0.1', 8080)) 54 | # 可参考:https://stackoverflow.com/questions/2444459/python-sock-listen 55 | serversocket.listen(1) 56 | print('http://127.0.0.1:8080') 57 | 58 | try: 59 | while True: 60 | conn, address = serversocket.accept() 61 | handle_connection(conn, address) 62 | finally: 63 | serversocket.close() 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | 69 | 代码的逻辑很简单,但是建议你在自己的电脑上敲一遍,然后Python2运行起来(用Python3的话需要做些调整),通过浏览器访问是否能收到正确响应。并且修改其中代码,观察结果。比如说修改上面`Content-Type: text/plain` 中的 `plain` 为 `html` ,然后Ctrl C结束进程,重新运行,刷新页面,看看结果。 70 | 71 | 理解这段代码很重要,这是Web服务最基本的模型,通过socket和HTTP协议,提供Web服务。建议你在理解上面的代码之前,不要继续往下学习。 72 | 73 | ## 简单的WSGI application 74 | 75 | 理解了上面的代码之后,我们继续看看WSGI协议,也就是我们一开头介绍的。WSGI协议分为两部分,其中一部分是Web Server或者Gateway,就像上面的代码一样,监听在某个端口上,接受外部的请求。另外一部分是Web Application,Web Server接受到请求之后会通过WSGI协议规定的方式把数据传递给Web Application,我们在Web Application中处理完之后,设置对应的状态和HEADER,之后返回body部分。Web Server拿到返回数据之后,再进行HTTP协议的封装,最终返回完整的HTTP Response数据。 76 | 77 | 这么说可能比较抽象,我们还是通过代码来演示下这个流程。我们先实现一个简单的application: 78 | 79 | # 文件位置:/code/chapter2/section2/wsgi_example/app.py 80 | # coding:utf-8 81 | 82 | 83 | def simple_app(environ, start_response): 84 | """Simplest possible application object""" 85 | status = '200 OK' 86 | response_headers = [('Content-type', 'text/plain')] 87 | start_response(status, response_headers) 88 | return ['Hello world! -by the5fire \n'] 89 | 90 | 这就是一个简单的application,那么我们要怎么运行它呢?我们先按照Python PEP3333文档上的实例代码来运行它。这是一个cgi的脚本。 91 | 92 | # 文件位置:/code/chapter2/section2/wsgi_example/gateway.py 93 | 94 | # coding:utf-8 95 | 96 | import os 97 | import sys 98 | 99 | from app import simple_app 100 | 101 | 102 | def run_with_cgi(application): 103 | environ = dict(os.environ.items()) 104 | environ['wsgi.input'] = sys.stdin 105 | environ['wsgi.errors'] = sys.stderr 106 | environ['wsgi.version'] = (1, 0) 107 | environ['wsgi.multithread'] = False 108 | environ['wsgi.multiprocess'] = True 109 | environ['wsgi.run_once'] = True 110 | 111 | if environ.get('HTTPS', 'off') in ('on', '1'): 112 | environ['wsgi.url_scheme'] = 'https' 113 | else: 114 | environ['wsgi.url_scheme'] = 'http' 115 | 116 | headers_set = [] 117 | headers_sent = [] 118 | 119 | def write(data): 120 | if not headers_set: 121 | raise AssertionError("write() before start_response()") 122 | 123 | elif not headers_sent: 124 | # Before the first output, send the stored headers 125 | status, response_headers = headers_sent[:] = headers_set 126 | sys.stdout.write('Status: %s\r\n' % status) 127 | for header in response_headers: 128 | sys.stdout.write('%s: %s\r\n' % header) 129 | sys.stdout.write('\r\n') 130 | 131 | sys.stdout.write(data) 132 | sys.stdout.flush() 133 | 134 | def start_response(status, response_headers, exc_info=None): 135 | if exc_info: 136 | try: 137 | if headers_sent: 138 | # Re-raise original exception if headers sent 139 | raise exc_info[0], exc_info[1], exc_info[2] 140 | finally: 141 | exc_info = None # avoid dangling circular ref 142 | elif headers_set: 143 | raise AssertionError("Headers already set!") 144 | 145 | headers_set[:] = [status, response_headers] 146 | return write 147 | 148 | result = application(environ, start_response) 149 | try: 150 | for data in result: 151 | if data: # don't send headers until body appears 152 | write(data) 153 | if not headers_sent: 154 | write('') # send headers now if body was empty 155 | finally: 156 | if hasattr(result, 'close'): 157 | result.close() 158 | 159 | if __name__ == '__main__': 160 | run_with_cgi(simple_app) 161 | 162 | 我们运行一下这个脚本: python gateway.py,在命令行上能够看到对应的输出: 163 | 164 | Status: 200 OK 165 | Content-type: text/plain 166 | 167 | Hello world! -by the5fire 168 | 169 | 对比下一开始我们通过socket写的server,这个就是一个最基本的HTTP响应了。如果输出给浏览器,浏览器会展示出`Hello world! -by the5fire`的字样。 170 | 171 | 我们再通过另外一种方式来运行我们的Application,用到的这个工具就是gunicorn。你可以先通过命令`pip install gunicorn`进行安装。 172 | 173 | 安装完成之后,进入到app.py脚本的目录。通过命令: `gunicorn app:simle_app` 来启动程序。这里的gunicorn就是一个Web Server。启动之后会看到如下输出: 174 | 175 | [2017-06-10 22:52:01 +0800] [48563] [INFO] Starting gunicorn 19.4.5 176 | [2017-06-10 22:52:01 +0800] [48563] [INFO] Listening at: http://127.0.0.1:8000 (48563) 177 | [2017-06-10 22:52:01 +0800] [48563] [INFO] Using worker: sync 178 | [2017-06-10 22:52:01 +0800] [48566] [INFO] Booting worker with pid: 48566 179 | 180 | 通过浏览器访问:http://127.0.0.1:8000 就能看到对应的页面了。 181 | 182 | 183 | ## 理解WSGI 184 | 185 | 通过上面的代码,你应该看到了简单的application中对WSGI协议的实现。你可以在`simple_app`方法中增加print语句来查看参数分别是什么。 186 | 187 | WSGI协议规定,application必须是一个callable对象,这意味这个对象可以是Python中的一个函数,也可以是一个实现了``__call__``方法的类的实例。比如这个: 188 | 189 | # 文件位置:/code/chapter2/section2/wsgi_example/app.py 190 | 191 | class AppClass(object): 192 | status = '200 OK' 193 | response_headers = [('Content-type', 'text/plain')] 194 | 195 | def __call__(self, environ, start_response): 196 | print(environ, start_response) 197 | start_response(self.status, self.response_headers) 198 | return ['Hello AppClass.__call__\n'] 199 | 200 | application = AppClass() 201 | 202 | 203 | 我们依然可以通过gunicorn这个WSGI Server来启动应用: ``gunicorn app:aplication``,再次访问 http://127.0.0.1:8000 看看是不是输出了同样的内容。 204 | 205 | 除了这种方式之外,我们可以通过另外一种方式实现WSGI协议,从上面 ``simple_app`` 和这里 ``AppClass.__call__``的返回值来看,WSGI Server中只需要一个可迭代的对象就行,callable也就是返回一个列表。那么我们可以用下面这种方式达到同样的结果: 206 | 207 | class AppClassIter(object): 208 | status = '200 OK' 209 | response_headers = [('Content-type', 'text/plain')] 210 | 211 | def __init__(self, environ, start_response): 212 | self.environ = environ 213 | self.start_response = start_response 214 | 215 | def __iter__(self): 216 | self.start_response(self.status, self.response_headers) 217 | yield 'Hello AppClassIter\n' 218 | 219 | 220 | 我们再次使用gunicorn来启动: ``gunicorn app:AppClassIter``,然后打开浏览器访问 http://127.0.0.1:8000,看看结果。 221 | 222 | 这里的启动命令并不是一个类的实例,而是类本身,为什么呢?通过上面两个代码,我们可以观察到能够被调用的方法会传environ和start_response过来,而现在这个实现,没有可调用的方式,所以就需要在实例化的时候通过参数传递进来,这样在返回body之前,可以先调用start_response方法。 223 | 224 | 所以我们可以推测出WSGI Server是如何调用WSGI Application的。大概代码如下: 225 | 226 | def start_response(status, headers): 227 | # 伪代码 228 | set_status(status) 229 | for k, v in headers: 230 | set_header(k, v) 231 | 232 | def handle_conn(conn): 233 | # 调用我们定义的application(也就是上面的simple_app或者是AppClass的实例或者是AppClassIter本身) 234 | app = application(environ, start_response) 235 | # 遍历返回的结果,生成response 236 | for data in app: 237 | response += data 238 | 239 | conn.sendall(response) 240 | 241 | 大概如此。 242 | 243 | 244 | ## WSGI中间件和Werkzeug(WSGI工具集) 245 | 246 | 理解了上面的逻辑,我们就可以继续行程了。 247 | 248 | 除了交互部分的定义,WSGI还定义了中间件部分的逻辑,这个中间件可以理解为Python中的一个装饰器,可以在不改变原方法的同时对方法的输入和输出部分进行处理。 249 | 250 | 比方说对返回body中的文字部分,把英文转换为中文等之类的操作。或者是一些更为易用的操作,比如对返回内容的封装,上面的例子我们是先调用start_response方法,然后再返回body,我们能不能直接封装一个Response对象呢,直接给对象设置header,而不是这种单独操作的逻辑。比如像这样: 251 | 252 | def simple_app(environ, start_response): 253 | response = Repsonse('Hello World', start_repsonse=start_response) 254 | response.set_header('Content-Type', 'text/plain') 255 | return response 256 | 257 | 这样不是更加自然。 258 | 259 | 因此就存在了Werkzeug这样的WSGI工具集。让你能够跟WSGI协议更加友好的交互。理论上我们可以直接通过WSGI协议的简单实现,也就是我们上面的代码,写一个Web服务。但是有了Werkzeug之后,我们可以写的更加容易。在很多Web框架中都是通过Werkzeug来处理WSGI协议的内容的。 260 | 261 | 262 | ## 参考文档 263 | 264 | * [Python CGI](https://www.the5fire.com/python-project6-cgi.html) 265 | * [gunicorn-sync源码](https://github.com/benoitc/gunicorn/blob/master/gunicorn/workers/sync.py#L176) 266 | * [gunicorn-wsgi部分代码](https://github.com/benoitc/gunicorn/blob/master/gunicorn/http/wsgi.py#L241) 267 | * [PEP3333中文](http://pep-3333-wsgi.readthedocs.io/en/latest/) 268 | * [PEP3333英文](https://www.python.org/dev/peps/pep-3333/) 269 | * [Werkzeug官网](http://werkzeug.pocoo.org/) 270 | * [Werkzeug中文文档](http://werkzeug-docs-cn.readthedocs.io/zh_CN/latest/) 271 | 272 | ## 扩展阅读 273 | 274 | * [ASGI英文文档](https://channels.readthedocs.io/en/latest/asgi.html) 275 | * [ASGI中文翻译](https://blog.ernest.me/post/asgi-draft-spec-zh) 276 | * [Django SSE](https://www.the5fire.com/message-push-by-server-sent-event.html) 277 | --------------------------------------------------------------------------------