ORM

一边写一个示例,一边回顾一下之前的内容,引出新的知识点展开讲解

回顾-创建项目

下面就从创建项目开始,一步一步先做一个页面出来。
一、先创建一个新的Django项目
项目名是:week20,App名是:app01
Python自动化开发学习20-Django
因为是通过PyCharm创建的项目,创建的时候填上了 app name 。所以PyCharm帮我么创建好了app同时也完成了注册。否则就去settings.py里面手动添加注册app

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
]

二、创建表结构
一共需要2张表,一张人员信息表,一张部门表。做一个外键关联。
人员信息表(UserInfo):

uid name age ip dept_id
1 Adam 22 192.168.1.101 1
2 Bob 31 192.168.1.102 1
3 Cara 26 192.168.1.103 2

部门表(Dept):

id name name_en
1 运维 Operation
2 开发 Development
3 市场 Marketing
4 销售 Sales

表结构的文件是app01/models.py

from django.db import models

# Create your models here.

class UserInfo(models.Model):
    uid = models.AutoField(primary_key=True)  # 自己建主键
    name = models.CharField(max_length=32, db_index=True)  # 加索引
    age = models.IntegerField()
    ip = models.GenericIPAddressField(protocol='ipv4', db_index=True)  # 使用ipv4验证
    dept = models.ForeignKey('Dept', models.CASCADE, to_field='id')  # 外键

class Dept(models.Model):
    name = models.CharField(max_length=32)
    name_en = models.CharField(max_length=32)

三、创建数据库
切换到命令行执行如下2条命令:

python manage.py makemigrations
python manage.py migrate

然后去Dept表里把部门先创建好,就按照上面的表格的内容。通过PyCharm就可以直接连接我们的Sqlite数据库并操作。
Python自动化开发学习20-Django

四、写对应关系
先把urls.py里面的对应关系写好:

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('dept/', views.dept),
]

然后就是完成处理函数和html页面了

五、写处理函数
处理函数是app01/views.py

from django.shortcuts import render

# Create your views here.

from app01 import models

def dept(request):
    depts1 = models.Dept.objects.all()
    return render(request, 'dept.html', {'depts1': depts1})

六、写页面的html
在templates文件夹里创建页面文件dept.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>部门列表</h1>
<ul>
    {% for row in depts1 %}
        <li>{{ row.id }} - {{ row.name }} - {{ row.name_en }}</li>
    {% endfor %}
</ul>
</body>
</html>

七、检查
上面的步骤都是一气呵成写下来的,写到这里可以检查一下了。运行之后,打开页面检查是否能在页面中显示部门的数据。

获取数据的3种方式

目前我们都是通过 models.Dept.objects.all() 这个方法来获取到数据的。现在看看另外的两种方式。
修改处理函数:

def dept(request):
    depts1 = models.Dept.objects.all()
    print(depts1)  # 每个元素都是一个对象
    # depts2 = models.Dept.objects.all().values()
    depts2 = models.Dept.objects.all().values('id', 'name')
    # 也可以只取出部分字段,这里不取name_en,到前端就获取不到
    # depts2依然是一个QuerySet类型,但是列表里的元素不是对象而是字典了
    # 在前端获取值的时候,取对象的属性和取字典的value都是点一个名称,所以前端用起来是一样的
    print(depts2)  # 每个元素都是一个字典
    depts3 = models.Dept.objects.all().values_list()  # 也是可以只取部分字段的
    print(depts3)  # 每个元素都是一个元组
    return render(request, 'dept.html',
                  {'depts1': depts1,
                   'depts2': depts2,
                   'depts3': depts3})

直接把3中方法的数据都返回给前端,前端也写3个列表来显示数据。修改后的templates/dept.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>部门列表(对象)</h1>
<ul>
    {% for row in depts1 %}
        <li>{{ row.id }} - {{ row.name }} - {{ row.name_en }}</li>
    {% endfor %}
</ul>
<h1>部门列表(字典)</h1>
<ul>
    {% for row in depts2 %}
        <li>{{ row.id }} - {{ row.name }} - {{ row.name_en }}</li>
    {% endfor %}
</ul>
<h1>部门列表(元祖)</h1>
<ul>
    {% for row in depts3 %}
        <li>{{ row.0 }} - {{ row.1 }} - {{ row.2 }}</li>
    {% endfor %}
</ul>
</body>
</html>

运行后print的结果:

<QuerySet [<Dept: Dept object (1)>, <Dept: Dept object (2)>, <Dept: Dept object (3)>, <Dept: Dept object (4)>]>
<QuerySet [{'id': 1, 'name': '运维', 'name_en': 'Operation'}, {'id': 2, 'name': '开发', 'name_en': 'Development'}, {'id': 3, 'name': '市场', 'name_en': 'Marketing'}, {'id': 4, 'name': '销售', 'name_en': 'Sales'}]>
<QuerySet [(1, '运维', 'Operation'), (2, '开发', 'Development'), (3, '市场', 'Marketing'), (4, '销售', 'Sales')]>

