用Django 做一个完整的博客网站

面对对象:想学习Django却无从下手,看了文档,能完成新手教程,但自己开发一套系统时却无从下手

需求

需求文档

博客:仅音译,英文名为Blogger,为Web Log的混成词。它的正式名称为网络日记;又音译为部落格或部落阁等,是使用特定的软件,在网络上出版、发表和张贴个人文章的人,或者是一种通常由个人管理、不定期张贴新的文章的网站。博客上的文章通常以网页形式出现,并根据张贴时间,以倒序排列。通常具备RSS订阅功能。博客是继MSN、BBS、ICQ之后出现的第4种网络交流方式,现已受到大家的欢迎,是网络时代的个人“读者文摘”,是以超级链接为入口的网络日记,它代表着新的生活、工作和学习方式。许多博客专注在特定的课题上提供评论或新闻,其他则被作为个人性的日记。一个典型的博客结合了文字、图像、其他博客或网站的链接及其它与主题相关的媒体,能够让读者以互动的方式留下意见,是许多博客的重要要素。大部分的博客内容以文字为主,但仍有一些博客专注在艺术、摄影、视频、音乐、播客等各种主题。博客是社会媒体网络的一部分。比较著名的有新浪等博客。


需求描述:博客系统分为用户端和作者端
用户端需求:

  • 能够通过搜索引擎搜索到比克内容,来到博客
  • 可以在博客中进行关键词搜索,展示出文章列表
  • 能够根据分类查看这一分类的文章
  • 访问首页,能看到从新到旧的文章列表
  • 能够通过RSS阅读器订阅博主的文章
  • 能够对文章进行评论
  • 能够配置友链,方便与网友链接

作者端需求:

  • 博客后台需要登录才能进入
  • 能够创建分类和标签
  • 能够以MarkDown格式编写文章
  • 能够上传文章配图,有版权声明
  • 能够配置导航
  • 作者更新后,订阅读者可以收到通知

需求评审和分析

评审的目的:

  • 明确需求点,避免理解上的歧义
  • 确认技术可行性,避免延期或者后面再修改需求
  • 确认工期,是否需要分期开发
需求评审

用户端部分

  • 能够通过搜索引擎搜索到比克内容,来到博客:
    技术上说,这属于seo的部分,提供sitemap到搜索引擎即可。页面需要对爬虫友好。需要跟产品经理明确,技术上无法保证一定能够通过搜索引擎找到博客,这取决于搜索引擎本身。

  • 可以在博客中进行关键词搜索,展示出文章列表
    需要明确搜索那些字段,比如标题、标签、分类等。如果是全文搜索,就要考虑数据量的问题,如果数据量大的话,就不能直接使用MYSQL的LIKE语句,需要增加全文搜索相关的技术栈,比如引入Whoosh,Solr,Elasticsearch这样的搜索引擎。

  • 能够根据分类查看这一分类的文章
    要明确有没有子分类,如果有子分类,那么子分类要不要在父分类下展示,以及子分类的层级有没有限制。

  • 访问首页,能看到从新到旧的文章列表
    首页文章排序的时候,有没有特例,比如有些文章需要置顶。是通过分页的方式展示列表,还是通过不断下拉加载的方式,每次加载多少条数据。

  • 能够通过RSS阅读器订阅博主的文章
    需要提供RSS格式数据的页面

  • 能够对文章进行评论
    是否需要前端查看所有的评论

  • 能够配置友链,方便与网友链接
    友链如何展示,是单独的页面还是直接在前台展示。


作者端部分:

  • 博客后台需要登录才能进入
    是否有多用户登录的需求,如果有,用户之间的权限如何划分。

  • 能够创建分类和标签
    是否有多级分类的标签的情况, 如果有,需要明确父类是否包含子集所关联的内容。

  • 能够以MarkDown格式编写文章
    作者编写文章的时候,那些是必填项,是否实时保存。

  • 能够上传文章配图,有版权声明
    版权声明具体是什么?

  • 能够配置导航
    导航是否分类,是否包含标签?需要产品经理给出明确的需求

  • 作者更新后,订阅读者可以收到通知
    这个博客系统并不要求读者登录,也就无法对读者进行实时通知。可以考虑增加邮件订阅功能,以邮件的方式通知读者。需要明确邮件的格式,作者是否需要控制邮件发送的开关。


需求列表

用户端部分

  • 网站需要对SEO友好,需要给搜索引擎提供XML格式的sitemap.
  • 博客需要提供搜索功能,搜索范围限定在标题、分类和标签,博客每天增量数据为10篇。
  • 能够根据分类查看这一分类下的所有文章,分类没有层级,只有一级分类,一篇文章只能属于一个分类
  • 访问首页时,文章从新到旧排列,可以设置多篇文章置顶。
  • 首页,标签页,分类页都需要提供分页需求,每页展示10篇文章,列表页展示文章的时候,需要展示摘要,默认文章前140字。
  • 可以通过RSS阅读器订阅博主的文章
  • 可以对文章进行评论,评论不需要盖楼,在文章页面展示即可,在侧边栏也需要展示最新评论
  • 能够配置友链,方便与网友链接。友链在一个页面展示,不需要分类,需要指定友链的权重,权重高的在前展示

作者端部分

  • 博客后台需要登录才能进入,当前没有多用户需求,以后可能会有,需要保持可扩展性
  • 能够创建分类和标签,一篇文章只能属于一个分类,可以有多个标签。标签和分类没有层级关系
  • 作者在后台设置文章标题,摘要,正文,分类和标签,不需要实时保存,文章格式默认Markdown,开发周期够的话,可考虑增加可视化编辑器。
  • 增加文章配图时,图片需要加水印,水印内容为网站
  • 导航只是分类,默认展示在顶部,每篇文章需要有浏览路径,组成为首页>文章分类>正文。导航的顺序由权重决定,最多可有6个,多的则在底部展示。
  • 作者更新后,读者可以收到通知,暂时不开发此功能

功能点梳理
  • 后端渲染页面,对SEO友好
  • 提供sitemap.xml文件,输出所有文章
  • 搜索功能,能够针对标题,分类,标签进行搜索
  • 根据分类和标签查看文章
  • 可以设置多篇文章置顶
  • 首页或者列表页需要展示文章摘要,可以自行填写,也会自动捕捉文章前140字
  • 首页和列表页需要分页,每页10个数据
  • 提供Rss页面
  • 文章支持评论,不需要盖楼,侧边栏能够展示最新评论
  • 评论模块需要增加验证码功能,避免被刷
  • 后台能够配置友链,在一个面展示
  • 用户可以通过用户名和秘密登录后台
  • 需要考虑多用户的情况,多用户时需要对分类、标签、文章、友链等权限隔离
  • 分类增删改查——需要字段id,名称,创建日期,创建人,是否置顶导航以及权重
  • 标签增删改查——需要字段id,名称,创建日期,创建人,
  • 文章增删改查——需要字段id,标题,摘要,正文,分类,标签,状态,创建日期,创建人
  • 侧边栏展示需要的模块,需要字段id,类型,标题,内容,创建日期和创建人。

模块划分
  • 内容模块: 首页、分类列表页、标签列表页、友链页
  • 评论模块: 用户添加评论和展示评论
  • 侧栏模块: 侧栏展示的内容
  • 功能模块: sitemap和RSS页面
  • 用户管理: 登录和权限控制
  • 内容管理: 分类、文章、侧栏内容的增删改查

框架基础

WSGI协议简述

WSGI是描述web server(网络服务器)如何与web application(网络应用)通信的规范。
框架和服务器都遵循WSGI的规范,他们就可以随意组合使用,如下图:
在这里插入图片描述
WSGI的请求过程(如图):
在这里插入图片描述
在这里插入图片描述

  • 浏览器到WSGI Server:浏览器发送的请求先到WSGI Server
  • environ:WSGI Server 将http请求中的参数等信息封装到environ字典中
  • WSGI Server到 WSGI App:app是我们在后台编写的程序,WSGI Server开始调用后台app,将environ和WSGI Server中自己的start_response函数注入到后台app中
  • 逻辑处理:后台app里的函数接受environ和start_response,进行逻辑处理后返回一个可迭代对象,可迭代对象中的元素为HTTP的正文
  • WSGI App到WSGI Server:后台app函数处理完后,会先调用start_response函数将HTTP状态码、报文头等信息返回给 WSGI Server,然后再将函数的返回值作为http的正文(响应body)返回给WSGI Server。
  • WSGI Server到浏览器:WSGI Server将从app中得到的所有信息封装成一个response返回给浏览器。

实现简单的WSGI服务

编写一个符合WSGI标准的http处理函数

# client.py
def hello(environ, start_response):
    status = "200 OK"
    response_headers = [('Content-Type', 'text/html')]
    start_response(status, response_headers)
    path = environ['PATH_INFO'][1:] or 'hello'
    return [b'<h1> %s </h1>' % path.encode()]

该方法获取environ字典中的 path_info,也就是请求路径在前端展示。

接着,我们需要一个服务器启动WSGI服务器来处理验证,可使用python内置的WSGI模块wsgiref

# server.py
from wsgiref.simple_server import make_server
from client import hello


def main():
    server = make_server('localhost', 8001, hello)
    print('Serving HTTP on port 8001...')
    server.serve_forever()


if __name__ == '__main__':
    main()

