IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> Django框架 -> 正文阅读

[Python知识库]Django框架

Django框架

Django 框架是Python Web较早或历史较长,并且稳定、功能齐全的框架。

目前主要用于开发后台管理、服务运维站点平台、云计算运维平台及信息化管理系统等。

django框架适合初级开发人员,快速掌握和使用。对于高级开发人员来说,主要优化其不足的功能。

官方文档:https://docs.djangoproject.com/zh-hans/2.1/

django的重要的版本: 1.x(1.9/1.11), 2.x(2.1, 2.2, 2.3), 3.x(3.0, 3.1, 3.2)
请添加图片描述

一、Django 入门

【建议】每一个django项目,都有它自己的环境

1.1 安装

基于pip安装

pip install django==2.1

在交互的环境中,验证的django

>>> import django
>>> django.__version__

1.2 django-admin命令

1.2.1 创建项目命令

django-admin startproject mysite

项目结构说明:

  • 最外层的: 根目录只是你项目的容器, Django 不关心它的名字,你可以将它重命名为任何你喜欢的名字。
  • manage.py: 一个让你用各种方式管理 Django 项目的命令行工具。你可以阅读 django-admin and manage.py 获取所有 manage.py 的细节。
  • 里面一层的mysite/包含你的项目,它是一个纯 Python 包。它的名字就是当你引用它内部任何东西时需要用到的 Python 包名。 (比如 mysite.urls).
  • mysite/__init__.py:一个空文件,告诉 Python 这个目录应该被认为是一个 Python 包。如果你是 Python 初学者,阅读官方文档中的 更多关于包的知识
  • mysite/settings.py:Django 项目的配置文件。如果你想知道这个文件是如何工作的,请查看 Django settings 了解细节。
  • mysite/urls.py:Django 项目的 URL 声明,就像你网站的“目录”。阅读 URL调度器 文档来获取更多关于 URL 的内容。
  • mysite/wsgi.py:作为你的项目的运行在 WSGI 兼容的Web服务器上的入口。阅读 如何使用 WSGI 进行部署 了解更多细节。

1.2.2 创建应用

django项目的结构:由多个app组成的, 一个app即为一个模块。

django中自带了 admin站点管理、session会话管理、log日志等app应用。

django-admin startapp <应用名称>

当项目创建成功之后,项目中包含了一个 manage.py脚本,可以执行此脚本也可以执行django-admin的命令。

python manage.py startapp <应用名>

应用目录结构:

- migrations 包含所有生成当前应用的数据模型迁移文件脚本
- apps.py  应用的配置信息
- admin.py 配置站点下的应用信息    【一般】
- models.py 应用的所有模型类声明所在的脚本 【重点】
- tests.py 应用测试的脚本 
- views.py 应用的所有Web接口  【重点】

【重点】views和models的关系

- Django采用 MVC设计思想,设计出自己的MTV
- 在Views中,角色是Controller控制器, 通过Models将数据加载出来,再通过T(emplate)模板将数据渲染出来,最后将渲染后的HTML封装成Response响应对象返回给Django框架

- models 数据模型, 采用了ORM框架,实现数据的CURD操作。同时支持模型之间的外键关联操作。

1.2.3 运行服务

python manage.py runserver 

默认启动了 127.0.0.1:8000Web服务

可以在runserver后面指定 端口:

python manage.py runserver 9000

可以指定ip的绑定的host:

python manage.py runserver 0:9000

0:表示为0.0.0.0:,绑定当前host主机的实际IP地址。

在浏览器访问:

- http://localhost:9000
- http://127.0.0.1:9000
- http://10.36.174.32:9000

【注意】修改主工程目录下的settings.py文件

ALLOWED_HOSTS = ['*']  # 白名单

1.2.4 创建Web接口

第一步:创建view处理函数

from django.http import HttpResponse

def index(request):
   return HttpResponse('hi, django')
   

【说明】view处理函数的参数必须包含request,它是 WSGIRequest类的实例,也是HttpRequest的子类。可以从request对象中获取客户端请求的路径以及请求头和body数据。

第二步: 配置路由

方式1: 直接在主路由中配置(主工程包/urls.py)

from login.views import index
from django.urls import path
from django.contrib import admin  # 自带的admin应用

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

方式2:创建应用的子路由urls.py(复制主路由的urls.py)

from .views import index
from django.urls import path

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

使用子路由时,必须要做2件事件:

- 将应用模块 添加到 settings.py中的INSTALLED_APPS 列表中 [实际上不需要]
- 将子路由添加到主路由中

修改主urls.py

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

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

1.2.5 创建模型类

第一步:配置数据库

采用默认的数据库 sqlite3

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

第二步: 创建模型类

from django.db import models

# Create your models here.
class User(models.Model):
    # 默认情况下 存在 id主键
    user_id = models.IntegerField(primary_key=True)
    name = models.CharField(max_length=20, unique=True)
    pwd = models.CharField(max_length=100)
    
    

第三步: 配置INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'login',
]

login是应用名

第四步: 生成数据表(模型类)的迁移文件

python manage.py makemigrations

【注意】确认模型类所在的app是否已添加到settings.py的INSTALLED_APPS列表中。

第五步: 执行数据模型类的迁移文件

python manage.py migrate

【注意】第一次迁移时,会自动迁移django自带app下的迁移文件.

【重要】如果修改了模型类, 必须要makemigrationsmigrate

1.2.6 shell调试模型

进入django shell中

python manage.py shell
>>> from login.models import User
>>> User.objects.all()  

【注意】objects是什么?

- objects是Model的元类中创建Manage类实例, 也是QuerySet查询结果类实例
- objects提供了对模型的查询相关方法 【非常重要】

1.2.7 模型类操作

查询数据

User.objects.all() 查询所有的数据,返回可迭代的对象(QuerySet实例),元素是模型类的实例对象。

QuerySet实例方法

- all()
- first()
- last()
- get(属性名=属性值)  # pk主键列名属性
- delete() # 模型类的实例对象
- filter(属性名=属性值)
- count() 统计记录个数

创建模型类实例

u1 = User(name="disen", pwd="123", phone="17791692095")
u2 = User()
u2.name="jack"
u2.pwd="123"
u2.phone="18999971112"

模型类实例方法

- save() 保存数据
- delete() 删除数据
- refresh_from_db() 同步数据(从数据库的表中更新当前实例的属性)

1.2.8 admin站点管理

参考文档:https://docs.djangoproject.com/zh-hans/2.1/ref/contrib/admin/

django本身提供了admin站点管理应用, 在每一个app中提供了admin.py脚本,可以将当前应用的models.py中模型类,添加到admin站点中,以方便管理员管理模型对应的数据。

第一步: 创建后台管理员账号(超级管理员)

python manage.py createsuperuser
>> username:  admin
>> email:  610039018@qq.com
>> password: admin123
>> password (again): admin123
>> Bypass password validation and create user anyway? [y/N]: :y

启动服务之后,可以访问/admin进入站点后台管理页面。

可以尝试创建后台管理人员账号, 将activestaff status勾选上。并添加login应用的管理User模型的权限。

第二步:将login应用中的模型类添加到admin.py中

from django.contrib import admin

from .models import User

# @admin.register(User)
class UserAdmin(admin.ModelAdmin):
    list_display = ('user_id', 'name', 'phone', 'email')
    list_display_links = ('name', )
    list_filter = ('name', 'phone')
    list_editable = ('phone', 'email')
    search_fields = ('name', 'phone', 'email')

admin.site.register(User, UserAdmin)

修改模型类:

主要添加Field字段参数的blank和verbose_name

class User(models.Model):
    # 默认情况下 存在 id主键
    user_id = models.IntegerField(primary_key=True, blank=True,verbose_name='用户ID')
    name = models.CharField(max_length=20, unique=True,verbose_name='用户名')
    pwd = models.CharField(max_length=100, verbose_name='口令')
    # 添加手机号和邮箱
    phone = models.CharField(max_length=11, null=True, verbose_name='手机', blank=True)
    email = models.CharField(max_length=50, null=True, verbose_name='邮箱', blank=True)

    def __str__(self):
        return f'{self.user_id } <{self.name}>'

    class Meta:
        verbose_name = 'Vip会员'
        verbose_name_plural = verbose_name

【站点中显示app应用的中文名称】

第一步:在xxx app的__init__.py文件中,添加如下代码:

# 自模块下的AppConfig的子类
default_app_config = 'testingapp.apps.TestingappConfig'

第二步:在TestingappConfig中添加verbose_name属性

