django2.2 简单博客 一

博客项目

无论进行什么项目,首先进行需求分析。

需求分析

要建立最初始需求文档,每个功能要一个一个梳理,建立一个个功能点,清晰,然后需要客户过目,事后要放入项目归档。项目管理很关键。但是客户往往是跳跃性的想法,我们需要不断的沟通交流才能设计好的服务。
需求理清后第一件事就是建表,改setting配置(因为这个在启动时就加载,如果启动后修改可能不会刷新。)

我们可以通过快速迭代,不断的更新版本以寻求怎样的功能能提高市场占有率。

千万不要过度设计。

要设计窗口吗?
不需要,只要借助浏览器。
选型,调查一下。基本上都是用BS,虽然也有一些问题,兼容性问题,就是不能用IE。

最重要的是博文和用户,即用户管理,博文管理。

数据库选择
CMDB配置管理数据库( Configuration Management Database,CMDB)是一个逻辑数据库,包含了配置项全生命周期的信息以及配置项之间的关系(包括物理关系、实时通信关系、非实时通信关系和依赖关系)。

MIS管理信息系统(管理信息系统–Management Information System),主要指的是进行日常事务操作的系统。这种系统主要用于管理需要的记录,并对记录数据进行相关处理,将处理的信息及时反映给管理者的一套网络管理系统。

使用Mysql 8.0 InnoDB引擎(增删改查功能)。

流程化,流程系统更加麻烦。

先确定数据的表格,设立外键时类型和大小要一致。
先建立user表,sql语句如下

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(48) NOT NULL,
  `email` varchar(64) NOT NULL,
  `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'MD5加密\r\n',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

含有id 唯一标识 主键
,name 用户名,描述性字段
,email 用户电子邮箱
,password 用户密码,不能明文储存,采用单向加密,如MD5(缺点,可以反查,计算速度过快,穷举太快)

建立一个文章的post表

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(128) NOT NULL,
  `pubdata` datetime(6) NOT NULL,
  `author_id` int(11) NOT NULL,
  `content` text NOT NULL,
  PRIMARY KEY (`id`),
  KEY `author_id` (`author_id`),
  CONSTRAINT `post_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

含有id 唯一标识
,title 标题,描述性字段
,pubdata 发布日期,日期类型
,author_id,博文作者id,外键,必须是table user的id(外键可以不写,在逻辑层面做到也可以)

作者和用户id需要有一个关系,在创建文章时,auther_id必须存在于user(id)中,如果不存在则会抛异常。如果要删除post表中的内容,则可以直接删除,如果要删除user表中的内容,则需要设定级联删除ON DELETE CASCADE 级联删除 ON UPDATE CASCADE 级联更新,但一般不用这个。我们更希望在逻辑层面上删除,通过事务控制更好。
当然post表最重要的,content,类型是选择text(大约有6万多个字符),或者longtext(考虑使用,有必要吗?)。这是一个大字段,在应用时一般会再建立一张表单独放开,一般查询的时候不会用到这个大字段,频繁查询的字段放在post表中,这两张表也有联系,一一对应关系,post生成一个id后,content表才可以使用这个id,使用事务解决(但是如果被打断了怎么办,这是风险。)。同一个用户的附加信息要放在外面,即必填表和非必填项,事务控制在创建用户时一起添加这些信息,insert into,也可以用外键约束。

博客的数据库系统显然是一对多的关系,一个作者有多个博文。
content的选取?选取大小?图片如何处理?单独放一张表。
博文一般在几万字内,因此需要很大,选择text很合适,甚至longtext。
图片储存,博文就像HTML一样,图片通过路径信息将图片嵌入在内容中的,所以保存的内容还是字符串。
来源有两种:外链,通过URL链接访问,本站不用存储图片,但是容易引起盗链问题。
本站存储,需要提供博文的文本编辑器,提供图片上传到网站存储,并生成图片URL,这个URL嵌入到博客正文中。这个要解决图片存储的问题,水印问题,临时图片清理,在线压缩问题等等。使用磁盘阵列,云,分布式文件系统等,运维相关,术语要懂。

content不应该和频繁查询的字段放在同一张表中,应该单独放一张表。