执行server.py , 浏览器打开 http://localhost:8001/a ,即可验证


单线程和多线程的web server

单线程

# coding:utf-8

import socket
import time

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
body = '''Hello, world! <h1> from django 开发 </h1>'''
response_params = [
    'HTTP/1.0 200 OK',
    'Date: Sun, 27 may 2018 01:01:01 GMT',
    'Content-Type: text/html; charset=utf-8',
    'Content-Length: {}\r\n'.format(len(body.encode())),
    body,
]
response = '\r\n'.join(response_params)


def handle_connection(conn, address):
    print('oh, new conn', conn, address)
    time.sleep(10)
    request = b""
    while EOL1 not in request and EOL2 not in request:
        request += conn.recv(1024)
    print('r', request)
    conn.send(response.encode())
    conn.close()


def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serversocket.bind(('127.0.0.1', 8000))
    serversocket.listen(5)

    print('http://127.0.0.1:8000')
    try:
        while True:
            conn, address, = serversocket.accept()
            handle_connection(conn, address)

    finally:
        serversocket.close()


if __name__ == '__main__':
    main()

多线程

# coding:utf-8

import socket
import errno
import threading
import time

EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
body = '''Hello, world! <h1> from django 开发 </h1> - from {thread_name}'''
response_params = [
    'HTTP/1.0 200 OK',
    'Date: Sun, 27 may 2018 01:01:01 GMT',
    'Content-Type: text/html; charset=utf-8',
    'Content-Length: {length}\r\n',
    body,
]
response = '\r\n'.join(response_params)


def handle_connection(conn, address):
    print('oh, new conn', conn, address)
    time.sleep(10)
    request = b""
    while EOL1 not in request and EOL2 not in request:
        request += conn.recv(1024)
    print('r', request)
    current_thread = threading.currentThread()
    content_length = len(body.format(thread_name=current_thread.name).encode())
    print(current_thread.name)
    conn.send(response.format(thread_name=current_thread.name, length=content_length).encode())
    conn.close()


def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serversocket.bind(('127.0.0.1', 8000))
    serversocket.listen(5)
    print('http://127.0.0.1:8000')
    # serversocket.setblocking(0)
    try:
        i = 0
        while True:
            try:
                conn, address, = serversocket.accept()
            except socket.error as e:
                if e.args[0] != errno.EAGAIN:
                    raise
                continue
            i += 1
            print(i)
            t = threading.Thread(target=handle_connection, args=(conn, address), name='thread-%s' % i)
            t.start()

    finally:
        serversocket.close()


if __name__ == '__main__':
    main()


Web Server 和 Web Application

# server

import os
import sys

from client import *


def wsgi_to_bytes(s):
    return s.encode()


def run_with_cgi(app):
    environ = dict(os.environ.items())
    environ['wsgi.input'] = sys.stdin.buffer
    environ['wsgi.errors'] = sys.stderr
    environ['wsgi.version'] = (1, 0)
    environ['wsgi.multithread'] = False
    environ['wsgi.multiprocess'] = True
    environ['wsgi.run_once'] = True

    if environ.get('HTTPS', 'off') in ('on', '1'):
        environ['wsgi.url_scheme'] = 'https'
    else:
        environ['wsgi.url_scheme'] = 'http'

    headers_set = []
    headers_sent = []

    def write(data):
        out = sys.stdout.buffer
        if not headers_set:
            raise AssertionError("write() before start_response()")
        elif not headers_sent:
            status, response_headers = headers_sent[:] = headers_set
            out.write(wsgi_to_bytes('Status: %s\r\n' % status))
            for header in response_headers:
                out.write(wsgi_to_bytes('%s: %s\r\n' % header))
            out.write(wsgi_to_bytes('\r\n'))

        out.write(data)
        out.flush()

    def start_response(status, response_headers, exc_info=None):
        if exc_info:
            try:
                if headers_sent:
                    raise (exc_info[0], exc_info[1], exc_info[2])
            finally:
                exc_info = None
        elif headers_set:
            raise AssertionError('Header already set!')

        headers_set[:] = [status, response_headers]
        return write

    result = app(environ, start_response)

    try:
        for data in result:
            if data:
                write(data)
        if not headers_sent:
            write('')
    finally:
        if hasattr(result, 'close'):
            result.close()


if __name__ == '__main__':
    # print('http://127.0.0.1:8000')
    run_with_cgi(simple_app)

# application

def simple_app(environ, start_response):
    status = '200 ok'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [b'Hello world! -by cooper \n']


class AppClass(object):
    status = '200 ok'
    response_headers = [('Content-type', 'text/plain')]

    def __call__(self, environ, start_response):
        print(environ, start_response)
        start_response(self.status, self.response_headers)
        return [b'Hello AppClass.__call__\n']

app = AppClass()


class AppClassIter(object):
    status = '200 ok'
    response_headers = [('Content-type', 'text/plain')]

    def __init__(self, environ, start_response):
        self.environ = environ
        self.start_response = start_response

    def __iter__(self):
        self.start_response(self.status, self.response_headers)
        yield b"Hello AppClassIter\n"


牛刀小试——开发一个学员管理后台系统

如何阅读Django文档

Django是基于MVC或者MTV模式的框架,最终的目的就是解耦。把软件划分为一层一层的结构,便于日后维护。

Django 官方文档地址:

https://docs.djangoproject.com/zh-hans/4.0/intro/install/

学员管理系统的后台开发

安装Django+创建项目+创建应用

# 请在虚拟环境下安装
安装Django
pip install django~=1.11

创建项目
django-admin startproject student_sys

创建应用
python manage.py startapp student

编写代码

# student/models

from django.db import models

# Create your models here.
class Student(models.Model):
    SEX_ITEMS = [
        (1, '男'),
        (2, '女'),
        (3, '未知'),
    ]

    STATUS_ITEMS = [
        (0, '申请'),
        (1, '通过'),
        (2, '拒绝'),
    ]

    name = models.CharField(max_length=128, verbose_name='姓名')
    sex = models.IntegerField(choices=SEX_ITEMS, verbose_name='性别')
    email = models.EmailField(verbose_name='Email')
    qq = models.CharField(max_length=128, verbose_name='QQ')
    phone = models.CharField(max_length=128, verbose_name='电话')

    status = models.IntegerField(choices=STATUS_ITEMS, default=0, verbose_name='审核状态')
    created_time = models.DateTimeField(auto_now_add=True, editable=False, verbose_name='创建时间')

    def __str__(self):
        return '<Student: {}'.format(self.name)

    class Meta:
        verbose_name = verbose_name_plural = '学员信息'
# student/admin

from django.contrib import admin

from .models import Student


# Register your models here.
class StudentAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'sex', 'profession', 'email', 'qq', 'phone',
                    'status', 'created_time']
    list_filter = ['sex', 'status', 'created_time']
    search_fields = ['name', 'profession']
    fieldsets = (
        (None, {
            'fields': (
                'name',
                ('sex', 'profession'),
                ('email', 'qq', 'phone'),
                'status',
            ),
        }),
    )


admin.site.register(Student, StudentAdmin)

# student_sys/settings.py 

INSTALLED_APPS = [
    'student',

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

然后执行数据迁移
python manage.py makemigrations
python manage.py migrate

创建超级用户
python manage,py createsuperuser
# student_sys/settings.py
配置中文信息
LANGUAGE_CODE = 'zh-Hans'  # 语言

TIME_ZONE = 'Asia/Shanghai'  # 时区

USE_I18N = True  # 国际化

USE_L10N = True  # 如果USE_L10N设置为True,则区域设置指定的格式具有更高的优先级

USE_TZ = False  # 系统时区

开发首页

# student/views.py 

from django.shortcuts import render

# request 是Django对用户发送过来的HTTP请求的封装,可以在request对象中得到有用的信息
def index(request):
    words = 'World'
    return render(request, 'index.html', context={'words': words})

Django渲染模板或者静态页面

在渲染模板或者静态页面的时候,django会去每个app的目录下查找templates文件夹中的模板,查找的顺序是自上而下。这也就是说,如果有两个app下的templates中有相同命名的模板,上面的将会替代下面的。

{#student/template/index.html#}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>学员管理系统 -by cooper</title>
</head>
<body>
    {{ words }}
</body>
</html>
# student_sys/urls.py

from django.conf.urls import url
from django.contrib import admin
from student.views import index

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r"^$", index, name='index'),
]

到了这一步后,在浏览器输入 http://127.0.0.1:8000/ 就可以查看到相关内容。

输出数据


# student/views.py
将model中的数据添加进视图views中,就可以在模板中调用数据
from django.shortcuts import render
from .models import Student


# Create your views here.
def index(request):
    student = Student.objects.all()
    return render(request, 'index.html', context={'student': student})

