专栏首页python3Python自动化开发学习-RESTfu

Python自动化开发学习-RESTfu

RESTful API

RESTful API 是一种面向资源编程,也叫表征状态转移(英文:Representational State Transfer,简称REST)。 认为网络上所有的东西都是资源,对资源的操作无非就是增删改查。

传统的方法

比如有个资产的页面,URL是 www.example.com/asset。要对它进行增删改查,可能使用不同的url来区分:

  • www.example.com/addAsset :增加资产,一般是POST方法。
  • www.example.com/delAsset :删除资产,一般是POST方法。
  • www.example.com/editAsset :修改资产,一般是POST方法。
  • www.example.com/showAsset :显示资产,一般是GET方法。也可能使用 www.example.com/asset 作为url

这里的url一般使用的都是动词,表示是一个动作。

RESTful API 的规则

RESTful API 用一个url代指一个资源,既然是资源,这个词要用名词。那么这个url就是 www.example.com/asset 。增删改查都是通过这个url实现的,通过不同的method实现不同的方法,常用的是下面几个方法:

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

在django中,推荐使用CBV。当然FBV也不是不行。

RESTful API 设计指南

这篇貌似讲的很好,值得参考:http://www.ruanyifeng.com/blog/2014/05/restful_api.html

JsonResponse

使用API就会有很多序列化数据返回的操作。 之前当我们需要给前端返回序列化后的字符串时,往往都是先调用json.dumps()这个方法,然后再用HttpResponse()把字符串返回给前端。既然每次都要这么搞,于是django给我么封装了一个新方法,直接完成序列化和返回字符串。 JsonResponse这个类是HttpRespon的子类,通过它直接就可以把字典进行序列化并返回给前端。

>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
'{"foo": "bar"}'

默认只能传入一个字典,并且API要返回的数据应该也就是字典。但是如果一定要序列化一个其他的类型,比如列表,可以设置safe参数:

>>> response = JsonResponse([1, 2, 3], safe=False)

如果要自定义编码器,和json方法一样,通过下面的参数指定:

>>> response = JsonResponse(data, encoder=MyJSONEncoder)

这里的 encoder 参数就是原生的 json.dumps 的cls参数。源码里最后也是调用原生的 json.dumps 把 encoder 传给cls 的。 另外,也可以只定义类中的 default 方法,但是 JsonRespons 没有专门的参数来接收,不过调用原生的 json.dumps 时,会把 json_dumps_params 参数传递过去。也就是在 JsonRespons 里,可以把所有的 json.dumps 的参数先传给 json_dumps_params 。调用原生的 json.dumps 方法的源码是这样的:

data = json.dumps(data, cls=encoder, **json_dumps_params)

所以,可以这么用:

return JsonResponse(
        data={'obj': obj},
        json_dumps_params={'default': fn},  # 这个参数是传给原生的 json.dumps 执行的参数
    )

# 上面自然是要先定义好一个fn函数的,比如下面这样
def fn(obj):
    if hasattr(obj, 'isoformat'):
        return obj.strftime("%Y-%m-%d %T")

代码示例

这段代码用来从数据库获取数据,然后在前端动态的生成表格。 完整的代码在最后,前面是一步一步把这个功能给做出来。 处理函数主要负责两件事情:

  • 从数据库获取数据,返回给前端
  • 定制一个存有配置项的字典,定义好前端怎么显示这些数据,也返回给前端

准备(初始化)

在 urls.py 里写好对应关系:

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

from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('host/', views.HostView.as_view()),
]

写一个处理函数 views.py,这里用CBV,直接返回页面

from django.views import View

class HostView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'host.html')

前端的页面先返回一个空的表格,之后再填充表格内容:

<body>
<h1>主机列表</h1>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
       alert('初始化')
    }
</script>
</body>

测试一下,应该只能看到h1标签里的内容。页面初始化之后会弹一个alert。

从API接口获取数据

写一下前端的init()方法,发送一个AJAX请求到一个新的url,然后接收到返回的数据后,后台看一下:

<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
       $.ajax({
           url: '/api/host/',
           type: 'GET',
           dataType: 'JSON',
           success: function (arg) {
               console.log(arg)
           }
       })
    }
</script>

在 url.py 里再加一个api接口的对应关系:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('host/', views.HostView.as_view()),
    path('api/host/', views.HostApi.as_view()),
]

