# 2、Django基础二



* python: 3.6.5
* django: 2.1.8

## 1. 项目准备

### 1.1 配置虚拟环境

```bash
pip3 install pipenv
mkdir test && cd test/
pipenv --python 3.6.5
pipenv shell
```

### 1.2 安装Django

```bash
((test) ) [root@k8s test]# pipenv install django==2.1.8
```

### 1.3 创建项目

```bash
((test) ) [root@k8s test]# django-admin startproject project
```

### 1.4 修改时区

```bash
''' settings.py
ALLOWED_HOSTS = ["*"]

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'
'''
```

### 1.5 创建超级管理员

```bash
((test) ) [root@k8s project]# python manage.py makemigrations
No changes detected
((test) ) [root@k8s project]# python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

((test) ) [root@k8s project]# python manage.py createsuperuser
用户名 (leave blank to use 'root'): admin
电子邮件地址: admin@qq.com
Password: 
Password (again): 
密码跟 电子邮件地址 太相似了。
密码长度太短。密码必须包含至少 8 个字符。
这个密码太常见了。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
```

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2Fo7pQHBGT84kNSXcmyw7C%2Fimage-20211109131044184.png?alt=media\&token=28af92a6-9da9-4103-9918-89577aabfbda)



### 1.6 创建APP应用及注册

```bash
((test) ) [root@k8s project]# python manage.py startapp blog
```

```bash
''' settings.py 文件
INSTALLED_APPS = [
    ''''''
    'blog',
]
'''
```

## 2. 项目

### 2.1 数据模型分析

* 分类：　一个分类下有多个文章
* 标签：　一个文章下有多个标签，一个标签下有多个文章
* 文章：　标题、摘要、内容、创建时间、修改时间、归属分类、归属标签、作者

### 2.2 数据模型创建

* blog/models.py

```python
from django.db import models

class Categorys(models.Model):
    ''' 分类表 '''
    name = models.CharField(max_length=100,verbose_name='分类')

    def __str__(self):
        return self.name


class Tags(models.Model):
    ''' 标签表 '''
    name = models.CharField(max_length=100, verbose_name='标签')

    def __str__(self):
        return self.name


class Posts(models.Model):
    ''' 文章表 '''
    title = models.CharField(max_length=100,verbose_name='标题')
    body = models.TextField(verbose_name='内容')
    ct_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    mf_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
    excerpt = models.CharField(max_length=200, blank=True, null=True,verbose_name='内容摘要')
    looks = models.PositiveIntegerField(default=0,verbose_name='阅读量')
    categorys = models.ForeignKey(Categorys, on_delete=False,verbose_name='归属分类')
    tags = models.ManyToManyField(Tags, blank=True, null=True,verbose_name='归属标签')
    # author = models.ForeignKey(User)                         # 作者

    def __str__(self):
        return self.title
    
    # 自定义自增阅读量方法
    def increase_looks(self):
        self.looks += 1
        self.save(update_fields=['looks'])
```

### 2.3 数据库迁移

```bash
# 生成迁移脚本、将迁移信息写入数据库
((test) ) [root@k8s project]# python manage.py makemigrations
System check identified some issues:

WARNINGS:
blog.Posts.tags: (fields.W340) null has no effect on ManyToManyField.
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Categorys
    - Create model Posts
    - Create model Tags
    - Add field tags to posts

((test) ) [root@k8s project]# python manage.py  migrate
System check identified some issues:

WARNINGS:
blog.Posts.tags: (fields.W340) null has no effect on ManyToManyField.
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK
```

### 2.4 Admin后台注册模型

* blog/admin.py

```python
from django.contrib import admin
from blog.models import Category,Tag,Post

class PostAdmin(admin.ModelAdmin):
    list_display = ['title','excerpt', 'ct_time', 'mf_time', 'category']
    list_display_links = ['title']
    search_fields=['title']        # 以title搜索
    list_filter = ['category']     # 过滤
    list_per_page = 20             # 分页
    # list_editable = ['category'] # 列表编辑


admin.site.register(Category)
admin.site.register(Tag)
admin.site.register(Post,PostAdmin)
```

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FOl1gGjqDH9593j43eMUi%2Fimage-20211109143656401.png?alt=media\&token=f29f7f4a-ad38-49c2-b303-361ed688c94f)