{#student/template/index.html#}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>学员管理系统 -by cooper</title>
</head>
<body>
<ul>
    {% for student in student %}
    <li>
    {{ student.name }} - {{ student.get_status_display }}
    </li>
    {% endfor %}
</ul>

</body>
</html>

知识点

对于设置了choices的字段,django会提供一个方法来获取该字段所要展现的值。

在模型中我们定义了 
 STATUS_ITEMS = [
        (0, '申请'),
        (1, '通过'),
        (2, '拒绝'),
    ]
status = models.IntegerField(choices=STATUS_ITEMS, default=0, verbose_name='审核状态')

在视图中传入模型的数据,以便在模板中调用
在模板中 使用{{ student.get_status_display }}获取值,也就是对应的‘申请’‘通过’‘拒绝’。
如果使用{{ student.status}}则获取的是对应的键,也就是‘0’‘1’‘2

提交数据

# student/forms

from django import forms
from .models import Student


class StudentForm(forms.Form):
    name = forms.CharField(max_length=128, label='姓名')
    sex = forms.ChoiceField(choices=Student.SEX_ITEMS, label='性别')
    email = forms.EmailField(label='邮箱', max_length=128)
    qq = forms.CharField(label='QQ', max_length=128)
    phone = forms.CharField(label='电话', max_length=128)

也可以根据已有的model快速创建form表

# student/forms

from django import forms
from .models import Student


class StudentForm(forms.ModelForm):
    class Meta:
        model = Student
        fields = (
            'name', 'sex', 'email', 'qq', 'phone'
        )

如果有修改的字段,也可以声明出来

# student/forms

from django import forms
from .models import Student


class StudentForm(forms.ModelForm):
    qq = forms.IntegerField(max_value=128, label='QQ')
    class Meta:
        model = Student
        fields = (
            'name', 'sex', 'email',
            'qq', 'phone'
        )

还可以通过clean方法来修改字段声明

# student/forms

from django import forms
from .models import Student


class StudentForm(forms.ModelForm):
    def clean_qq(self):
        cleaned_data = self.cleaned_data['qq']
        if not cleaned_data.isdigit():
            raise forms.ValidationError('必须是数字!')
        return int(cleaned_data)

    class Meta:
        model = Student
        fields = (
            'name', 'sex', 'email',
            'qq', 'phone'
        )

cleaned_data 是Form根据字段类型对用户提交的数据转换后的结果

# student/views.py
在ModelForm中,可以省略手动构建Student的步骤

from django.shortcuts import render
from django.urls import reverse
from django.http import HttpResponseRedirect

from .forms import StudentForm
from .models import Student


# Create your views here.
def index(request):
    students = Student.objects.all()
    if request.method == "POST":
        form = StudentForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('index'))
    else:
        form = StudentForm()

    context = {
        'students': students,
        'form': form,
    }

    return render(request, 'index.html', context=context)

类方法

# student/model
早model中创建一个类方法

@classmethod
    def get_all(cls):
        return cls.objects.filter(sex=1)

# student/view
在view中调用这个类方法
students = Student.get_all()

调用类方法的时候,类本身或者类的对象会自动传给参数cls,在view中,类Student 传进了get_all()的cls中,返回了Student.objects.filter(sex=1)

将view改造成类方法

# student/views.py

from django.shortcuts import render
from django.urls import reverse
from django.http import HttpResponseRedirect
from django.views import View

from .forms import StudentForm
from .models import Student


# Create your views here.
class Indexview(View):
    template_name = 'index.html'

    def get_context(self):
        students = Student.get_all()
        context = {
            'students': students
        }
        return context

    def get(self, request):
        print('get')
        context = self.get_context()
        form = StudentForm()
        context.update({
            'form': form
        })
        return render(request, self.template_name, context=context)

    def post(self, request):
        print('post')
        form = StudentForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('index'))
        context = self.get_context()
        context.update({
            'form': form
        })
        return render(request, self.template_name, context=context)
# student_sys/urls.py
使用as_view 对 get 和 post 进行包装

from django.conf.urls import url
from django.contrib import admin
from student.views import Indexview

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r"^$", Indexview.as_view(), name='index'),
]

配置middleware

import time

from django.urls import reverse
from django.utils.deprecation import MiddlewareMixin


class TimeItMiddleware(MiddlewareMixin):
    def process_request(self, request):
        self.start_time = time.time()
        return

    def process_view(self, request, func, *args, **kwargs):
        if request.path != reverse('index'):
            return None

        start = time.time()
        response = func(request)
        costed = time.time() - start
        print('process view: {:.2f}s'.format(costed))
        return response

    def process_exception(self, request, exception):
        pass

    def process_template_response(self, request, response):
        return response

    def process_response(self, request, response):
        costed = time.time() - self.start_time
        print('request to response cose: {:.2f}s'.format(costed))
        return response

# settings.py

MIDDLEWARE = [
    'student.middlewares.TimeItMiddleware',

    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

单元测试

知识点

在DJango中运行测试用例时,如果使用的时SQLite数据库,Django会帮我们创建一个基于内存的测试数据库,用来测试。意味着,测试中所创建的数据,对开发环境还是上线环境都没有影响。

如果用的时MySQL数据库,Django会直接用配置的数据库(也就是setting中的数据库配置信息)的用户名和密码,创建一个名为test_student(应用名)_db的数据库用来测试。因此,这个数据库需要有建表和建库的权限。

# student/test.py

from django.test import TestCase
from .models import Student

from django.test import Client

# Create your tests here.
class StudentTestCase(TestCase):
    def setUp(self):
        Student.objects.create(
            name='cooper',
            sex=1,
            email='123@qq.com',
            qq='123',
            phone='123'
        )

    def test_create_and_sex_show(self):
        student = Student.objects.create(
            name='cooper',
            sex=1,
            email='123@qq.com',
            qq='123',
            phone='123'
        )
        self.assertEqual(student.sex_show, '男', '性别字段内容跟展示不一致')

    def test_filter(self):
        Student.objects.create(
            name='wuxin',
            sex=1,
            email='123@qq.com',
            qq='123',
            phone='123'
        )
        name = 'cooper'
        students = Student.objects.filter(name=name)
        self.assertEqual(students.count(), 1, '应该只存在一个名称为{}的记录'.format(name))

    def test_get_index(self):
        client = Client()
        response = client.get('/')
        self.assertEqual(response.status_code, 200, 'Status code must be 200!')

    def test_post_student(self):
        client = Client()
        data = dict(
            name='wuxin',
            sex=1,
            email='123@qq.com',
            qq='123',
            phone='123'
        )
        response = client.post('/', data)
        self.assertEqual(response.status_code, 302, 'status code must be 302"')

        response = client.get('/')
        self.assertTrue(b'test_for_post' in response.content, 'response content must contain test_for_post')

开发博客系统

编码规范

Python 之禅

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!


Python之禅 by Tim Peters
 
优美胜于丑陋(Python 以编写优美的代码为目标)
明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)
简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)
复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)
扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)
间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)
可读性很重要(优美的代码是可读的)
即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)
不要包容所有错误,除非你确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码)
当存在多种可能,不要尝试去猜测
而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)
虽然这并不容易,因为你不是 Python 之父(这里的 Dutch 是指 Guido )
做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)
如果你无法向人描述你的方案,那肯定不是一个好方案;反之亦然(方案测评标准)
命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

Python编码规范

  • 用4个空格作为缩进的层级
  • 每行长度不超过80个字符
  • 在二元操作符之前换行
  • 顶部的类或者方法隔两个空行;类内部的方法隔一个空行
  • 每个import独立一行来写
  • 三引号来说明文档字符串

Django的编码风格

  • import的引用顺序:标准库>>第三方库>>自定义的模块
  • 模板风格:{{ foo }} 两边留空格
  • view中的参数保持一致且使用 request
  • 模型中使用小写加下划线的方式来命名
  • 模型中定义的顺序如下:
  1. 字段定义
  2. 自定义managers属性
  3. class meta
  4. def str
  5. def save
  6. def get_absolute_url
  7. 其他方法
  8. 如果用到了带choices 参数的字段,choices的定义需要大写

拆分settings

将原来的settings.py 拆分为settings文件夹,文件夹里有__init__.py,base.py, develop.py,product.py。通用的基础配置就在base里,开发和生产配置可以根据需要进行相关的设置。
需要修改manage.py 和 wsgi.py的setdefault的内容

版本管理和协作 Git

这个网站可以帮助大家快速了解git

https://learngitbranching.js.org/?locale=zh_CN

奠定项目的基石——Model

创建项目和拆分settings

在虚拟环境中创建项目后,对settings进行拆分。
在bash环境中使用 命令:

# 创建settings文件夹并生成__init__.py文件
mkdir settings && touch settings/__init__.py

# 将原来settings.py的内容移到base.py里
mv settings.py settings/base.py

# 生成用来配置开发环境的develop.py文件
touch settings/develop.py
# develop.py

from .base import *

DEBUG = True

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
# setting 配置语言和时区

LANGUAGE_CODE = 'zh-Hans'  # 语言

TIME_ZONE = 'Asia/Shanghai'  # 时区

USE_I18N = True  # 国际化

USE_L10N = True  # 如果USE_L10N设置为True,则区域设置指定的格式具有更高的优先级

USE_TZ = False  # 系统时区
修改 manage.py 和 wsgi.py 
profile = os.environ.get('TYPEIDEA_PROFILE', 'develop')  # 修改这里
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "typeidea.settings.%s" % profile)  # 修改这里

pycharme知识点

这个时候如果要在pychame里快速启动的话 需要设置DJANGO_SETTINGS_MODULE:
在这里插入图片描述

配置git

配置 .gitignore

*.sqlite3
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
git 上传代码

git init
git add .
git commit -m '初始化提交'
git remote add origin git仓库地址
git push origin master

编写model层的代码

# 创建应用