CREATE TABLE `content` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` text NOT NULL,
  `post_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `post_id` (`post_id`),
  CONSTRAINT `content_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这里生产的SQL脚本不用来生成表,使用ORM工具来创建表,用来检查实体类构建是否正确。

在pycharm中新建一个项目,下面开始django之路。

使用pycharm创建一个项目后,在文件路径下的view,terminal输入pip list 里面两个包就是虚拟环境必须的包。pip和setuptools。
然后在settings中Project:name中的project interpreter,python解释器中,可以更改你用的python版本,安装的包等。
项目要用的包一定要有,不用的包绝对不要出现,

先完成核心业务
满足用户,注册登录登出,user表
博客的发文文章列表,文章详情,post和content表

多人博客项目
BS开发的Django;
Django采用MVC架构设计的开源的WEB快速开发框架。
优点:
快速开发,(Auth,Cache,模板,并不推荐使用)。
MVC设计模式
实用的管理后台(实际的后台管理是管理业务问题,Flask框架简单轻巧)
自带ORM,Template,Form,Auth核心(管理模块,有些能去除)
组件
简介的URL设计(不支持post,get)
插件丰富(大多数不是原生的)

缺点:
太大,东西全而大
同步阻塞!

这就是为什么很多人用tomado(基于协程的异步模型,效率高),大多数情况下用Django就够了,但是做网站光用django肯定是不行的,Django是动态网页服务,一定要做动静分离,做优化,动态用动态服务器,静态用静态服务器,效率更高(企业级应用)。
模板就是替换字符串,模板Jinja做不到前后端分离。后端只负责Json数据,传到前端后,靠JS静态加载数据,之间靠定义接口耦合,对http开放的API接口,restful风格。

Django版本使用1.11,可以支持2.7,-3.6的python版本。2.0以上的不在支持python2.7了。python2.7可以提供很多插件,也可以自己写,熟系框架后。

作为一个Python web框架,django需要安装python。

Django版本 Python版本
1.11 2.7,3.4,3.5,3.6,3.7(在1.11.17中添加)
2.0 3.4,3.5,3.6,3.7
2.1,2.2 3.5,3.6,3.7

windows安装Django很简单:直接pip install Django即可安装最新的正式版Django。在创建的新项目中的terminal下输入即可。
安装完后,先简单了解一下Django,在如图所示的库文件里可以看到:
django2.2 简单博客 一_第1张图片
django-admin。我们看看都有哪些命令:

Type 'django-admin help ' for help on a specific subcommand.
Available subcommands:
[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    runserver
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver
Note that only Django core commands are listed as settings are not properly configured (error: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SET
TINGS_MODULE or call settings.configure() before accessing settings.).

注意:这些命令都是在开发环境下才可以用即:
env
最常用的就是startproject,查看一下help:django-admin help startproject

usage: django-admin startproject [-h] [--template TEMPLATE]
                                 [--extension EXTENSIONS] [--name FILES]
                                 [--version] [-v {0,1,2,3}]
                                 [--settings SETTINGS]
                                 [--pythonpath PYTHONPATH] [--traceback]
                                 [--no-color] [--force-color]
                                 name [directory]

Creates a Django project directory structure for the given project name in the
current directory or optionally in the given directory.

positional arguments:
  name                  Name of the application or project.
  directory             Optional destination directory

optional arguments:
  -h, --help            show this help message and exit
  --template TEMPLATE   The path or URL to load the template from.
  --extension EXTENSIONS, -e EXTENSIONS
                        The file extension(s) to render (default: "py").
                        Separate multiple extensions with commas, or use -e
                        multiple times.
  --name FILES, -n FILES
                        The file name(s) to render. Separate multiple file
                        names with commas, or use -n multiple times.
  --version             show program's version number and exit
  -v {0,1,2,3}, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output,
                        2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g.
                        "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be
                        used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g.
                        "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output.

django-admin startproject blog . 在项目里创建Django项目,"."表示在本根目录下,不会另外创建目录,直接将当前目录作为项目目录。结构如下:
django2.2 简单博客 一_第2张图片
manage.py在这里我们创建的所有子命令都归这里管,项目管理的命令行工具。应用创建、数据库迁移等都用它完成。
blog中init目前没什么用;settings,全局配置的,文件不能少;URL做路由用,路径映射配置,默认情况下只配置/admin的路由;WSGI不用多说,就是WSGI协议。Django就是更加丰富的基于WSGI的框架。

先来看看settings:全局配置,数据库参数。(!!!注意配置文件千万别删除,即使修改也要注释掉原有的配置)

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

__file__指的是当前的.py文件路径是什么,abspath,它的绝对路径是什么,dirname(os.path.abspath(file))绝对路径所对应的目录是什么,之后再一个dirname,指的是再上一级目录,全局量,项目根目录。作为一个基路径,供其他对象使用。例如(该文件下的):

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

将数据库文件sqlite加入进项目根目录。但我们这里用自己的数据库。

ALLOWED_HOSTS = [] # 允许的主机

INSTALLED_APPS = [  # 安装的app,我们自己app也可以加进来
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [  # 中间件,类似于拦截器
    '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',
]

ROOT_URLCONF = 'blog.urls'  # 所有的URL配置从哪里开始,就是简单的多级路由,在blog包下的urls开始,最后靠动态加载。

TEMPLATES = [  # 模板配置
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],  # 模板在哪里
        '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',
            ],
        },
    },
]

AUTH_PASSWORD_VALIDATORS = [  # 用户验证相关,我们一般用自己的验证方式。
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/  # 国际化,这些需要本地化,一会儿再说。

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'  # 静态路由,我们一般用nginx实现,不会走这里。

看看urls:

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

urlpatterns = [  # 可以访问到某个包下面,启用一个后台管理界面。
    path('admin/', admin.site.urls),
]

这里的urls取得是一个函数,

数据库的配置
我们将默认参数注释,将下面的代码替换即可:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'blog', # 库名
        'USER': 'mydatabaseuser', # 按连接数据库的用户名写
        'PASSWORD': 'mypassword',  # 密码
        'HOST': '127.0.0.1', # 数据库主机ip
        'PORT': '3306',# 数据库端口 mysql一般是3306
    }
}

这样就配好了一个数据库连接。OPTIONS选项要参考Mysql文档。最好的学习还是去看官方django文档。

mysql数据库引擎
内建的引擎有:postgresql,mysql,sqlite3,oracle四种(前面加django.db.backends.即可),当然我们还需要数据库驱动。官方建议安装 mysqlclient,用pip install mysqlclient即可安装, 最新版的是mysqlclient-1.4.4(请按博客日期比较)。如果安装的是最新版的django和python一般不会出问题。出问题会出error:C++ 14.0 is required ,原因是编译环境,即使安装了C++14也没用,解决办法是直接安装编译好的文件。直接百度搜索即可找到解决办法,(一般是直接下载安装编译好的文件即可)不赘述。

创建应用

创建用户应用:python manage.py startapp user,这是对user的应用,还需要post,content。输入之后会在项目根目录下创建一个user目录。
django2.2 简单博客 一_第3张图片
init 空
admin 后台管理
apps app的一些配置
models 一些模块创建在这里
tests 测试用
views 视图函数放在这里,
migrations 迁移,用来生成数据库的表,增量型。里面的init是空的。

创建好后要在settings中的apps要加入user。进行注册应用。

模型models

模型方法:

字段类 说明
AutoField 自增的整数字段。如果不指定,django会为模型自动增加主键字段
BooleanField 布尔值字段,对应表单控件CheckboxInput
NullBooleanField 比BooleanField多一个null值
CharField 字符串,max_length设定字符串长度,对应表单控件TextInput
TextField 大文本字段,一般超过4K个字符使用,对应表单控件TextInput
IntegerField 整数字段
BigIntegerField 更大整数字段,8字节
DateField 使用python的datetime.date实例表示的日期,auto_now=False每次修改对象自动设置为当前时间。auto_now_add=False对象第一个创建时自动设置为当前时间。auto_now,auto_now_add,default互斥。对应控件为TextInput,关联了一个JS编写的日历控件
TimeField 使用python的Datetime.time实例表示时间,参数同上
DateTimeField 使用python的datetime.datetime实例表示的日期,同上
FileFiled 一个上传文件的字段
ImageField 继承了FileFiled的所有属性和方法,但对上传的文件进行校验,确保是一个有效的图片

字段选项

说明
db_column 表中字段的名称。如果未指定,则使用属性名
primary_key 是否主键
unique 是否唯一键
default 缺省值,不是数据库字段的缺省值,而是新对象产生时被填入的缺省值
null 表的字段是否可以为null,默认False
blank django表单验证中,是否可以不填写,默认为False
db_index 字段是否有索引

关系型字段类

说明
ForeignKey 外键,表示一对多
MangToManyField 表示多对多
OneToOneField 一对一

一对多时,自动创建会增加_id后缀。从一访问多,使用对象.小写模型类_set;从一访问一,使用对象.小写模型类。
访问id 对象.属性_id

创建user的model类

基类models.Model
表名不指定则默认使用_。使用Meta类修改表名。
在modles中写下如下代码:

class User(models.Model):
    class Meta:
        db_table = 'user' # 数据库表名
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=48,null=False)
    email = models.CharField(max_length=64,null=False,unique=True)
    password = models.CharField(max_length=128,null=False)

    def __repr__(self):
        return "{} {} {}".format(self.id, self.name, self.email)

    __str__ = __repr__

迁移Migration
迁移:从模型定义生成数据库的表
生成迁移文件
在写好model时在,根目录下的terminal输入python manage.py makemigrations,生成迁移文件。
此时在user包下的migration包中会出现第一个迁移文件。如下:

from django.db import migrations, models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='User',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('name', models.CharField(max_length=48)),
                ('email', models.CharField(max_length=64, unique=True)),
                ('password', models.CharField(max_length=128)),
            ],
            options={
                'db_table': 'user',
            },
        ),
    ]

这是一个脚本文件,一般都会用sql脚本来做,不会采用这种方式。如果修改了model,需要调用makemigrations后,序号会增加,当前是0001_initial。

执行迁移,生成数据库的表输入:
python manage.py migrate即可
可以在数据库中看到文件生成了。
django2.2 简单博客 一_第4张图片
可以看到user表建立了,并且所有设置均正确。sql语句如下:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(48) NOT NULL,
  `email` varchar(64) NOT NULL,
  `password` varchar(128) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

如果数据库连接不成功,请注意密码,host,库名是否输入正确。

django后台管理

1、创建管理员
管理员用户名
密码
命令python manage.py createsuperuser,创建超级用户即可。输入用户名密码即可。
2、本地化
setting.py 中设置语言,时区,名称可以查看django\contrib\admin\locale目录

3、启动WEB server看看是否成功:python manage.py runserver

4、登录后台管理
http://127.0.0.1:8000/admin可以看到后台管理界面
5、注册应用模块
在user应用的admin.py添加

from django.contrib import admin
from .models import User

admin.site.register(User)

在后台管理界面就可以对user进行增删改了

路由

由python的WSGI库编写的WEB框架中,路由功能就是实现URL模式匹配和处理函数之间的映射。对于django也是如此。路由配置要在项目的urls.py中配置,也可以实现多级匹配,在每一个应用中,建立一个urls.py文件配置路由映射。
url函数
url(regex,view,kwargs=None,name=Name),进行模式匹配
regex:正则表达式,与之匹配的URL会执行对应的第二个参数view
view:用于执行与正则表达式匹配的URL请求
kwargs:视图使用字典类型的参数
name:用来反向获取URL

先做一个示例:

from django.http import HttpRequest,HttpResponse

def index(request):
    return HttpResponse(b'good work') # 可以是Json,不过参数必须是字典
    # return JsonResponse({'user':'good work'}) # 这个就是异步调用常用的。


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

在网页中能访问http://127.0.0.1:8000/index。但是不能访问http://127.0.0.1:8000/index/(这里是404报错)
也就是说在1.1版本的浏览器发起两次请求一次是index,一次是index/,服务器会根据你写的路径自动添加/。
在项目中首页多数使用HTML显示,为了加载速度快,一般多使用静态页面,如果首页内容过多,还有部分数据要变化,则将变化部分使用AJAX技术从后台获取数据,在前台动态的修改DOM树,然后将数据渲染出来即可,要用到异步技术,用JS将数据处理,这样后端就是单纯的数据服务。

本次我们只是实验,将使用模板技术实现。

模板

如果使用react实现前端页面,其实Django就没有必须使用模板,它其实就是一个后台服务程序,接受请求,响应数据。接口设计可以是纯粹的Restful风格,这次我们还是用模板技术实现html。
前端发起HTTP请求,后端发送数据结果,数据是由Json传送,或者restful风格的API,就是HTTP连接,配合上get post方法,数据提交靠Json(在request对象里,不同的框架在不同的地方)。

模板的目的就是为了可视化,将数据按照一定布局格式输出,而不是为了数据处理,所以一般不会有复杂的处理逻辑。模板的引入实现了业务逻辑和显示格式的分离,这样,在开发中,就可以分工协作,页面开发完成页面布局设计,后台开发完成数据处理逻辑的实现。就是一个填空题,填数据就完事了。

python的模板引擎默认使用Django template language(DTL)构建。

模板配置
在settings.py中设置模板项目的路径:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, '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',
            ],
        },
    },
]

先分析一下源码是啥;
DIRS列表,定义模板文件的搜索路径顺序,这里路径咋database下有示例,我们照着写,即os.path.join(BASE_DIR, ‘Templates’),即项目根目录(这里是blog-test)下的Templates文件(自己设定),这里写模板的话属于全局模板,我们也可以在user,app下建立单独的模板。
APP_DIRS 是否在每个已经安装的应用中查找模板。应用自己目录下有templates目录,例如django/contrib/admin/templates。如果应用需要可分离、可重用,建议把模板放在应用目录下。

下载是将文件打开,然后分解成数据流一个一个传给你。
渲染是将文件打开,将数据传输给浏览器,解析成DOM树。

模板渲染

模板处理的两个步骤,加载模板,渲染。
在这里我们可以使用render函数。看栗子:

from django.http import HttpRequest,HttpResponse
from django.template import loader,RequestContext

from django.shortcuts import render
def index(request):

    # tp = loader.get_template('index.html')
    # context = RequestContext(request,{'abc':123})
    # return HttpResponse(tp.render(context))
    return render(request, 'index.html',context={'abc':123})  # (request, template_name, context=None, content_type=None, status=None, using=None):
    # return HttpResponse(b'good work')

从源码可知render函数会返回HttpResponse对象,参数含义见字面意思,请求,模板名,数据字典,数据类型,状态码,。

模板页如下




    
    hello


    here is the content 

{{abc}}

将模板页放入Templates目录下,名称是index.和html。使用浏览器访问即可。

下面介绍DTL的一些常用语法

DTL常用语法

变量

语法{{ variable }}
变量名有字母数字下划线点号组成。
点号使用时,遵循以下原则:
1、字典查找,例如foo[“bar”],把foo当做字典,bar当做key;
2、属性或方法的查找,例如foo.bar,把foo当做对象,bar当做属性或方法;
3、数字索引查找,例如foo[bar],把foo当做列表,使用索引访问。
如果变量未能找到,则缺省插入空白字符串;在模板中调用方法,不能加小括号,自然也不能传递数据;{{ my_dict.keys }}这样是可以的。

模板标签

if/else 标签
基本语法格式:
{% if condition %}
… display
{% endif %}
或者
{% if condition %}
… display
{% elif condition2 %}
… display
{% else %}
… display
{% endif %}
条件支持and,or,not
注意,这是标签断开的,所以不能用缩进表示,必须有结束标签

for标签
{% for athlete in athlete_list %}
{{athlete.name}}
{% endfor %}
常用变量:

变量 说明
forloop.counter 当前循环冲1开始的计数
forloop.counter0 当前循环从0开始的计数
forloop.revcounter 当前循环的末尾开始倒计数至1
forloop.revcounter0 当前循环的末尾开始倒计数至0
forloop.first 是否是第一次进入循环
forloop.last 是否是最后一次进入循环
forloop.parentloop 循环嵌套时,当前循环的外层循环

给标签增加一个reversed使得该列表被反向迭代:
{% for athlete in athlete_list reverse %}
{{athlete.name}}
{% empty %}
…如果被迭代列表是空的或者不存在,则执行empty
{% endfor %}

可以嵌套使用for标签。
testfor.html模板,看栗子:




    
    test for


    {% for k,v in dic.items %}
  • {{forloop.counter}} {{k}},{{v}}
  • {% endfor %}

    {% for k,v in dic.items %}
  • {{forloop.counter0}} {{k}},{{v}}
  • {% endfor %}

    {% for k,v in dic.items %} {{forloop.first}} {{forloop.last}}
  • {{forloop.revcounter0}} {{k}},{{v}}
  • {% endfor %}

    {% for k,v in dic.items %}
  • {{forloop.revcounter}} {{k}},{{v}}
  • {% endfor %}


    {% for k in lis reversed%}
  • {{forloop.counter}} {{k}}
  • {% endfor %}

比较简单,不多说,需要注意的是在render里的参数要和for里的列表或者字典名一致,并且是个字典;如return render(request, 'testfor.html',context={'dic':d,'lis':lisx})

ifequal/ifnotequal标签
比较两值,当他们相等时,显示在{% ifequal/ifnotequal %}之中所有的值。和if标签类似,支持else标签。例如:

{% ifequal/ifnotequal a b %}
	

hello

{%endifequal%}

其他标签
csrf_tiken用于跨站请求伪造保护,防止跨站攻击。

注释标签{# 注释内容 #}

过滤器

类似于带有通过条件的管道,在变量被显示前修改它。
语法很简单{{ 变量 | 过滤器 }},例如{{name|lower}} 变量name被过滤器处理后大写变为小写。可以被套接,一个接一个。
过滤器传参
有些过滤器可以传递参数,过滤器的参数在冒号之后,并且一定是双引号包裹。例如{{bio|truncatewords:“30”}},截取bio的前30个词。
{{list|join:","}}将list的元素用逗号连接起来。
其他过滤器:

过滤器 说明 举例
first 取列表第一个元素
last 去列表最后一个元素
yesno 变量可以是Ture、False、None,其参数给定逗号分隔的三个值,返回三个的其中一个,顺序就是true对应第一…如果参数只有两个,None等效false处理 {{value|yesno:“yeah,no,maby”}}
add 加法,参数为负就是减法 数字加{{value|add:“122”}},列表合并{{list|add:“newlist”}},
divisibleby 能否被整除 {{value|divisibleby:“2”}},能否被2整除
addslashes 在反斜杠、单引号或者双引号前加上反斜杠 {{value|addslashes}},
length 返回变量的长度 {% if list|length >1 %}
default 变量等价False则使用缺省值 {{value|default:“dddd”}},
default_if_none 变量为none使用缺省值 {{value|default_if_none:“122”}},
date 按照指定格式的字符串参数格式化date或者datetime对象 {{pub_date|date:“n j Y”}},n表示1-12月,j表示1-31日,Y表示年,可以查看https://docs.djangoproject.com/en/2.0/ref/templates/builtins/#date

示例输出奇偶行不同颜色:

    {% for k,v in dic.items %}
  • {{forloop.counter}} {{k}},{{v}}
  • {% endfor %}

一般不会用style,多用class,以便随时修改。

用户功能设计与实现

我们只实现基本功能,接受用于通过post方法提交的注册信息,提交的数据是JSON格式。检查email是否已经存在于数据库表中,如果存在则返回错误状态码,如果不存在则将用户提交的数据存入表中,整个过程都采用AJAX异步过程,用户提交JSON数据,服务器端获取数据后处理,返回JSON。

URL:/user/reg
METHOD:POST(django做不到将方法一起映射。)

路由配置
有多种方式,可以直接放在blog下的urls,也可以放在user下的urls里,建议放在user下,这里在user包下创建一个urls。目录结构如下:
django2.2 简单博客 一_第5张图片
这里要用到postman或者一些测试工具,大家自己安装啦,postman是免费的,不过要注册。使用在这里不多说,看示例:
因为现在先做注册提交数据的处理函数,先解决连接问题,先将settings中的MIDDLEWARE(中间件)中的’django.middleware.csrf.CsrfViewMiddleware’,注释掉,因为在使用post提交数据时如果用的是一些测试工具,会出现Forbidden (CSRF cookie not set.): /user/reg,说你的cookie没有设定,因此测试时注释掉会更好。
之后:
在views中写入以下代码:(处理提交过来的数据)

from django.http import JsonResponse,HttpRequest,HttpResponse,HttpResponseBadRequest


import json
import logging

formt = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=formt,level=logging.INFO)
def reg(request:HttpRequest):
    try:
        playload = json.loads(request.body.decode())  # 请求有问题时
        email = playload['email']
        name = playload['name']
        password = playload['password']
        print(email,name,password)
        return HttpResponse("hey welcome")
    except Exception as e:
        logging.info(e)
        return HttpResponseBadRequest()  # HttpResponseBadRequest不是except子类,不能用raise无参构造

在user中的urls中写入以下代码:

from django.urls import path
from user.views import reg

urlpatterns = [
    path('reg', reg),
]

与blog下的urls一样,只提供路由匹配,因此在blog下的urls要修改一下,因为django将请求先按照blog下的urls中的 urlpatterns进行路由匹配,在决定放在哪个APP中,因此修改如下: path('user/', include('user.urls')),将path转到user下的urls,include就是将这种字符串转化为路径,并按照路径加载模块,在这里可以实现二级匹配。此时通过post提交数据就可以看到输入和输出如下:
django2.2 简单博客 一_第6张图片
其中要注意的是使用postman提交数据时json字符串要用双引号
输出
此时我们能拿到用户注册的数据了

Json处理

我们用simplejson,比标准库好用一些,而且功能较强。安装用pip install simplejson;浏览器端提交的数据在请求的body中,要用到json解析,因此用更方便的simplejson,将上面代码的json替换为simplejson就可以了。

因为HttpResponse的错误类型都不是exception类,因此都不能使用raise无参构建,应该使用return,这些类都继承自HttpResponse,只有一个Http404是继承自Exception。前端可以通过状态码来判断是否成功

将上面的代码增加邮箱检查,用户信息保存,就要用到Django的模型操作。只需要将之前在user下的models中写的代码导入即可,如from .models import User,这样就可以进行注册进数据库了:代码如下

from django.http import JsonResponse,HttpRequest,HttpResponse,HttpResponseBadRequest
from .models import User
import simplejson
import logging

formt = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=formt,level=logging.INFO)

def reg(request:HttpRequest):
    try:
        playload = simplejson.loads(request.body)  # 请求有问题时
        email = playload['email']
        name = playload['name']
        password = playload['password']
        query = User.objects.filter(email=email)
        print(query)
        if query:
            return HttpResponseBadRequest()
        name = playload['name']
        password = playload['password']
        user = User()
        user.email = email
        user.name = name
        user.password = password
        try:
            user.save()  # 将注册数据存进数据库
            return JsonResponse({'user':user.id})
        except:
            raise
        # return HttpResponse("hey welcome")
    except Exception as e:
        logging.info(e)
        return HttpResponseBadRequest()  # HttpResponseBadRequest不是except子类,不能用raise无参构造

先介绍一下模型操作;
管理器对象
Django会为模型类提供一个objects对象,他是django.db.models.manager.Manager类型,用于数据库交互,当定义模型类的时候没有指定管理器,则Django会为模型类提供一个object的管理器。如果在模型类中指定管理器后,Django不再会提供默认的objects管理器了。这是Django的模型进行数据库查询查询操作的接口,Django应用的每个模型都至少有一个管理器。

Django ORM
数据库的校验validation是在对象的Save和Update方法上。
对模型对象的CRUD,被DjangoORM转换成相应的SQL语句操作不同的数据源。

查询

查询集:
查询会返回结果的集,他是django.db.models.query.QuerySet类型。
惰性求值,即创建查询集不会带来任何数据库的访问,直到调用数据时才会访问数据库,在迭代、序列化、if语句中都会立即求值,和sqlalchemy一样。返回的是可迭代对象。
缓存,每个查询集都包含一个缓存来最小化对数据库的访问。新建查询集缓存为空,首次对查询集求值时,会发生数据库查询,Django会把查询的结果存在这个缓存中,并返回请求的结构,接下来对查询集求值将使用缓存结构。

看栗子:

[user.name for user in User.objects.all()]
[user.name for user in User.objects.all()]  # 这两个没有使用缓存,查询两次库
\~~~~~~~~~~~~~~~~~~~~~~~~~~
qs = User.objects.all()
[user.name for user in qs]  # 使用了缓存,因为使用了同一个查询集

限制查询集(类似于python的切片)
查询集可以直接使用索引下标的方式,相当于SQL语句中的elimit和offset子句。
例如:

qs = User.objects.all()[20:40]  # limit 20 offset 20
qs = User.objects.all()[20:30]  # limit 10 offset 20

过滤器

名称 说明
all() 全选
filter() 过滤,返回满足条件的数据
exclude() 排除,排除满足条件的数据
order_by() 排序
values() 返回一个对象字典的列表,像json

filter(k1=v1).filter(k2=v2)等价于filter(k1=v1,k2=v2)
filter(pk=10)pk就是主键,不用关系主键字段名,也可以使用主键名来做条件。

返回单值得方法

名称 说明
get() 仅返回单个满足条件的对象,如果未能返回对象则抛出DoserNotExist异常;如果能返回多条,抛出MultipleObjectsReturned异常
count() 返回当前查询的总条数
first() 返回第一个对象,limit 1
last() 返回最后一个对象
exist() 判断查询集中是否有数据,如果有则返回True

字段查询表达式(Field lookup)
字段查询表达式可以作为filter(),exclude(),get()的参数,实现where子句。
语法:属性(字段名)名称__比较运算符=值,注意这里是双下划綫(一般来说数据库不存在连续的两个下划线命名的字段。)

名称 栗子 说明
exact filter(isdeleted=false), filter(isdeleted__exact=false) 严格等于,可省略不写
contains exclude(title__contains=‘地’) 是否包含,大小写敏感,等价于like “%地%”
startswith,endswith 以什么开头或结尾
isnull,isnotnull 是否为null
iexact, icontains, istartswith, iendswith i的意思是忽略大小写
in filter(pk__in=[1,2,3,4]) 是否在指定范围中
gt,gte,lt,lte 大于、大于等于,小于,小于等于
year、month、day、week_day、hour、minute、second filter(pub_date__year=200) 对日期类型属性属性处理

Q对象
虽然Django提供传入条件的方式,但是不方便,因此提供了Q对象来解决。
Q对象是django.db.models.Q,可以使用&(and)、|(or)操作符来组成逻辑表达式,~表示not。
可使用&|和Q对象来构造复杂的逻辑表达式
过滤器函数可以使用一个或多个Q对象,如果混用关键字参数和Q对象,那么Q对象必修位于关键字参数的前面,所有参数都将and在一起。

看栗子:

User.objects.filter(pk__gt=6).filter(pk__lt=10) 与

其他的一样,过于简单不多说。

注册接口完善

认证

HTTP协议是无状态协议,为了解决无状态产生的问题,出现了cookie和session技术。

传统的session-cookie机制
浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为是第一次请求,会返回一个新的session id给浏览器端。浏览器只要不关闭,这个session id就会随着每一次请求重新发给服务器端,服务器端查找这个session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。
session是会话级别的,可以在这个会话session中创建很多数据,连接断开,session清除,包括session id。这个session id还需要有过期机制,一段时间如果没有发起请求,认为用户已经断开,就清除session。浏览器端也会清除响应的cookie信息。
服务器端保存着大量的session信息,很消耗服务器内存,而且如果多服务器部署,还要考虑session共享的问题,比如redis,memcached等方案。

无session方案
既然服务器只是需要一个ID来表示身份,那么不使用session也可以创建一个ID返回给客户端。但是,要保证客户端无法篡改(耗时长,无法实现算法等)。
服务器生成一个标识,并使用某种算法对标识签名。服务器端收到客户端发来的标识,需要检查签名。缺点就是,加密解密都需要cpu计算,无法让浏览器自己主动检查过期的数据以清除。可以添加时间戳。
这种技术简称JWT(Json WEB Token)

JWT
是一种采用Json方式安装传输信息的方式。我们使用PyJWT。安装pip install pyjwt,地址https://pypi.org/project/PyJWT/1.5.3/,在单点登录应用广泛。

jwt原理
先看一段代码,以及输出结果,就能了解JWT原理,当然你也可以通过看源码分析原理。

import jwt
from blog.settings import SECRET_KEY
password = SECRET_KEY
# encode(self,
#                payload,  # type: Union[Dict, bytes]
#                key,  # type: str
#                algorithm='HS256',  # type: str
#                headers=None,  # type: Optional[Dict]
#                json_encoder=None  # type: Optional[Callable]
#                ):
payload = {'name':'1234', 'test':'456789'}
pwd = jwt.encode(payload, password)
print(1,pwd)
header, pld, siq = pwd.split(b'.')
import base64
def fix(src):
    rem = len(src) % 4
    return src + b'=' * rem
print(2,base64.urlsafe_b64decode(fix(header)))
print(3,base64.urlsafe_b64decode(fix(pld)))
print(4,base64.urlsafe_b64decode(fix(siq)))

from jwt.algorithms import get_default_algorithms
al_obj = get_default_algorithms()['HS256']
print(5,al_obj)
newkey = al_obj.prepare_key(password)
print(6,newkey)

sig_input,_,_ = pwd.rpartition(b'.')
print(7,sig_input)
cryp = al_obj.sign(sig_input, newkey)
print(8,cryp)
print(9,siq)

输出

1 b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiMTIzNCIsInRlc3QiOiI0NTY3ODkifQ.SHXiW2ZimmVwdtbiHtu-dEeELQ8ZQb_YrLE7GAQ5cGE'
2 b'{"typ":"JWT","alg":"HS256"}'
3 b'{"name":"1234","test":"456789"}'
4 b'Hu\xe2[fb\x9aepv\xd6\xe2\x1e\xdb\xbetG\x84-\x0f\x19A\xbf\xd8\xac\xb1;\x18\x049pa'
5 
6 b'hr18c2t(0@=f!a3jr=!2a6!=mk7o9o3f9a^d#l^4o#)25&(wlp'
7 b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiMTIzNCIsInRlc3QiOiI0NTY3ODkifQ'
8 b'Hu\xe2[fb\x9aepv\xd6\xe2\x1e\xdb\xbetG\x84-\x0f\x19A\xbf\xd8\xac\xb1;\x18\x049pa'
9 b'SHXiW2ZimmVwdtbiHtu-dEeELQ8ZQb_YrLE7GAQ5cGE'

由上可知jwt生成的token分成三部分
1、header,由数据类型、加密算法构成
2、payload,需要传输的数据,一般来说放入python对象即可,会被序列化
3、signature,签名部分。是前面2部分数据分别base64编码后使用点号连接后,加密算法使用key计算好一个结果,再被base64编码,得到签名。
由此,所有的数据都是明文传输,只是做了base64,如果是敏感信息,就不要使用jwt。数据签名的目的不是为了隐藏数据,而是保证数据不被篡改,如果数据篡改了,发回到服务器,服务器用key再计算一遍,肯定无法匹配。

密码

使用邮箱加密码登录。
邮箱要求唯一是可以的,密码如何储存?

早期是明文存储,之后使用MD5,但是由于现在计算机计算能力强,容易被暴力破解,使用反查的方式找到密码。在之后就是加盐,使用hash(password+salt)的结果存入到数据库中,就算拿到数据库的密码反查,也没用。而且要随机加盐,增加破解难度。

密码等容易遭受暴力破解如穷举,因此我们用慢hash算法,如bcrypt,会让每一次计算都很慢,达到秒级,这样穷举时间长,以当前的计算机运行能力都需要很久。

bcrypt

安装 pip install bcrypt
注意:如果在python的terminal里一直安装不上,显示超时time out,或者无法连接,则先runserver一次即可安装了,应该是在项目根目录下某些东西尚未加载导致的。
看示例代码:

import bcrypt
import datetime
from blog.settings import SECRET_KEY


password = SECRET_KEY
print(1,bcrypt.gensalt())
print(2,bcrypt.gensalt())
salt = bcrypt.gensalt()
tx = bcrypt.hashpw(password.encode(),salt)
print(3,tx)
start = datetime.datetime.now()
tx = bcrypt.hashpw(password.encode(),salt)
end = datetime.datetime.now()
print(4,tx)
print((end - start).total_seconds())

输出

1 b'$2b$12$2ia5yzV1/1DhI3CNRMxlj.'
2 b'$2b$12$X/vOVCGNh5YmB6Au6Z7ATu'
3 b'$2b$12$3wbS/EwNBXhBJyQObBQYBuIe0XnbXhOBX3KhQvo0PtYWliqapZuU.'
4 b'$2b$12$3wbS/EwNBXhBJyQObBQYBuIe0XnbXhOBX3KhQvo0PtYWliqapZuU.'
0.238363

由此可以看出,相同的盐,前后加密结果一样,不同的盐前后不同,时间在0.238363秒左右,因此穷举很费时间。我们先采用这种简单的方式进行密码锁。

因此修改之前的注册代码

from django.http import JsonResponse,HttpRequest,HttpResponse,HttpResponseBadRequest
from .models import User
import bcrypt
import jwt
import simplejson
import logging
import datetime
from django.conf import settings


formt = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=formt,level=logging.INFO)

def gen_token(user_id):
    return jwt.encode(
        {'user_id':user_id,  # 增加时间戳 判断是否重发
         'timestamp': int(datetime.datetime.now().timestamp())  # 时间取整
         }, settings.SECRET_KEY, 'HS256'
    ).decode()  # 转换为字符串


def userindex(request):
    return HttpResponse('hey good job')


def checkmail():
    return HttpResponse()


def reg(request:HttpRequest):
    # print(1,request.GET)
    # print(2,request.method)
    # print(3,request.POST)
    # print(4,request.path)
    # print(5,request.headers)
    try:
        playload = simplejson.loads(request.body)  # 请求有问题时
        email = playload['email']
        name = playload['name']
        password = playload['password']
        query = User.objects.filter(email=email)
        print(query)
        if query:
            return HttpResponseBadRequest()
        name = playload['name']
        password = bcrypt.hashpw(playload['password'].encode(), bcrypt.gensalt())
        user = User()
        user.email = email
        user.name = name
        user.password = password
        try:
            user.save()
            return JsonResponse({'token':gen_token(user.id)})
        except:
            raise
        # return HttpResponse("hey welcome")
    except Exception as e:
        logging.info(e)
        return HttpResponseBadRequest()  # HttpResponseBadRequest不是except子类,不能用raise无参构造

使用postman收到了发来的token

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ0aW1lc3RhbXAiOjE1NjkyNDIzMjN9.c7EGVl4QgAbESXNZkGc-P1KWf1Mj7RrUc1uq998Ub0Y"
}

接下来解决用户登录接口的设计
接受用户通过post方法提交的登录信息,提交的数据是JSON格式。
从user表中的email找出匹配的一条记录,验证密码是否正确。
URL:/user/login
METHOD:POST

首先配置路由:
在user的urls的urlpatterns下写入path(‘login’,userlog),并从users.views import userlog
在views中写入以下代码。

def userlog(request):
    try:
        playload = simplejson.loads(request.body)
        email = playload['email']
        password = playload['password']
        user = User.objects.filter(email=email)
        user = user.first()  # 取单值
        if not user:
            return HttpResponseBadRequest()
        if not bcrypt.checkpw(password.encode(),user.password.encode()):  # 取出的为str类型
            return HttpResponseBadRequest()
        return JsonResponse({
            'user':{
                'user_id':user.id,
                'name':user.name,
                'password':user.password
            },'token':gen_token(user.id)
        })
    except Exception as e:
        logging.info(e)
        print('~~~~~~~~~')
        return HttpResponseBadRequest()  # HttpResponseBadRequest不是except子类,不能用raise无参构造

这里要注意,如果你往mysql数据库中存的是bytes,取出的则是str(bytes),encode一下就多了b’content’字符,会出现 Thread-1 8740 Invalid salt这种报错。因此存入数据库一定要转为str即decode()。登录成功返回json

{
    "user": {
        "user_id": 7,
        "name": "sesin",
        "password": "$2b$12$vV/b1tFhCyZLRRc.YuuOfO8/a3oR38BFCWlf5DA04MfFb/FLg2XMK"
    },
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo3LCJ0aW1lc3RhbXAiOjE1NjkyOTIzNjF9.iVd194IsxFxYYz5iXiZ75s9ZTgRzywOZAzxjg55z9kk"
}

这里可以设置cookie,只需要jsonobj.set_cookie(‘jwt’,token)即可。

认证接口

接下来考虑如何获取浏览器提交的token信息。有以下两种
1、使用Header中的Authorization
通过这个给header增加token信息,通过header发送数据,方法可以是post,get。
2、自定义header,使用JWT来发送token,我们选择第二种方式

认证

基本上所有业务都需要认证用户的信息。
这里比较时间戳,如果过期就直接抛未认证401,客户端收到后就跳转到登录页。如果没有提交user id,就直接重新登录。如果用户查到了,填充user对象。

request-》时间戳比较-》user id 比较-》向后执行

在django.contrib.auth中也提供了许多方法,这里主要介绍其中的三个:
1、authenticate(**credentials)
提供了用户认证,即验证用户名以及密码是否正确,使用user=authenticate(username=’’,password=’’)
2、login(HttpRequest,user,backend=None)
该函数接受一个HttpRequest对象,以及一个认证了的User对象
此函数使用django的session框架给某个已认证的用户附加上session等信息。
3、logout(request)
注销用户,该函数接受一个httpRequest对象,无返回值。当调用该函数时,当前请求的session信息会全部清除,该用户即使没有登录,使用该函数也不会报错。
还提供了一个装饰器来判断是否登录django.contrib.auth.decorators.login_required本项目使用了无session版本,且用户信息自己建表管理,所以我们自己实现认证代码。

可以采用中间件技术自定义认证中间件,官方定义在Django的request和response处理过程中,由框架提供的hook钩子。但是这种方法会拦截所有的请求和响应,我需要判断是不是需要拦截的view函数,所以,还有一个方法,装饰器。

采用装饰器

在需要认证的view函数上增强认证功能,写一个装饰器函数。谁需要认证,就在这个view函数上应用这个装饰器。这样可以实现认证分离,并且简单。

看这一部分的代码:

AUTH_EXPIRE = 8*60*60


def authenticate(view):
    def wrapper(request:HttpRequest):
        payload = request.META.get('HTTP_JWT')  # 通过 Meta 中含有所有post过来的参数,如果在header中含有JWT,则名字为HTTP_JWT,
        if not payload:
            return HttpResponse(status=401)
        try:
            payload = jwt.decode(payload,settings.SECRET_KEY, algorithms=['HS256'])
            print(payload)
        except:
            return HttpResponse(status=401)
        # 验证过期
        current = datetime.datetime.now().timestamp()
        if (current - payload.get('timestamp', 0))>AUTH_EXPIRE:
            return HttpResponse(status=401)

        try:
            user_id = payload.get('user_id',-1)
            user = User.objects.filter(pk=user_id).get()
            request.user = user
        except Exception as e:
            logging.info(e)
            return HttpResponse(status=401)

        ret = view(request)
        return ret
    return wrapper


@authenticate
def test(request):
    return HttpResponse('JWT test')

当然别忘了在user/urls/下加入path路径,并导入test,这样我们就实现了认证的接口,其中包括时间过期等,验证等。通过postman中的header写入token完成测试django2.2 简单博客 一_第7张图片
收到一个JWT test,
如果在token中加入了exp,则jwt.decode(payload,settings.SECRET_KEY, algorithms=['HS256'])会自动验证时间,不需要后面的代码了。exp写的是时间过期点,必须取整。
因此修改的gen_token如下:

def gen_token(user_id):
    return jwt.encode(
        {'user_id':user_id,  # 增加时间戳 判断是否重发
         'exp': int(datetime.datetime.now().timestamp() + AUTH_EXPIRE)  # 时间取整,加入
         }, settings.SECRET_KEY, 'HS256'
    ).decode()  # 转换为字符串

之后我们来做博文接口:

博文接口

博文功能如下:

功能 函数 request方法 路径
发布(增) pub post /pub
看文章(查) get get /(\d+)
列表(分页) getall get /

首先创建博文应用python manage.py startapp post,然后将post注册进settings.py中,然后才可以迁移。
先做一些准备工作,将上面的路径配置好,在post文件夹中添加urls.py文件,在~\blog\urls.py中添加 path(‘post/’, include(‘post.urls’)),在post/views中添加

from django.urls import path
from .views import pub,get,getall


urlpatterns = [
    path('', getall),
    path('pub', pub),
    path('',get),
]

定义views的函数:

from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,HttpResponseNotFound

def pub(request:HttpRequest):
    return HttpResponse('pub')

def get(request:HttpRequest, id=0):
    return HttpResponse('get')

def getall(request:HttpRequest):
    return HttpResponse('getall')

即可。这里注意(2.0版本的django语法变化,1,11直接使用正则表达式即可):要从URL捕获值,需要使用尖括号。
捕获的值可以选择包括转换器类型。例如,用于 捕获整数参数。如果不包括转换器/,则匹配除字符以外的任何字符串。无需添加斜杠。嫌麻烦则可以使用re_path支持正则表达式即可。

模型:

先将模型写好。很简单就是将之前的数据库模型写好即可。看代码:

from user.models import User

class Post(models.Model):
    class Meta:
        db_table = 'post'
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=200, null=False)
    pubdate = models.DateTimeField(null=False)
    # author_id = models.IntegerField(null=False)  # sqlalchemy这样写但是这里不行
    author = models.ForeignKey(User, on_delete=None)  # 指定外键,migrate会生成author_id字段
	# self.content可以访问Content实例,其内容是self.content.content
    def __repr__(self):
        return "post {} {} {}".format(self.id, self.title, self.author)

class Content(models.Model):
    class Meta:
        db_table = 'content'
    post = models.OneToOneField(Post, None, to_field='id')  # 由下面可以看到这里字典创建了一个id主键。一对一,外键引用post_id
    content = models.TextField(null=False)

    def __repr__(self):
        return "post {} {}".format(self.id, self.content[:20])

使用migrate创建完成后查看数据库信息。

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(200) NOT NULL,
  `pubdate` datetime(6) NOT NULL,
  `author_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `post_author_id_2343ddae_fk_user_id` (`author_id`),
  CONSTRAINT `post_author_id_2343ddae_fk_user_id` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `content` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` longtext NOT NULL,
  `post_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `post_id` (`post_id`),
  CONSTRAINT `content_post_id_c6de7cfd_fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

可以看到没有啥问题,这里一定要检查一下,不然万一和你预想的不同,就需要及时更改。

pub接口实现

用户从浏览器提交Json数据,包含title,content。提交博文需要认证用户,从请求的header中验证jwt。
即post->@authenticate pub ->return post_id
看代码:

from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,HttpResponseNotFound,JsonResponse
from user.views import authenticate
from .models import Post,Content
from user.models import User
import datetime
import simplejson

@authenticate
def pub(request:HttpRequest):
    post = Post()
    content = Content()
    try:
        payload = simplejson.loads(request.body)
        post.title = payload['title']
        post.author = User(id=request.user.id)  # 从request中取得user_id 并注入到post.author中,后面有两个空字符串,即User类的name和email。但我们只需要id就可以了
        post.pubdate = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))  # 这里必须包含时区
        post.save()
        content.content = payload['content']
        content.post = post
        content.save()
        return JsonResponse({'post_id':post.id})
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()

通过postman使用post登录后,收取到的token放在post/pub下的header里,并且在body中写入title和content。点击发送即可收到,post_id了,注意:在pubdate中必须包含时区不然会出现G:\blog-test\venv\lib\site-packages\django\db\models\fields\__init__.py:1423: RuntimeWarning: DateTimeField Post.pubdate received a naive datet错误。但是文章依旧可以存储成功。

get接口实现

我们根据post_id查询博文并返回,查看博文并不需要认证。但是修改博文需要认证。同样list接口也不需要认证。

request get -> get Post by id ->return post+content

def get(request:HttpRequest, id):  # 接收url中发来的id
    try:
        id = int(id)
        post = Post.objects.get(pk=id)  # 取得单值
        if post:
            return JsonResponse({
                'post':{
                    'post_id':post.id,
                    'title':post.title,
                    'author':post.author.name,
                    'author_id':post.author.id,
                    'postdate':post.pubdate.timestamp(),
                    'content':post.content.content,
                        }
            })
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()

在url中输入http://127.0.0.1:8000/post/2,使用get方法即可收到return的带有文章详细信息的Json了。

getall接口实现

简单的说一下这个接口,发起get请求,通过查询字符串查询第几页数据。
即request: get -> get all (page=1) -> return post list
当然这里要注意分页信息。一般有,当前页page,行数限制size,总页数pages,记录总数count,因为page 和size的处理基本一致,所以为了更加方便的使用分页信息,这里写一个页码处理函数,方便以后修改。

def validate(d:dict, name:str, type_func, default, validate_func):
    try:
        result = type_func(d.get(name,default))
        result = validate_func(result, default)
    except:
        result = default
    return result

def getall(request:HttpRequest):
    page = validate(request.GET,'page',int,1,lambda x,y: x if x>0 else y)
    size = validate(request.GET,'size',int,20,lambda x,y: x if x>0 and x < 101 else y)

    try:  # 按id倒排
        start = (page -1)*size

        posts = Post.objects.order_by('-id')
        count = posts.count()
        posts = posts[start:start+size]

        return JsonResponse({
            'posts':[
                {
                    'post_id':post.id,
                    'title':post.title
                } for post in posts
            ],'pagination':{
                'page':page,
                'size':size,
                'count':count,
                'pages':math.ceil(count/size)
            }
        })
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()

以上是博客后端的内容。

你可能感兴趣的:(博客)