知识点整理:不但可以获取对象,现在也可以获取到字典或元祖,而且还能只获取部分的key
models.Dept.objects.all()
QuerySet类型,内部元素是对象
models.Dept.objects.all().values()
QuerySet类型,内部元素是字典,可以只取部分字段
models.Dept.objects.all().values_list()
QuerySet类型,内部元素是元祖,可以只取部分字段

回顾-一对多跨表操作

接下来把UserInfo也在网页中显示出来。
对应关系,urls.py

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('dept/', views.dept),
    path('user/', views.user),
]

处理函数,views.py

def user(request):
    u1 = models.UserInfo.objects.filter(uid__gt=0)  # __gt就是大于的意思,换个写法取到全部
    return render(request, 'user.html', {'u1': u1})

显示页面,templates/user.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户列表</h1>
<table border="1">
    <thead>
    <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>IP</th>
        <th>Dept.(id)</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u1 %}
        <tr uid="{{ row.uid }}" dept_id="{{ row.dept_id }}">
        <td>{{ row.uid }}</td>
        <td>{{ row.name }}</td>
        <td>{{ row.age }}</td>
        <td>{{ row.ip }}</td>
        <td>{{ row.dept_id }}</td>
        <td>{{ row.dept.name }}</td>
        <td>{{ row.dept.name_en }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
</body>
</html>

在实际的应用中,页面中不需要显示出id的信息,所以 id 和 Dept.(id) 这两列是不需要显示的。我们可以删除这两列,但是后续的操作可能还是需要用到 id 的信息的。这里是通过自定义属性的方式把 id 信息隐藏在页面中了。既不用显示出来,但是页面中用 id 的信息,需要的时候可以获取到对应的id。

跨表操作-双下划线

还有一种跨表操作,使用双下划线。我们已经可以用点来实现跨表了,双下划綫同样可以跨表,两者的应用场景不同。
修改处理函数,views.py

def user(request):
    u1 = models.UserInfo.objects.filter(uid__gt=0)
    # 现在用另外的两种获取数据的方法来进行跨表
    # u2 = models.UserInfo.objects.filter(uid__gt=0).values('dept.name')
    # 上面会报错,这里要跨表操作不能用点,而是要用双下划线,如下
    u2 = models.UserInfo.objects.filter(uid__gt=0).values('name', 'dept__name', 'dept__name_en')
    u3 = models.UserInfo.objects.filter(uid__gt=0).values_list('name', 'dept__name', 'dept__name_en')
    return render(request, 'user.html', {'u1': u1,
                                         'u2': u2,
                                         'u3': u3})

修改显示页面,templates/user.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户列表(对象)</h1>
<table border="1">
    <thead>
    <tr>
        <th>name</th>
        <th>age</th>
        <th>IP</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u1 %}
        <tr uid="{{ row.uid }}" dept_id="{{ row.dept_id }}">
        <td>{{ row.name }}</td>
        <td>{{ row.age }}</td>
        <td>{{ row.ip }}</td>
        <td>{{ row.dept.name }}</td>
        <td>{{ row.dept.name_en }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
<h1>用户列表(字典)</h1>
<table border="1">
    <thead>
    <tr>
        <th>name</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u2 %}
        <tr>
        <td>{{ row.name }}</td>
        <td>{{ row.dept__name }}</td>
        <td>{{ row.dept__name_en }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
<h1>用户列表(元组)</h1>
<table border="1">
    <thead>
    <tr>
        <th>name</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u3 %}
        <tr>
        <td>{{ row.0 }}</td>
        <td>{{ row.1 }}</td>
        <td>{{ row.2 }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
</body>
</html>

知识点整理:
获取数据的3种方法中的第一种,页面中获取到的元素直接是对象,对对象用点就可以进行跨表
另外的两种方法,获取到的不再是对象了,而是字典和元组。这时候取值要传字符串,要跨表就得在字符串中使用双下划线

显示序号-for循环中的forloop

在模板语言的for循环里还有一个forloop,通过这个可以取到到序号:

  • forloop.counter :序号,从1开始
  • forloop.counter0 :序号,从0开始
  • forloop.revcounter :序号,倒序,从1开始
  • forloop.revcounter0 :序号,倒序,从0开始
  • forloop.first :是否是第一个
  • forloop.last :是否是最后一个
  • forloop.parentloop :有嵌套循环的情况下,获取父类的以上6个值。字典的形式,可以继续通过点来取到具体的值

修改user.html测试效果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户列表(forloop)</h1>
<table border="1">
    <thead>
    <tr>
        <th>序号1</th>
        <th>序号2</th>
        <th>序号3</th>
        <th>序号4</th>
        <th>是否第一个</th>
        <th>是否最后一个</th>
        <th>name</th>
        <th>age</th>
        <th>IP</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u1 %}
        <tr uid="{{ row.uid }}" dept_id="{{ row.dept_id }}">
        <td>{{ forloop.counter }}</td>
        <td>{{ forloop.counter0 }}</td>
        <td>{{ forloop.revcounter }}</td>
        <td>{{ forloop.revcounter0 }}</td>
        <td>{{ forloop.first }}</td>
        <td>{{ forloop.last }}</td>
        <td>{{ row.name }}</td>
        <td>{{ row.age }}</td>
        <td>{{ row.ip }}</td>
        <td>{{ row.dept.name }}</td>
        <td>{{ row.dept.name_en }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
<h1>用户列表(forloop.parentloop)</h1>
<table border="1">
    <thead>
    <tr>
        <th>parentloop</th>
        <th>name</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for i in u2 %}
        {% for row in u2 %}
            <tr>
            <td>{{ forloop.parentloop }}</td>
            <td>{{ row.name }}</td>
            <td>{{ row.dept__name }}</td>
            <td>{{ row.dept__name_en }}</td>
            </tr>
        {% endfor %}
    {% endfor %}
    </tbody>