处理函数直接返回字典:

class HostApi(View):

    def get(self, request, *args, **kwargs):
        ret = {'status': True,
               'message': None,
               'data': None,
               'error': None,
               }
        ret['message'] = 'API接口测试'
        return JsonResponse(ret)

从API接口获取数据2

这里换个方法来实现上面的处理函数。返回的数据不用字典记录,而是用类来记录。没啥差别,就是原来是用中括号来操作的,现在可以用点来操作。最后返回的时候还是要返回字典的,可以用 .__dict__() 来得到这样的一个字典:

class BaseResponse(object):
    def __init__(self):
        self.status = True
        self.message = None
        self.data = None
        self.error = None

class HostApi(View):

    def get(self, request, *args, **kwargs):
        response = BaseResponse()  # 先实例化
        table_config = [
            {
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'title': "端口号",
                'display': 1,
            }
        ]
        response.data = {'table_config': table_config}  # 用点来操作,就是给类的属性赋值
        return JsonResponse(response.__dict__)

前端处理返回的数据

把之前前端页面里AJAX请求的success的回调函数写完整。如果返回status是True,则把参数传递给接下来的处理的函数。否则弹一个alert():

<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
       $.ajax({
           url: '/api/host/',
           type: 'GET',
           dataType: 'JSON',
           success: function (arg) {
               // console.log(arg)
               if (arg.status){
                    createThead(arg.data.table_config)
               }else{
                   alert(arg.error)
               }
           }
       })
    }

    function createThead(config){
        console.log(config)
    }
</script>

如此AJAX请求也完成了:发送了请求,接收了返回结果,然后把返回的结果交给之后的函数进行处理。接下来是就是完善createThead()这个函数了。这里要根据收到的title生成表格的thead的标签:

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

到现在这步,可以在前端看到表格的表头的内容。并且表头是根据后端返回的字典动态生成的。

准备数据库

到这里要后端返回数据了,表结构都还没建,我这里设计了三张表:

class UserInfo(models.Model):
    """用户表"""
    name = models.CharField(max_length=32)
    age = models.IntegerField()

class BusinessUnit(models.Model):
    """业务线"""
    name = models.CharField(max_length=32)

class Host(models.Model):
    """主机列表"""
    host_type_choices = ((1, '服务器'),
                         (2, '防火墙'),
                         (3, '路由器'),
                         (4, '交换机'),
                         )
    host_type = models.IntegerField(choices=host_type_choices)
    hostname = models.CharField(max_length=32)
    port = models.IntegerField()
    business_unit = models.ForeignKey(BusinessUnit, models.CASCADE)
    user = models.ForeignKey(UserInfo, models.CASCADE)

主要用主机列表,其他2张之后可以测试一下对跨表的支持,先一起建好。然后去数据库了随便加几条数据。

后端的处理函数(view),返回更多的数据

到这里,已经可以通过后端返回的字段名在前端动态的生成表头了。接下来把表的内容也显示出来,接着完善后端的处理函数,给前端返回更多的数据。下面是处理函数,根据table_config的配置,去数据库里去对应的字段,然后返回给前端。下面是目前处理函数完整的代码:

class HostApi(View):

    def get(self, request, *args, **kwargs):
        response = BaseResponse()  # 先实例化
        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
            }
        ]
        field_list = []
        for item in table_config:
            if item['field']:
                field_list.append(item['field'])

        # 写一个try,也可以把上面的内容都放进来,
        try:
            result = models.Host.objects.values(*field_list)
            result = list(result)
            response.data = {'table_config': table_config,
                             'data_list': result,
                             }
        except Exception as e:
            response.status = False
            # response.error = str(e)  # 错误信息,用下面的模块可以看到错误产生的位置
            import traceback
            response.error = traceback.format_exc()  # 返回详细的错误信息,包括哪个文件的哪一行
            print(response.error)
        return JsonResponse(response.__dict__)

这里主要就是去数据库里获取数据,然后把获取的QuerySet转成列表也放到response对象里,方便最后返回。 这里注意table_config的配置里有2种特殊的情况:

  • display为0,前端不显示的列。但是依然要把数据传给前端,之后会用到这里的数据
  • field为None,前端要显示,但是数据不是数据库里数据的列,之后会提供填充其中内容的方法

