From 22c1c17929026a66b5c8a4aedada64717c2e5466 Mon Sep 17 00:00:00 2001 From: Joel Sleppy Date: Wed, 1 Jun 2022 16:26:23 -0400 Subject: [PATCH] Allow annotations in browse views (#81) * Render/sort by annotations in browse views * Update pre-commit hooks --- .pre-commit-config.yaml | 6 ++--- bread/bread.py | 12 ++++++++- tests/test_browse.py | 60 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c4821ae..5466e41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v4.2.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,11 +20,11 @@ repos: - id: black - repo: https://github.com/pycqa/isort - rev: 5.6.4 + rev: 5.10.1 hooks: - id: isort - repo: https://gitlab.com/pycqa/flake8 - rev: master + rev: 3.9.2 hooks: - id: flake8 diff --git a/bread/bread.py b/bread/bread.py index 0d64aa5..ba9451b 100644 --- a/bread/bread.py +++ b/bread/bread.py @@ -188,6 +188,10 @@ def success_url(self): # The individual view classes we'll use and customize in the # omnibus class below: class BrowseView(BreadViewMixin, ListView): + # Include in any colspec to allow Django ORM annotations. + # Will skip init-time validation for that column. + is_annotation = object() + # Configurable: columns = [] filterset = None # Class @@ -211,7 +215,9 @@ def __init__(self, *args, **kwargs): if fieldspec: try: # In Django 3.1.13+, order_by args are validated here - queryset = self.model.objects.order_by(fieldspec) + queryset = ( + super(BrowseView, self).get_queryset().order_by(fieldspec) + ) # Force Django < 3.1.13 to build the query here so it will validate the order_by args str(queryset.query) except FieldError: @@ -556,6 +562,10 @@ def __init__(self): if self.browse_view.columns: for colspec in self.browse_view.columns: + if any(entry == self.browse_view.is_annotation for entry in colspec): + # this column renders an annotation which is not present on the + # model class until querytime. + continue column = colspec[1] validate_fieldspec(self.model, column) diff --git a/tests/test_browse.py b/tests/test_browse.py index 1274630..20a5158 100644 --- a/tests/test_browse.py +++ b/tests/test_browse.py @@ -1,9 +1,11 @@ import json from unittest.mock import patch +from django.db.models.functions import Upper from django.urls import reverse from bread.bread import BrowseView +from tests.models import BreadTestModel from .base import BreadTestCase from .factories import BreadTestModelFactory @@ -317,3 +319,61 @@ def test_not_sorting_on_column(self): self.assertEqual(200, rsp.status_code) rsp.render() self.assertEqual([], json.loads(rsp.context_data["valid_sorting_columns_json"])) + + +class AnnotationsBrowseTest(BreadTestCase): + class BrowseClass(BrowseView): + queryset = BreadTestModel.objects.annotate(loud_name=Upper("name")) + columns = [ + ("Name", "name"), + ("Loud Name", "loud_name", BrowseView.is_annotation, "loud_name"), + ] + + extra_bread_attributes = { + "browse_view": BrowseClass, + } + + def test_rendering_annotation(self): + self.set_urls(self.bread) + items = [BreadTestModelFactory() for __ in range(5)] + self.assertTrue( + any(item for item in items if item.name != item.name.upper()), + "all item names already uppercase, this test will not be meaningful", + ) + self.give_permission("browse") + + url = reverse(self.bread.get_url_name("browse")) + request = self.request_factory.get(url) + request.user = self.user + rsp = self.bread.get_browse_view()(request) + self.assertEqual(200, rsp.status_code) + + rsp.render() + body = rsp.content.decode("utf-8") + for item in items: + self.assertIn(item.name, body) + self.assertIn(item.name.upper(), body) + + def test_sort_by_annotation(self): + self.set_urls(self.bread) + self.give_permission("browse") + d = BreadTestModelFactory(name="denise") + a = BreadTestModelFactory(name="alice") + e = BreadTestModelFactory(name="elise") + b = BreadTestModelFactory(name="bernice") + c = BreadTestModelFactory(name="clarice") + + url = reverse(self.bread.get_url_name("browse")) + "?o=1" + request = self.request_factory.get(url) + request.user = self.user + rsp = self.bread.get_browse_view()(request) + self.assertEqual(200, rsp.status_code) + + rsp.render() + self.assertListEqual( + [0, 1], + json.loads(rsp.context_data["valid_sorting_columns_json"]), + "bread did not consider our annotation column sortable", + ) + results = rsp.context_data["object_list"] + self.assertListEqual([a, b, c, d, e], list(results), "results were not sorted")