python manage.py startapp blog

数据表关系

一对一关系(OneToOneField):两张数据表,第一张数据表的某行数据只与第二张的某行数据相关,同时第二张的某行数据只与第一张的某行数据相关
一对多关系(ForeignKey):两张或两张以上数据表,第一张的某行数据可以与第二张的一行或者多行数据关联,但是第二张的某行数据只与第一张的某一行数据关联。
多对多关系(ManyToManyField):两张或者两张以上数据表,第一张的某行数据可以与第二张的某行或者多行数据相关联,反之亦然。

# blog/models

from django.contrib.auth.models import User
from django.db import models


# 分类
class Category(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )
    name = models.CharField(max_length=50, verbose_name='名称')
    status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name='状态') # 正整数的整数类型
    is_nav = models.BooleanField(default=False, verbose_name='是否为导航')
    owner = models.ForeignKey(User, verbose_name='作者')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = '分类'
        verbose_name_plural = '分类'


class Tag(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )
    name = models.CharField(max_length=10, verbose_name='标签')
    status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name='状态')
    owner = models.ForeignKey(User, verbose_name='作者')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name  = '标签'
        verbose_name_plural = '标签'


class Post(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_DRAFT = 2
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
        (STATUS_DRAFT,  '草稿'),
    )
    title = models.CharField(max_length=255, verbose_name='标题')
    desc = models.CharField(max_length=1024, blank=True, verbose_name='摘要')
    content = models.TextField(verbose_name='正文', help_text='正文必须为markdown模式')
    status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name='状态')
    category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name='分类')
    tag = models.ManyToManyField(Tag, verbose_name='标签')  # 数据表多对多
    owner = models.ForeignKey(User, verbose_name='作者')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = '文章'
        verbose_name_plural = '文章'
        ordering = ['-id']

创建config应用
python manange.py startapp config
# config/models

from django.db import models
from django.contrib.auth.models import User


# Create your models here.
class Link(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )
    title = models.CharField(max_length=50, verbose_name='标题')
    href = models.URLField(verbose_name='链接')
    status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name='状态')
    weight = models.PositiveIntegerField(default=1, choices=zip(range(1,6), range(1,6)),
                                         verbose_name='权重', help_text='权重高展示顺序靠前')
    owner = models.ForeignKey(User, verbose_name='作者')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = '友链'
        verbose_name_plural = '友链'


class SideBar(models.Model):
    STATUS_SHOW = 1
    STATUS_HIDE = 0
    STATUS_ITEMS = (
        (STATUS_SHOW, '展示'),
        (STATUS_HIDE, '隐藏'),
    )
    SIDE_TYPE = (
        (1, 'HTML'),
        (2, '最新文章'),
        (3, '最热文章'),
        (4, '最近评论'),
    )
    title = models.CharField(max_length=50, verbose_name='标题')
    display_type = models.PositiveIntegerField(default=1, choices=SIDE_TYPE, verbose_name='展示类型')
    content = models.CharField(max_length=500, blank=True, verbose_name='内容', help_text='如果设置的不是HTML,可为空')
    status = models.PositiveIntegerField(default=STATUS_SHOW, choices=STATUS_ITEMS, verbose_name='状态')
    owner = models.ForeignKey(User, verbose_name='作者')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = '侧边栏'
        verbose_name_plural = '侧边栏'

创建comment 应用
python manage.py startapp comment
# comment/models

from django.db import models
from ..blog.models import Post


# Create your models here.
class Comment(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )
    target = models.ForeignKey(Post, verbose_name='评论目标')
    content = models.CharField(max_length=2000, verbose_name='内容')
    nickname = models.CharField(max_length=50, verbose_name='昵称')
    website = models.URLField(verbose_name='网站')
    email = models.EmailField(verbose_name='邮箱')
    status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name='状态')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = '评论'
        verbose_name_plural = '评论'
        
在settings 中加入应用
INSTALLED_APPS = [
    'blog',
    'config',
    'comment',

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
根据定义的模型导出数据表
python manage.py check
python manage.py makemigrations
python manage.py migrate

就会得到这样一张表
在这里插入图片描述

ORM的基本概念

ORM:对象关系映射,把定义的对象(类)映射到对应的数据表上。

Django中,在model中定义的每个类,都会被映射为一张表。

有choices的字段,在admin后台,Django 会提供一个下拉列表框让用户选择

Queryset的概念

在Model层,Django给Model增加了一个objects属性来提供数据操作的接口,比如 如果想拿到某个已经定义好的模型的全部数据,就可以使用 模型名.objects.all()来获取该模型的全部数据。它返回的是QuerySet对象。
本质上,Django中的QuerySet是一个懒加载的对象。有需要的时候才会进行数据加载
链式调用:执行一个对象的方法之后得到的结果还是这个对象


开发管理后台

配置admin页面

将model模型注册到admin中,就可以在后台看见模型的定义的字段
# blog/admin
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html

from .models import Category, Tag, Post


# Register your models here.
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'status', 'is_nav', 'owner', 'post_count', 'created_time')
    fields = ('name', 'status', 'is_nav',)

    # 登陆的用户就是作者
    def save_model(self, request, obj, form, change):
        """
        :param request: 当前请求内容的封装,request.user就是当前用户
        :param obj: 当前要保存的对象
        :param form: 页面提交的表单对象
        :param change: 标志本次保存的数据是新增还是更新
        :return:
        """
        obj.owner = request.user
        return super().save_model(request, obj, form, change)

    def post_count(self, obj):
        return obj.post_set.count()  # post_set 这代表什么
    post_count.short_description = '文章数量'


@admin.register(Tag)
class TadAdmin(admin.ModelAdmin):
    list_display = ('name', 'status', 'owner', 'created_time')
    fields = ('name', 'status')

    def save_model(self, request, obj, form, change):
        """
        :param request: 当前请求内容的封装,request.user就是当前用户
        :param obj: 当前要保存的对象
        :param form: 页面提交的表单对象
        :param change: 标志本次保存的数据是新增还是更新
        :return:
        """
        obj.owner = request.user
        return super().save_model(request, obj, form, change)


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'category', 'status', 'owner', 'created_time', 'operator']  # 后台展示的数据
    list_display_links = []  # 配置哪些字段可以作为链接, 也适用于双下划线
    list_filter = ['category', 'tag', ]  # 也适用于双下划线
    search_fields = ['title', 'category__name']  # 双下划线指定搜索关联

    actions_on_top = True
    actions_on_bottom = True
    save_on_top = True

    fields = (
        ('category', 'title'),
        'desc',
        'status',
        'content',
        'tag',
    )

    # 自定义方法
    def operator(self, obj):
        return format_html(
            '<a href="{}">编辑</a>',
            reverse('admin:blog_post_change', args=(obj.id,))  # blog_post_change 这代表什么?
        )
    operator.short_description = '操作'


    def save_model(self, request, obj, form, change):
        obj.owner = request.user
        return super().save_model(request, obj, form, change)

重写save_model方法,在保存的时候将登录用户作为作者。

再聊聊数据表的对应关系。 分类和文章是一对多的关系,1个文章只能是一个分类,一个分类却可以有多个文章,所以文章在设置过滤属性的时候,有2个以上的分类,才会显示在过滤容器里,另外,分类如果删除的话,文章也会被删除!


定制admin

# blog/admin/Postadmin
    class CategoryOwnerFilter(admin.SimpleListFilter):
        """
        自定义过滤器只显示当前用户分类
        """
        title = '分类过滤器'
        parameter_name = 'owner_category'  # 查询URL参数的名字

        # 返回要展示的内容和查询用的id
        def lookups(self, request, model_admin):
            return Category.objects.filter(owner=request.user).values_list('id', 'name')

        # queryset 是对PostAdmin所有数据的传入参数
        def queryset(self, request, queryset):
            print('r', queryset)
            category_id = self.value()
            if category_id:
                return queryset.filter(category_id=self.value())
            return queryset
将自定义的类放在过滤器接口中,就可在后台显示。
list_filter = [CategoryOwnerFilter, 'tag', ]
# blog/admin/postadmin

    def get_queryset(self, request):
        qs = super(PostAdmin, self).get_queryset(request)  # 获取数据
        return qs.filter(owner=request.user)  # 只显示当前用户的数据
blog/admin/postadmin
替换fields进一步控制页面布局。

    fieldsets = (
        ('基础配置', {
            'description': '基础配置描述',
            'fields': (
                ('title', 'category'),
                'status',
            ),
        }),
        ('内容', {
            'description': '填写主要内容',
            'fields':(
                'desc',
                'content',
            ),
        }),
        ('额外信息', {
            'description': '其他信息',
            'classes': ('collapse',),
            'fields':  ('tag',),
        }),
    )

有这样一个需求:将后台描述摘要的字段变成多行多列的方式

blog/adminforms
创建forms文件,修改相关字段

from django import forms


class PostAdminForm(forms.ModelForm):
    desc = forms.CharField(widget=forms.Textarea, label='摘要', required=False)

blog/admin/postadmin
将修改后的字段添加到admin中

form = PostAdminForm

需求点:需要在分类页面直接编辑文章

# blog/admin
class PostInline(admin.StackedInline):
    fields = ('title', 'desc', 'owner')  # 可以添加的字段
    extra = 1
    model = Post
