如何在Django Project中实现JsonData AutoUpdating Multi-Select DropDown with Semantic-UI
基于版本:pyton3.6, django2.0, semantic2.2

需求

因为个人更倾向于semantic-ui,牵扯的知识点还是比较多的,包括django的template, template-tag, semantic-ui中的样式,以及部分jquery,所以写下来供初学者参考如何将semantic-ui 与django 结合.

  1. models.py有Cargo 和 InspectionType 2个modle类:
class InspectionType(models.Model):
    name = models.CharField(max_length=16, default=NOT_PROVIDED)


class Cargo(models.Model):
    name = models.CharField(max_length=16, default=NOT_PROVIDED)
    availableinspections = models.ManyToManyField(InspectionType)
  1. 前端因InspectionRecord的需求,Cargo Selection改变时,自动更新itemselections的choices Menu(当create | update POST时,后台同样需要校验上传的cargo与itemselections的匹配性,但这不是本文的重点)
class InspectionRecord(models.Model):
    cargo = models.ForeignKey(Cargo, on_delete=models.CASCADE)
    itemselections = models.ManyToManyField(InspectionType)
    [...]

设计

cargo对应的是select(signal), 直接用ModelForm的默认模板;

itemselections是multi-select, ModelForm默认的模板不太够用,首先一个大方块,个人觉得不美观,再者当数据变更时刷新直接在页面上显示,搞不好稍微延迟的话有碍用户体验.决定采用semantic-UI中的dropdown(multi-select).

因为需要更新的数据是由cargo select的变更触发的,所以绑定到cargo控件的change上最合适不过了;

由selection changed Event 触发一个get请求,由server返回一个jsondata-response,将当前的cargo instance对应的inspectiontype数据返回给页面,然后更新itemselections的menu.

实现

  1. itemselections 的multi-select dropdown

    <div class="ui multiple selection dropdown" id="id_itemselections">
        <input name="insp_types" type="hidden">
        <i class="dropdown icon"></i>
        <div class="default text">select inspectionitems</div>
        <div class="menu">
            <!-->menu item template as 
                <div class="item" data-value="1">example</div>
            <-->
        </div>
    </div>
    
    <script>
        // semantic-ui 控件通常需要声明来实现对jq动作的实现.
        $(function(){
            $('#id_itemselections').dropdown();
        });
    </script>

    这里需要注意的是menu内部的item, 在$(.dropdown).dropdown('setup menu', datavalues)中,datavalues的格式与item属性的对应关系:

    <div class-'item' data-value='1' data-text='foo'>boo</div>

    {'value': '1', 'text': 'foo', 'name': 'boo'}

    在datavalues中value和text被自动匹配为prefixed with a 'data-', 而真正的div-content-text对应name.

  2. cargo select的change事件

    $('#id_cargo').change(function (e) {
        $.getJSON(('/get_json/'+$(this).val()+'data/'), function (json) {
            $('#id_itemselections').dropdown('setup menu', json); // reset menu
            $('#id_itemselections').dropdown('clear', json); // clear selections
        });
    });

    这里重点是对dropdown的官方文档的解读,可能是我理解能力不强,看了好久才明白到底如何使用.

    但在具体说之前先来改一下getJSON()的url参数.

    尽管'/get_json/'+$(this).val()+'data/'没有错,但既然用django,这样使用urlpatterns中的path的确太low了.path是经常会改动的,这会儿这么写,一旦添加其他的json-data-response-url时,很可能就想改成/inspection_types/<int:cgo>/.json了,或者过几天又想改成缩写什么的.一个2个还好办,一旦项目丰富起来,可能自己都高不清楚都在哪些地方使用了这个url,更别提这个url通常被拆成数个js字符串+变量或数值了.

    既然django提供了{% url %},那我们就来好好使用它.

    但django渲染{% url 'path-name' arg %}是在向client返回response之前,也就是说'{% url %}'内部不使用变量参数还好办,一旦有参数是通过页面某个事件提供的,模板渲染无从知道参数的情况,更无法直接使用该参数.我的折中办法是给他个初始值作为站位符,当变量有实在值要向url字符串内传参时就通过replace()方法来替换站位符.

    $.getUrl = function (index) {
        return "{% url 'get-insptypes-json' 0 %}".replace("0", index.toString());
    };

    这里对jquery不慎熟悉的可以顺便看一下jquery据名方法是如何声明的,之后可以通过$.getUrl(val)来使用了.

    我觉得这个办法还成.

    接着来看semantic-ui关于dropdown的开发文档.

    dropdown>usage上半部分主要是examples, 高级用法看下边的 action , behavior部分:

    Specifying Select Action

    Dropdowns have multiple built-in actions that can occur on item selection. You can specify a built-in action by passing its name to settings.action or pass a custom function that performs an action.

    Action Description
    activate (Default) Selects current item, adjusts dropdown value and changes dropdown text
    combo Same as activate, but updates previous elements text instead of self
    [...] [...]

    简单来说就是可以通过如下jquery设定来实现更个性化的效果,比如只更新选项值不更新selection-text等.

    $('#myselections').dropdown(
        {action: 'combo'}
     );

    Behaivor

    All the following behaviors can be called using the syntax:

    $('.your.element')
      .dropdown('behavior name', argumentOne, argumentTwo)
    ;
    Behavior Description
    setup menu(values) Recreates dropdown menu from passed values. values should be an object with the following structure: { values: [ {value, text, name} ] }.
    change values (values) Changes dropdown to use new values
    refresh Refreshes all cached selectors and data
    toggle Toggles current visibility of dropdown
    [...] [...]

    怎么讲?Behavior基本就是更下一级的,具体的每个行为细节吧.比如更新数据源,清除text,显示dropdown,收起之类.

    通过上边的action和behavior可以看出,semantic-ui的效果是由一组顺序执行的behavior组成的action,加上语义化的css样式组合来实现的(可能还有动画细节).如果碰到默认的效果不能满足需求时就去找action,看看是否有给定的选项,如果没有就再向下看有那些behavior可使用,而不是直接上去就event.preventDefault().

    尽最大可能使用官方默认提供的方案和配置,这即有利于后期的管理和维护,也是我们使用semantic-ui的初衷,毕竟我只是想有效的实现某些前端效果,而不是成为一个前端高手,术业有专攻.

  3. sever端get-jsondata-view

    上边我们知道了.dropdown('setup menu', values)中参数格式{ values: [ {value, text, name} ] },

    以及其与menu的item中属性的对应关系,这里我们在view层直接返回固定格式的jsondata-context就好,避免了自己用js再实现一边jsondataparser.

    #urls.py
    from inspectionsapp import views as app_views
    
    urlpatterns = [
       #[...]
        path('get_insptypes_<int:cgo>.json/', app_views.getJsonInspectionTypeChoices, name='get-insptypes-json'),
    ]
    
    # views.py
    from django.urls import reverse_lazy
    from django.http import JsonResponse
    from django.contrib.auth.decorators import login_required
    
    @login_required(login_url=reverse_lazy('user-login'))
    def getJsonInspectionTypeChoices(request, cgo):
        cargo = Cargo.objects.get(pk=cgo)
        if cargo:
            datalist = list()
            for i in cargo.availableinspections.all():
                datalist.append({'value': i.pk, 'name': i.name})
            return JsonResponse({'values': datalist})

    django2.x中引入了path代替之前的rul, 熟悉之后还是非常好用的;

    不熟悉reverse_lazy和login_required可以去看django的开发文档;

    这里我们返回了完整格式的jsondata,在jquery中直接就可以使用.

    1. Unittest
