Django源码解析:preserved_filters的实现

Django的Admin是以Model为驱动的数据管理系统,他会根据你所定义的Model自动为你生成数据的基础管理介面,从而快速的实现数据的CRUD操作,同时其也提供了相当强大的扩展性,使你可以根据需求实现个性化的数据操作介面,真可谓异常的强大。

虽然Django的Admin如此的强大,但也存在许多的不足,好在开发社区也是非常的活跃,通过不断的更新迭代,逐步解决了我们日常开发中所遇到的问题。今天,我们就来聊聊Django Admin引入的preserved_filters的实现。

在Django 1.6以前,我们在使用Admin进行数据管理时,很不爽的一点就是当我们在数据管理列表页面中通过筛选、搜索找到我们所需要的数据集,然后对数据集中的某个数据进行修改、删除等操作后,系统跳转回到数据管理列表页面,此时我们所筛选、搜索的条件全没了,页面又回到了所有数据状态,如果我们还需要操作刚才所筛选的数据集,那么还得按刚才的操作步骤逐一筛选、搜索我们所要的数据集,那是相当的麻烦。为了解决此问题,我们扩展了Django的Admin,通过在页面间传递ChangeList页面的过滤参数,返回ChangeList页面时还原过滤参数,从而解决了以上问题,实现了操作的连贯性,我们给它起名叫keep_list_parms。

后来Django发布了1.6版本,我们惊喜的发现Django 1.6中已经原生实现了以上的需求,但因为我们的系统对Admin进行了较多的扩展,并且部分特性是依赖于keep_list_parms的特性,如果我们要将系统支持提升到1.6版,就需要对我们原有的实现和Django的原生实现进行统一,为此,我们特地研究了Django 1.6的源码,结果发现我们的实现与Django的实现有着惊人的一致,只需通过简单的调整修改,就实现了兼容性的调整工作。

Django的官方文档中并没有对这一特性做特别的描述, 也许是因为这属于系统的特性,而非开发接口和扩展点的原因,对于一般开发人员来说无关紧要,但如果需要进行深入的扩展开发,就有可能会涉及到该特性,就比如我们自己的系统就利用该特性实现了关联数据在同一介面内的统一管理,很好的保证了管理数据工作的连贯性。因此了解特性的实现,对于高层次的开发来说,是非常有必要的,今天,就来给大家剖析一下Django的原生实现。

我们知道Django的Admin是围绕着Model的数据进行管理的,其主要的管理页面我们可以分为两类:一类是所有数据的索引列表页面,通过这个页面,我们可以进行数据的筛选和搜索,从而找到我们需要进行具体操作的数据,我们称之为列表页;另一类就是具体操作的页面了,通过这类页面可对具体的数据进行针对性的操作,我们暂且称之为操作页,如Add、Change、Delete等都属于该类。我们通过列表页找到我们所需要操作的数据,进入相应的操作页面,通过操作页面,对数据进行操作,当操作完成后,通常是会跳转回到列表页,以便于我们可以继续对其它的数据进行查找和操作,看图:

《Django源码解析:preserved_filters的实现》 Admin页面关系图.png

从这个过程中我们可以看出所有的操作都是以列表页为起点和终点的,因此我们可以认为列表页就是单个Model所有管理页面的中心页,维护好该中心页的操作状态,是保证我们操作连贯性的根本。但我们都知道Web是无状态的,因此我们需要制定有一种机制来维护列表页的状态,我们知道列表页的数据过滤和搜索等操作,是通页面的GET参数来传递的,如果能将这些GET参数保存下来,当从操作页返回列表页时,将这些参数还原,那么我们就可以恢复到操作之前的列表页的状态了,那么我们又该如何保存列表页的GET参数呢?我们可以将列表页的所有GET参数编码成一个GET参数值,并把该值当作操作页的一个GET参数,当从操作页返回时,再还原该参数就可以了,好像不是很好理解,看看下面的转换过程就好理解了:

# 1、我们假设当前是用户Model的列表页,我们检索了用户名为lili的且职员状态为True的用户,GET参数如下
?q=lili&is_staff__exact=1

# 2、我们将该列表所有GET参数转换为一个GET参数值,即将“q=lili&is_staff__exact=1”参数进行urlencode操作(urlencode的目的是为转义特殊字符),得到结果
q%3Dlili%26is_staff__exact%3D1

# 3、然后我们将该值做为操作页的一个GET参数,假设该参数名为_changelist_filters,那么该参数的GET表示为
_changelist_filters=q%3Dlili%26is_staff__exact%3D1

# 4、假设操作页URL如下
/app_label/model/add/?name=lili