</table>
<h1>用户列表(forloop.parentloop.counter)</h1>
<table border="1">
    <thead>
    <tr>
        <th>父循环的序号</th>
        <th>当前循环的序号</th>
        <th>name</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for i in u3 %}
        {% for row in u3 %}
            <tr>
            <td>{{ forloop.parentloop.counter }}</td>
            <td>{{ forloop.counter }}</td>
            <td>{{ row.0 }}</td>
            <td>{{ row.1 }}</td>
            <td>{{ row.2 }}</td>
            </tr>
        {% endfor %}
    {% endfor %}
    </tbody>
</table>
</body>
</html>

回顾-引入静态文件

这里先准备好jQuery,后面要用到。在项目目录下创建一个static文件夹用来存放我们的静态文件。然后在settings.py的最后加上路径:

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

回顾-添加数据的示例

先来写html。添加数据要有一个添加按钮,按钮需要绑定事件,这里要用到js。事件是弹出一个模态对话框。对话框里填入数值,但是部门要用下拉列表来做。下拉列表的选项需要处理函数先去获取 depts1 = models.Dept.objects.all() ,然后返回给页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .hide{
            display: none;
        }
        .shade{
            position: fixed;
            top: 0;
            right: 0;
            left: 0;
            bottom: 0;
            background: black;
            opacity: 0.5;
            z-index: 10;
        }
        .add-modal{
            position: fixed;
            height: 300px;
            width: 400px;
            margin-left: -200px;
            top: 100px;
            left: 50%;
            z-index: 20;
            background: white;
            padding: 20px 50px;
        }
    </style>
</head>
<body>
<h1>用户列表</h1>
<div>
    <input id="add-user" type="button" value="添加" />
</div>
<table border="1">
    <thead>
    <tr>
        <th>序号1</th>
        <th>name</th>
        <th>age</th>
        <th>IP</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u1 %}
        <tr uid="{{ row.uid }}" dept_id="{{ row.dept_id }}">
        <td>{{ forloop.counter }}</td>
        <td>{{ row.name }}</td>
        <td>{{ row.age }}</td>
        <td>{{ row.ip }}</td>
        <td>{{ row.dept.name }}</td>
        <td>{{ row.dept.name_en }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
<div class="shade hide"></div>
<div class="add-modal hide">
    <form action="/user/" method="POST">
        <p><input type="text" placeholder="name" name="name"></p>
        <p><input type="text" placeholder="age" name="age"></p>
        <p><input type="text" placeholder="IP" name="ip"></p>
        <p>
            <select name="dept-id">
                {% for op in depts1 %}
                    <option value="{{ op.id }}">{{ op.name }}({{ op.name_en }})</option>
                {% endfor %}
            </select>
        </p>
        <p>
            <input type="submit" value="提交">
            <input id="cancel" type="button" value="取消">
        </p>
    </form>
</div>
<script src="/static/js/jquery-1.12.4.js"></script>
<script>
    $(function () {
        $('#add-user').click(function () {
            $('.shade, .add-modal').removeClass('hide')
        });
        $('#cancel').click(function () {
            $('.shade, .add-modal').addClass('hide')
        })
    })
</script>
</body>
</html>

修改处理函数,这次要写一个GET方法,还要写一个POST方法。

def user(request):
    if request.method == 'GET':
        u1 = models.UserInfo.objects.filter(uid__gt=0)
        depts1 = models.Dept.objects.all()
        return render(request, 'user.html', {'u1': u1, 'depts1': depts1})
    elif request.method == 'POST':
        dic1 = {
            'name': request.POST.get('name'),
            'age': request.POST.get('age'),
            'ip': request.POST.get('ip'),
            'dept_id': request.POST.get('dept-id'),
        }
        models.UserInfo.objects.create(**dic1)
        return redirect('/user/')

完成之后,现在就可以愉快的添加数据了。不过目前数据验证我们还没法做。

AJAX

数据验证

接着上面的示例,现在就来实现简单的验证。这里要实现的是服务器端的验证。模态对话框里提交表单的页面增加一个按钮,然后在jQuery里绑定事件。下面只贴上修改的部分代码

<!-- 提交表单的部分 -->
<div class="add-modal hide">
    <form action="/user/" method="POST">
        <p><input type="text" placeholder="name" name="name" id="name"></p>
        <p><input type="text" placeholder="age" name="age" id="age"></p>
        <p><input type="text" placeholder="IP" name="ip" id="ip"></p>
        <p>
            <select name="dept-id" id="dept-id">
                {% for op in depts1 %}
                    <option value="{{ op.id }}">{{ op.name }}({{ op.name_en }})</option>
                {% endfor %}
            </select>
        </p>
        <p>
            <input type="submit" value="提交">
            <input type="button" id="ajax-submit" value="Ajax提交" />
            <input id="cancel" type="button" value="取消">
        </p>
    </form>
</div>

上面只是添加了一个按钮,另外是把所有需要提交的input和select标签加上了id属性,方便用比较简单的逻辑来获取到属性。
下面就要用jQuery来发一个Ajax请求,$.ajax 这个就是jQuery提供的Ajax的功能。

<!-- jQuery部分 -->
<script>
    $(function () {
        $('#add-user').click(function () {
            $('.shade, .add-modal').removeClass('hide')
        });
        $('#cancel').click(function () {
            $('.shade, .add-modal').addClass('hide')
        });
        $('#ajax-submit').click(function () {
            $.ajax({
                url: '/ajax_add_user/',
                type: 'POST',
                data: {
                    'name': $('#name').val(),
                    'age': $('#age').val(),
                    'ip': $('#ip').val(),
                    'dept-id': $('#dept-id').val()},
                success: function (data) {
                    if(data == 'OK'){
                        location.reload()
                    }else{
                        alert(data)
                    }
                }
            })
        })
    })
</script>

例子中我们的Ajax请求是发送到 url: '/ajax/', 这里的,先去配置一下urls.py

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('dept/', views.dept),
    path('user/', views.user),
    path('ajax_add_user/', views.ajax_add_user),
]

