Python自动化开发学习-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实现不同的方法,常用的是下面几个方法:
在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 adminfrom django.urls import pathfrom app01 import viewsurlpatterns = [ path('admin/', admin.site.urls), path('host/', views.HostView.as_view()),]
写一个处理函数 views.py,这里用CBV,直接返回页面
from django.views import Viewclass HostView(View): def get(self, request, *args, **kwargs): return render(request, 'host.html')
前端的页面先返回一个空的表格,之后再填充表格内容:
<body><h2>主机列表</h2><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>
测试一下,应该只能看到h2标签里的内容。页面初始化之后会弹一个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 = Noneclass 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种特殊的情况:
错误信息的优化
处理函数里加了个try,可以把处理函数的全部过程都写到try里进行捕获。如果捕获到异常,就会返回异常信息给前端。前端已经用arg.status来确认是否有异常返回了,下面会再优化一下前端异常显示的效果。
另外这里用了一个traceback模块,traceback对象中包含出错的行数、位置等数据,貌似也很有用。用例子中的方法就可以拿到了。等下面的小节把前端显示优化之后,可以随便哪句语句添加或者删除个字符搞个语法错误,测试效果。
这里加了一个createTbody()方法,作用是把数据填充到表格里去。另外还有一个showError()方法,作用是如果收到的是后端捕获的异常信息,在标题下面显示出来。下面也是目前前端的完整代码:
<body><h2>主机列表</h2><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'); $('h2').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'); $('h2').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><h2>主机列表</h2><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'); $('h2').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 = Noneclass 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><h2>主机列表</h2><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'); $('h2').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);
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。