## 3. Django 博客首页 和 详情页

### 3.1 URL分析

* 首页： 域名根路径
* 文章： 每个文章对应不同的URL
  * 当用户访问 <网站域名>/post/1/ 时，显示的是第一篇文章的内容，
  * 当用户访问 <网站域名>/post/2/ 时，显示的是第二篇文章的内容。这里数字代表了第几篇文章，

### 3.2 创建 urls.py 文件

* blog/urls.py

```
from django.urls import re_path
from blog import views

urlpatterns = [
    # 首页
    re_path(r'^$', views.index, name='index'),

    # ^: 以什么开头， $是以什么结尾。 [0-9]指单个数字, +代表前面的字符出现1次或者多次。
    # (?P<pk>[0-9]+) 关键字匹配
    # /post/1/   ====> 1满足正则规则的， 将pk=1
    re_path(r'^post/(?P<id>[0-9]+)/$', views.detail, name='detail'),
]
```

### 3.3 首页||文章详情视图

* blog/views.py

```
from django.shortcuts import get_object_or_404, render
from blog.models import Post


def index(request):
    posts = Post.objects.all()
    return render(request, 'blog/index.html', context={'posts':posts})

def detail(request,id):
    '''
      get_object_or_404 方法，
      其作用就是当传入的 id 对应的 Post 在数据库存在时，就返回对应的 post ，
      如果不存在，就给用户返回一个 404 错误，表明用户请求的文章不存在。
    '''
    post = get_object_or_404(Post, id=id)
    post.increase_looks() # 执行阅读量自增方法
    return render(request, 'blog/detail.html', context={'post': post})
```

### 3.4 Model中自定义ID方法

* blog/models.py

```
class Post(models.Model):
    ...
    def __str__(self):
        return self.title
    # 自定义 get_absolute_url 方法
    # 记得从 django.urls 中导入 reverse 函数
    # reverse相当于Flask里面的url_for， 根据视图函数名称反向获取对应的路由地址.
    # /post/1/
    def get_absolute_url(self):
        return reverse('detail', kwargs={'id': self.id})
```

### 3.5 项目 urls.py 添加 APP 路径映射

* project/urls.py

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path(r'', include('blog.urls')),
]
```

### 3.6 html 文件

```markup
# templates/blog/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>欢迎来到博客首页</h1>
</body>
</html>
```

```markup
# templates/blog/detail.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{{ post.title }}
{{ post.created_time }}
{{ post.modified_time }}
{{ post.body }}

</body>
</html>
```



### 3.5 设置静态文件static和模板templates文件

* 这里 static 与 项目同级， 存放静态文件
* 这里 templates 与 项目同级， 存放 html 文件

```python
# project/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        ...
    },
]


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

STATIC_URL = '/static/'

# 静态文件存放路径
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]
```

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FpwXP7b6w4sBKzregoncw%2Fimage-20211109155858097.png?alt=media\&token=5e81cabf-3a8d-42ff-b744-b4d58f8b6aad)

## 4. Admin后台集成 markdown 格式编写

* 参考： https://pypi.org/project/django-mdeditor/

### 4.1 安装django-mdeditor

```bash
# 使用别的版本上传图片可能会报错，参考官网
pipenv install django-mdeditor==0.1.18
```

### 4.2 注册 mdeditor APP

* project/settings.py

```python
INSTALLED_APPS = [
    ...
    'mdeditor',
]


# 当上传图片的时候会自动在项目下创建  uploads/editor 目录， editor 是下面 MDEDITOR_CONFIGS 定义
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'