然后是处理函数

def ajax_add_user(request):
    dic1 = {
        'name': request.POST.get('name'),
        'age': request.POST.get('age'),
        'ip': request.POST.get('ip'),
        'dept_id': request.POST.get('dept-id'),
    }
    for k, v in dic1.items():
        if not v:
            return HttpResponse('字段 %s 不能为空' % k)
    else:
        models.UserInfo.objects.create(**dic1)
        return HttpResponse('OK')

完成,现在就有了简单的验证。服务器端会对提交过来的数据进行验证,所有数据都不能为空,如果为空就返回错误信息。否则验证通过,在数据库添加数据。页面收到服务端返回的字符串后,会触发 success 回调函数。返回验证通过就刷新页面,否则弹出框显示返回的错误信息。

优化验证

上面的验证比较简陋,个各种情况验证不是本节要讲的。这里要讲的是即使你的验证再完善也可能会有遗漏。漏过验证的数据就会提交到去更新数据库。如果数据不符合数据库的数据格式,比如age提交一个字符串,那么程序会报错。报错系统并不会崩溃,我们调试的时候可以看到错误信息,但是客户端是不知道发生了什么的。
下面就通过try来捕获异常,之后可以返回自定义的消息内容,或者也可以把异常信息返回

import json
def ajax_add_user(request):
    ret = {'status': True, 'error': None, 'data': None}
    try:
        dic1 = {
            'name': request.POST.get('name'),
            'age': request.POST.get('age'),
            'ip': request.POST.get('ip'),
            'dept_id': request.POST.get('dept-id'),
        }
        for k, v in dic1.items():
            if not v:
                ret['status'] = False
                ret['error'] = '字段 %s 不能为空' % k
                break
        else:
            models.UserInfo.objects.create(**dic1)
    except Exception as e:
        print(e)
        ret['status'] = False
        ret['error'] = '请求错误:%s' % str(e)
    return HttpResponse(str(ret['error']))
    # return HttpResponse(json.dumps(ret))

AJAX请求有一个error的回调函数,也可以处理请求错误返回信息。下面的AJAX补充只是里会用到。

序列化返回的消息(JSON)

到这里为止,我们Ajax请求,都是用HttpResponse返回结果的。目前返回也只需要使用HttpResponse,不要其他的方法。
HttpResponse返回的内容是字符串,使用JSON序列化字符串,就可以返回更多的信息了,并且客户端处理起来也很方便。上面的例子已经这么做了。把例子中最后的return修改成返回JSON字符串。
然后修改html来处理返回的JSON字符串。另外再优化一个错误消息的显示方式,不要弹出框,写个span标签显示了页面中:

<!-- 上面的代码不变,就不贴了 -->
<!-- 模态对话框,添加了一个span标签显示错误信息 -->
<div class="add-modal hide">
    <form action="/user/" method="POST">
        <p><input type="text" placeholder="name" name="name" id="name"></p>
        <p><input type="text" placeholder="age" name="age" id="age"></p>
        <p><input type="text" placeholder="IP" name="ip" id="ip"></p>
        <p>
            <select name="dept-id" id="dept-id">
                {% for op in depts1 %}
                    <option value="{{ op.id }}">{{ op.name }}({{ op.name_en }})</option>
                {% endfor %}
            </select>
        </p>
        <p><span id="error-msg"> </span></p>
        <p>
            <input type="submit" value="提交">
            <input type="button" id="ajax-submit" value="Ajax提交" />
            <input id="cancel" type="button" value="取消">
        </p>
    </form>
