Skip to content

Commit 1a4094b

Browse files
committed
get_list_exact_or_404 util function
1 parent ed079a7 commit 1a4094b

File tree

1 file changed

+57
-2
lines changed

1 file changed

+57
-2
lines changed

core/utils.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818

1919
# Image utils
2020
from io import BytesIO
21-
from typing import Any
21+
from typing import Any, Unpack
2222

2323
import PIL
2424
from django.conf import settings
2525
from django.core.files.base import ContentFile
26+
from django.db import models
2627
from django.forms import BaseForm
27-
from django.http import HttpRequest
28+
from django.http import Http404, HttpRequest
29+
from django.shortcuts import get_list_or_404
2830
from django.template.loader import render_to_string
2931
from django.utils.html import SafeString
3032
from django.utils.timezone import localdate
@@ -188,3 +190,56 @@ def get_client_ip(request: HttpRequest) -> str | None:
188190
return ip
189191

190192
return None
193+
194+
195+
Filterable = models.Model | models.QuerySet | models.Manager
196+
ListFilter = dict[str, list | tuple | set]
197+
198+
199+
def get_list_exact_or_404(klass: Filterable, **kwargs: Unpack[ListFilter]) -> list:
200+
"""Use filter() to return a list of objects from a list of unique keys (like ids)
201+
or raises Http404 if the list has not the same length as the given one.
202+
203+
Work like `get_object_or_404()` but for lists of objects, with some caveats :
204+
205+
- The filter must be a list, a tuple or a set.
206+
- There can't be more than exactly one filter.
207+
- There must be no duplicate in the filter.
208+
- The filter should consist in unique keys (like ids), or it could fail randomly.
209+
210+
klass may be a Model, Manager, or QuerySet object. All other passed
211+
arguments and keyword arguments are used in the filter() query.
212+
213+
Raises:
214+
Http404: If the list is empty or doesn't have as many elements as the keys list.
215+
ValueError: If the first argument is not a Model, Manager, or QuerySet object.
216+
ValueError: If more than one filter is passed.
217+
TypeError: If the given filter is not a list, a tuple or a set.
218+
219+
Examples:
220+
Get all the products with ids 1, 2, 3: ::
221+
222+
products = get_list_exact_or_404(Product, id__in=[1, 2, 3])
223+
224+
Don't work with duplicate ids: ::
225+
226+
products = get_list_exact_or_404(Product, id__in=[1, 2, 3, 3])
227+
# Raises Http404: "The list of keys must contain no duplicates."
228+
"""
229+
if len(kwargs) > 1:
230+
raise ValueError("get_list_exact_or_404() only accepts one filter.")
231+
key, list_filter = next(iter(kwargs.items()))
232+
if not isinstance(list_filter, (list, tuple, set)):
233+
raise TypeError(
234+
f"The given filter must be a list, a tuple or a set, not {type(list_filter)}"
235+
)
236+
if len(list_filter) != len(set(list_filter)):
237+
raise ValueError("The list of keys must contain no duplicates.")
238+
kwargs = {key: list_filter}
239+
obj_list = get_list_or_404(klass, **kwargs)
240+
if len(obj_list) != len(list_filter):
241+
raise Http404(
242+
"The given list of keys doesn't match the number of objects found."
243+
f"Expected {len(list_filter)} items, got {len(obj_list)}."
244+
)
245+
return obj_list

0 commit comments

Comments
 (0)