# 5、则将_changelist_filters参数附上以后操作页的完整URL如下:
/app_label/model/add/?name=lili&_changelist_filters=q%3Dlili%26is_staff__exact%3D1

通地上面的转换过程,我们实现了将列表的过滤参数保存下来的功能,当返回列表页时,我们只需要保存下来的列表过滤参数还原即可,Django给这一特性的命名为preserved_filters,翻译过来就是保存的过滤器,下面我们来看看代码的实现。

# django/contrib/admin/options.py
class ModelAdmin(BaseModelAdmin):
    …
    def get_preserved_filters(self, request):
        """
        返回当前请求页面所对应的列表页的过滤参数,并将其转换为一个新的GET查询参数,该参数名为_changelist_filters。
        """
        match = request.resolver_match  # 这里获取当前请求所匹配的URL相关的信息
        if self.preserve_filters and match:
            opts = self.model._meta
            current_url = '%s:%s' % (match.app_name, match.url_name)
            changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
            if current_url == changelist_url:
                # 如果当前请求的页面就是列表页,则直接参数从GET中获取列表的过滤参数
                preserved_filters = request.GET.urlencode()
            else:
                # 如果当前请求的页面不是列表页,则从GET参数_changelist_filters中获取列表的过滤参数
                preserved_filters = request.GET.get('_changelist_filters')

            if preserved_filters:
                # 将列表过滤参数转换为名为_changelist_filters的单一GET参数
                return urlencode({'_changelist_filters': preserved_filters})
        return ''

以上代码实现了同上文列出的1-3的转换过程的逻辑,通过调用该方法,我们得到了以_changelist_filters为名称保存的列表过滤参数(即上文中的_changelist_filters= q%3Dlili%26is_staff__exact%3D1),我们还需要用该参数与实际页面的URL地址进行合并,以便在跳转到相应的页面后将列表过滤参数保存在该页面的GET参数中,因为URL的很多操作是在Django的模板中进行的,因此Django将其写成了一个自定义标签的方法,代码如下:

# django/contrib/admin/templatetags/admin_urls.py
@register.simple_tag(takes_context=True)
def add_preserved_filters(context, url, popup=False, to_field=None):
"""
向指定的URL地址添加列表过滤参数。
:param context: 此context为试图向模板传递的context对象,要求包含有opts和preserved_filters参数,Admin的相应试图中都包含有这两个值。
"""
    opts = context.get('opts')
    preserved_filters = context.get('preserved_filters')    # 通过context获取列表过滤参数

    parsed_url = list(urlparse(url))
    parsed_qs = dict(parse_qsl(parsed_url[4]))  # 获取url现有的GET查询参数
    merged_qs = dict()

    if opts and preserved_filters:
        preserved_filters = dict(parse_qsl(preserved_filters))  # 将列表过滤参数转换为字典的形式

        match_url = '/%s' % url.partition(get_script_prefix())[2]   # 此句的作用是移除Django项目的URL前缀。
        try:
            match = resolve(match_url)  # 获取URL反向解析的匹配信息
        except Resolver404:
            pass
        else:
            current_url = '%s:%s' % (match.app_name, match.url_name)
            changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
            if changelist_url == current_url and '_changelist_filters' in preserved_filters:
                # 这里是验证当前的url是否就是列表页的url,如果是,则将列表过滤参数还原到url当中
                preserved_filters = dict(parse_qsl(preserved_filters['_changelist_filters']))

        merged_qs.update(preserved_filters)

    if popup:
        from django.contrib.admin.options import IS_POPUP_VAR
        merged_qs[IS_POPUP_VAR] = 1
    if to_field:
        from django.contrib.admin.options import TO_FIELD_VAR
        merged_qs[TO_FIELD_VAR] = to_field

    merged_qs.update(parsed_qs)

    parsed_url[4] = urlencode(merged_qs)
    return urlunparse(parsed_url)

从以上的代码中我们可以看出,add_preserved_filters方法其实做了两个方向的事情,一是将列表过滤参数保存到非列表页的GET参数中(上文列出的4-5的转换过程),二是将列表过滤参数还原到列表页的GET参数中,因此我们在调用该方法来实现列表过滤参数的保存的还原的时候,就无需考虑当前的页面到底是列表页还是操作页了,因为方法已经帮我们自己搞定了。

最后我们来看看Admin的相关试图方法中,都直接或间接的为模板传递了preserved_filters参数,这为模板的调用add_preserved_filters方法提供了支持,那么至此,整个preserved_filters的实现逻辑就已经非常清晰了。

好了,今天的preserved_filters实现原理及源码解析就到这了,希望能够对大家有所帮助,Bye~

    原文作者:Kidwind
    原文地址: https://www.jianshu.com/p/fc95db9ab5d4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