class TestingappConfig(AppConfig):
    name = 'testingapp'
    verbose_name = '自动化测试'

1.2.9 配置多数据库

一个django项目,可以存在多个数据库连接,在settings.py的DATABASES字典对象中配置,如下所示:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    },
    'db1': {
       'ENGINE': 'django.db.backends.mysql',
       'HOST': '180.76.121.47',
       'PORT': 3306,
       'USER': 'root',
       'PASSWORD': 'rootxaqf2101',
       'NAME': 'testdb'
    }
}

1.2.10 反向生成模型

django-admin或manager.py中提供了 inspectdb命令,可以将已存在的表生成模型类,命令如下:

python manager.py inspectdb --database db1 > testingapp/models.py

连接db1数据库,将库中所有表全部生成模型类,并写入到testingapp应用下的models.py脚本中。

1.2.11 重写Manager

由于,模型类操作时,默认选择default数据库,因此需要重新指定模型类的objects实例的类型,需要先声明一个models.Manager类的子类,并重写get_queryset()方法,如下:

class DB1Manager(models.Manager):
    def get_queryset(self):
        return self._queryset_class(model=self.model, using='db1', hints=self._hints)

之后,在模型类中,显式地增加objects成员,如下:

class TbPerson(models.Model):
    person_id = models.IntegerField(primary_key=True, verbose_name='ID')
    phone = models.CharField(max_length=11, blank=True, null=True, verbose_name='手机号')
    # ... 省略

    objects = DB1Manager() # 指定using为db1数据库的查询集实例
    
    class Meta:
        managed = False
        db_table = 'tb_person'
        verbose_name_plural = verbose_name = '人员信息'

1.2.12 重写模型类之save

对于模型类拥有objects成员,只能用于查询和修改,如果是新增,则需要重写save方法,保证数据写入到正确的数据库中。

class TbPerson(models.Model):
    person_id = models.IntegerField(primary_key=True, verbose_name='ID')
    phone = models.CharField(max_length=11, blank=True, null=True, verbose_name='手机号')
    # ... 省略

    objects = DB1Manager() 
    
     def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        return super(TbPerson, self).save(using='db1')
    
    class Meta:
        managed = False
        db_table = 'tb_person'
        verbose_name_plural = verbose_name = '人员信息'

1.3 views视图与模板

1.3.1 请求路径参数

from .views import index, update_user

urlpatterns = [
    path('', index),
    path('<user_id>/', update_user)
]
def update_user(request, user_id):
    return HttpResponse('修改用户ID:%s' % user_id)

1.3.2 模板配置

首先在项目的根目录下,创建一个templates目录,并在settings.py的TEMPLATES配置DIRS模板路径,如下:

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',
            ],
        },
    },
]

1.3.3 创建模板的html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>更新用户</title>
</head>
<body>
   <h3> {{ user.name }} 用户信息修改</h3>
</body>
</html>

1.3.4 在views渲染模板

在templates目录下创建

def update_user(request, user_id):
    # 通过模型将user_id对应的数据表的数据加载到
    user = User.objects.get(pk=user_id)

    # 将数据渲染到模板中,并返回HttpResponse对象
    # render(request, template_name, context= {})
    resp = render(request, 'update.html', {'user': user})
    return resp

【练习】查看所有用户信息,显示到模板上

def view_users(request):
    # 获取所有的用户信息
    users = User.objects.all()
    return render(request, 'user_list.html', {'users': users})

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
</head>
<body>
<h3>查询用户结果</h3>
<ul>
    {% for u in users %}
        <li> {{ u.id }} - {{ u.name }} - {{ u.phone }} </li>
    {% endfor %}
</ul>

</body>
</html>
from django.urls import path

from .views import index,update_user, view_users

urlpatterns = [
    path('', index),
    path('<int:user_id>/', update_user),
    path('list/', view_users )
]

【Http404】抛出404异常

def update_user(request, user_id):
    # 通过模型将user_id对应的数据表的数据加载到
    try:
        user = User.objects.get(pk=user_id)
    except User.DoesNotExist:
        raise Http404('未找到 {}'.format(user_id))
    # 将数据渲染到模板中,并返回HttpResponse对象
    # render(request, template_name, context= {})
    resp = render(request, 'update.html', {'user': user})
    return resp
from django.shortcuts import render, get_object_or_404