错误信息的优化 处理函数里加了个try,可以把处理函数的全部过程都写到try里进行捕获。如果捕获到异常,就会返回异常信息给前端。前端已经用arg.status来确认是否有异常返回了,下面会再优化一下前端异常显示的效果。 另外这里用了一个traceback模块,traceback对象中包含出错的行数、位置等数据,貌似也很有用。用例子中的方法就可以拿到了。等下面的小节把前端显示优化之后,可以随便哪句语句添加或者删除个字符搞个语法错误,测试效果。

前端显示效果

这里加了一个createTbody()方法,作用是把数据填充到表格里去。另外还有一个showError()方法,作用是如果收到的是后端捕获的异常信息,在标题下面显示出来。下面也是目前前端的完整代码:

<body>
<h1>主机列表</h1>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
        $.ajax({
            url: '/api/host/',
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h1').after(tag);
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,即一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    td.innerHTML = row[configItem.field];
                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }
</script>
</body>

修改table_config的内容,调整前端显示的数据 前端的表格都是通过后端传递来的数据动态生成的。在上面模板的基础上,现在要修改表格显示的内容,只需要去后端调整table_config就可以了,比如改成这样,这里有跨表操作:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
            },
            {
                'field': 'business_unit__name',
                'title': "业务线",
                'display': 1,
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
            }
        ]

主机类型暂时没有办法,因为数据库里记录的值只是数值。而这个数值具体表示的内容是在内存里的。要显示内容首先要获得 models.Host.host_type_choices 然后通过数值拿到对应的文本内容。后面继续优化后应该会有解决的办法。

封装

先暂时写到这里,现在要把前端的js代码做一个封装,做成一个通用的组件。封装的知识点在之前学习jQuery的最后讲过,这里就用上了。封装好的代码如下:

(function ($) {

    var requestURL;
    function init() {
        $.ajax({
            url: requestURL,
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h1').after(tag);
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    td.innerHTML = row[configItem.field];
                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }

    $.extend({
        'show_table': function (url) {
            requestURL = url;
            init();
        }
    })
})(jQuery);

现在前端页面只要先引用这个js文件,然后调用一下extend里的show_table方法就和之前一样了:

<body>
<h1>主机列表</h1>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/show-table.js"></script>
<script>
    $(function () {
        $.show_table('/api/host/');
    });
</script>
</body>

封装之后的js文件,其实就是一个插件了,可以灵活的运用到其他要生成表格的场景里。

输出字符串格式化

这里要进一步定制输出的内容。之前只能输出数据库里的内容。现在是把数据库的内容作为原始数据,但是输出到页面的内容可以通过format方法格式化后再最终展示出来。table_config里再加一个text属性。text内部有content属性,这个是最终要输出的内容,可以像format那样使用{}把需要格式化的内容标记出来。然后再在text内部的kwargs里,指定前面的这些占位符所对应的具体内容,这里面又用了@来标记这不是一个字符串,而是要取对应的字段的值。 所有的{}和@标记都是等到前端再处理的,后端只是进行设置,现在的table_config如下:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
                'text': None,  # 上面不显示,所以这里text有没有都没关系
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
                'text': {'content': '端口:{port}', 'kwargs': {'port': '@port'}}
            },
            {
                'field': 'business_unit__id',
                'title': "业务线ID",
                'display': 0,
            },
            {
                'field': 'business_unit__name',
                'title': "业务线",
                'display': 1,
                'text': {'content': '{n}(id:{id})', 'kwargs': {'n': '@business_unit__name', 'id': '@business_unit__id'}}
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
                'text': {'content': '{type}', 'kwargs': {'type': '@host_type'}}
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
                'text': {'content': '<a href="/api/host/{id}">查看详细</a>', 'kwargs': {'id': '@id'}}
            },
        ]

不显示的字段,display设置为0,那么就不显示了,所以text属性是用不到的。但是其他字段里可以通过@取到这个字段的值了。 有的显示的字段,我也没设置text,那么等下前端处理的时候,还是按照之前的方法来进行展示 最后的操作字段,现在可以加上任意内容了。这里写了一个a标签,并且href里加上了主机id。

前端代码 之前已经完成了封装,所以这里就是修改js文件里的内容。 之前是通过 td.innerHTML = row[configItem.field] 显示内容的。现在这个方法保留,在没有text属性的时候继续按这个来显示。否则,显示content的内容并且根据kwargs的内容进行格式化。前端是没有格式化方法的,这里自己写了一个(下一节展开),完整的代码如下:

(function ($) {

    var requestURL;
    function init() {
        $.ajax({
            url: requestURL,
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h1').after(tag);
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    if (!configItem.text){
                        td.innerHTML = row[configItem.field];
                    }else{
                        var kwargs = {};
                        // 把configItem.text.kwargs的内容存到上面的kwargs里
                        // 没有@开头的原样放过去,以@开头的做特殊处理
                        $.each(configItem.text.kwargs, function (key, value) {
                            if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                kwargs[key] = row[_value]
                            }else{
                                kwargs[key] = value
                            }
                        });
                        td.innerHTML = configItem.text.content.format(kwargs);
                    }

                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }

    // 为字符串创建format方法,用于字符串格式化
    String.prototype.format = function (args) {
        return this.replace(/\{(\w+)\}/g, function (substring, args2) {
            return args[args2];
        })
    };

    $.extend({
        'show_table': function (url) {
            requestURL = url;
            init();
        }
    })
})(jQuery);

在前端增加format方法

这里要在Sting对象的原型里添加一个format()方法,让前端的字符串也可以像python那样,对字符串进行格式化输出。代码就下面简单的几行,正则匹配然后用replace做替换。不过替换的内容又是一个function,逻辑有点复杂了,总之先拿着现成的用把,稍微改改大概也行。暂时没有完全理解:

    // 为字符串创建format方法,用于字符串格式化
    String.prototype.format = function (args) {
        return this.replace(/\{(\w+)\}/g, function (substring, args2) {
            return args[args2];
        })
    };

为td定制属性

首先table_config里再加一个属性attr,用来定制td标签的属性:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
                'attr': {'k1': 'v1', 'k2': 'v2'}
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
                'text': {'content': '端口:{port}', 'kwargs': {'port': '@port'}},
                'attr': {'original': '@port'}
            },
        ]

然后在js插件里,td.innerHTML赋值之后,添加到tr标签里之前,插入下面这段,为td标签设置属性:

                    // 为td添加属性
                    if (configItem.attr){
                        $.each(configItem.attr, function (name, value) {
                            if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                td.setAttribute(name, row[_value]);
                            }else{
                                td.setAttribute(name, value);
                            }
                        })
                    }

                    $(tr).append(td)

这里添加属性的时候,也支持@符号。 把单元格的原始数据保留一份在td的某个属性里,这样做的好处是,如果你支持在表格里做数据修改。当你要保存修改的时候,先通过js代码检查单元格里现在的内容和之前留在td属性里的原始内容是否一致。不一致才提交给后台进行更新,如果一致,那么这个单元格不需要更新。

双@标记

用什么表情都无所谓,但是这里需要一个新的标记,标记一个新的数据显示的方法。 这里解决之前显示 models.Host.host_type_choices 的问题了。后端返回的response.data里开辟一个key(global_dict),用来存放这类数据

            # 获取global_dict
            global_dict = {
                'business_unit': list(models.BusinessUnit.objects.values_list('id', 'name')),
                'host_type': models.Host.host_type_choices,
            }

            response.data = {'table_config': table_config,
                             'data_list': result,
                             'global_dict': global_dict,
                             }

这样的数据格式不但放在内存里的choices可以用,ForeignKey使用 .values_list()方法也能生成一样的数据,所以也能用。这种方法是不跨表的,适合条目比较少的情况。如果表里行数很多的话就不适合了,一方面所有的条目都会传递给客户端,另一方面前端是遍历查找。 这里需要一个新的标记,标记是去global_dict里去查找对应的内容。所以用了两个@。那么table_config现在要这么写:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
                'attr': {'k1': 'v1', 'k2': 'v2'}
            },
            {
                'field': 'business_unit',
                'title': "业务线_不跨表",
                'display': 1,
                'text': {'content': '{n}', 'kwargs': {'n': '@@business_unit'}}
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
                'text': {'content': '{type}', 'kwargs': {'type': '@@host_type'}}
            },
        ]

前端的实现 先处理response.data.global_dict数据的接收。所有的数据都是在AJAX的success方法里在参数arg里,原先已经有2个方法了,这里再增加一个方法,保存global_dict数据:

                    initGlobal(arg.data.global_dict);  // AJAX的success函数里新加这个方法
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)