# blog/admin/category
使用inlines 接口  有ForeignKey一对多关系的外键才可以。

inlines = [PostInline, ]

定制site

需求点:用户模块管理需要跟文章分类等数据分开

typeidea/custome_site
首先创建站点文件
from django.contrib.admin import AdminSite


class CustomSite(AdminSite):
    site_header = 'Typeidea'
    site_title = 'Typeidea 管理后台'
    index_title = '首页'


custom_site = CustomSite(name='cus_admin')

然后在注册处,将所需要的模块转移至新站点
@admin.register(Post, site=custom_site)  # 将post模块转到新站点

转移至新站点的名称也需要改变
    def operator(self, obj):
        return format_html(
            '<a href="{}">编辑</a>',
            reverse('cus_admin:blog_post_change', args=(obj.id,))
        )

    operator.short_description = '操作'
然后在路由中添加新站点的地址:
from django.conf.urls import url
from django.contrib import admin

from .custom_site import custom_site

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^super_admin/', custom_site.urls)
]

需求点:为了降低以后的维护成本,将相同逻辑的代码进行抽象。

typeidea/base-admin.py
创建一个新文件,将经常使用的方法进行抽象

from django.contrib import admin


class BaseOwnerAdmin(admin.ModelAdmin):
    """
    1.用来自动补充文章、分类、标签、侧边栏、友链这些Model的owner字段
    2.用来针对queryset过滤当前用户的数据
    """
    exclude = ('owner',)

    def save_model(self, request, obj, form, change):
        obj.owner = request.user
        return super(BaseOwnerAdmin, self).save_model(request, obj, form, change)

    def get_queryset(self, request):
        qs = super(BaseOwnerAdmin, self).get_queryset(request)
        return qs.filter(owner=request.user)

blog/admin
继承上面新创建的类。
class CategoryAdmin(BaseOwnerAdmin):

记录操作日志

ModelAdmin提供两个方法, log_addition 和 log_change

def log_addition(self, request, object, message):
        """
        Log that an object has been successfully added.

        The default implementation creates an admin LogEntry object.
        """
        from django.contrib.admin.models import LogEntry, ADDITION
        return LogEntry.objects.log_action(
            user_id=request.user.pk,  # 用户id
            content_type_id=get_content_type_for_model(object).pk,   #  类型id
            object_id=object.pk,  # 模型id
            object_repr=str(object),  # 模型名称
            action_flag=ADDITION,  # 操作标记
            change_message=message,  # 记录信息
        )

    def log_change(self, request, object, message):
        """
        Log that an object has been successfully changed.

        The default implementation creates an admin LogEntry object.
        """
        from django.contrib.admin.models import LogEntry, CHANGE
        return LogEntry.objects.log_action(
            user_id=request.user.pk,
            content_type_id=get_content_type_for_model(object).pk,
            object_id=object.pk,
            object_repr=str(object),
            action_flag=CHANGE,
            change_message=message,
        )
blog/admin

@admin.register(LogEntry, site=custom_site)
class LogEntryAdmin(admin.ModelAdmin):
    list_display = ['object_repr',  'object_id', 'action_flag', 'user', 'change_message']

开发面向用户的界面

确定有多少个页面后,编写URL代码

# typeidea/urls.py

from django.conf.urls import url
from django.contrib import admin

from .custom_site import custom_site
from blog.views import post_list, post_detail
from config.views import links

urlpatterns = [
    url(r'^$', post_list),
    url(r'^category/(?P<category_id>\d+)/$', post_list),
    url(r'^tag/(?P<tag_id>\d+)/$', post_list),
    url(r'^post/(?P<post_id>\d+).html$', post_detail),
    url(r'^links/$', links),
    url(r'^admin/', admin.site.urls),
    url(r'^super_admin/', custom_site.urls)
]

第一次编写view代码

# blog/view

from django.shortcuts import render
from django.http import HttpResponse
from .models import Category, Tag, Post

# Create your views here.
def post_list(request, category_id=None, tag_id=None):
    if tag_id:
        try:
            tag = Tag.objects.get(id=tag_id)
        except Tag.DoesNotExist:
            post_list = []
        else:
            post_list = tag.post_set.filter(status=Post.STATUS_NORMAL)  # 有数据表关系的,可以反向查询,通过tag找到post
    else:
        post_list = Post.objects.filter(status=Post.STATUS_NORMAL)
        if category_id:
            post_list = post_list.filter(category_id=category_id)
    return render(request, 'blog/list.html', locals())



def post_detail(request, post_id):
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        post = None
    return render(request, 'blog/detail.html', locals())
list.html
<ul>
    {% for post in post_list %}
    <li>
        <a href="/post/{{ post.id }}.html">{{ post.title }}</a>
        <div>
            <span>作者: {{ post.owner.username }}</span>
            <span>分类: {{ post.category.name }}</span>
        </div>
        <p>{{ post.desc }}</p>
    </li>
    {% endfor %}
</ul>
detail.html

{% if post %}
<h1>{{ post.title }}</h1>
<div>
    <span>作者: {{ post.owner.username }}</span>
    <span>分类: {{ post.category.name }}</span>
</div>
<hr/>
<p>
    {{ post.content }}
</p>
{% endif %}

第二次编写view和html

from django.shortcuts import render
from django.http import HttpResponse
from .models import Category, Tag, Post

# Create your views here.
def post_list(request, category_id=None, tag_id=None):
    tag = None
    category = None

    if tag_id:
        try:
            tag = Tag.objects.get(id=tag_id)
        except Tag.DoesNotExist:
            post_list = []
        else:
            post_list = tag.post_set.filter(status=Post.STATUS_NORMAL)  # 有数据表关系的,可以反向查询,通过tag找到post
    else:
        post_list = Post.objects.filter(status=Post.STATUS_NORMAL)
        if category_id:
            try:
                category = Category.objects.get(id=category_id)
            except Category.DoesNotExist:
                category = None
            else:
                post_list = post_list.filter(category_id=category_id)
    return render(request, 'blog/list.html', locals())



def post_detail(request, post_id):
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        post = None
    return render(request, 'blog/detail.html', locals())
{% if tag %}
标签页: {{ tag.name }}
{% endif %}

{% if category %}
分类页: {{ category.name }}
{% endif %}

<ul>
    {% for post in post_list %}
    <li>
        <a href="/post/{{ post.id }}.html">{{ post.title }}</a>
        <div>
            <span>作者: {{ post.owner.username }}</span>
            <span>分类: {{ post.category.name }}</span>
        </div>
        <p>{{ post.desc }}</p>
    </li>
    {% endfor %}
</ul>

类方法:有前置装饰器@classmethod 的就是类方法。类方法的第一个参数就是类本身。一般命名为cls。
静态方法: 使用前置装饰器@staticmethod的是静态方法,不需要转换为实例就可以直接使用。

blog/model/post

@staticmethod
    def get_by_tag(tag_id):
        try:
            tag = Tag.objects.get(id=tag_id)
        except Tag.DoesNotExist:
            tag = None
            post_list = []
        else:
            post_list = tag.post_set.filter(status=Post.STATUS_NORMAL).select_related('owner', 'category')  # 有数据表关系的,可以反向查询,通过tag找到post
        return post_list, tag

    @staticmethod
    def get_by_category(category_id):
        try:
            category = Category.objects.get(id=category_id)
        except Category.DoesNotExist:
            category = None
            post_list = []
        else:
            post_list = category.post_set.filter(status=Post.STATUS_NORMAL).select_related('owner', 'category')
        return post_list, category

    @classmethod
    def latest_posts(cls):
        queryset = cls.objects.filter(status=cls.STATUS_NORMAL)
        return queryset
blog/view
def post_list(request, category_id=None, tag_id=None):
    tag = None
    category = None

    if tag_id:
        post_list, tag = Post.get_by_tag(tag_id)
    elif category_id:
        post_list, category = Post.get_by_category(category_id)
    else:
        post_list = Post.latest_posts()
    return render(request, 'blog/list.html', locals())
导航栏的配置
blog/model/category
获取导航栏的数据
 @classmethod
    def get_navs(cls):
        categories = cls.objects.filter(status=cls.STATUS_NORMAL)
        nav_category = []
        normal_category = []
        for cate in categories:
            if cate.is_nav:
                nav_category.append(cate)
            else:
                normal_category.append(cate)
        return {
            'navs': nav_category,
            'categories': normal_category,
        }
将数据添加到view
context.update(Category.get_navs())
将数据应用在模板
<div>顶部分类:
    {% for cate in navs %}
    <a href="/category/{{ cate.id }}/">{{ cate.name }}</a>
    {% endfor %}
</div>
<hr/>

{% if tag %}
标签页: {{ tag.name }}
{% endif %}

{% if category %}
分类页: {{ category.name }}
{% endif %}

<ul>
    {% for post in post_list %}
    <li>
        <a href="/post/{{ post.id }}.html">{{ post.title }}</a>
        <div>
            <span>作者: {{ post.owner.username }}</span>
            <span>分类: {{ post.category.name }}</span>
        </div>
        <p>{{ post.desc }}</p>
    </li>
    {% endfor %}
</ul>

<hr/>
<div>底部分类:
    {% for cate in categories %}
    <a href="/category/{{ cate.id }}/">{{ cate.name }}</a>
    {% endfor %}
</div>