# 下面可有可无，如果自定义话，修改下面文件内容
MDEDITOR_CONFIGS = {
    'default':{
        'width': '90% ',  # 自定义编辑框宽度
        'heigth': 500,  # 自定义编辑框高度
        'toolbar': ["undo", "redo", "|",
                    "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|",
                    "h1", "h2", "h3","h4", "h5", "h6", "|",
                    "list-ul", "list-ol", "hr", "|",
                    "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime"
                    "emoji", "html-entities", "pagebreak", "goto-line", "|",
                    "help", "info",
                    "||", "preview", "watch", "fullscreen"],  # 自定义编辑框工具栏
        'upload_image_formats': ["jpg", "jpeg", "gif", "png", "bmp", "webp"],  # 图片上传格式类型
        'image_folder': 'editor',  # 图片保存文件夹名称
        'theme': 'default',  # 编辑框主题 ，dark / default
        'preview_theme': 'default',  # 预览区域主题， dark / default
        'editor_theme': 'default',  # edit区域主题，pastel-on-dark / default
        'toolbar_autofixed': True,  # 工具栏是否吸顶
        'search_replace': True,  # 是否开启查找替换
        'emoji': True,  # 是否开启表情功能
        'tex': True,  # 是否开启 tex 图表功能
        'flow_chart': True,  # 是否开启流程图功能
        'sequence': True, # 是否开启序列图功能
        'watch': True,  # 是否开启实时预览
        'lineWrapping': False,  # 是否换行
        'lineNumbers': False  # 是否显示行号
    }
    
}
```

### 4.3 添加 mdeditor URL

* project/urls.py

```python
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.conf import settings
...

urlpatterns = [
    ...
    url(r'mdeditor/', include('mdeditor.urls'))        # 添加路径
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

### 4.4 修改数据库字段类型

* blog/models.py

```python
from django.db import models
from mdeditor.fields import MDTextField

class Post(models.Model):
    ''' 文章表 '''
    body = MDTextField(verbose_name='内容')
```

### 4.5 生成数据文件迁移到数据库

```bash
python manage.py makemigrations 
python manage.py migrate
```

### 4.6 测试是否正常

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FVfbkOb8aMXK7p1O56TbS%2Fimage-20211109185221682.png?alt=media\&token=35e13cc4-b1bd-4eaf-a38b-a3c951c6503a)

### 4.7 django-mdeditor 错误

我们启动项目，进入文章发布页面。提示出错：

```
render() got an unexpected keyword argument 'renderer'
```

解决办法： 注释这一行

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2Fkdw5fbUZOphsvopeVRIG%2F4_20181010020705_975.jpg?alt=media\&token=5ad48e71-f253-49d2-ab82-aafbdc2340e3)

## 5. 渲染代码高亮显示，自动生成侧边目录

### 5.1 安装插件

```bash
# markdown 代码渲染插件， pygments 高亮插件
pipenv install markdown pygments
```

### 5.2 修改原 view.detail 视图

```python
from django.shortcuts import get_object_or_404, render
from django.utils.text import slugify
from markdown.extensions.toc import TocExtension
from blog.models import Post
import markdown
import re

'''其他略...'''

def detail(request,id):
    post = get_object_or_404(Post, id=id)
    post.increase_looks() # 执行阅读量自增方法
    md = markdown.Markdown(extensions=[
        'markdown.extensions.extra',
        'markdown.extensions.codehilite',
        # 'markdown.extensions.toc',    # 使用这个方法显示的数字描边，无法显示中文： http://xxxx/post/3/#_14
        TocExtension(slugify=slugify),  # TocExtension 在实例化时其 slugify 参数可以接受一个函数，这个函数将被用于处理标题的锚点值
    ])

    post.body = md.convert(post.body)

    # 匹配生成的目录中包裹在 ul 标签中的内容，如果不为空，说明目录，就把 ul 标签中的值提取出来赋值给 post.toc；否则，将 post 的 toc 置为空字符串
    m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S)
    post.toc = m.group(1) if m is not None else ''
    return render(request, 'blog/detail.html', context={'post': post})
```

### 5.3 修改详情页： detail.html

{% hint style="info" %}
代码高亮： https://highlightjs.org/&#x20;

Github: https://github.com/highlightjs/highlight.js/tree/main/src/styles&#x20;

网络 CSS 文件： https://unpkg.com/browse/@highlightjs/cdn-assets@11.3.1/styles/
{% endhint %}