# mysetup是我自己的一个module,提供一些配置好的setup方法,主要就是创建各种model实例.
import json
from django.http import JsonResponse
from django.urls import reverse
from inspectionsapp.tests import mysetup

class GetInspectionTypeChoicesJsonTest(TestCase):
    def setUp(self):
        insptypes = mysetup.create_model_instances(InspectionType, 4)
        cargo = mysetup.create_model_instances(Cargo)
        [cargo.availableinspections.add(i) for i in insptypes[:3]]
        mysetup.create_user()

    def test_cant_get_json_data_without_login(self):
        response = self.client.get(reverse('get-insptypes-json', args=(1,)))
        self.assertRedirects(
            response,
            reverse('user-login')+'?next=' +
            reverse('get-insptypes-json', args=(1,))
        )

    def test_get_json_data_with_loggedin(self):
        self.client.login(username='test_user', password='123123')
        response = self.client.get(reverse('get-insptypes-json', args=(1,)))
        self.assertIsInstance(response, JsonResponse)
        js_data = {'values': [{'value': 1, 'name': 'inspectiontype_1'},
                              {'value': 2, 'name': 'inspectiontype_2'},
                              {'value': 3, 'name': 'inspectiontype_3'}]}
        self.assertEqual(
            json.loads(response.content),
            js_data
        )

这里对比一下django的{% url 'url-name' arg %}reverse('url-name', args=(var,))方法, 二者都是通过path=('get_data_by/<int:pk>/', get_data_view, name='get-data')来生成固定格式的url,初学的时候这两个好容易搞混.特别是arg的name为'pk', 而通过'pk'调用value的过程是在get_data_view(request, pk):...时实现的.

写在最后的话:不是很会写技术blog,单纯是觉得在解决上述问题时在墙内能查到的文章都很有限.虽然都是很基础的东西,但牵扯到了js,jquery,html,django,python,另外还有各种如json,http等方面,估计许多跟我一样的看了些初级静态网页开发视频教程就上路的同学,开始着手实现心中觉得再普通不过的功能和module时,一定会被各种细节困扰.
坚持下来,python就是这个样子,'万能胶'的神效是体现在对需要链接的各个部分都充分了解的情况下才体现出来的.
就在写这篇时才发现自己的笔记里,就躺着2个月前刚上手时被这个功能给绊住的记录,最后仅实现了个基本的getJSONResponse就弄不下去了,结尾给自己的评语是"For the moment, while I'v tried form and MBVs, it's too complicated and inflexible to use. Model is the best!".2个月里补充了django各个部分的知识,还学了TDD,被许多新东西绊住,也得到了更多.
朋友坚持就是胜利!

郭东北
1 声望0 粉丝