侧边栏的展示
获取侧边栏的数据
config/model/sidebars
@classmethod
    def get_all(cls):
        return cls.objects.filter(status=cls.STATUS_SHOW)
将数据传给view
context = {
        'post': post,
        'sidebars': SideBar.get_all()
    }
在模板中应用数据
<div>侧边栏展示:
    {% for sidebar in sidebars %}
    <h4>{{ sidebar.title }}</h4>
    {{ sidebar.content }}
    {% endfor %}
</div>

@property(装饰器):既要保护类的封装特性,又要让开发者可以使用“对象.属性”的方式操作操作类属性,除了使用 property() 函数,Python 还提供了 @property 装饰器。通过 @property 装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对“()”小括号。

封装侧边栏

blog/model
增加两个给文章加权的字段和一个最热文章的方法
	pv = models.PositiveIntegerField(default=1)
    uv = models.PositiveIntegerField(default=1)

    @classmethod
    def hot_posts(cls):
        return cls.objects.filter(status=cls.STATUS_NORMAL).only('id', 'title').order_by('-py')
在这里插入代码片
将不同的数据源处理成能够html展示的格式

class SideBar(models.Model):
    STATUS_SHOW = 1
    STATUS_HIDE = 0
    STATUS_ITEMS = (
        (STATUS_SHOW, '展示'),
        (STATUS_HIDE, '隐藏'),
    )

    DISPLAY_HTML = 1
    DISPLAY_LATEST = 2
    DISPLAY_HOT = 3
    DISPLAY_COMMENT = 4
    SIDE_TYPE = (
        (DISPLAY_HTML, 'HTML'),
        (DISPLAY_LATEST, '最新文章'),
        (DISPLAY_HOT, '最热文章'),
        (DISPLAY_COMMENT, '最近评论'),
    )

    title = models.CharField(max_length=50, verbose_name='标题')
    display_type = models.PositiveIntegerField(default=1, choices=SIDE_TYPE, verbose_name='展示类型')
    content = models.CharField(max_length=500, blank=True, verbose_name='内容', help_text='如果设置的不是HTML,可为空')
    status = models.PositiveIntegerField(default=STATUS_SHOW, choices=STATUS_ITEMS, verbose_name='状态')
    owner = models.ForeignKey(User, verbose_name='作者')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = '侧边栏'
        verbose_name_plural = '侧边栏'

    @property
    def content_html(self):
        from blog.models import Post
        from comment.models import Comment

        result = ''
        if self.display_type == self.DISPLAY_HTML:
            result = self.content
        elif self.display_type == self.DISPLAY_LATEST:
            context = {
                'posts': Post.latest_posts()
            }
            result = render_to_string('config/blocks/sidebar_posts.html', context)
        elif self.display_type == self.DISPLAY_HOT:
            context = {
                'posts': Post.hot_posts()
            }
            result = render_to_string('config/blocks/sidebar_posts.html', context)
        elif self.display_type == self.DISPLAY_COMMENT:
            context = {
                'comments': Comment.objects.filter(status=Comment.STATUS_NORMAL)
            }
            result = render_to_string('config/blocks/sidebar_comment.html', context)
        return result


    @classmethod
    def get_all(cls):
        return cls.objects.filter(status=cls.STATUS_SHOW)
sidebar_posts.html
<ul>
    {% for post in posts %}
    <li>
        <a href="/post/{{ post.id }}.html">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
sidebar_comment.html
<ul>
    {% for comment in comments %}
    <li>
        <a href="/post/{{ comment.target_id }}.html">{{ comment.target.title }}</a> |
    {{ comment.nickname }} : {{ comment.content }}
    </li>
    {% endfor %}
</ul>
list.html
<div>侧边栏展示:
    {% for sidebar in sidebars %}
    <h4>{{ sidebar.title }}</h4>
    {{ sidebar.content_html}}
    {% endfor %}
</div>

整理模板代码

base.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock title %} - Typeidea 博客系统</title>
</head>
<body>
<div>顶部分类:
    {% for cate in navs %}
    <a href="/category/{{ cate.id }}/">{{ cate.name }}</a>
    {% endfor %}
</div>
<hr/>

{% block main %}
{% endblock main %}

<hr/>
<div>底部分类:
    {% for cate in categories %}
    <a href="/category/{{ cate.id }}/">{{ cate.name }}</a>
    {% endfor %}
</div>

<div>侧边栏展示:
    {% for sidebar in sidebars %}
    <h4>{{ sidebar.title }}</h4>
    {{ sidebar.content_html}}
    {% endfor %}
</div>

</body>
</html>

开-闭原则: 对扩展开放,对修改关闭。通过继承原有的类来实现新的需求。

解耦硬编码

from django.conf.urls import url
from django.contrib import admin

from .custom_site import custom_site
from blog.views import post_list, post_detail
from config.views import links

urlpatterns = [
    url(r'^$', post_list, name='index'),
    url(r'^category/(?P<category_id>\d+)/$', post_list, name='category-list'),
    url(r'^tag/(?P<tag_id>\d+)/$', post_list, name='tag-list'),
    url(r'^post/(?P<post_id>\d+).html$', post_detail, name='post-list'),
    url(r'^links/$', links, name='links'),
    url(r'^admin/', admin.site.urls, name='admin'),
    url(r'^super_admin/', custom_site.urls, name='super-admin')

用reverse对编码进行解耦,用name参数代替以前的硬编码。

理解 class-based view

闭包:在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。

getattr: getattr() 函数用于返回一个对象属性值。

>>>class A(object):
...     bar = 1
... 
>>> a = A()
>>> getattr(a, 'bar')        # 获取属性 bar 值
1
>>> getattr(a, 'bar2')       # 属性 bar2 不存在,触发异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'bar2'
>>> getattr(a, 'bar2', 3)    # 属性 bar2 不存在,但设置了默认值
3
>>>
重写blog/view 以类的形式映射到模板

from django.shortcuts import render
from django.http import HttpResponse
from .models import Category, Tag, Post
from config.models import SideBar
from django.views.generic import DetailView
from django.views.generic import ListView
from django.shortcuts import get_object_or_404


# Create your views here.

class CommonViewMixin:
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'sidebars': SideBar.get_all()
        })
        context.update(Category.get_navs())
        return context


class IndexView(CommonViewMixin, ListView):
    queryset = Post.latest_posts()
    paginate_by = 5
    context_object_name = 'post_list'
    template_name = 'blog/list.html'


class CategoryView(IndexView):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        category_id = self.kwargs.get('category_id')
        category = get_object_or_404(Category, pk=category_id)
        context.update({
            'category': category
        })
        return context

    def get_queryset(self):
        queryset = super().get_queryset()
        category_id = self.kwargs.get('category_id')
        return queryset.filter(category_id=category_id)


class TagView(IndexView):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        tag_id = self.kwargs.get('tag_id')
        tag = get_object_or_404(Tag, pk=tag_id)
        context.update({
            'tag': tag
        })
        return context

    def get_queryset(self):
        queryset = super().get_queryset()
        tag_id = self.kwargs.get('tag_id')  # self.kwargs的数据是从URL的定义中拿到的
        return queryset.filter(tag_id=tag_id)


class PostDetailView(CommonViewMixin, DetailView):
    queryset = Post.latest_posts()
    template_name = 'blog/detail.html'
    context_object_name = 'post'
    pk_url_kwarg = 'post_id'


from django.conf.urls import url
from django.contrib import admin

from .custom_site import custom_site
from blog.views import  IndexView, CategoryView, TagView, PostDetailView
from config.views import links

