168 lines
5.0 KiB
Python
168 lines
5.0 KiB
Python
import operator
|
|
from dataclasses import dataclass
|
|
from datetime import date
|
|
from functools import reduce, wraps
|
|
from typing import Any, Callable, Generator, Literal, TypeVar
|
|
from urllib.parse import urlencode
|
|
|
|
from django.db.models import Q
|
|
from django.http import HttpRequest
|
|
from django.shortcuts import redirect
|
|
|
|
|
|
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
|
|
"""
|
|
Divides without triggering division by zero exception.
|
|
Returns 0 if denominator is 0.
|
|
"""
|
|
try:
|
|
return numerator / denominator
|
|
except ZeroDivisionError:
|
|
return 0
|
|
|
|
|
|
def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> object:
|
|
"""
|
|
Safely get the nested attribute from an object.
|
|
|
|
Parameters:
|
|
obj (object): The object from which to retrieve the attribute.
|
|
attr_chain (str): The chain of attributes, separated by dots.
|
|
default: The default value to return if any attribute in the chain does not exist.
|
|
|
|
Returns:
|
|
The value of the nested attribute if it exists, otherwise the default value.
|
|
"""
|
|
attrs = attr_chain.split(".")
|
|
for attr in attrs:
|
|
try:
|
|
obj = getattr(obj, attr)
|
|
except AttributeError:
|
|
return default
|
|
return obj
|
|
|
|
|
|
def truncate_(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
|
|
return (
|
|
(f"{input_string[: length - len(ellipsis)].rstrip()}{ellipsis}")
|
|
if len(input_string) > length
|
|
else input_string
|
|
)
|
|
|
|
|
|
def truncate(
|
|
input_string: str, length: int = 30, ellipsis: str = "…", endpart: str = ""
|
|
) -> str:
|
|
max_content_length = length - len(endpart)
|
|
if max_content_length < 0:
|
|
raise ValueError("Length cannot be shorter than the length of endpart.")
|
|
|
|
if len(input_string) > max_content_length:
|
|
return f"{input_string[: max_content_length - len(ellipsis)].rstrip()}{ellipsis}{endpart}"
|
|
|
|
return (
|
|
f"{input_string}{endpart}"
|
|
if len(input_string) + len(endpart) <= length
|
|
else f"{input_string[: length - len(ellipsis) - len(endpart)].rstrip()}{ellipsis}{endpart}"
|
|
)
|
|
|
|
|
|
T = TypeVar("T", str, int, date)
|
|
|
|
|
|
def generate_split_ranges(
|
|
value_list: list[T], split_points: list[T]
|
|
) -> Generator[tuple[T, T], None, None]:
|
|
for x in range(0, len(split_points) + 1):
|
|
if x == 0:
|
|
start = 0
|
|
elif x >= len(split_points):
|
|
start = value_list.index(split_points[x - 1]) + 1
|
|
else:
|
|
start = value_list.index(split_points[x - 1]) + 1
|
|
try:
|
|
end = value_list.index(split_points[x])
|
|
except IndexError:
|
|
end = len(value_list)
|
|
yield (value_list[start], value_list[end - 1])
|
|
|
|
|
|
def format_float_or_int(number: int | float):
|
|
return int(number) if float(number).is_integer() else f"{number:03.2f}"
|
|
|
|
|
|
OperatorType = Literal["|", "&"]
|
|
|
|
|
|
@dataclass
|
|
class FilterEntry:
|
|
condition: Q
|
|
operator: OperatorType = "&"
|
|
|
|
|
|
def build_dynamic_filter(
|
|
filters: list[FilterEntry | Q], default_operator: OperatorType = "&"
|
|
):
|
|
"""
|
|
Constructs a Django Q filter from a list of filter conditions.
|
|
|
|
Args:
|
|
filters (list): A list where each item is either:
|
|
- A Q object (default AND logic applied)
|
|
- A tuple of (Q object, operator) where operator is "|" (OR) or "&" (AND)
|
|
|
|
Returns:
|
|
Q: A combined Q object that can be passed to Django's filter().
|
|
"""
|
|
op_map: dict[OperatorType, Callable[[Q, Q], Q]] = {
|
|
"|": operator.or_,
|
|
"&": operator.and_,
|
|
}
|
|
|
|
# Convert all plain Q objects into (Q, "&") for default AND behavior
|
|
processed_filters = [
|
|
FilterEntry(f, default_operator) if isinstance(f, Q) else f for f in filters
|
|
]
|
|
|
|
# Reduce with dynamic operators
|
|
return reduce(
|
|
lambda combined_filters, filter: op_map[filter.operator](
|
|
combined_filters, filter.condition
|
|
),
|
|
processed_filters,
|
|
Q(),
|
|
)
|
|
|
|
|
|
def redirect_to(default_view: str, *default_args):
|
|
"""
|
|
A decorator that redirects the user back to the referring page or a default view if no 'next' parameter is provided.
|
|
|
|
:param default_view: The name of the default view to redirect to if 'next' is missing.
|
|
:param default_args: Any arguments required for the default view.
|
|
"""
|
|
|
|
def decorator(view_func):
|
|
@wraps(view_func)
|
|
def wrapped_view(request: HttpRequest, *args, **kwargs):
|
|
next_url = request.GET.get("next")
|
|
if not next_url:
|
|
from django.urls import (
|
|
reverse, # Import inside function to avoid circular imports
|
|
)
|
|
|
|
next_url = reverse(default_view, args=default_args)
|
|
|
|
response = view_func(
|
|
request, *args, **kwargs
|
|
) # Execute the original view logic
|
|
return redirect(next_url)
|
|
|
|
return wrapped_view
|
|
|
|
return decorator
|
|
|
|
|
|
def add_next_param_to_url(url: str, nexturl: str) -> str:
|
|
return f"{url}?{urlencode({'next': nexturl})}"
|