def update_user(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    resp = render(request, 'update.html', {'user': user})
    return resp

1.3.5 模板的url指令

第一步: 在路由中声明url路径时,可以指定name属性(path())

path('<int:user_id>/', update_user, name='update'),
path('list/', view_users, name='ulist')

第二步:在template模板文件中

<a href="{% url 'ulist' %}">查看所有用户</a>

【重要】{% url %} 指令是反向查找路径,好处,无论url路径如何修改,都可以反向获取完整的请求路径。另外格式可以带有namespace:

 <a href="{% url 'user:ulist' %}">查看所有用户</a>

user:代表是include()方法中指定的namespace属性值。

主路由中添加子路由时,可以同时指定的namespace

urlpatterns = [
    path('user/', include('login.urls', namespace='user')),
    path('admin/', admin.site.urls),
]

但是在子路由urls.py文件中,必须指定app_name应用名称。

app_name = 'user'
urlpatterns = [
    path('', index),
    path('<int:user_id>/', update_user, name='update'),
    path('user_list/', view_users, name='ulist')
]

1.4 form表单提交

1.4.1 修改模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>更新用户</title>
</head>
<body>
   <h3> {{ user.name }} 用户信息修改</h3>
   <a href="{% url 'user:ulist' %}">查看所有用户</a>
   <form method="post">
       {% csrf_token %}
       <label>用户名</label> <input name="name" value="{{ user.name }}"><br>
       <label>口令</label> <input name="pwd" type="password" value="{{ user.pwd }}"><br>
       <label>手机号</label> <input name="phone"  value="{{ user.phone }}"><br>
       <label>邮箱 </label> <input name="email"  value="{{ user.email }}"><br>
       <button>提交</button>
   </form>
</body>
</html>

【注意】csrf_token指令,防止跨域伪造请求,当渲染模板时,django会生成一个token默认保存到session中,同时在html中生成一个隐藏域。一般post提交form表单时,必须声明此指令。如果不想使用csrf_token,可用在settings.py中去掉django.middleware.csrf.CsrfViewMiddleware中间件。

1.4.2 修改view视图函数

def update_user(request: HttpRequest, user_id):
    user = get_object_or_404(User, pk=user_id)

    # 判断当前请求的方法
    if request.method == 'GET':
        # 通过模型将user_id对应的数据表的数据加载到
        # 将数据渲染到模板中,并返回HttpResponse对象
        # render(request, template_name, context= {})
        resp = render(request, 'update.html', {'user': user})
        return resp

    # 处理POST请求
    user.name = request.POST.get('name')
    user.pwd = request.POST.get('pwd')
    user.phone = request.POST.get('phone')
    user.email = request.POST.get('email')

    # 更新数据
    user.save()
    return HttpResponseRedirect(reverse('user:ulist'))

【注意】request是HttpRequest类实例, 包含了请求头和请求体相关的数据以及客户端的环境信息。GET是查询参数,POST是表单参数, FILES是上传的文件, META是客户端环境信息,COOKIES是客户存储的cookies信息。body是请求体的原始数据。

1.5 CBV的view视图

1.5.1 TemplateView应用

如果html模板中不需要后端的数据来渲染时,可以直接使用此类进行资源url的GET请求的处理视图。

path('about/', TemplateView.as_view(template_name='about.html')),

TemplateView在渲染模板时,自动将request传入,因此在模板文件中,可以直接使用request变量,如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>关于我们</title>
</head>
<body>
  <h3>disen@千锋教育西安校区</h3>
  <h4>610039018@qq.com</h4>
  <p>{{ request.method }}</p>
</body>
</html>

当然,也可以实现TemplateView的子类,子类的写法:

from django.views.generic import TemplateView

class AboutView(TemplateView):
    template_name = "about.html"
urlpatterns = [
    # 将视图类转化为视图函数
    path('about/', AboutView.as_view()),
]

1.5.2 ListView应用

如果需要从模型加载数据并显示其列表,则可以使用ListView类。

使用步骤如下:

  • 创建ListView的子类

    from django.shortcuts import render
    from django.views.generic import ListView
    
    from .models import TbPerson
    
    class PersonListView(ListView):
        model = TbPerson
    
  • 创建模板文件

    根据当前ListView子类所在的app模板及model模型类,创建相应的目录及模板文件,如当前app为testingapp,因此在templates目录下创建其子目录,并在其子目录中,创建tbperson_list.html(tbperson是TbPerson模型类名,_list是后辍名)。

  • 根根ListView中渲染模板的上下文变量,将数据渲染到的HTML中

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>所有人员信息</title>
    </head>
    <body>
     <h1>人员信息</h1>
     <table>
         <thead>
            <th>ID</th>
            <th>Name</th>
            <th>Phone</th>
         </thead>
         <tbody>
            {% for person in object_list %}
                <tr>
                    <td>{{ person.person_id }}</td>
                    <td>{{ person.name }}</td>
                    <td>{{ person.phone }}</td>
                </tr>
            {% endfor %}
    
         </tbody>
     </table>
    </body>
    </html>
    

    object_list是ListView渲染模板时的可用上下文变量, 表示为model模型类的queryset查询结果集。

  • 添加urls路径

    urlpatterns = [
        path('persons/', PersonListView.as_view()),
    ]
    

1.5.3 View应用

View类是TemplateView、ListView、CreateView等类的父类,主要实现了针对不同请求方法的转发功能。在View类中,提供与请求方法对应的请求处理函数(同普通view视图函数一样)。

1.5.3.1 dispatch分发

View类的业务处理最核心的方法是dispatch(), 源码如下:

class View:
    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def dispatch(self, request, *args, **kwargs):
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

1.5.3.2 自定义View
from django.views import View
from django.shortcuts import render

class RoleView(View):
    def get(self, request):
        return render(request, 'xx.html', locals())
    def post(self, request):
        pass
    def put(self, request):
        pass
    def delete(self, request):
        pass

详细的代码如下:

class RoleView(View):
    def get(self, request):
        roles = TbRole.objects.all()
        return render(request, 'role/list.html', locals())

    def post(self, request):
        name = request.POST.get('name')
        code = request.POST.get('code')
        # pk非自增的情况下,获取最大的pk并加1
        max_role_id = TbRole.objects.aggregate(max_id=models.Max('role_id')).get('max_id')
        role = TbRole.objects.create(name=name, code=code, role_id=max_role_id+1)
        role.save()

        return JsonResponse({
            'code': 0,
            'msg': '新增成功'
        })

    def delete(self, request):
        role_id = request.GET.get('role_id')
        # 如果role_id不存在时怎么处理?
        try:
            role = TbRole.objects.get(pk=role_id)
            role.delete()
            return JsonResponse({
                'code': 0,
                'msg': '删除成功!'
            })
        except:
            return JsonResponse({
                'code': 1,
                'msg': '删除失败!'
            })

    def put(self, request:WSGIRequest):
        # 获取请求body中的数据
        data = request.body.decode('utf-8')
        print('--body--', data)
        # 解析body的数据(application/x-www-form-urlencoded)
        forms = {}
        for kv in data.split('&'):
            k,v = kv.split('=')
            forms[k] = unquote(v)

        role_id = forms.get('role_id')
        name = forms.get('name')
        code = forms.get('code')
        role = TbRole.objects.get(pk=role_id)
        role.name = name
        role.code = code
        role.save()

        return JsonResponse({
            'code': 0,
            'msg': '成功'
        })

1.6 django的单元测试

1.6.1 声明单元测试类

from django.test import TestCase

from .models import User
# Create your tests here.

class TestUser(TestCase):
    def test_save(self):
        User.objects.create(name='disen', pwd='123')
        users = User.objects.all()
        self.assertGreater(users.count(), 0, '末查找数据')

1.6.2 单元测试的断言

单元测试类中提供了丰富的断言方法, 统一格式:

self.assertXXXX(..., msg='断言失败的消息')

断言失败之后, 则会抛出AssertionError异常

常用的断言方法:

assertGreater(a, b, msg)  断言a > b
assertIs(a, b, msg) 断言a是b
assertEqual(a, b, msg) 断言a==b
assertLess(a, b, msg) 断言a<b
assertContains(a, b,msg) 断言a是b集合的元素 
assertQuerysetEqual(qs, values, msg=)
assertIsNotNone(a, msg)
assertIsNone(a, msg)

1.6.3 运行单元测试

python manage.py test <应用名>

1.6.4 注意事项

  • 多个单元测试的方法,默认是按方法名称的ASCII值排列。一般在方法名的上添加序号。如test_01_save(),test_02_add()等。
  • django的单元测试的每个方法都有自己的测试数据库,相互之间数据没有关系。
  • 单元测试的方法名,一般以test_开头。

【练习】

from django.test import TestCase

from .models import User
# Create your tests here.

class TestUser(TestCase):
    def test_save(self):
        print('---save--')
        u = User.objects.create(name='disen', pwd='123') # 自动保存
        users = User.objects.all()
        self.assertGreater(users.count(), 0, '末查找数据')

    def test_update(self):
        print('--update--')
        User.objects.create(name='disen', pwd='123')
        user = User.objects.get(name='disen') # 模型类抛出 DoesNotExist
        self.assertIsNotNone(user, 'disen用户不存在')

        user.pwd = '456'
        user.save()

        user = User.objects.get(pk=user.pk)
        self.assertEqual(user.pwd, '456', '更新失败')

1.6.5 测试view视图

通过django.test.Client类,实现对view视图的请求

from urllib.parse import urlencode

from django.test import TestCase

from .models import User


class TestUserView(TestCase):
    def test_list(self):
        User.objects.create(name='disen', pwd='123', phone='110')
        User.objects.create(name='jack', pwd='123', phone='120')

        resp = self.client.get('/user/user_list/')
        self.assertEqual(resp.status_code, 200, '请求失败')

        print(resp.context['users'])
        # self.assertQuerysetEqual(resp.context['users'], [], msg='结果中包含数据')

    def test_update(self):
        u1 = User.objects.create(name='disen', pwd='123', phone='110') # pk=1
        u2 = User.objects.create(name='jack', pwd='123', phone='120') # pk=2

        u1.name = 'disen888'
        u1.phone='17791692095'

        data = urlencode({
            'name': u1.name,
            'phone': u1.phone,
            'pwd': '123'
        })

        resp = self.client.post('/user/1/', data=data,
                                content_type='application/x-www-form-urlencoded')
        print(resp.status_code, resp.content, resp.url)
        self.assertEqual(resp.status_code, 302, '未获取重定向的地址')
        resp2 = self.client.get(resp.url)
        print(resp2.content)

1.7 静态资源文件

1.7.1 应用中的静态资源

在 app应用模块目录下, 创建static目录,存放js,css和图片等相关的静态资料。

1.7.2 项目中的静态资源

在项目的目录下创建static目录, 要求在settings.py文件配置:

STATIC_URL = '/static/'
STATICFIELS_DIRS = [
    os.path.join(BASE_DIR, 'static')
]

1.7.3 在模板中使用

在static/css/login.css 文件,内容如下:

label {
    display: inline-block;
    width: 100px;
    text-align: right;
    height: 50px;
    line-height: 50px;
    background-color: yellowgreen;
}

body{
    background-image: url('/static/images/bg2.jpeg');
    background-repeat: no-repeat;
}

在模板文件中(update.html), 先执行{% load static %}指令加载静态资源,然后再使用{% static 'css/login.css' %} 获取静态资源的完整路径。

<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>更新用户</title>
    <link rel="stylesheet" href="{% static 'css/login.css' %}">
</head>
...

1.8 定制admin的表单

1.8.1 User模型admin表单

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    list_display = ('user_id', 'name', 'phone', 'email')
    list_display_links = ('name', )
    list_filter = ('name', 'phone')
    list_editable = ('phone', 'email')
    search_fields = ('name', 'phone', 'email')

    # fields = ('name', 'phone', 'email', 'pwd')
    fieldsets = [
        ('必填信息', {'fields': ('name', 'pwd')}),
        ('其它信息', {'fields': ('email', 'phone'),
                     'classes': ('collapse',)
                    })
    ]
    list_per_page = 10 # 分页效果

1.8.2 声明关系模型


class Order(models.Model):
    pay_status_list = [
        (0, '未支付'),
        (1, '已支付'),
        (2, '已完成')
    ]

    title = models.CharField(max_length=50, verbose_name='订单标题')
    price = models.FloatField(verbose_name='订单金额', default=0)
    pay_status = models.IntegerField(verbose_name='订单状态',
                                     choices=pay_status_list, default=0)
                                     
    user = models.ForeignKey(User, 
                             on_delete=models.CASCADE, # models.SET_NULL
                             related_name='orders', verbose_name='用户')

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = verbose_name_plural = '用户订单'

on_delete声明级联删除时的操作,可以设置models.CASCADmodels.SET_NULLmodels.SET_DEFAULT

【重要】ForeignKey()中的related_name说明

- related_name 即为User动态添加一个属性,方便查找关系的Order数据
- 即 User.orders 属性,集合,表示某一个用户的所有订单信息
- 如果related_name不指定时, 默认添加的属性为 order_set

1.8.3 注册模型

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price', 'pay_status', 'user')
    ordering = ['price']
    list_filter = ['user']

1.8.4 inlines显示

class OrderInline(admin.TabularInline):
    model = Order
    extra = 1
    
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    ...

    inlines = [OrderInline]

主要是一端模型类上,编辑时可以同时添加关联子类模型的数据。

1.8.5 自定义字段函数

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price', 'pay_status', 'user_name')
    ordering = ['price']
    list_filter = ['user']
	
	# 自定义显示字段的属性
    def user_name(self, instance):
        return instance.user.name
    
    user_name.admin_order_field = 'user_id' # 指定排序字段
    user_name.short_description = '用户名'

【注意】自定义属性方法时,必须提供两个参数,除self之外,instance表示当前admin装饰模型实例对象。

【重点】short_description类似于Field字段的verbose_name属性。

1.8.6 外键关联实体属性搜索

如果一个实例支持搜索时,需要满足objects.filter()方法的规则,如:

@admin.register(TbPersonRole)
class PersonRoleAdmin(admin.ModelAdmin):
    list_display = ['person', 'role']
    fields = ['person', 'role']
    # 可搜索的字段:person, person_id, pr_id, role, role_id
    # ?支持外键关联关系的内部属性(字段)的搜索
    # 搜索方法使用是 objects.filter()方法,条件的写法,应该满足filter过滤规则
    search_fields = ['person__name', 'role__name']

1.9 综合项目任务

参考一个电商类的app页面,分析页面的数据,设计表或模型类。

1.9.1 实现用户登录功能

request.session[‘user_id’] = u.user_id 将数据存储在session中

session的使用示例

def index(request: HttpRequest):
    print(type(request))
    if request.session.get('user_id', None):
        if request.GET.get('logout', '0') == '1':
            del request.session['user_id']
            return HttpResponse('用户退出登录')
        return HttpResponse('用户已登录')

    request.session['user_id'] = '123'
    return HttpResponse('Session 信息已添加')

1.9.2 实现用户注册功能

1.9.3 设计商品或产品模型类

1.9.4 设计订单类

1.9.5 实现产品列表显示页面

1.9.6 实现用户购买商品或产品功能

1.9.7 设计我的订单页面

  • 查看订单
  • 取消订单
  • 评价功能, 设计订单评价模型

1.10 WSGIRequest类

所有WSGI请求信息的封装类,包含请求报文的数据和客户端相关信息。

1.10.1 请求数据

一个HTTP请求的数据包含

  • 路径参数
    • 路由中需要 <int:参数名> 参数的类型可以不提供。
    • 参数名必须存在 路由对应的view视图处理函数的形参中,形参必然是request,后台依次是路径参数
    urls.py
       ...
       path('items/<page>/<item_type>/',  views.all_items)
       
       
    views.py
        def all_items(request, page, item_type):
             pass
    
  • 查询参数

    从request的GET属性读取,GET是QueryDict类型,即为字典类型。

    查询单个参数: request.GET.get(‘参数名’)

    查询数组的参数: request.GET.getlist(‘参数名’)

  • 表单参数

    从request.POST读取的数据,也是QueryDict类型。

    如果请求方法非post时,需要从request.body读取数据。

    【注意】请求头的Content-Type可以是application/x-www-form-urlencodedmultipart/form-data

  • json数据

    json数据是在请求体中的,即是从request.body属性中读取的。

    request.body是bytes字节数据类型,需要自己解码和反序列化为dict或list。

    【注意】请求头的Content-Type必须是application/json

  • 上传的文件

    request.FILES 存放客户端上传的文件信息,它也是一个QueryDict字典类型,前端文件字段的名称是QueryDict字典的key, 对应的value是一个UploadedFile文件类实例对象。

1.10.2 请求头
  • content_type 请求数据类型
  • method 请求方法
  • path 请求路径
  • META[‘HTTP_AUTHORIZATION’] 获取请求头的身份信息
  • META[‘HTTP_REFERER’] 当前请求的来源(上一个页面的地址)
  • META[‘HTTP_USER_AGENT’] 请求客户端应用名称
1.10.3 客户端数据
  • META[‘HTTP_HOST’] 当前服务主机的地址
  • META[‘REMOTE_ADDR’] 当前客户端的IP地址
  • COOKIES 客户端存放的所有Cookie, 是QueryDict字典类型
1.10.4 session数据

request.session是客户端将数据存储在后端的一种方式,默认存储在数据库的django_session表中,客户端的Cookie中存在一个sessionid,存放的数据即为django_session表的session_key。请求处理时,根据cookie中的session_id,确认是哪一个客户端,从而获取客户端存储在后端的数据(session中)。

在view视图函数中,用法如下:

request.session['login_user'] = 'disen666'

二、模型类进阶

2.1 Field类型

通用属性: verbose_name, null, blank, primary_key, unique, default, db_column, name

2.1.1 CharField

2.1.2 IntegerField

2.1.3 TextField

2.1.4 日期时间字段

共有三个相关的字段类:DateField/TimeField/DateTimeField

重要的属性:

auto_now 每次保存时,都会更新时间

auto_now_add 只有第一次保存时,保存时间

2.1.5 文件与图片字段

如果使用图片字段 ImageField,则需要安装pillow库

另外,可以指定upload_to,width_field, height_field三个字段

# Create your models here.
class LoginUser(models.Model):
    name = models.CharField(max_length=20, unique=True)
    pwd = models.CharField(max_length=100)
    phone = models.CharField(max_length=11)

    # upload_to 相对于MEDIA_ROOT配置的目录
    # ImageField需要安装 pillow库
    head = models.ImageField(upload_to='images', blank=True, null=True, verbose_name='头像')
    last_login_time = models.DateTimeField(auto_now=True,verbose_name='最后登录时间')
    regist_time = models.DateTimeField(auto_now_add=True, verbose_name='添加时间')

    def __str__(self):
        return self.name

    def head_img(self):
        return '<img width=50 height=50 src="{}" >'.format(self.head.url)

    head_img.short_description='用户头像'

    # 声明Meta元信息
    class Meta:
        db_table = 'tb_login'
        verbose_name = verbose_name_plural = '会员登录信息'
        ordering = ['-last_login_time', '-regist_time']

【注意】如果想存储上传图片的宽和高,则添加两个字段,并通过width_field和height_field来指定两个字段

class User(models.Model):
    ...
    hegiht = models.IntField(default=0)
    width = models.InteField(default=0)
    
    head = models.ImageField(upload_to='images', 
                             width_field='width', 
                             height_field= 'height')

另外, 在settings.py配置MEDIA_URLMEDIA_ROOT

# 配置文件或图片的媒体存储路径和URL访问路径
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'medias')