urlpatterns = [
    url(r'^$', IndexView.as_view(), name='index'),
    url(r'^category/(?P<category_id>\d+)/$', CategoryView.as_view(), name='category-list'),
    url(r'^tag/(?P<tag_id>\d+)/$', TagView.as_view(), name='tag-list'),
    url(r'^post/(?P<post_id>\d+).html$', PostDetailView.as_view(), name='post-detail'),
    url(r'^links/$', links, name='links'),
    url(r'^admin/', admin.site.urls, name='admin'),
    url(r'^super_admin/', custom_site.urls, name='super-admin')

引入前端框架Bootstrap

数据之于网站,相当于灵魂之于人类。网站的前端相当于人的形体外貌。
HTML是骨架;CSS是皮肤;Javascript是肢体动作

Bootstrap的基本用法

Bootstrap框架提供了

  • 页面脚手架:样式重置、浏览器兼容、栅格系统、简单布局
  • 基础CSS样式:代码高亮、排版、表单、表格等、
  • 组件:tab、pill、导航、弹窗、顶部栏、card
  • Javascript插件:动态功能,比如,下拉菜单、模态窗口、进度条

容器: 定义元素时增加container 的class。
class=‘container-fluid’ 这种容器始终占据屏幕100%

栅格系统: 页面被划分为12列,通过展示内容占据多少列来展示其宽度。

bootstrap demo
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django企业开发实战</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"
          media="screen">
</head>
<body>
<div class="container" style="border: 1px solid red;">
    <h1>Hello World!</h1>
</div>
<div class="container" style="border: 1px solid red;">
    <div class="row">
        <div class="col-9" style="border: 1px solid blue;">
            <div style="height: 500px;">内容区</div>
        </div>
        <div class="col-3" style="border: 1px solid blue;">
            <div style="height: 500px">边栏区</div>
        </div>
    </div>
</div>
<footer class="container" style="border: 1px solid red;">
    底部区域
</footer>

</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Typeidea blog</title>
    <meta name="viewport" content="width=sevice-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.css">
    <style>
        .post {
            margin-bottom: 5px;
        }
    </style>
</head>
<body>
<div class="container head">
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <a class="navbar-brand" href="#">首页</a>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link" href="#">Python</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Django实战</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Tornado</a>
                </li>
            </ul>
            <form class="form-inline my-2 my-lg-0">
                <input class="form-control mr-sm-2" type="search" placeholder="Search"
                aria-label="Search">
                <button class="btn btn-outline-success" type="submit">搜索</button>
            </form>
        </div>
    </nav>
    <div class="jumbotron">
        <h1 class="display-4">Typeidea</h1>
        <p class="lead">基于django的多人博客系统</p>
    </div>
</div>
<div class="container main">
    <div class="row">
        <div class="col-9 post-list">
            <div class="card post">
                <div class="card-body">
                    <h5 class="card-title"><a href="#">这里是标题</a> </h5>
                    <span class="card-link">作者:<a href="#">胡阳</a> </span>
                    <span class="card-link">分类:<a href="#">Python</a> </span>
                    <span class="card-link">标签:
                        <a href="#">Python</a>
                        <a href="#">Django</a>
                        <a href="#">经验</a>
                    </span>
                    <p class="card-text">Somethinks<a href="#">完整内容</a> </p>
                </div>
            </div>

            <div class="card post">
                <div class="card-body">
                    <h5 class="card-title"><a href="#">这里是标题</a> </h5>
                    <span class="card-link">作者:<a href="#">胡阳</a> </span>
                    <span class="card-link">分类:<a href="#">Python</a> </span>
                    <span class="card-link">标签:
                        <a href="#">Python</a>
                        <a href="#">Django</a>
                        <a href="#">经验</a>
                    </span>
                    <p class="card-text">Somethinks<a href="#">完整内容</a> </p>
                </div>
            </div>

            <div class="card post">
                <div class="card-body">
                    <h5 class="card-title"><a href="#">这里是标题</a> </h5>
                    <span class="card-link">作者:<a href="#">胡阳</a> </span>
                    <span class="card-link">分类:<a href="#">Python</a> </span>
                    <span class="card-link">标签:
                        <a href="#">Python</a>
                        <a href="#">Django</a>
                        <a href="#">经验</a>
                    </span>
                    <p class="card-text">Somethinks<a href="#">完整内容</a> </p>
                </div>
            </div>
            <a href="?page={{ page_obj.previous_page_number }}">上一页</a>
            Page 1 of 1
            <a href="?page={{ page_obj.next_page_number }}">下一页</a>
        </div>
        <div class="col-3">
            <div class="card sidebar">
                <div class="card-body">
                    <h4 class="card-title">关于博主</h4>
                    <p>
                        网名:wuxinpy 多年python工程师
                    </p>
                </div>
            </div>
        </div>
    </div>
    <footer class="footer">
        <div class="container">
            <hr/>
            <nav class="nav category">
                <a href="#" class="nav-link">读书</a>
                <a href="#" class="nav-link">产品</a>
                <a href="#" class="nav-link">经验</a>
            </nav>
        </div>
        <div class="container power">
            <span class="text-muted">Power by wuxin</span>
        </div>
    </footer>
</div>
</body>
</html>
调整模板结构

THEME = 'default'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'themes', THEME, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
加入bootstrap样式
base.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock title %} - Typeidea 博客系统</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.css">
    <style>
        .post {
            margin-bottom: 5px;
        }
    </style>
</head>

<body>
<div class="container head">
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <a class="navbar-brand" href="/">首页</a>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav mr-auto">
                {% for cate in navs %}
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'category-list' cate.id %}">{{ cate.name }}</a>
                </li>
                {% endfor %}
            </ul>
        <form class="form-inline my-2 my-lg-0" action="GET">
            <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
            <button class="btn-outline-success" type="submit">搜索</button>
        </form>
        </div>
    </nav>
    <div class="jumbotron">
        <h1 class="display-4">Typeidea</h1>
        <p class="lead">基于django的多人博客系统</p>
    </div>
</div>
<div class="container main">
    <div class="row">
        <div class="col-9 post-list">
            {% block main %}
            {% endblock main %}
        </div>
        <div class="col-3">
            {% block sidebar %}
            {% for sidebar in sidebars %}
            <div class="card sidebar">
                <div class="card-body">
                    <h4 class="card-title">{{ sidebar.title }}</h4>
                    <p>
                        {{ sidebar.content_html }}
                    </p>
                </div>
            </div>
            {% endfor %}
            {% endblock sidebar %}
        </div>
    </div>
</div>
<footer class="footer">
    {% block footer %}
    <div class="container">
        <hr/>
        <nav class="nav category">
            {% for cate in categories %}
            <a href="{% url 'category-list' cate.id %}" class="nav-link">{{ cate.name }}</a>
            {% endfor %}
        </nav>
    </div>
    <div class="container power">
        <span class="text-muted">power by wuxin</span>
    </div>
    {% endblock footer %}
</footer>

</body>
</html>
list.html

{% extends './base.html' %}

{% block title %}
{% if tag %}
标签页: {{ tag.name }}
{% elif categoty %}
分类页:{{ category.name }}
{% else %}
首页
{% endif %}
{% endblock title %}

{% block main %}
    {% for post in post_list %}
    <div class="card post">
    <div class="card-body">
        <h5 class="card-title"><a href="{% url 'post-detail' post.id %}">{{ post.title }}</a> </h5>
        <span class="card-link">作者: <a href="#">{{ post.owner.username }}</a> </span>
        <span class="card-link">分类: <a href="{% url 'category-list' post.category_id %}">{{ post.category.name }}</a> </span>
        <span class="card-link">标签:
            {% for tag in post.tag.all %}
                <a href="{% url 'tag-list' tag.id %}">{{ tag.name }}</a>
            {% endfor %}
        </span>
        <span class="card-text">{{ post.desc }}<a href="{% url 'post-detail' post.id %}">完整内容</a> </span>
    </div>
    </div>
    {% endfor %}

{% if page_obj %}
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
Page {{ page_obj.number }} of {{ paginator.num_pages }}.
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
{% endif %}

{% endblock main %}

detail.html

{% extends './base.html' %}

{% block title%} {{ post.title }} {% endblock title %}


{% block main %}
{% if post %}
<h1>{{ post.title }}</h1>
<div>
    <span>作者:<a href="#">{{ post.owner.username }}</a> </span>
    <span>分类:<a href="{% url 'category-list' post.category_id %}">{{ post.category.name }}</a> </span>
    <span>创建时间:{{ post.created_time }} </span>
</div>
<hr/>
<p>
    {{ post.content }}
</p>
{% endif %}
{% endblock main %}

配置线上资源


base.py
STATIC_URL = '/static/'

STATIC_ROOT = '/tmp/static'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'themes', THEME, 'static'),
]

base.html

{% load static %}  # 导入静态资源
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock title %} - Typeidea 博客系统</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="{% static 'bootstrap.css' %}">  # 导入静态样式
    <style>
        .post {
            margin-bottom: 5px;
        }
    </style>
</head>

完成整个博客系统

需求点:增加搜索和作者过滤

blog/view
class SearchView(IndexView):
    def get_context_data(self, **kwargs):
        context = super(SearchView, self).get_context_data()
        context.update({
            'keyword': self.request.GET.get('keyword', '')
        })
        return context

    def get_queryset(self):
        queryset = super(SearchView, self).get_queryset()
        keyword = self.request.GET.get('keyword')
        if not keyword:
            return queryset
        return queryset.filter(Q(title__icontains=keyword) | Q(desc__icontains=keyword))
urls.py
url(r'search/$', SearchView.as_view(), name='search'),
base.html
<form class="form-inline my-2 my-lg-0" action="/search/" method="get">
            <input class="form-control mr-sm-2" type="search" name="keyword" placeholder="Search" aria-label="Search"
            value="{{ keyword }}">
            <button class="btn-outline-success" type="submit">搜索</button>
        </form>

需求点:作者过滤

class AuthorView(IndexView):
    def get_queryset(self):
        queryset = super(AuthorView, self).get_queryset()
        author_id = self.kwargs.get('owner_id')
        return queryset.filter(owner_id=author_id)
url(r'^author/(?P<owner_id>\d+)/$', AuthorView.as_view(), name='author'),

需求点:增加友链

config/view

from django.shortcuts import render
from django.views.generic import ListView
from blog.views import CommonViewMixin
from .models import Link


# Create your views here.
class LinkListView(CommonViewMixin, ListView):
    queryset = Link.objects.filter(status=Link.STATUS_NORMAL)
    template_name = 'config/links.html'
    context_object_name = 'link_list'
   url(r'^links/$', LinkListView.as_view(), name='links'),
links.html
{% extends 'blog/base.html' %}
{% block title %} 友情链接 {% endblock title %}

{% block main %}
    <table class="table">
    <thread>
        <tr>
            <th scope="col">#</th>
            <th scope="col">名称</th>
            <th scope="col">网址</th>
        </tr>
    </thread>
    <tbody>
    {% for link in link_list %}
    <tr>
        <th scope="row">{{ forloop.counter }}</th>
        <td>{{ link.title }}</td>
        <td><a href="{{ link.href }}">{{ link.href }}</a> </td>
    </tr>
    {% endfor %}
    </tbody>
    </table>
{% endblock main %}