</div>
<script src="/static/js/jquery-1.12.4.js"></script>
<!-- 修改了success的匿名函数的内容,现在data是返回的JSON字符串 -->
<script>
    $(function () {
        $('#add-user').click(function () {
            $('.shade, .add-modal').removeClass('hide')
        });
        $('#cancel').click(function () {
            $('.shade, .add-modal').addClass('hide')
        });
        $('#ajax-submit').click(function () {
            $.ajax({
                url: '/ajax_add_user/',
                type: 'POST',
                data: {
                    'name': $('#name').val(),
                    'age': $('#age').val(),
                    'ip': $('#ip').val(),
                    'dept-id': $('#dept-id').val()},
                success: function (data) {
                    var obj = JSON.parse(data);
                    if(obj.status){
                        location.reload()
                    }else{
;                        $('#error-msg').text(obj.error)
                    }
                }
            })
        })
    })
</script>

小结-Ajax知识点

Ajax请求的语法:

$.ajax({
    url: '/ajax/',  // 提交到哪里
    type: 'POST',  // 以什么方式提交
    data: {'k1': 'v1', 'k2': 'v2'},  // 提交的数据,字典的形式
    // 服务的返回数据之后触发的函数,回调函数
    // 下面的匿名函数中的参数data是服务端返回的字符串
    success: function (data) {
        alert(data)
    }
})

其他Ajax请求:

  • $.ajax(url,[settings])
  • $.get(url,[data],[fn],[type])
  • $.getJSON(url,[data],[fn])
  • $.getScript(url,[callback])
  • $.post(url,[data],[fn],[type])

推荐还是用基本的第一个,并且其他方法本质上还是调用了第一个方法来实现的。其他的请求方法知道一下,看见能认识就好,自己用的话,用第一个就好了,这个是本质。
Ajax返回:
建议,永远让服务端返回序列化的JSON字符串。这个虽然不是必须的,但是大家都是这么玩的。

import json
return HttpResponse(json.dumps(dic))

返回使用HttpResponse,不要用别的。redirect肯定是不能用的,页面不会跳转。render返回的也是字符,所以可以这么用,但是render返回还要渲染否则页面没数据,并且页面也不好处理。

示例-删除功能

要做删除功能,需要在表格的每一行增加一列,放置按钮。顺便把编辑按钮也一起加上,稍后再绑定事件。
修改表格增加一列:

<table border="1">
    <thead>
    <tr>
        <th>序号1</th>
        <th>name</th>
        <th>age</th>
        <th>IP</th>
        <th>Dept.(name)</th>
        <th>Dept.(name_en)</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    {% for row in u1 %}
        <tr uid="{{ row.uid }}" dept_id="{{ row.dept_id }}">
        <td>{{ forloop.counter }}</td>
        <td>{{ row.name }}</td>
        <td>{{ row.age }}</td>
        <td>{{ row.ip }}</td>
        <td>{{ row.dept.name }}</td>
        <td>{{ row.dept.name_en }}</td>
        <td>
            <a class="edit">编辑</a>|<a class="delete">删除</a>
        </td>
        </tr>
    {% endfor %}
    </tbody>
</table>

然后是jQuery里绑定事件。要删除某条数据,需要获取到该条数据uid。uid我们之前已经存放到页面的tr标签里了,获取到uid提交之后就交给处理函数了。另外页面也要变化,这里不需要刷新页面(刷新也是可以的)。只要收到删除成功的请求后把这个re给remove掉就可以了。

    $(function () {
        $('.delete').click(function () {
            var tr_obj = $(this).parent().parent();  // 保留这个tr的标签对象,确认数据删除后remove掉
            var uid = tr_obj.attr('uid');  // 取到uid
            $.ajax({
                url: '/ajax_del_user/',
                type: 'POST',
                data: {'uid': uid},
                success: function (data) {
                    var obj = JSON.parse(data);
                    if(obj.status){
                        tr_obj.remove()
                    }else{
                        $('#error-msg').text(obj.error)
                    }
                }
            })
        });
    })

处理函数比较简单,根据uid查找到数据后delete掉

def ajax_del_user(request):
    ret = {'status': True, 'error': None, 'data': None}
    try:
        uid = request.POST.get('uid')
        models.UserInfo.objects.filter(uid=uid).delete()
    except Exception as e:
        ret['status'] = False
        ret['error'] = '请求错误:%s' % e
    return HttpResponse(json.dumps(ret))

示例-编辑功能

编辑还是用模态对话框来实现,为了避免混淆,就单独再搞一个。直接把之前的删除的div复制一份。把class改掉。
页面里的所有元素的id也都要改,id不能重复,这里不用id了都删掉。
提交按钮也不要了,Ajax提交要的并且起一个新的id名。取消按钮把id换成class,删除页面的取消按钮和jQuery的绑定操作哪里也相应的修改一下

<div class="edit-modal hide">
    <h1>编辑用户</h1>
    <form action="/user/" method="POST">
        <p><input type="text" placeholder="name" name="name"></p>
        <p><input type="text" placeholder="age" name="age"></p>
        <p><input type="text" placeholder="IP" name="ip"></p>
        <p>
            <select name="dept-id">
                {% for op in depts1 %}
                    <option value="{{ op.id }}">{{ op.name }}({{ op.name_en }})</option>
                {% endfor %}
            </select>
        </p>
        <p><span id="error-msg"> </span></p>
        <p>
            <input type="button" id="ajax-edit-submit" value="Ajax提交" />
            <input class="cancel" type="button" value="取消">
        </p>
    </form>