需要在主路由文件中,通过django.conf.urls.static.static函数将MEDIA资源添加进来:

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

from storems import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('login.urls'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

2.1.6 自定义上传图片的文件名

# Create your models here.
class LoginUser(models.Model):
    name = models.CharField(max_length=20, unique=True)
 	# ...
    def get_filename(self, filename):
        # print(type(self), filename)
        ext_name = filename.split('.')[-1]
        newfilename = self.name
        return 'images/{}.{}'.format(newfilename, ext_name)

    head = models.ImageField(upload_to=get_filename, blank=True, null=True, verbose_name='头像')
    #...

upload_to可以是images的目录名称,也可以自定义的函数.

2.1.7 admin站点显示html字段

from django.utils.html import format_html

class XXX(models.Model):
   # ...
   img = models.ImageField(...)
   def img_html(self):
       return format_html('<img src="{}">', self.img.url)
   img_html.short_description = '图片'

在admin.py脚本中:

from django.contrib import admin
form .models import XXX

@admin.register(XXX)
class XXXAdmin(admin.ModelAdmin):
    list_diplay = ('img_html', ...)

2.1.8 FloatField

2.2 字段约束

常见约束:

- default
- primary_key
- unique
- null
- blank
- max_length
- verbose_name
- db_column

2.3 关系模型

通用属性: on_delete 级联操作

2.3.1 ForeignKey外键关系

关系字段放在多端,

如果没有指定related_name属性, 在一端获取多端数据时, 多端模型类名小写+_set

如:

class Goods(models.Model):
    title = models.CharField(max_length=50, db_index=True)
    summary = models.CharField(max_length=100, blank=True,null=True,verbose_name='简介')
    content = models.TextField(verbose_name='产品说明', blank=True, null=True)
    cover = models.ImageField(upload_to='goods')
    price = models.FloatField(verbose_name='单价')
    brand_name = models.CharField(max_length=50, verbose_name='品牌')

    def __str__(self):
        return self.title

    class Meta:
        db_table = 'tb_goods'
        verbose_name = verbose_name_plural = '商品信息'

class GoodsImages(models.Model):
    goods = models.ForeignKey(Goods, on_delete=models.CASCADE, verbose_name='商品名称')
    img = models.ImageField(verbose_name='图片', upload_to='goods')
    ord_num = models.IntegerField(default=0, verbose_name='序号')
    label = models.CharField(max_length=20, verbose_name='标签')

    def __str__(self):
        return self.label

    class Meta:
        db_table = 'tb_goods_images'
        verbose_name_plural = verbose_name = '商品图库'
g1 = Goods.objects.first()
g1.goodsimages_set.all()  # 获取商品的多张图片实例

使用related_name, 指定为images

g1.images.all()

2.3.2 OneToOne 一对一关系

关系字段可以放在任何的一端上, 如 LoginUser - > Account( user )

对于LoginUser实例 默认可以通过 account 属性来访问它的 账户信息. 当然如果Account类声明user属性的OneToOne()方法中指定了related_name时,通过它的名称来访问,如 related_name = ‘ac’, user.ac : Account

如:

# 默认情况下
user = LoginUser.objects.get(pk=1)
user.account.money

# 在OneToOne()方法中指定了related_name为 ac
user = LoginUser.objects.get(pk=1)
user.ac.money

2.3.3 ManyToMany多对多关系

2.4 CURD操作

2.4.1 常用的CURD操作

- 创建或插入新的数据:  Create
	- User.objects.create(属性=值,...)
	- User(属性=值,...).save()
	- u = User()
	  u.属性=值
	  u.save()
	 
- 获取数据(模型实例对象):READ
	- 单个数据或对象的获取
		- User.objects.get(pk=主键值)
		- User.objects.first()
		- User.objects.last()
		
	- 获取多个数据或对象
		- User.objects.all()
		- User.objects.filter()
		- User.objects.values('字段名1', '字段名2', ...)
		  返回 QuerySet< [{'字段名': 字段值},  {..}] >
		
- 更新数据: Update
	- u = User.objects.get(pk=主键值)
	  u.属性=值
	  u.save()  
	- u.refresh_from_db()  从数据库中读取最新的数据
	
- 删除数据
	- 单个对象删除
		- u.delete()
		- User.objects.first.delete()
		- User.objects.get(pk=主键值).delete()
	- 删除多个对象
		- User.objects.all().delete() 清空表
		- User.objects.filter(属性=属性值).delete() 

2.4.2 关系模型的CURD

示例模型类:

from django.db import models

# Create your models here.
from goods.models import Goods
from login.models import LoginUser


class Order(models.Model):
    title = models.CharField(max_length=50)
    price = models.FloatField(default=0, verbose_name='应付金额')
    address = models.CharField(max_length=100, verbose_name='收货地址')
    phone = models.CharField(max_length=11)
    pay_state = models.IntegerField(verbose_name='支付状态',
                                    choices=((0, '未支付'), (1, '已支付')))

    pay_time = models.DateTimeField(verbose_name='支付时间', null=True)
    create_time = models.DateTimeField(verbose_name='下单时间', auto_now_add=True)

    user = models.ForeignKey(LoginUser, on_delete=models.SET_NULL, null=True)

    def __str__(self):
        return self.title

    class Meta:
        db_table = 'tb_order'
        verbose_name_plural = verbose_name = '会员订单信息'
        ordering = ('-pay_time', '-price')

class OrderDetail(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, verbose_name='订单')
    goods = models.ForeignKey(Goods, on_delete=models.CASCADE, verbose_name='商品')
    num = models.IntegerField(verbose_name='数量', default=1)

    def __str__(self):
        return '{} - {}'.format(self.goods.title,  self.num)


    class Meta:
        db_table = 'tb_order_detail'
        verbose_name_plural = verbose_name = '订单详情信息'
user = User.objects.get(pk=1)
o1 = Order()
o1.title = ''
o1.address=''
o1.phone=''

# 向Order对象中关联用户对象
o1.user = user  # 对象关系
o1.user_id = user.id # 表的外键值关系

goods_list = Goods.objects.all()[:3] # 获取前三个商品

# 将商品添加到订单中 (借助订单详情表)
o1.save()
o1.price = 0

# 第一种方式: 创建OrderDetail实例
for g in goods_list:
	OrderDetail.objects.create(order=o1, goods=g, num=1)
	o1.price += g.price * 1
	
# 第二种方式: 通过Order反向引用属性方式
for g in goods_list:
	od = OrderDetail(goods=g,num=1)
	o1.orderdetail_set.add(od) # 关联集合方式
	o1.price += g.price * 1

o1.save() # 警告od应该先保存

关系模型的更新操作

# 直接更新
o1.orderdetail_set.filter(goods_id=1).update(num=5)

# 先获取对象再修改
od = o1.orderdetail_set.filter(goods_id=1).first()
od.num -= 1
od.save()

关系模型的删除操作

# 删除订单中的某一商品
o1.orderdetail_set.filter(goods_id=1).delete()

# 删除订单
o1.delete()

2.4.3 QuerySet查询

2.4.3.1 filter 查询条件
User.objects.filter(属性名=属性值)
User.objects.filter(属性名__方法名=条件值)

__方法名常见的有:

contains 包含,类似于SQL的like
startswith 以xxx开头
endswith 以 xxx结尾的
isnull  数据表的数据是 <null>
isnotnull 数据表的数据是非 <null>
lt 小于
gt 大于
lte 小于等于
gte 大于等于

# 时间条件之后,还可以添加 lt,gt,lte, gte的条件
# Order.objects.filter(pay_time__day__gte=24)
year  年
month 月
day   日
hour  小时
minute 分钟
second 秒
2.4.3.2 exclude排除条件

在查询时,可以指定哪些数据不要的条件,即满足条件的数据除外。

# 查询 除24号之外所有订单
Order.objects.exclude(pay_time__day=24)
2.4.3.3 Q表达式

django.db.models.Q 提供多个条件的查询,支持 and与、or或、not非逻辑关系的查询,但以&, |, ~等符号依次表示与、或、非等逻辑关系。

Q表达式同filter()或exclude()等方法一样,都是通过模型类的属性=值方式。

# 查询 除24号之外所有订单
Order.objects.filter(Q(pay_time__day__lt=24) | Q(pay_time__day__gt=24))


Order.objects.filter(~Q(pay_time__day=24))
# 查询5月的24日之前的,或者西安市且金额大于1000所有订单
Order.objects.filter(
    Q(Q(pay_time__day__lt=24) & Q(pay_time__month=5)) |
    Q(Q(address__startswith='西安') & Q(price__gt=1000))
)

练习:

# 查询5月的24日之前的未支付的订单
# Order.objects.filter(pay_time__lt='2021-05-24', pay_state=0)
Order.objects.filter(Q(pay_time__lt='2021-05-24') & Q(pay_state=0))
Order.objects.filter(Q(pay_time__lt='2021-05-24', pay_state=0))
# 查询用户名为disen的订单且订单金额不少于1000
# 可以使用关联的属性
Order.objects.filter(user__name='disen', price__gte=1000)
2.4.3.4 F表过式

django.db.models.F 可以在更新时提取属性的值

# 所有disen购买的订单一律1折
Order.objects.filter(user__name='disen', pay_state=0).update(price=F('price')*0.1)
# 5月20日这一天下订单(未支付的)的价格 优惠 520
Order.objects.filter(create_time__month=5,create_time__day=20, pay_state=0, price__gte=520).update(price=F('price')-520)
# 将所有用户的手机号前面添加029-
# 手机号目前是的长度为11位
# [问题] F 提取的内容支持数值计算,针对字符串的不是特别突出。
LoginUser.objects.all().update(phone='029-'+F('phone'))

2.4.4 重写模型的save方法

模型的save()用于数据的存储(永久性写入数据库中),在模型类中可以重写save()方法,将重要的字段信息预先处理,再进行永久化存储。

2.4.4.1 make_password的使用
from djang.contrib.auth.hashers import make_password, BCryptSHA256PasswordHasher

class LoginUser(models.Model):
   ...
   def save(self, force_insert=False, force_update=False, using=None,
       update_fields=None):
       # make_password() 生成密文,PCK
       # check_password() 验证密文的
       if len(self.pwd) < 30:
           # 明文口令,加密
           # hasher的参数值可以参考django.conf.global_settings的PASSWORD_HASHERS
           # hasher默认为default, PBKDF2PasswordHasher
           self.pwd = make_password(self.pwd, hasher=BCryptSHA256PasswordHasher())
           print('self.pwd', self.pwd)

		return super(LoginUser, self).save()
2.4.4.2 check_password的使用
# Create your views here.
class LoginView(View):
    def get(self, request):
        return render(request, 'user/login.html')

    def post(self, request):
        ...
        # 查询用户
        login_user = LoginUser.objects.filter(name=name).first()  # QuerySet
        if login_user and check_password(pwd, login_user.pwd):
            request.session['login_user'] = {'id': login_user.id,
                                             'name': login_user.name,
                                             'head': login_user.head.url if login_user.head else ''
                                             }
            return redirect('/')

        context['errors'] = '用户名或口令错误'
        return render(request, 'user/login.html', context)

2.4.5 重定义objects

每一个模型类,都存在一个objects隐性对象, 它的类型是models.Manager

objects是模型类的元类(ModelBase)添加的.

objects是用于数据的查询及更新的, 默认查询的数据是全部,返回是QuerySet.

当默认查询结果集中不包含特定状态(条件)的数据时,需要重新定义,达到简化查询条件的目的。如,用户注销后(未物理删除,只是修改了状态),每次查询数据时,应该提供一个用户是否注销的条件。

# 声明models.Manager的子类
class LoginUserManager(models.Manager):
    # 获取查询结果集,重写BaseManager类的方法
    def get_queryset(self):
        return super().get_queryset().filter(is_del=False)

在LoginUser模型类中显式声明objects

# 添加新的属性,表示是否注销
is_del = models.BooleanField(default=False)

# 声明objects
objects = LoginUserManager()  # 自定义
object_set = models.Manager() # 默认的

注销disen123用户:

LoginUser.object_set.filter(name='disen123').update(is_del=True)

2.4.6 原生SQL查询

1.模型类的QuerySet.raw()

2.模型类的QuerySet.extra()

3.django.db.connection数据库连接对象

2.4.6.1 raw方式
qs = LoginUser.object_set.raw('select * from tb_login where id>=5')
print(list(qs))
2.4.6.2 extra方式

select和select_params

# 查询用户的订单数量和用户信息
qs = LoginUser.object_set.extra(
    select={'orders':'select count(1) from tb_order t2 where t2.user_id=tb_login.id'})

where和params条件与参数:

LoginUser.object_set.extra(
     where=['name=%s', 'phone like %s'],
     params=['disen123', '177%'])
2.4.6.4 db.connection

djang.db提供了一个属性connection,表示当前项目与数据库的连接

可以通过connection获取连接的cursor游标对象,然后再通过cursor对象进行sql语句执行。同pymysql(mysqlclient)的cursor。

用法:

from djang.db import connection

with connection.cursor() as c:
    # pymysql的cursor.execute(sql, args=None)
    # 注意: django的connection执行的sql语句中只能包含%s, 不支持%(name)s
    sql = 'select id,name,pwd,head from tb_login where name=%s'
    c.execute(sql, params=['disen123'])
    ret = c.fetchone() # fetchall()  # 返回一个是tuple或list[tuple, ]

2.5 事务

django与数据库交互时,每一个模型的执行都会提交一次事务。

django中提供了一个手动提交或开启事务的方法, 需要使用django.db.transaction对象。

另外,在settings.py文件的配置数据库的OPTIONS选项中,可以配置事务的隔离级别。

2.5.1 transaction事务对象

transaction提供了保存还原点(savepoint), 开启事务(begin), 提交事务(commit), 回滚事务(rollback), 回滚到指到的还原点(rollback to )。

用法1:

from djang.db import transaction, connection

with transaction.get_connection(using='db1').cursor() as c:
    # sqlite3 integer类型的主键默认自增
    c.execute('create table tb_login(id integer primary key,name,pwd)')  # 执行DDL语句时,会自动提交事务
    c.execute('insert into tb_login(name,pwd) values(%s, %s)', params=['disen', '123'])
    c.execute('delete from tb_login where name=%s', params=['disen'])
    c.execute('insert into tb_login(name, pwd) values(%s,%s)', params=['lucy', '123'])
   

【注意】在上下文环境中,执行多条语句时,事务不会自动提交。只有退出上下文环境时,才会提交事务。

用法2:

cursor = transaction.get_connection(using='db1').cursor()
transaction.set_autocommit(False, using='db1')

cursor.execute(sql, params=None)
...

transaction.commit(using='db1')

用法3: 保存还原点

#.. sql的cud操作
sid1 = transaction.savepoint(using='db1')
#... sql的cud操作
#...当出现异常时可以回滚到sid1现场
transaction.savepoint_commit(sid1, using='db1')
transaction.savepoint_rollback(sid1, using='db1')
transaction.commit(using='db1')

2.5.2 事务的隔离级别

在settings.py的数据库配置中,使用OPTIONS配置:

DATABASES = {
   'default': {
       'ENGINE': '',
       'NAME': '',
       'OPTIONS': {
           'isolation_level': 'repeatable_read' 
       }
   }
}

除了默认的repeatable_read可重复读之外,还有

- read_uncommitted   读未提交
- read_committed     读已提交
- serializable       串行化

2.5.3 行级锁

模型类操作时,可以通过objects的select_for_update()进行查询数据。

用法:

# 行级锁 数据库必须要支持才行
# 必须在事务中执行,可以借助transaction.atomic()

# 关闭事务的自动提交
transaction.set_autocommit(False, using=None)
with transaction.atomic():
     u = Userinfo.objects.select_for_update().get(pk=1)
     u.pwd='789'
     u.save()

2.5.4 view视图函数上的事务

可以在view视图处理函数(web接口的处理函数)上使用transaction.atomic装饰器,使得view函数内部的模型操作处理事务内(开始事务),如果出现异常,则会自动回滚事务。虽然事务内操作数据比较安全,但也会影响Web接口响应的速度。

用法:

# main.views
from django.db import transaction
from .models import Userinfo

@transaction.atomic(savepoint=True)
def update_user(request, pk):
    if request.method == 'POST':
        return HttpResponse('<h3 style="color:red">不支持POST方法的请求</h3>')

    u = Userinfo.objects.select_for_update(nowait=False).get(pk=pk)
    name = request.GET.get('name', '')
    pwd = request.GET.get('pwd', '')
    if name and pwd:
        u.username = name
        u.pwd = pwd
        u.save()
        return HttpResponse('<h3>修改成功</h3>')

    return HttpResponse('<h3 color="yellow">修改失败, 用户名不能为空</h3>')

测试接口: http://127.0.0.1:8000/user/2/?name=disen&pwd=123

三. Django模板进阶

3.1 布局指令

3.1.1 block块

可以在父模板中,声明结构的区域(块), 在子模板中可以重写内容.

用法:

{% block name %}
{% endblock %}

3.1.2 extends继承

文档的整体结构从extends指定的父模板继承下来

用法:

{% extends "模板文件位置" %}

3.1.3 include包含

可以将其模板内容插入到当前的位置

用法:

{% include "模板文件位置" %}

3.2 模板标签

3.2.1 for指令

用法1:

{% for v in objs %}
   {{ forloop.counter }}  {{ v.name }}
{% endfor %}

【注意】forloop是for循环对象,包含counter记数器(从1开始)。还包括firstlast

用法2:

{% for v in objs %}
   {{ forloop.counter }}  {{ v.name }}
   
   {% empty %}
   当前不存在数据
{% endfor %}

3.2.2 if指令

格式1:

{% if 属性 %}

{% endif %}

格式2:

{% if 属性 >|<|==|<= | >= 值 %}

{% endif %}

格式3:

{% if 属性|关系表达式 %}
    内容1
    
    {% else %}
	内容2
{% endif %}

格式4:

{% if 属性|关系表达式 %}
    内容1
    {% elif 属性|关系表达式 %}
    {% else %}
	内容2
{% endif %}

3.2.3 url指令

格式:

{% url "namespace:name" 路径参数 %}

[说明]

namespace :  主路由包含子路由时指定的,
          如 path('order/', include('order.urls', namespace="order"))
name:  一般在子路由的path()声明路径的名称,如
	     path('del_cart/<pk>/',  views.remove_cart, name="delcart")

有效的url指令:

{% url "order:delcart" 3 %}

生成的url路径: /order/del_cart/3/

3.2.4 变量标签

使用{{ 变量名 }} 获取变量的值

变量标签中使用.语法, 包含:

  • 对象的属性,如g.title
  • 对象的方法,此方法必须有返回值
  • 可迭代对象的索引,如果超过索引范围,则不显示
  • dict对象的key

示例:

def show_cart(request):
    test_items = ['A', 'B', 'C']
    title = 'hi,test,django variable tag'
    test_dict = {'name': 'disen', 'age': 20}
    return render(request, 'carts.html', locals())
<div>
    {{ test_items.3 }}
    {{ test_items.pop }}
    {{ test_dict.name }}
    {{ title.title }}
    {{ title.10 }}
</div>

3.2.5 其它指令与标签

  • {% widthratio 变量 分母 分子 %} 乘法
  • {% load static %} 加载静态资源
  • {% autoescape on %} {{ html }} {% endautoescape %} 显示源内容
  • {% autoescape off %} {{ html }} {% endautoescape %} 显示转义之后内容

3.3 标签过滤器

3.3.1 内置过滤器

3.3.2 自定义过滤器

在主项目包下的__init__.py中定义:

from django.template.defaultfilters import register

# 自定义模板过滤器
@register.filter(is_safe=True)
def short(value, arg=2):
    return value[:arg]


@register.filter(is_safe=True)
def input(value, type='text'):
    html = '<input value="{}" type="{}" size=2>'
    return format_html(html, value, type
                       

四、CBV视图设计

4.1 View类

View类,是一个视图父类, 主要实现了as_view()和dispatch()两个函数。

4.2 TemplateView类

4.3 自定义View类

五、Django中间件

六、Django的缓存配置

七、Django的Session配置

八、Django的信号

九、Logging日志

十、DjangoRESTFramework

参考文档: https://www.django-rest-framework.org/

django-rest-framework是基于Django实现RESTful接口规则, 依据restful四个设计原则,实现请求与响应数据的序列化与处理。

10.1 安装环境

python3.7

django 2.1

参考安装 说明: https://pypi.org/project/djangorestframework/3.11.0/

pip install djangorestframework==3.11.0

10.2 入门示例

10.2.1 配置settings

在settings.py的INSTALLED_APPS列表中添加rest_framework

INSTALLED_APPS = [
    '...'
    'rest_framework',
]

10.2.2 配置主路由

在主路由中引入api-auth/

urlpatterns = [
    path('', index),
    path('admin/', admin.site.urls),
    ...
    re_path(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

10.2.3 创建api包

在api包下创建views.py脚本,内容如下:

from goods.models import Goods
from rest_framework import serializers, viewsets, routers

# 模型类序列化
class GoodsSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Goods
        fields = '__all__'

# 模型类视图集(提供 get, post, put, delete等方法)
class GoodsViewset(viewsets.ModelViewSet):
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer


# 创建默认的路由对象
router = routers.DefaultRouter()
router.register('goods', GoodsViewset)

10.2.4 添加api路由

将api包下的router路由配置到主路由中

urlpatterns = [
    path('', index),
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    re_path(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

10.3 perssions权限验证

可以在settings中配置全局API接口的权限验证,也可以在单个viewset或APIView中配置局部的。

10.3.1 全局验证

在settings.py可以如下配置DEFAULT_PERMISSION_CLASSES

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ]
}

DjangoModelPermissionsOrAnonReadOnly验证模型类操作的权限,所有的模型对应的viewset接口必须具有它的相应操作权限,否则只能查看。

除此之外,rest_framework自带了很多其它权限:

- AllowAny  
- IsAuthenticated
- IsAdminUser
- IsAuthenticatedOrReadOnly
- DjangoModelPermissions  # add, change, delete, view
- DjangoModelPermissionsOrAnonReadOnly
- DjangoObjectPermissions

10.3.2 局部验证

可以在viewset类中指定验证类,如下:

class GoodsViewset(viewsets.ModelViewSet):
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer
	
    permission_classes = [
        #permission.VipUserHasMoney,
        permissions.DjangoModelPermissionsOrAnonReadOnly
    ]

也可以自定义一个验证类,如VipUserHasMoney类, 只需要重写has_permssion方法即可:

class VipUserHasMoney(BasePermission):
    def has_permission(self, request, view):
        if 'login_user' in request.session:
            print('---->session--user')
            from login.models import LoginUser
            # 获取当前请求用户的对象
            request.user = LoginUser.object_set.get(pk=request.session['login_user']['id'])
            return request.user.ac.money > 0
        return False

10.4 APIView类

10.5 authentication授权

十一、项目周

11.1 项目管理

11.1.1 项目进度规则

- 开发人员 2人
- 开发时间(7天)
- 项目需求文档(原型截图,每一个截图下提供功能描述(多个), 功能描述时,最好设计一下数据关系-数据以json格式进行描述。)
- 项目开发进度表(合理化、理性化)

11.1.2 日报

每一组组长发邮件,邮件的规范如下:

标题: 组名+日期+项目名+日报

收件人: 610039018@qq.com, 两位就业老师QQ邮箱, 张思聪副总监

内容:

两位总监,

? 您好!

? 预计完成任务:

? 实际完成任务(可以是百分比):总体完成 90%, 其中最慢的是xxx模块,原因是个人电脑 或 沟通中产生分歧或技术点不明确。

? 问题的:截图

附件: 任务分配详情表 (明日的计划任务+今日任务的完成实际情况)

上报时间:晚上12点之前。

11.1.3 gitee仓库

所有的项目代码(正常运行)托管到gitee平台上。

11.1.3.1 创建组织

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i7f0jbIJ-1638535206529)(D:\docs\XA2101\week10\images\image-20210531094704193.png)]

11.1.3.2 添加开发者成员

进入到组织的设置页面,左侧点击或选择成员管理,以下是成员管理的页面,点击【添加成员】可以添加成员。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUNRj49Y-1638535206532)(D:\docs\XA2101\week10\images\image-20210531095143237.png)]

