首页 > 解决方案 > 如何将链接模型中的交叉表样式项聚合到其父项?

问题描述

我希望从如下所示的模型结构构建一个交叉表视图。我想要一个矩阵,其中日历日期在 LHS 下方,课程名称在顶部,每个单元格中的内容表示该日期/课程上存在预订实例。

简化模型结构:

class Calendar(models.Model):
    crCalDate = models.DateField(primary_key=True)

class Course(models.Model):
    ceName = models.CharField()

class CourseBooking(models.Model):
    cbCourse = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='booked_course')
    cbDay = models.ForeignKey(Calendar, on_delete=models.CASCADE, related_name='booked_day')

目前我的视图函数的查询集:

class DayView(ListView):

    def get_queryset(self):
        # construct index of course tuples
        course_index = [(cn['id'], 'course' + str(i)) for i, cn in enumerate(Course.objects.order_by('id').values('id'))]
        coursebookings = CourseBooking.objects.filter(cbDay__crCalDate__gte=dt.datetime.today()).values('id', 'cbDay__crCalDate', 'cbCourse__id', 'cbCourse__ceName')
        calendar_dates = list(Calendar.objects.filter(crCalDate__gte=dt.datetime.today()).order_by('crCalDate').values('crCalDate', 'crSunrise', 'crSunset'))
        for cb in coursebookings:
            for i, cal in enumerate(calendar_dates):
                if cal['crCalDate'] == cb['cbDay__crCalDate']:
                    course = [ci[1] for ci in course_index if ci[0] == cb['cbCourse__id']]
                    if course:
                        calendar_dates[i][course[0]] = 'Occupied'
                    break
        return calendar_dates

这一切都感觉相当乏味和昂贵,我想知道我是不是走错了路,没有成功地尝试annotate()aggregate()函数来实现合适的摘要查询。

作为奖励,我真的希望能够显示每个日历日每个课程的预订行数,而不是我目前插入的文本。表示这一点的附加模型如下:

class BookLine(models.Model):
    blTime = models.TimeField(verbose_name='Booking Time')
    blDay = models.ForeignKey(CourseBooking, on_delete = models.CASCADE, db_index=True, related_name='bookinglines')

非常感谢任何有关实现此目标的更好方法的指导,甚至验证此方法(除了我上面的奖金要求)。

标签: pythondjango

解决方案


看起来还不错。您在 for 循环之外有三个数据库查询,因此这不是 N+1 查询问题。我不能说我一眼就能理解接下来发生的事情。

您可以只使用一个查询来获取使用和CourseBooking值注释的对象或值的列表。您还可以按这些值进行过滤。然后,您可以只运行一次遍历所有这些对象或值,设置一个 dict-of-dicts 。ceNamecrCalDatecalendar_dates[date][course]

然后,您必须获取任何课程字典中存在的一组课程,并将其转换为列表以用作顶部标题。然后处理日期,将每个课程字典替换为具有适当表元素值的课程列表。

我认为这段代码可能会更清晰,但如果你拥有的代码正在工作,那么你可能不值得花时间更改它。但是,它可以让您计数,最简单的方法是将内部课程字典制作成 collections.Counter 对象。请注意,对于任何键,计数器的默认值为 0,您永远不会得到 KeyError。

table = {}
for obj in course_bookings:  # annotated with date_anno and course_anno
    date, course = obj.date_anno, obj.course_anno
    if date not in table:
         table[date] = Counter() # or make table a DefaultDict
    table[date][course] += 1

course_headers = set()
for course_dict in table:
   course_headers.update( course_dict.keys() )

course_headers = sorted( list( course_headers))
date_column = sorted( table.keys() )

然后,您将通过迭代date_columncourse_headers以正确的顺序提取计数来构建列表列表,然后再将它们传递给呈现。或者,您可以使用我最近发现的一个技巧:

class DotDict( dict):
    def __getattr__( self, key):
        return self[attr] # or with a default None, self.get( attr, None)

这是一个 dict 在所有方面,除了你不能有用地使用setattr它,因为它getattr与索引它一样......所以 Django 模板引擎可以引用{{dotdict.key}} 而不需要花哨的自定义模板标签。从 Counter 继承也同样有效。


推荐阅读