</div>

编辑页面和新增页面的差别主要是编辑页面的输入框里是需要填入默认值的,包括select框也要选中对应的选项。这里Ajax请求的data部分用了一个新的更简单的方法,之后再展开。

    $(function () {
        $('.edit').click(function () {
            $('.shade, .edit-modal').removeClass('hide');
            var uid = $(this).parent().parent().attr('uid');
            // 把uid保存在页面里,提交修改的时候需要用到
            $('.edit-modal form').attr('uid', uid);
            var dept_id = $(this).parent().parent().attr('dept_id');
            // 给select下拉列表填上值
            $('.edit-modal form select').val(dept_id);
            // 下面是为其他input填值,课上跳过了。
            var obj = $(this).parent().siblings('td').first().next();
            $('.edit-modal form :text').each(function () {
                $(this).val(obj.text());
                obj = obj.next();
            })
        });
        $('#ajax-edit-submit').click(function () {
            $.ajax({
                url: '/ajax_edit_user/',
                type: 'POST',
                data: $(this).parents('form').serialize()+'&'+'uid='+$('.edit-modal form').attr('uid'),
                success: function (data) {
                    var obj = JSON.parse(data);
                    if(obj.status){
                        location.reload()
                    }else{
                        $('#error-msg').text(obj.error)
                    }
                }
            })
        });
    })

处理函数都是一样的,这里要用update更新数据。

def ajax_edit_user(request):
    ret = {'status': True, 'error': None, 'data': None}
    try:
        uid = request.POST.get('uid')
        dic1 = {
            'name': request.POST.get('name'),
            'age': request.POST.get('age'),
            'ip': request.POST.get('ip'),
            'dept_id': request.POST.get('dept-id'),
        }
        for k, v in dic1.items():
            if not v:
                ret['status'] = False
                ret['error'] = '字段 %s 不能为空' % k
                break
        else:
            models.UserInfo.objects.filter(uid=uid).update(**dic1)
    except Exception as e:
        ret['status'] = False
        ret['error'] = '请求错误:%s' % e
    return HttpResponse(json.dumps(ret))

Ajax使用serialize() 提交form表单

上面的例子已经使用了serialize() 来获取提交请求的data数据。这里不需要去一个一个获取了。使用serialize() 方法可以直接把form表单里的所有的name和对应的值一次获取到。
例子中还有个问题,就是还要提交一个uid,这个uid不在表单里。这里有两个方法。
一、为uid写一个input标签,然后把标签隐藏了。这样表单里就有uidle并且页面上也不会显示出来
二、对serialize() 方法获取到的值进行再加工。serialize()方法把表单里的内容序列化成了字符串,如例子中那样可以再追加上我们的字符串

外键操作-多对多

首先更新我们的表结构,我们已经有人员信息表(UserInfo)和部门表(Dept)。部门表这里不需要了。再创建一张客户信息表(CustomerInfo)。一家客户可以有多个人员负责,一个人员也可以同时负责多家客户,这就是一个多对多的关系。

自定义关系表

一个多对多的关系在数据库中除了有两张被关联的表之外,还要有一张结合表。人员信息表(UserInfo)原本就有了,现在要加上客户信息表(CustomerInfo)和结合表(UserToCustomer)。表结构如下:

from django.db import models

# Create your models here.