添加成员的页面,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNbr1hrt-1638535206532)(D:\docs\XA2101\week10\images\image-20210531095622813.png)]

添加完用户之后,再点击【添加】按钮,在弹出的确认页面中,点击【提交】,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jfKq4YHi-1638535206533)(D:\docs\XA2101\week10\images\image-20210531095725153.png)]

11.1.3.3 创建组织的仓库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EevlBzcW-1638535206534)(D:\docs\XA2101\week10\images\image-20210531095820355.png)]

点击【新建仓库】,在打开的页面中,根据实际情况填写信息,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RRNOhOQV-1638535206535)(D:\docs\XA2101\week10\images\image-20210531100034455.png)]

仓库创建成功之后,跳转到以下页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o18OBGyS-1638535206535)(D:\docs\XA2101\week10\images\image-20210531100256385.png)]

提供了本地创建git仓库的相关命令,以及上传本地仓库的方式。

11.1.3.4 配置仓库的开发人员

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xbgdp16b-1638535206536)(D:\docs\XA2101\week10\images\image-20210531100433542.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RhYahXU1-1638535206537)(D:\docs\XA2101\week10\images\image-20210531100501010.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1GbkeIHH-1638535206538)(D:\docs\XA2101\week10\images\image-20210531100531877.png)]