实现评论功能

target = models.CharField(max_length=128, verbose_name='评论目标')
comment/forms
from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    nickname = forms.CharField(
        label='昵称',
        max_length=50,
        widget=forms.widgets.Input(
            attrs={'class': 'form-control', 'style': 'width: 60%'}
        )
    )
    email = forms.CharField(
        label='Email',
        max_length=50,
        widget=forms.widgets.EmailInput(
            attrs={'class': 'form-control', 'style': 'width: 60%'}
        )
    )
    website = forms.CharField(
        label='网站',
        max_length=100,
        widget=forms.widgets.URLInput(
            attrs={'class': 'form-control', 'style': 'width: 60%'}
        )
    )
    content = forms.CharField(
        label='内容',
        max_length=500,
        widget=forms.widgets.Textarea(
            attrs={'class': 'form-control', 'rows': 6, 'cols': 60}
        )
    )

    def clean_content(self):
        content = self.cleaned_data.get('content')
        if len(content) <10:
            raise forms.ValidationError('内容太短了!')
        return content

    class Meta:
        model = Comment
        fields = ['nickname', 'email', 'website', 'content']

comment/model
@classmethod
    def get_by_target(cls, target):
        return cls.objects.filter(target=target, status=cls.STATUS_NORMAL)
blog/view/postdetailview
class PostDetailView(CommonViewMixin, DetailView):
    queryset = Post.latest_posts()
    template_name = 'blog/detail.html'
    context_object_name = 'post'
    pk_url_kwarg = 'post_id'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'comment_form': CommentForm,
            'comment_list': Comment.get_by_target(self.request.path),
        })
        return context
comment/view
from django.shortcuts import render
from django.shortcuts import redirect
from django.views.generic import TemplateView

from .forms import CommentForm


# Create your views here.
class CommentView(TemplateView):
    http_method_names = ['post']
    template_name = 'comment/result.html'

    def post(self, request, *args, **kwargs):
        comment_form = CommentForm(request.POST)
        target = request.POST.get('target')

        if comment_form.is_valid():
            instance = comment_form.save(commit=False)
            instance.target = target
            instance.save()
            succeed = True
            return redirect(target)
        else:
            succeed = False

        context = {
            'succeed': succeed,
            'form': comment_form,
            'target': target,
        }
        return self.render_to_response(context)

comment/result.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>评论结果页</title>
    <style>
        body {TEXT-ALIGN: center;}
        .result {
            text-align: center;
            width: 40%;
            margin: auto;
        }
        .errorlist {color: red;}
        ul li {
            list-style-type: none;
        }
    </style>
</head>
<body>
<div class="result">
    {% if succeed %}]
    评论成功
    <a href="{{ target }}">返回</a>
    {% else %}
    <ul class="errorlist">
        {% for field, message in form.error.items %}
        <li>{{ message }}</li>
        {% endfor %}
    </ul>
    <a href="javascript:window.history.back();">返回</a>
    {% endif %}
</div>
</body>
</html>
    url(r'^comment/$', CommentView.as_view(), name='comment'),

Django template tag(自定义标签)

comment_block

from django import template

from comment.forms import CommentForm
from comment.models import Comment

register = template.Library()


@register.inclusion_tag('comment/block.html')
def comment_block(target):
    return {
        'target': target,
        'comment_form': CommentForm,
        'comment_list': Comment.get_by_target(target)
    }
block.html
<hr/>
    <div class="comment">
    <form class="form-group" action="/comment/" method="post">
        {% csrf_token %}
        <input name="target" type="hidden" value="{{ target }}"/>
        {{ comment_form }}
        <input type="submit" value="写好了!"/>
    </form>

    <ul class="list-group">
        {% for comment in comment_list %}
        <li class="list-group-item">
            <div class="nickname">
                <a href="{{ comment.website }}">{{ comment.nickname }}</a>
                <span>{{ comment.created_time }}</span>
            </div>
            <div class="comment-content">{{ comment.content }}</div>
        </li>
        {% endfor %}
    </ul>
    </div>
detail.html
{% extends './base.html' %}
{% load comment_block %}
{% block title%} {{ post.title }} {% endblock title %}


{% block main %}
{% if post %}
<h1>{{ post.title }}</h1>
<div>
    <span>作者:<a href="#">{{ post.owner.username }}</a> </span>
    <span>分类:<a href="{% url 'category-list' post.category_id %}">{{ post.category.name }}</a> </span>
    <span>创建时间:{{ post.created_time }} </span>
</div>
<hr/>
<p>
    {{ post.content }}
</p>
{% endif %}


    {% comment_block request.path %}
{% endblock main %}

配置Markdown文章的支持

pip install mistune
comment/form
    def clean_content(self):
        content = self.cleaned_data.get('content')
        if len(content) < 10:
            raise forms.ValidationError('内容太短了!')
        content = mistune.markdown(content)
        return content
关闭Django的自动转码
{% autoescape off %}
{{ comment.content }}
{% endautoescape %}
blog.post.model
content_html = models.TextField(verbose_name='正文html代码', blank=True, editable=False)
  def save(self, *args, **kwargs):
        self.content_html = mistune.markdown(self.content)
        super().save(*args, **kwargs)

配置代码高亮

{% block extra_head %}
<link rel="stylesheet" href="https://cdn.bootcss.com/highlight.js/9.12.0/styles/googlecode.min.css">
<script src="https://cdn.bootcss.com/highlight.js/9.12.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
{% endblock extra_head %}

增加访问统计

访问统计的四种方式:
基于当次访问后端实时处理
基于当次访问后端延迟处理——Celery(分布式任务队列)
前端通过JavaScript埋点或者img标签页统计
基于Nginx日志分析统计

如何确定用户访问某个文章:
根据用户IP和浏览器类型等信息生成MD5来标记这个用户(缺点:用户会重合,同一个IP下有很多用户)
系统生成唯一的id,放置到用户cookie中(缺点:切换浏览器会生成新的用户)
让用户登录(最准确,但难度最大)

blog/middleware/user_id
import uuid

USER_KEY = 'uid'
TEN_YEARS = 60*60*24*365*10
class UserIDMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        uid = self.generate_uid(request)
        request.uid = uid
        response = self.get_response(request)
        response.set_cookie(USER_KEY, uid, max_age=TEN_YEARS, httponly=True)
        return response

    def generate_uid(self, request):
        try:
            uid = request.COOKIES[USER_KEY]
        except KeyError:
            uid = uuid.uuid4().hex
        return uid
MIDDLEWARE = [
    'blog.middleware.user_id.UserIDMiddleware',
blog/view/postdetailview
    def get(self, request, *args, **kwargs):
        response = super().get(request, *args, **kwargs)
        self.handle_visited()
        return response

    def handle_visited(self):
        increase_pv = False
        increase_uv = False
        uid = self.request.uid
        pv_key = "pv:%s:%s" % (uid, self.request.path)
        uv_key = "uv:%s:%s:%s" % (uid, str(date.today()), self.request.path)
        if not cache.get(pv_key):
            increase_pv = True
            cache.set(pv_key, 1, 1*60)

        if not cache.get(uv_key):
            increase_uv = True
            cache.set(uv_key, 1, 24*60*60)

        if increase_uv and increase_pv:
            Post.objects.filter(pk=self.object.id).update(pv=F('pv') + 1, uv=F('uv') + 1)
        elif increase_pv:
            Post.objects.filter(pk=self.object.id).update(pv=F('pv') + 1)
        elif increase_uv:
            Post.objects.filter(pk=self.object.id).update(uv=F('uv') + 1)

配置Rss 和sitemap

rss

from django.contrib.syndication.views import Feed
from django.urls import reverse
from django.utils.feedgenerator import Rss201rev2Feed

from .models import Post


class ExtendedRSSFeed(Rss201rev2Feed):
    def add_item_elements(self, handle, item):
        super().add_item_elements(handle, item)
        handle.addQuickElement('content:html', item['content_html'])


class LatestPostFeed(Feed):
    feed_type = ExtendedRSSFeed
    title = "Typeidea Blog System"
    link = "/rss/"
    description = "typeidea is a blog system power by django"

    def items(self):
        return Post.objects.filter(status=Post.STATUS_NORMAL)[:5]

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return item.desc

    def item_link(self, item):
        return reverse('post-detail', args=[item.pk])

    def item_extra_kwargs(self, item):
        return {'content_html': self.item_content_html(item)}

    def item_content_html(self, item):
        return item.content_html
sitemap

from django.contrib.sitemaps import Sitemap
from django.urls import reverse

from .models import Post


class PostSitemap(Sitemap):
    changefreq = "always"
    priority = 1.0
    protocol = 'https'

    def items(self):
        return Post.objects.filter(status=Post.STATUS_NORMAL)

    def lastmod(self, obj):
        return obj.created_time

    def location(self, obj):
        return reverse('post-detail', args=[obj.pk])


第三方插件增强管理后台

xadmin的使用

xadmin 和 Django2.0 集成遇到的错误以及办法。
xadmin源码地址:https://github.com/sshwsfc/xadmin

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

django2.0 采用formtools 2.1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值