-
Notifications
You must be signed in to change notification settings - Fork 5
/
mixins.py
161 lines (135 loc) · 5.58 KB
/
mixins.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import collections
from functools import reduce
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from django.db import models
from django.db.models import Q
from django.http import HttpResponse
from django.urls import NoReverseMatch, reverse
from backoffice_extensions.helpers import create_csv_from_data
from backoffice_extensions.settings import (
PRIMARY_BG_COLOR,
PRIMARY_COLOR,
TITLE,
URL_NAMESPACE,
)
if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse
class BackOfficeViewMixin:
uses_template: bool = True
template_name: Optional[str] = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.template_name and self.uses_template:
raise NotImplementedError("You should specify the template_name attribute.")
def get_extra_context(self) -> Dict:
"""Adds default context to the backoffice views. Overwrite to add more
context to the view.
"""
try:
index_url = reverse(f"{URL_NAMESPACE}:index")
except NoReverseMatch:
index_url = ""
try:
sign_out = reverse(f"{URL_NAMESPACE}:sign-out")
except NoReverseMatch:
sign_out = ""
try:
sign_in = reverse(f"{URL_NAMESPACE}:sign-in")
except NoReverseMatch:
sign_in = ""
return {
"backoffice_title": TITLE,
"backoffice_primary_bg_color": PRIMARY_BG_COLOR,
"backoffice_primary_color": PRIMARY_COLOR,
"index_url": index_url,
"sign_out": sign_out,
"sign_in": sign_in,
}
class SearchListMixin:
"""Mixin to add search functionality to default ListView
Django view."""
search_param: str = "search"
search_fields: List = []
def _search_filter(self, queryset) -> "models.QuerySet":
"""Applies search filtering to queryset."""
search_query = self.request.GET.get(self.search_param) # type: ignore
if search_query:
queryset_filter = [
Q(**{f"{search_field}__icontains": search_query})
for search_field in self.search_fields
]
if queryset_filter:
queryset = queryset.filter(
reduce(lambda x, y: x | y, queryset_filter)
).distinct()
return queryset
def get_queryset(self) -> "models.QuerySet":
"""Checks the 'search' variable to allow generic search."""
queryset = super().get_queryset() # type: ignore
queryset = self._search_filter(queryset)
return queryset
def get_extra_context(self) -> Dict:
"""Adds search data."""
context: Dict = super().get_extra_context() # type: ignore
context[self.search_param] = self.request.GET.get(self.search_param, "") # type: ignore
parameters = self.request.GET.copy() # type: ignore
parameters = parameters.pop(self.page_kwarg, True) and parameters.urlencode() # type: ignore
context["parameters"] = parameters
return context
class CSVExportMixin:
"""Mixin to allow export CSV data."""
filename: str = "data.csv"
queryset: "models.QuerySet" = None
filterset_class = None
fields: List = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.queryset is None:
raise NotImplementedError("You should specify the queryset attribute.")
def get_csv_response(self, data: Dict) -> "HttpResponse":
response = HttpResponse(content_type="text/csv")
response[
"Content-Disposition"
] = f'attachment; filename="{self.get_filename()}"'
create_csv_from_data(data, stream=response)
return response
def get_filename(self) -> str:
return self.filename
def get_queryset(self) -> "models.QuerySet":
return self.queryset
def _default_convert_value(self, value: Any) -> Any:
"""Default value converter."""
if isinstance(value, models.Manager):
value = ", ".join([str(element) for element in value.all()])
elif hasattr(value, "__call__"):
value = value()
return value
def convert_value(self, value: Any) -> Tuple[Any, bool]:
"""Function to handle the value. It should return a tuple where the first
value is the handled value and the second is if the value was handled or not.
This method should be overwrite by children classes.
"""
return value, False
def get(self, request: "HttpRequest", *args, **kwargs) -> "HttpResponse":
items: "models.QuerySet" = self.get_queryset()
if self.filterset_class:
_filter = self.filterset_class(request.GET, queryset=items, request=request)
items = _filter.qs
fields: List = [
field[0] if isinstance(field, tuple) else field for field in self.fields
]
if "id" not in fields:
fields = ["id"] + fields
data: collections.OrderedDict = collections.OrderedDict()
for field in fields:
data[field] = []
for item in items.iterator():
for field in fields:
value = getattr(item, field)
value, converted = self.convert_value(value)
# If not handled, uses the default value converters
if not converted:
value = self._default_convert_value(value)
data[field].append(value)
return self.get_csv_response(data=data)
ExportMixin = CSVExportMixin # Alias for compatibility