11.1.3.5 配置gitee用户的公钥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dRc2bHgY-1638535206539)(D:\docs\XA2101\week10\images\image-20210531100746883.png)]

11.1.3.6 综合练习

组长:

gitee:
- 创建组织
- 组织中添加开发人员
- 组织中新建仓库
- 配置仓库的开发人员(组织中的开发成员)

本地(git-bash)
- 选择一个项目
- 进入项目目录
- git init  初始化项目, 在当前目录下创建一个.git目录(本地仓库)
- 【可选】如果本地仓库出现错误时,可以尝试将.git删除,重新再来
- vi .gitignore 文件
  .idea
  __pycache__
- git add .
- git status 当前的git cache中是否包含 .gitignore中的文件,如果不包含则正常,如果包含了,则重新再来。[rm -rf .git] [git init]
- git commit -m "初始提交"
- git config -l  查看是否包含user.name和user.email,对比gitee的config配置
- gic config --global user.name "用户名"
- git config --global user.email "邮箱"
- git remote add origin git@gitee.com:组织名/仓库名.git
- 【第一次上传】git push -u origin master

组员: git-bash

- 确认在gitee平台是否可见组织及仓库
- git config --global user.name ""
- git config --global user.email ""
- 找到一个存放项目代码的目录(仓库的名称不存在于当前目录)
- git clone git@gitee.com:组织名/仓库名.git
- cd 仓库目录
- git config -l
- 修改或新增一个文本文件,写入测试内容
- git add .
- git commit -m "修改或新增xxx文件"
- git push
- 查看gitee平台,仓库的内容是否发生了变化 ,如果内容上传成功,则OK。