```markup
    <link rel="stylesheet" href="//unpkg.com/@highlightjs/cdn-assets@11.3.1/styles/github.min.css">
    <script src="//unpkg.com/@highlightjs/cdn-assets@11.3.1/highlight.min.js"></script>
    <script>hljs.highlightAll();</script>

    <!- safe 表示是安全的，不用转义 ->
    { post.body |safe }}


    {% block toc %}
    <!-- 在模板中通过判断 post.toc 是否为空，来决定是否显示侧栏目录 -->
        {% block toc %}
        {% if post.toc %}
        <div class="widget widget-content"
            style="width: 20%; height: 100%; background-color: aliceblue; position: fixed; overflow: auto; right: 0; top:0;">
            <h3 class="widget-title">文章目录</h3>
            <div class="toc">
                <ul>
                    {{ post.toc|safe }}
                </ul>
            </div>
        </div>
        {% endif %}
        {% endblock toc %}
```



### 5.4 测试效果

```bash
# 使用 `markdown.extensions.toc`
http://127.0.0.1:8000/posts/8/#_1
http://127.0.0.1:8000/posts/8/#_3


# 使用 TocExtension(slugify=slugify)
http://127.0.0.1:8000/posts/8/#我是标题一
http://127.0.0.1:8000/posts/8/#我是标题二下的子标题
```

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FUg3ojCdxfBfZnqvBJzFm%2Fimage-20211110135155909.png?alt=media\&token=31d6ad09-1dcb-4ba9-b2c3-2252fcd081ea)

### 5.5 界面太丑，使用开源摸版(百度一下)

例如： (自行删除，和继承)

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FHXxGcggkzF7bxGOqJxFl%2Fimage-20211110201840106.png?alt=media\&token=8e7ae8b9-ae9d-4a03-97ee-565fa92ebff7)

## 6. 定制摸版标签

### 6.1 定制摸版标签

我的APP名是blog， 那我这里创建的 **blog\_template\_tags.py** ，存放摸版的标签 (如果APP多并且需要多个摸版标签的，可以单独创建一个 python 的文件包，所有的摸版标签放在单独的文件内)

* blog/blog\_template\_tags.py

```python
from django import template
from blog.models import Tag, Category,Post
from django.db.models.aggregates import Count


# 创建模板库对象
register = template.Library()

# 所有分类摸版标签， 并统计各个分类下的文章数
@register.simple_tag
def get_categories():
    return Category.objects.annotate(num_posts=Count('post'))
    # return Category.objects.all()
    # return Category.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
    

# 所有标签的摸版标签
@register.simple_tag
def get_tags():
    return Tag.objects.all()


#  所有文章摸版标签
@register.simple_tag
def get_posts():
    return Post.objects.all()
```

### 6.2 注册摸版标签

* project/settings.py

```python
TEMPLATES = [
    {
    ''' 略 '''
            'libraries': {
                'blog_template_tags': 'blog.blog_template_tags',  # 摸版标签路径
            }
        },
    },
]
```

### 6.3 使用摸版标签

* templates/blog/base.html

```html
{% load static %}

{% load blog_template_tags %}


                        <!-- 分类 -->
                        {% get_categories as categories %}
                        {% get_posts as posts %}
                        {% for categorie in categories %}
                        <li class="nav-item ">
                            <a data-toggle="collapse" href="#{{ categorie.id }}">
                                <i class="fas fa-layer-group"></i>
                                <p>{{ categorie.name }}({{ categorie.num_posts }})</p>
                                <span class="caret"></span>
                            </a>
                            <div class="collapse" id="{{ categorie.id }}">
                                <ul class="nav nav-collapse">
                                    {% for post in posts %}
                                    {% if post.category_id == categorie.id %}
                                    <li>
                                        <a href="#">
                                            <span class="sub-item">{{ post.title }}</span>
                                        </a>
                                    </li>
                                    {% endif %}
                                    {% endfor %}
                                </ul>
                            </div>
                        </li>
                        {% endfor %}
                        <!-- 分类结束 -->
```

### 6.4 测试效果

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2Fcct36bLoOVCSGE24BS8r%2Fimage-20211110201625609.png?alt=media\&token=9e01d3a8-9586-4dcc-816d-615f52b77c73)