调用的方法,就是把这个数据暂存到一个在插件内部是全局有效的变量GLOBAL_DICT里,这样做应该是方便在插件内部的其他方法里调用:

    // 用户保存当前作用域内的“全局变量”
    var GLOBAL_DICT = {};

    function initGlobal(globalDict) {
        $.each(globalDict, function (k, v) {
            GLOBAL_DICT[k] = v;
        })
    }

然后来处理@@的解析,在原来的@的解析的if里再增加一个分支:

                        var kwargs = {};
                        // 把configItem.text.kwargs的内容存到上面的kwargs里
                        // 没有@开头的原样放过去,以@开头的做特殊处理
                        $.each(configItem.text.kwargs, function (key, value) {
                            if(value.startsWith('@@')){
                                var global_name = value.substring(2, value.length);
                                // console.log(GLOBAL_DICT[global_name]);
                                $.each(GLOBAL_DICT[global_name], function (index, arr) {
                                    if (arr[0] === row[global_name]){
                                        kwargs[key] = arr[1];
                                        return false;  // 匹配到一个,就退出遍历
                                    }
                                });
                            } else if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                kwargs[key] = row[_value]
                            }else{
                                kwargs[key] = value
                            }
                        });

这里用的是遍历的方式来查找的,所以如果列表太长就不太适合了。放在内存中的choices应该都不会很长。如果是ForeignKey,现在有2个方法可以显示了。这个方法不跨表,但是数据太多就不适合了。

完整的代码:

路由的对应关系,urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('host/', views.HostView.as_view()),
    path('api/host/', views.HostApi.as_view()),
]

表结构,models.py

class UserInfo(models.Model):
    """用户表"""
    name = models.CharField(max_length=32)
    age = models.IntegerField()

class BusinessUnit(models.Model):
    """业务线"""
    name = models.CharField(max_length=32)

class Host(models.Model):
    """主机列表"""
    host_type_choices = ((1, '服务器'),
                         (2, '防火墙'),
                         (3, '路由器'),
                         (4, '交换机'),
                         )
    host_type = models.IntegerField(choices=host_type_choices)
    hostname = models.CharField(max_length=32)
    port = models.IntegerField()
    business_unit = models.ForeignKey(BusinessUnit, models.CASCADE)
    user = models.ForeignKey(UserInfo, models.CASCADE)

处理函数,views.py

class BaseResponse(object):
    def __init__(self):
        self.status = True
        self.message = None
        self.data = None
        self.error = None

class HostView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'host.html')

class HostApi(View):

    def get(self, request, *args, **kwargs):
        response = BaseResponse()  # 先实例化
        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
                'attr': {'k1': 'v1', 'k2': 'v2'}
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
                'text': None,  # 上面不显示,所以这里text有没有都没关系
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
                'text': {'content': '端口:{port}', 'kwargs': {'port': '@port'}},
                'attr': {'original': '@port'}
            },
            {
                'field': 'business_unit__id',
                'title': "业务线ID",
                'display': 0,
            },
            {
                'field': 'business_unit__name',
                'title': "业务线",
                'display': 1,
                'text': {'content': '{n}(id:{id})', 'kwargs': {'n': '@business_unit__name', 'id': '@business_unit__id'}}
            },
            {
                'field': 'business_unit',
                'title': "业务线_不跨表",
                'display': 1,
                'text': {'content': '{n}', 'kwargs': {'n': '@@business_unit'}}
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
                'text': {'content': '{type}', 'kwargs': {'type': '@@host_type'}}
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
                'text': {'content': '<a href="/api/host/{id}">查看详细</a>', 'kwargs': {'id': '@id'}}
            },
        ]
        field_list = []
        for item in table_config:
            if item['field']:
                field_list.append(item['field'])

        # 写一个try,也可以把上面的内容都放进来,
        try:
            result = models.Host.objects.values(*field_list)
            result = list(result)

            # 获取global_dict
            global_dict = {
                'business_unit': list(models.BusinessUnit.objects.values_list('id', 'name')),
                'host_type': models.Host.host_type_choices,
            }

            response.data = {'table_config': table_config,
                             'data_list': result,
                             'global_dict': global_dict,
                             }
        except Exception as e:
            response.status = False
            # response.error = str(e)  # 错误信息,用下面的模块可以看到错误产生的位置
            import traceback
            response.error = traceback.format_exc()  # 返回详细的错误信息,包括哪个文件的哪一行
            print(response.error)
        return JsonResponse(response.__dict__)