【注意】除了第一次上传(first commit)之外,每一次上传(push)之前必须先更新(pull)。如果本地仓库的代码强制替换远程仓库(gitee),则执行git push --force -u origin master.

11.2 改造后台User

参考:https://docs.djangoproject.com/zh-hans/2.1/topics/auth/customizing/

11.2.1 创建项目和应用

django-admin startproject meituanms
cd meituanms
django-admin startapp login

11.2.2 创建User模型

在login应用模块的models.py中,添加如下代码:

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

class User(AbstractUser):
    """
    AbstractUser 本身包含的属性:
     - username, password, first_name,last_name,email
     - is_superuser  是否为超级用户
     - groups        用户所在的组
     - user_permissions 用户的权限
     - is_active     是否激活
     - is_staff      是否为职员,默认是,用户需要登录
     - date_joined   加入时间
    """
    phone = models.CharField(max_length=11, verbose_name='手机号', blank=True, null=True, unique=True)
    head = models.CharField(max_length=50, verbose_name='用户头像', blank=True, null=True)
    sex = models.CharField(max_length=2, default='男')
    city = models.CharField(max_length=10, blank=True, null=True)

11.2.3 配置admin

在login应用的admin.py脚本中,添加如下代码:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext, gettext_lazy as _

from .models import User

