白日依山尽,黄河入海流。欲穷千里目,更上一层楼。 -- 唐·王之涣

django regroup对象是外键时无法正常使用

背景

之前写过一个博客项目,在归档页面用Django的标签regroup 实现了按年分组之后在按月分组的功能,本次在开发Todo任务项目的时候,给任务做了分组,想在首页实现分组展示,同时的使用regroup 标签,却无法实现分组。

确认用法是没有问题的。那原因就出现在被分组的对象上

regroup 原理分析

通过查看源码了解到

1、regroup 是定义在 django.template.defaulttags 中函数,其核心是调用 RegroupNode 函数

1
2
3
4
@register.tag
def regroup(parser, token):
... ...
return RegroupNode(target, expression, var_name)

2、RegroupNode 执行 render函数,其核心是调用 python的内部方法 groupbyvar_name 赋值(var_name 就是使用regroup的时候最后的as xxx

3、所以问题的核心是在使用 groupby 函数说

Python groupby函数

1、我们先用一个普通对象来介绍下groupby的用法

1
2
3
4
5
6
7
8
9
10
11
todos_v2 = [
{'group': 'group-1', 'name': 'todo-1-1'},
{'group': 'group-1', 'name': 'todo-1-2'},
{'group': 'group-2', 'name': 'todo-2-1'},
{'group': 'group-2', 'name': 'todo-2-2'},
{'group': 'group-3', 'name': 'todo-3-1'}
]
for k, v in groupby(todos_v2, key=lambda x: x.get('group')):
print(k, v)
for item in v:
print(" ", item)

输出的结果是:

1
2
3
4
5
6
7
8
group-1 <itertools._grouper object at 0x102e21c40>
{'group': 'group-1', 'name': 'todo-1-1'}
{'group': 'group-1', 'name': 'todo-1-2'}
group-2 <itertools._grouper object at 0x102dfe5e0>
{'group': 'group-2', 'name': 'todo-2-1'}
{'group': 'group-2', 'name': 'todo-2-2'}
group-3 <itertools._grouper object at 0x102e21c40>
{'group': 'group-3', 'name': 'todo-3-1'}

看到是有效分组的。

2、groupby 的源码是在python的itertools.py文件中 369行开始。其继承 object 实现了一些私有方法,

核心代码

1
2
3
4
@staticmethod # known case of __new__
def __new__(*args, **kwargs): # real signature unknown
""" Create and return a new object. See help(type) for accurate signature. """
pass

关于 groupby 的使用说明:

1
2
3
4
5
6
itertools.groupby 
@overload
def __new__(cls,
iterable: Iterable[_T1],
key: None = ...) -> groupby[_T1, _T1]
Create and return a new object. See help(type) for accurate signature.
  • 从上面提到的博客项目(按created_time分组, created_time是Post对象的中的一个普通字段)和任务项目(按group分组,group是外键关联字段) 做regroup分组的时候,唯一的区别是分组对象不同。

  • 另外知道在对QuerySet分组排序的时候,key对应的不能是instance ,也从侧面说明 问题出现在 外键字段上。

比如

1
2
3
4
5
6
7
8
9
from todo.models import Todo

todos = Todo.objects.all()
# 如下会报错, obj.group 是 group instance
# TypeError: '<' not supported between instances of 'TodoGroup' and 'TodoGroup'
# todos_sorted = sorted(todos, key=lamba obj: obj.group)
# 如下正确
todos_sorted_1 = sorted(todos, key=lamba obj: obj.group.name)
todos_sorted_2 = sorted(todos, key=lamba obj: obj.status)

3、结合上面提到的RegroupNode 代码中的groupby用法,核心在于key 参数。

问题解决

所以对于特殊的分组 建立在外键字段上的 regroup,可以通过对 queryset 列表 进行在外键字段属性上的排序结果传给 template进行regroup分组

所以最终的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# views.py
def todo_list(request):
param = request.GET.get('status')
if param:
todos = Todo.objects.filter(is_deleted=False).filter(status__exact=int(param))
else:
todos = Todo.objects.filter(is_deleted=False)
todos_sorted = sorted(todos, key=lambda x: x.group.name)
return render(request, 'todo/list.html', {'todos': todos_sorted})

# todo/list.html
... ...
{% regroup todos by group as group_list %}
{% for group in group_list %}
{{ group.grouper }}
{% for todo in group.list %}
{{ todo.name }}
{% endfor %}
{% endfor %}
... ...

最终的TODO 任务首页如下

Todo Index List

项目代码地址
Todo项目V1版本,适用于新手学习,如果觉得不错希望不吝点赞关注哦~


如果觉得文章对你有所帮忙,欢迎点赞收藏,或者可以关注个人公众号 全栈运维 哦,一起学习,一起健身~

作者

Colin

发布于

2022-08-05

许可协议