class CustomerInfo(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField(max_length=32)

class UserInfo(models.Model):
    uid = models.AutoField(primary_key=True)  # 自己建主键
    name = models.CharField(max_length=32, db_index=True)  # 加索引
    age = models.IntegerField()
    ip = models.GenericIPAddressField(protocol='ipv4', db_index=True)  # 使用ipv4验证
    dept = models.ForeignKey('Dept', models.CASCADE, to_field='id')  # 外键

class UserToCustomer(models.Model):
    user_obj = models.ForeignKey('UserInfo', models.CASCADE, to_field='nid')
    customer_obj = models.ForeignKey('CustomerInfo', models.CASCADE, to_field='id')

class Dept(models.Model):
    name = models.CharField(max_length=32)
    name_en = models.CharField(max_length=32)

上面是手动创建的所有的表,并且在结合表里手动写了两个一对多关联。这样也创建完成了一个多对多关系。这也是一种创建多对多关系的方法,自定义关系表。这种方法可以自定义这个表,我们可以根据需要再添加上别的字段。

自动创建关系表

结合表也是可以不用手动创建的,而是由Django自动帮我么创建。把上面的结合表去掉,在客户信息表(CustomerInfo)多对一个 models.ManyToManyField ,之后Django会自动帮我么创建好结合表:

from django.db import models

# Create your models here.

class CustomerInfo(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField(max_length=32)
    userInfo = models.ManyToManyField('UserInfo')

class UserInfo(models.Model):
    uid = models.AutoField(primary_key=True)  # 自己建主键
    name = models.CharField(max_length=32, db_index=True)  # 加索引
    age = models.IntegerField()
    ip = models.GenericIPAddressField(protocol='ipv4', db_index=True)  # 使用ipv4验证
    dept = models.ForeignKey('Dept', models.CASCADE, to_field='id')  # 外键

# class UserToCustomer(models.Model):
#     user_obj = models.ForeignKey('UserInfo', models.CASCADE, to_field='nid')
#     customer_obj = models.ForeignKey('CustomerInfo', models.CASCADE, to_field='id')

class Dept(models.Model):
    name = models.CharField(max_length=32)
    name_en = models.CharField(max_length=32)

自动创建只能帮我们创建一张3个字段的表:自增id,关联表的主键,被关联表的主键。如果想加额外的数据就只能用自定义关系表来创建额外的字段了。

设置关联关系

ORM都是通过类来进行数据库操作的。自定义关系表,直接可以获得结合表的类,直接操作结合表就可以进行数据库操作了。这部分都是旧知识点了,就不举例了。创建一个关联关系的方法:

UserToCustomer.objects.create(user_obj_uid=1, customer_obj_id=1)

对于自动创建关联关系表,由于并没有结合表的类,无法直接对结合表进行操作。这里可以获取到对象,比如客户表id=1的那条数据对象,使用提供的方法对这个对象的关联系进行操作,添加、删除、清除、设置。

obj = CustomerInfo.objects.get(id=1)  # 先获取到一个对象,下面都是对id=1的关联关系进行操作
obj.userInfo.add(1)  # 添加一个关系
obj.userInfo.add(2, 3, 4)  # 多个参数添加多个关系
obj.userInfo.add(*[2, 3, 4])  # 通过列表添加多个关系
obj.userInfo.remove(1)  # 删除一个关系,同样支持多个参数或列表
obj.userInfo.clear()  # 清除这个id的所有的关系
obj.set([3, 5, 7])  # 设置关系。这个id的其他关系都会清除,最后只有这个列表中的关系。相当于先清除在添加。这里没星号

上面没有获取的方法,获取的方法和之前获取数据的方法一样。models.UserInfo.objects 后面能使用什么方法,这里的obj就可以使用什么方法。比如:.all() 所有被关联的表的对象。all() 方法获取到的一定是一个QuerySet对象,在这里里面的每个元素是一个被关联的表 UserInfo 的对象。

显示客户列表(查)

如果上面还没有把表结构更新到数据库,现在就去更新一下

python manage.py makemigrations
python manage.py migrate

现在只有空表,这里先看怎么显示数据,所以还得自己手动去数据库里加上点数据。除了添加客户表还要去结合表里也加点数据。
先准备好urls.py的对应关系和views.py的处理函数。下面是处理函数:

def customer(request):
    if request.method == 'GET':
        customers = models.CustomerInfo.objects.all()
        return render(request, 'customer.html', {'customers': customers})
    elif request.method == 'POST':
        # 一会还要做添加
        pass

这里直接把CustomerInfo表里的所有的对象都传到页面了,在页面里遍历这个customers就能获取到里面所有的数据,包括被关联的UserInfo。html页面文件customer.html如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>客户列表</h1>
<table border="1">
    <thead>
    <tr>
        <th>客户名称</th>
        <th>e-mail</th>
        <th>负责人</th>
    </tr>
    </thead>
    <tbody>
    {% for customer in customers %}
        <tr>
        <td>{{ customer.name }}</td>
        <td>{{ customer.email }}</td>
        <td>
{#            可以去掉下面的注释,查看customer.userInfo.all的内容#}
{#            {{ customer.userInfo.all }}#}
            {% for user in  customer.userInfo.all %}
                {{ user.name }}
            {% endfor %}
        </td>
        </tr>
    {% endfor %}
    </tbody>
</table>
</body>
</html>

因为是多对多的关系,customer.userInfo.all 里是所有的被关联的对象,可能是多个。这里就需要再一个for循环遍历每一个被关联的对象,然后获取到被关联对象里的属性。

添加客户(增)

页面简单点直接放在客户列表的下面好了。关联客户需要使用下拉列表,现在可以关联多个客户,所以要用复选的下拉列表(multiple),通过form提交到后台要获取值就需要用getlist来获取多个值。客户列表的后面接着写:

<h1>添加客户</h1>
<form action="/customer/" method="POST">
    <p><input type="text" placeholder="name" name="name"></p>
    <p><input type="text" placeholder="email" name="email"></p>
    <p>
        <select name="users" multiple>
            {% for user in users %}
                <option value="{{ user.uid }}">{{ user.name }}</option>
            {% endfor %}
        </select>
    </p>
        <p>
            <input type="submit" value="提交">
            <input type="button" id="ajax-submit" value="Ajax提交" />
        </p>
</form>

处理函数,不但要写POST方法,GET方法现在还需要多提交一个UserInfo给前台的下拉列表:

def customer(request):
    if request.method == 'GET':
        customers = models.CustomerInfo.objects.all()
        # user表也要传到前端,添加的时候下拉列表要用到
        users = models.UserInfo.objects.all()
        return render(request, 'customer.html', {'customers': customers, 'users': users})
    elif request.method == 'POST':
        name = request.POST.get('name')
        email = request.POST.get('email')
        users = request.POST.getlist('users')  # 这里有多个值要用getlist来获取一个列表
        obj = models.CustomerInfo.objects.create(name=name, email=email)  # 创建记录并获取对象
        obj.userInfo.add(*users)  # 添加关联的记录
        return redirect('/customer/')

上面例子中,添加客户数据的同时获取到返回值。这里不用再去数据库里查找了,直接对这个返回的对象进行关联关系的操作。

用Ajax提交-Ajax知识补充

这里直接上例子,并且对Ajax进行一些扩展。在页面上增加事件绑定,添加客户的后面接着写:

<script src="/static/js/jquery-1.12.4.js"></script>
<script>
    $(function () {
        $('#ajax-submit').click(function () {
            $.ajax({
                url: '/ajax_add_customer/',
                type: 'POST',
                // data: {'name': 'Test', 'email': 'test@test.com', 'users': [1, 2, 3]},  // 提交测试数据
                data: $(this).parents('form').serialize(),
                dataType: 'JSON',  // 下面匿名函数中的data现在直接就是JSON对象了
                traditional: true,  // 默认无法提交列表的,要提交列表得加上这个,否则交上去的是None
                success: function (data) {
                    // var obj = JSON.parse(data);  // 上面转了,这里就不用再手动转了
                    if(data.status){
                        location.reload()
                    }
                },
                error: function () {
                    alert('后台发生未知错误')
                }
            })
        })
    })
</script>

还有处理函数:

def ajax_add_customer(request):
    ret = {'status': True, 'errpr': None, 'data': None}
    name = request.POST.get('name')
    email = request.POST.get('email')
    users = request.POST.getlist('users')
    obj = models.CustomerInfo.objects.create(name=name, email=email)
    obj.userInfo.add(*users)
    return HttpResponse(json.dumps(ret))

Ajax补充知识点

使用serialize() 方法可以直接把form表单里的所有的name和对应的值一次获取到。之前用过了
dataType: 'JSON', 原本返回的是字符串,现在会直接把字符串转成JSON对象
traditional: true,默认无法提交列表,提交后数据会变成None提交出去。要想提交列表得加上这句
error:匿名函数,当后台抛出异常后会执行这个函数。后台用try捕获到的异常不会执行这里。所以可以不用在处理函数里用try捕获所有的错误,只捕获需要做处理的那部分错误,或者干脆都不捕获。这里写发生未知错误的时候客户端显示的信息。

编辑功能(改)-打开新url页面操作

这里用打开新url的方式来做编辑功能。打开新url虽然要新建一个页面,但是也有它的应用场景。如果一个页面的内容比较多,那么可能是放到模态对话框中种,甚至一个页面都不够放。对于如果页面内容会很多的场景,使用打开新url的方式会更好
原来的页面里只要在表格每行的最后加上一个编辑的按钮实现跳转即可

        <td>
            <a href="/customer-edit-{{ customer.id }}/">编辑</a>
        </td>

这里用的是a标签,直接发送一个带id的GET请求,urls.py中的对应关系应该这么写:

    path('customer-edit-<int:customer_id>/', views.customer_edit),

接下来是处理函数:

def customer_edit(request, customer_id):
    if request.method == 'GET':
        customer = models.CustomerInfo.objects.filter(id=customer_id).first()
        users = models.UserInfo.objects.all()
        return render(request, 'customer-edit.html', {'customer': customer, 'users': users})
    elif request.method == 'POST':
        dic = {
            'name': request.POST.get('name'),
            'email': request.POST.get('email'),
        }
        obj = models.CustomerInfo.objects.filter(id=customer_id)
        obj.update(**dic)
        users = request.POST.getlist('users')
        obj.first().userInfo.set(users)
        return redirect('/customer/')

GET请求部分,传了3个值给前端。customer就是当前被编辑的客户的属性,前端自动填充到input框里。users传递的是员工的属性,前端要提取其中的uid和name,放到selec的选项中。
POST方法需要分别提交因为其实数据库是是两张表,这里的关联修改用的是set方法。选择的是哪些就设置关联哪些。
编辑页面,customer-edit.html。自动填充数据之前也都会,就是select多选的默认选中有点点变化。input框直接用模板语言在页面里就填上了,select框通过jQuery赋值语句val选上:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>编辑客户列表</h1>
<form action="/customer-edit-{{ customer.id }}/" method="POST">
    <p><input type="text" placeholder="name" name="name" value="{{ customer.name }}"></p>
    <p><input type="text" placeholder="email" name="email" value="{{ customer.email }}"></p>
    <p>
        <select name="users" multiple>
            {% for user in users %}
                <option value="{{ user.uid }}">{{ user.name }}</option>
            {% endfor %}
        </select>
    </p>
    <p>
        <input type="submit" value="提交">
    </p>
</form>
<script src="/static/js/jquery-1.12.4.js"></script>
<script>
    $(function () {
        var user_list = [];
        {% for user in customer.userInfo.all %}
            user_list.push({{ user.uid }});
        {% endfor %}
        $('select').val(user_list)
    })
</script>
</body>
</html>

最后的删除就不写了。
另外讲了一对多和多对多,都是单向的操作,一直没讲反查。应该是下节的内容