# Register your models here.
class MyUserAdmin(UserAdmin):
    list_display = ('username', 'is_active', 'is_staff', 'is_superuser', 'phone', 'sex', 'city')
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        ('VIP会员', {'fields': ('phone', 'sex', 'city', 'head')}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
                                       'groups', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )

admin.site.register(User, MyUserAdmin)

11.2.4 配置settings.py

声明AUTH_USER_MODEL变量,如下所示:

# 配置授权用户
AUTH_USER_MODEL = 'login.User'

11.2.5 迁移与创建超级用户

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser  <回车>
User:  admin
Email:  610039018@qq.com
Password:  admin123
Password(Again):  admin123

11.3 Django权限

参考:https://docs.djangoproject.com/zh-hans/2.1/topics/auth/default/

11.3.1 权限描述

django的权限涉及的表:

  1. django_content_type表,保存app应用模块的信息(id, app_label, model)

    每一条记录,表示一个实际的models.Model子类名称(小写)

    如果通过sql语句创建的表,则手动向django_content_type添加记录,以便添加权限和验证权限。

  2. auth_permission表,用于声明每一个models模型类的四个权限,通过content_id来关联哪一个模型。

    字段: id, name(文本描述), content_id(模型类的id), codename(实际上使用的)

    codename 格式:

    add|change|delete|view_model名称(同django_content_type表的model字段相同的)

  3. auth_user_user_permissions(默认的)或login_user_user_permissions(重写了AUTH_USER_MODEL), 授权用户的权限。

    字段: id, user_id (用户ID), permission_id (权限ID)

查看用户的相关权限的sql语句:

select u.username, p.codename
from auth_user u
join auth_user_user_permissions auup
on u.id = auup.user_id
join auth_permission p
on auup.permission_id = p.id;

通过用户模型查看权限(非超级管理员 is_superuser=False)

from django.contrib.auth.models import *
disen = User.objects.get(username='disen')
disen.user_permissions.all()

如, 查询当前用户是否具有add_goods权限

disen.user_permissions.filter(codename='add_goods')

11.3.2 添加与删除权限

参考django.contrib.auth.models.User类的相关方法。

添加权限之前,先查询权限(根据codename):

p1=Permission.objects.get(codename='change_goods')

重新设置权限:

disen.user_permissions.set([p1])

将goods和order订单的所有权限,添加给disen用户:

from django.db.models import Q
ps = Permission.objects.filter(Q(codename__endswith='goods') | Q(codename__endswith='order'))

disen.user_permissions.set(ps)  # .add(p1, p2, p3...)

清除disen用户的所有权限

disen.user_permissions.clear()

删除单个权限:

p1 = Permission.objects.get(codename='add_order')
disen.user_permissions.remove(p1)

11.3.3 验证权限

除了user_permissions.filter(codename=)查询之外,还可以通过has_perm(‘app_label.codename’)方式验证.

验证单个权限:

disen.has_perm('goods.add_goods') # 返回 True或False

验证多个权限:

disen.has_perms(['goods.add_goods', 'order.add_order'])

在view视图函数中验证是否具有权限:

  • 在view处理函数内部进行验证 [正常]
  • 使用django.contrib.auth.decorates.permission_required(‘order.add_order’)

11.4 富文本的集成

11.4.1 tinymce

参考:https://pypi.org/project/django-tinymce/2.8.0/

pip安装

pip install django-tinymce==2.8.0

11.4.2 mdeditor

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

pip安装

pip install django-mdeditor==0.1.18

11.5 Form表单类

ettings.py

声明AUTH_USER_MODEL变量,如下所示:

# 配置授权用户
AUTH_USER_MODEL = 'login.User'

11.2.5 迁移与创建超级用户

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser  <回车>
User:  admin
Email:  610039018@qq.com
Password:  admin123
Password(Again):  admin123

11.3 Django权限

参考:https://docs.djangoproject.com/zh-hans/2.1/topics/auth/default/

11.3.1 权限描述

django的权限涉及的表:

  1. django_content_type表,保存app应用模块的信息(id, app_label, model)

    每一条记录,表示一个实际的models.Model子类名称(小写)

    如果通过sql语句创建的表,则手动向django_content_type添加记录,以便添加权限和验证权限。

  2. auth_permission表,用于声明每一个models模型类的四个权限,通过content_id来关联哪一个模型。

    字段: id, name(文本描述), content_id(模型类的id), codename(实际上使用的)

    codename 格式:

    add|change|delete|view_model名称(同django_content_type表的model字段相同的)

  3. auth_user_user_permissions(默认的)或login_user_user_permissions(重写了AUTH_USER_MODEL), 授权用户的权限。

    字段: id, user_id (用户ID), permission_id (权限ID)

查看用户的相关权限的sql语句:

select u.username, p.codename
from auth_user u
join auth_user_user_permissions auup
on u.id = auup.user_id
join auth_permission p
on auup.permission_id = p.id;

通过用户模型查看权限(非超级管理员 is_superuser=False)

from django.contrib.auth.models import *
disen = User.objects.get(username='disen')
disen.user_permissions.all()

如, 查询当前用户是否具有add_goods权限

disen.user_permissions.filter(codename='add_goods')

11.3.2 添加与删除权限

参考django.contrib.auth.models.User类的相关方法。

添加权限之前,先查询权限(根据codename):

p1=Permission.objects.get(codename='change_goods')

重新设置权限:

disen.user_permissions.set([p1])

将goods和order订单的所有权限,添加给disen用户:

from django.db.models import Q
ps = Permission.objects.filter(Q(codename__endswith='goods') | Q(codename__endswith='order'))

disen.user_permissions.set(ps)  # .add(p1, p2, p3...)

清除disen用户的所有权限

disen.user_permissions.clear()

删除单个权限:

p1 = Permission.objects.get(codename='add_order')
disen.user_permissions.remove(p1)

11.3.3 验证权限

除了user_permissions.filter(codename=)查询之外,还可以通过has_perm(‘app_label.codename’)方式验证.

验证单个权限:

disen.has_perm('goods.add_goods') # 返回 True或False

验证多个权限:

disen.has_perms(['goods.add_goods', 'order.add_order'])

在view视图函数中验证是否具有权限:

  • 在view处理函数内部进行验证 [正常]
  • 使用django.contrib.auth.decorates.permission_required(‘order.add_order’)

11.4 富文本的集成

11.4.1 tinymce

参考:https://pypi.org/project/django-tinymce/2.8.0/

pip安装

pip install django-tinymce==2.8.0

11.4.2 mdeditor

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

pip安装

pip install django-mdeditor==0.1.18

11.5 Form表单类

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2021-12-04 13:24:12  更:2021-12-04 13:24:53 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 2:39:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码