前端主页,host.html

<body>
<h1>主机列表</h1>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/show-table.js"></script>
<script>
    $(function () {
        $.show_table('/api/host/');
    });
</script>
</body>

前端插件,show-table.js

(function ($) {

    // 用户保存当前作用域内的“全局变量”
    var GLOBAL_DICT = {};

    var requestURL;
    function init() {
        $.ajax({
            url: requestURL,
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    initGlobal(arg.data.global_dict);
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h1').after(tag);
    }

    function initGlobal(globalDict) {
        $.each(globalDict, function (k, v) {
            GLOBAL_DICT[k] = v;
        })
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    if (!configItem.text){
                        td.innerHTML = row[configItem.field];
                    }else{
                        var kwargs = {};
                        // 把configItem.text.kwargs的内容存到上面的kwargs里
                        // 没有@开头的原样放过去,以@开头的做特殊处理
                        $.each(configItem.text.kwargs, function (key, value) {
                            if(value.startsWith('@@')){
                                var global_name = value.substring(2, value.length);
                                // console.log(GLOBAL_DICT[global_name]);
                                $.each(GLOBAL_DICT[global_name], function (index, arr) {
                                    if (arr[0] === row[global_name]){
                                        kwargs[key] = arr[1];
                                        return false;  // 匹配到一个,就退出遍历
                                    }
                                });
                            } else if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                kwargs[key] = row[_value]
                            }else{
                                kwargs[key] = value
                            }
                        });
                        td.innerHTML = configItem.text.content.format(kwargs);
                    }

                    // 为td添加属性
                    if (configItem.attr){
                        $.each(configItem.attr, function (name, value) {
                            if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                td.setAttribute(name, row[_value]);
                            }else{
                                td.setAttribute(name, value);
                            }
                        })
                    }

                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }

    // 为字符串创建format方法,用于字符串格式化
    String.prototype.format = function (args) {
        return this.replace(/\{(\w+)\}/g, function (substring, args2) {
            return args[args2];
        })
    };

    $.extend({
        'show_table': function (url) {
            requestURL = url;
            init();
        }
    })
})(jQuery);

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • python 对文件的操作

    U :  换行符不同,有的是\n  \n\r \r  ,只能和r一起使用 r+U,转换成 \n。

    用户2398817
  • 使用Python中的config配置

    Python中有ConfigParser类,可以很方便的从配置文件中读取数据(如DB的配置,路径的配置),所以可以自己写一个函数,实现读取config配置。

    用户2398817
  • 基于pycharm的python开发配置

    虽然基于命令行+文本编辑可以完成python程序开发,并熟悉操作系统下的命令操作。但是基于集成开发环境的开发能够更有效率。因此选择一个适合的集成开发工具是重要...

    用户2398817
  • 揭秘微信身份证背后的高科技——人脸识别技术

    在日常生活中,相信很多人都会遇到需要证明“我就是我”的尴尬处境,如果碰巧未带身份证,就只好陷入有苦难言的无奈之中。最近,全国首张微信身份证发行,从此只要带着手机...

    钱塘数据
  • 格灵深瞳:人脸识别最新进展以及工业级大规模人脸识别实践探讨 | 公开课笔记

    用户1737318
  • iOS开发中在swift项目中使用HandyJSON将字典转成模型

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/de...

    用户1451823
  • OpenCV人脸识别之一:数据收集和预处理

    本系列人脸识别文章用的是opencv2,最新版的opencv3.2的代码请参考文章: 《OpenCV之识别自己的脸——C++源码放送》; 《人脸识别源码运行指南...

    用户1332428
  • CCIE知识点总结——安全及高级特性

    版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/...

    魏晓蕾
  • 基于上下文信息分离的无监督运动目标检测(文末附有论文及源码下载)

    【导读】今天我们主要分享基于上下文的技术用于目标检查。深层神经网络被训练用于利用来自除该区域(上下文)以外的任何其他地方的信息来预测区域内的光流,而另一个网络则...

    计算机视觉战队
  • Java基础系列(二十五):接口进阶

    在前面章节的学习中,我们对于接口和抽象类都已经有了一个大概的理解和消化,现在我们将从语法层面和设计层面两个方向来分析它们到底有什么不同:

    Vi的技术博客

扫码关注云+社区

领取腾讯云代金券