entirius-django-utils 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_utils/__init__.py +0 -0
- django_utils/admin/__init__.py +23 -0
- django_utils/admin/base_admin.py +31 -0
- django_utils/api/__init__.py +0 -0
- django_utils/api/base_response.py +47 -0
- django_utils/api/decorators.py +189 -0
- django_utils/api/errors.py +24 -0
- django_utils/api/exceptions.py +132 -0
- django_utils/api/filter.py +20 -0
- django_utils/api/pagination.py +37 -0
- django_utils/api/responses.py +80 -0
- django_utils/api/utils.py +15 -0
- django_utils/api/v2_errors.py +185 -0
- django_utils/apps.py +10 -0
- django_utils/domains/__init__.py +1 -0
- django_utils/domains/domain.py +17 -0
- django_utils/json/__init__.py +0 -0
- django_utils/json/encoder.py +16 -0
- django_utils/managers/__init__.py +0 -0
- django_utils/managers/enhance_manager.py +149 -0
- django_utils/migrations/__init__.py +0 -0
- django_utils/models/__init__.py +0 -0
- django_utils/models/base_model.py +30 -0
- django_utils/settings.py +13 -0
- django_utils/tools/__init__.py +2 -0
- django_utils/tools/t9n.py +103 -0
- django_utils/tools/text.py +33 -0
- django_utils/workers/__init__.py +0 -0
- django_utils/workers/command.py +103 -0
- entirius_django_utils-2.0.0.dist-info/METADATA +65 -0
- entirius_django_utils-2.0.0.dist-info/RECORD +33 -0
- entirius_django_utils-2.0.0.dist-info/WHEEL +4 -0
- entirius_django_utils-2.0.0.dist-info/licenses/LICENSE +373 -0
django_utils/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from django_utils.settings import ADMIN_THEME
|
|
2
|
+
|
|
3
|
+
if ADMIN_THEME == "unfold":
|
|
4
|
+
from django.contrib import admin
|
|
5
|
+
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
|
|
6
|
+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
7
|
+
from django.contrib.auth.models import Group, User
|
|
8
|
+
from unfold.admin import ModelAdmin
|
|
9
|
+
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
|
|
10
|
+
|
|
11
|
+
admin.site.unregister(User)
|
|
12
|
+
admin.site.unregister(Group)
|
|
13
|
+
|
|
14
|
+
@admin.register(User)
|
|
15
|
+
class UserAdmin(BaseUserAdmin, ModelAdmin):
|
|
16
|
+
# Forms loaded from `unfold.forms`
|
|
17
|
+
form = UserChangeForm
|
|
18
|
+
add_form = UserCreationForm
|
|
19
|
+
change_password_form = AdminPasswordChangeForm
|
|
20
|
+
|
|
21
|
+
@admin.register(Group)
|
|
22
|
+
class GroupAdmin(BaseGroupAdmin, ModelAdmin):
|
|
23
|
+
pass
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from django_utils.settings import ADMIN_THEME
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
match ADMIN_THEME:
|
|
12
|
+
case "unfold":
|
|
13
|
+
try:
|
|
14
|
+
from unfold.admin import ModelAdmin, StackedInline, TabularInline
|
|
15
|
+
except ImportError:
|
|
16
|
+
logger.error("Unfold admin theme is not installed, run: pip install django-unfold")
|
|
17
|
+
from django.contrib.admin import ModelAdmin, StackedInline, TabularInline
|
|
18
|
+
case _:
|
|
19
|
+
from django.contrib.admin import ModelAdmin, StackedInline, TabularInline
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseModelAdmin(ModelAdmin):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseStackedInline(StackedInline):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseTabularInline(TabularInline):
|
|
31
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .errors import ErrorInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseResponse:
|
|
10
|
+
errors = None
|
|
11
|
+
|
|
12
|
+
def __init__(self, errors: list[ErrorInfo] = None):
|
|
13
|
+
self.errors = errors
|
|
14
|
+
|
|
15
|
+
if errors is None:
|
|
16
|
+
self.errors = []
|
|
17
|
+
|
|
18
|
+
def add_error(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
code: str,
|
|
22
|
+
affected_values: list[str] | None = None,
|
|
23
|
+
affected_field: str | None = None,
|
|
24
|
+
extra: dict | None = None,
|
|
25
|
+
):
|
|
26
|
+
if code in [error.code for error in self.errors]:
|
|
27
|
+
for error in self.errors:
|
|
28
|
+
if error.code == code:
|
|
29
|
+
if affected_values:
|
|
30
|
+
error.affected_values = list(set(error.affected_values + affected_values))
|
|
31
|
+
break
|
|
32
|
+
else:
|
|
33
|
+
self.errors.append(ErrorInfo(message, code, affected_values, affected_field, extra))
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def add_errors(self, errors: list[ErrorInfo | dict]):
|
|
37
|
+
for error in errors:
|
|
38
|
+
if isinstance(error, ErrorInfo):
|
|
39
|
+
self.add_error(error.message, error.code, error.affected_values, error.affected_field, error.extra)
|
|
40
|
+
elif isinstance(error, dict):
|
|
41
|
+
message = error.get("message", None)
|
|
42
|
+
code = error.get("code", None)
|
|
43
|
+
if isinstance(message, str) and isinstance(code, str):
|
|
44
|
+
self.add_error(message, code, error.get("affected_values", None))
|
|
45
|
+
else:
|
|
46
|
+
continue
|
|
47
|
+
return self
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import logging
|
|
7
|
+
from functools import partial, wraps
|
|
8
|
+
|
|
9
|
+
from django.contrib import auth
|
|
10
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
11
|
+
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
|
12
|
+
from django_regional.models import Country
|
|
13
|
+
from webargs.djangoparser import use_args
|
|
14
|
+
|
|
15
|
+
from django_utils import settings
|
|
16
|
+
from django_utils.api.exceptions import JWTException
|
|
17
|
+
|
|
18
|
+
from .exceptions import MethodNotAllowed, Unauthorized, handle_exception
|
|
19
|
+
from .responses import PaginatedResponse, Response, to_json_response
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def api_view(view):
|
|
25
|
+
"""
|
|
26
|
+
Wraps view function or method on a class based view,
|
|
27
|
+
enforcing standard response formatting and exception handling.
|
|
28
|
+
|
|
29
|
+
Decorated function must return a Response object.
|
|
30
|
+
Response class can be found in helpers/response.py
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@wraps(view)
|
|
34
|
+
def _wrapped(request, *args, **kwargs):
|
|
35
|
+
try:
|
|
36
|
+
response = view(request, *args, **kwargs)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
response = handle_exception(e, request)
|
|
39
|
+
finally:
|
|
40
|
+
if isinstance(response, (Response, PaginatedResponse)):
|
|
41
|
+
return to_json_response(response)
|
|
42
|
+
elif isinstance(response, HttpResponseRedirect):
|
|
43
|
+
return response
|
|
44
|
+
elif isinstance(response, HttpResponse):
|
|
45
|
+
return response
|
|
46
|
+
else:
|
|
47
|
+
JsonResponse({"error": "view returned unsupported response type"})
|
|
48
|
+
|
|
49
|
+
return _wrapped
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def authenticate_optional(view):
|
|
53
|
+
@wraps(view)
|
|
54
|
+
def _wrapped(request, *args, **kwargs):
|
|
55
|
+
try:
|
|
56
|
+
request.user = auth.authenticate(request)
|
|
57
|
+
except JWTException:
|
|
58
|
+
request.user = None
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
response = view(request, *args, **kwargs)
|
|
62
|
+
return response
|
|
63
|
+
|
|
64
|
+
return _wrapped
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def authenticate(view):
|
|
68
|
+
@wraps(view)
|
|
69
|
+
def _wrapped(request, *args, **kwargs):
|
|
70
|
+
request.user = auth.authenticate(request)
|
|
71
|
+
request.customer = request.user.customer if request.user is not None else None
|
|
72
|
+
response = view(request, *args, **kwargs)
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
return _wrapped
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def require_authentication(view):
|
|
79
|
+
@wraps(view)
|
|
80
|
+
def _wrapped(request, *args, **kwargs):
|
|
81
|
+
user = request.user
|
|
82
|
+
passed = user is not None and user.is_authenticated
|
|
83
|
+
if passed:
|
|
84
|
+
result = view(request, *args, **kwargs)
|
|
85
|
+
return result
|
|
86
|
+
else:
|
|
87
|
+
raise Unauthorized
|
|
88
|
+
|
|
89
|
+
return _wrapped
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def require_http_method(*methods):
|
|
93
|
+
def _wrapper(view):
|
|
94
|
+
@wraps(view)
|
|
95
|
+
def _wrapped(request, *args, **kwargs):
|
|
96
|
+
passed = request.method in methods
|
|
97
|
+
if passed:
|
|
98
|
+
result = view(request, *args, **kwargs)
|
|
99
|
+
return result
|
|
100
|
+
else:
|
|
101
|
+
raise MethodNotAllowed
|
|
102
|
+
|
|
103
|
+
return _wrapped
|
|
104
|
+
|
|
105
|
+
return _wrapper
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ip_getter(view_func):
|
|
109
|
+
"""
|
|
110
|
+
Dekorator, który pobiera adres IP z nagłówka i zapisuje go do request.ip.
|
|
111
|
+
Domyślnie sprawdza nagłówek X-Forwarded-For, a następnie X-Real-IP.
|
|
112
|
+
Jeśli nie ma tych nagłówków, używa request.META.get('REMOTE_ADDR').
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
@wraps(view_func)
|
|
116
|
+
def _wrapped_view(request, *args, **kwargs):
|
|
117
|
+
# Próba pobrania IP z różnych nagłówków HTTP
|
|
118
|
+
if (last_session_ip := request.headers.get(settings.HEADER_IP_ADDRESS, None)) is not None:
|
|
119
|
+
try:
|
|
120
|
+
ipaddress.ip_address(last_session_ip)
|
|
121
|
+
request.last_session_ip = last_session_ip
|
|
122
|
+
except ValueError:
|
|
123
|
+
logger.warning(f"IP address {last_session_ip} is not valid")
|
|
124
|
+
|
|
125
|
+
response = view_func(request, *args, **kwargs)
|
|
126
|
+
return response
|
|
127
|
+
|
|
128
|
+
return _wrapped_view
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def save_ip_and_country(view_func):
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
request needs to have headers with keys:
|
|
135
|
+
- settings.HEADER_IP_ADDRESS
|
|
136
|
+
- settings.HEADER_COUNTRY
|
|
137
|
+
|
|
138
|
+
and customer object in request.customer
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@wraps(view_func)
|
|
143
|
+
def _wrapped_view(request, *args, **kwargs):
|
|
144
|
+
response = view_func(request, *args, **kwargs)
|
|
145
|
+
|
|
146
|
+
customer = request.customer
|
|
147
|
+
if not customer:
|
|
148
|
+
return response
|
|
149
|
+
|
|
150
|
+
if (last_session_ip := request.headers.get(settings.HEADER_IP_ADDRESS, None)) is not None:
|
|
151
|
+
try:
|
|
152
|
+
ipaddress.ip_address(last_session_ip)
|
|
153
|
+
customer.last_session_ip = last_session_ip
|
|
154
|
+
except ValueError:
|
|
155
|
+
logger.warning(f"IP address {last_session_ip} for customer {customer.uid} is not valid")
|
|
156
|
+
|
|
157
|
+
if (last_session_country := request.headers.get(settings.HEADER_COUNTRY, None)) is not None:
|
|
158
|
+
try:
|
|
159
|
+
country = Country.objects.get(iso2=last_session_country)
|
|
160
|
+
customer.last_session_country = country
|
|
161
|
+
except ObjectDoesNotExist:
|
|
162
|
+
logger.warning(f"Country {last_session_country} for customer {customer.uid} is not valid")
|
|
163
|
+
try:
|
|
164
|
+
customer.save()
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to save customer {customer.uid} last session data: {e}")
|
|
167
|
+
return response
|
|
168
|
+
|
|
169
|
+
return _wrapped_view
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def verify_captcha(view_func):
|
|
173
|
+
try:
|
|
174
|
+
from django_captcha.output.decorators import verify_recaptcha
|
|
175
|
+
except ImportError:
|
|
176
|
+
return view_func
|
|
177
|
+
|
|
178
|
+
return verify_recaptcha(view_func)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Uses webargs django parser
|
|
182
|
+
# BASICS https://webargs.readthedocs.io/en/latest/quickstart.html#basic-usage
|
|
183
|
+
# MODULE API https://webargs.readthedocs.io/en/latest/api.html#module-webargs.djangoparser
|
|
184
|
+
# HOW TO USE WHAT'S BELOW https://webargs.readthedocs.io/en/latest/advanced.html#mixing-locations
|
|
185
|
+
# HOW TO USE WITH DATACLASSES https://github.com/lovasoa/marshmallow_dataclass
|
|
186
|
+
# TODO Think about autodocs with apispec and apispec-decorator-crawler
|
|
187
|
+
parse_body = partial(use_args, location="json")
|
|
188
|
+
parse_form = partial(use_args, location="form")
|
|
189
|
+
parse_parameters = partial(use_args, location="query")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ErrorInfo:
|
|
10
|
+
message: str
|
|
11
|
+
code: str
|
|
12
|
+
affected_values: list[str] | None = None
|
|
13
|
+
affected_field: str | None = None
|
|
14
|
+
extra: dict | None = None
|
|
15
|
+
|
|
16
|
+
def to_dict(self):
|
|
17
|
+
result = {"message": self.message, "code": self.code}
|
|
18
|
+
if self.affected_values:
|
|
19
|
+
result["affected_values"] = self.affected_values
|
|
20
|
+
if self.affected_field:
|
|
21
|
+
result["affected_field"] = self.affected_field
|
|
22
|
+
if self.extra:
|
|
23
|
+
result["extra"] = self.extra
|
|
24
|
+
return result
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from django.http import HttpRequest
|
|
8
|
+
from marshmallow.exceptions import MarshmallowError
|
|
9
|
+
from marshmallow.exceptions import ValidationError as MarshmallowValidationError
|
|
10
|
+
|
|
11
|
+
from ..settings import LOG_HTTP_CODE_GTE
|
|
12
|
+
from .base_response import BaseResponse
|
|
13
|
+
from .errors import ErrorInfo
|
|
14
|
+
from .responses import Response
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APIException(BaseResponse, Exception):
|
|
20
|
+
data: dict = {}
|
|
21
|
+
message: str = "Error"
|
|
22
|
+
status: str = "ERR"
|
|
23
|
+
status_code: int = 500
|
|
24
|
+
|
|
25
|
+
def __init__(self, data=None, message=None, status=None, errors: list[ErrorInfo] = None) -> None:
|
|
26
|
+
if errors is None:
|
|
27
|
+
errors = []
|
|
28
|
+
super().__init__(errors)
|
|
29
|
+
self.data = self.data if data is None else data
|
|
30
|
+
self.message = self.message if message is None else message
|
|
31
|
+
self.status = self.status if status is None else status
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return f"{self.status}: {self.message}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BadRequest(APIException):
|
|
38
|
+
message = "Invalid request"
|
|
39
|
+
status = "BAD_REQUEST"
|
|
40
|
+
status_code = 400
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EmailTaken(BadRequest):
|
|
44
|
+
message = "Email already in use"
|
|
45
|
+
status = "EMAIL_TAKEN"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ValidationError(BadRequest):
|
|
49
|
+
message = "Validation Error"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Unauthorized(APIException):
|
|
53
|
+
message = "Requires authentication"
|
|
54
|
+
status = "UNAUTHORIZED"
|
|
55
|
+
status_code = 401
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class JWTException(Unauthorized):
|
|
59
|
+
def __init__(self, message, status, code):
|
|
60
|
+
self.message = message
|
|
61
|
+
self.status = status
|
|
62
|
+
self.code = code
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Forbidden(APIException):
|
|
66
|
+
message = "Permission denied"
|
|
67
|
+
status = "FORBIDDEN"
|
|
68
|
+
status_code = 403
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NotFound(APIException):
|
|
72
|
+
message = "Resource not found"
|
|
73
|
+
status = "NOT_FOUND"
|
|
74
|
+
status_code = 404
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MethodNotAllowed(APIException):
|
|
78
|
+
message = "Invalid http method"
|
|
79
|
+
status = "METHOD_NOT_ALLOWED"
|
|
80
|
+
status_code = 405
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_django_exception(e): ...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def handle_marshmallow_exception(e):
|
|
87
|
+
def normalize(e):
|
|
88
|
+
if isinstance(e, MarshmallowValidationError):
|
|
89
|
+
return normalize(e.normalized_messages())
|
|
90
|
+
elif isinstance(e, dict):
|
|
91
|
+
return {key: normalize(elem) for key, elem in e.items()}
|
|
92
|
+
elif isinstance(e, list):
|
|
93
|
+
return [normalize(elem) for elem in e]
|
|
94
|
+
else:
|
|
95
|
+
return e
|
|
96
|
+
|
|
97
|
+
if isinstance(e, MarshmallowValidationError):
|
|
98
|
+
status = "BAD_REQUEST"
|
|
99
|
+
status_code = 400
|
|
100
|
+
message = "Validation error"
|
|
101
|
+
data = normalize(e)
|
|
102
|
+
else:
|
|
103
|
+
status = "ERR"
|
|
104
|
+
status_code = 500
|
|
105
|
+
message = "Unhandeld Validaion Exception"
|
|
106
|
+
data = {}
|
|
107
|
+
|
|
108
|
+
return status, status_code, message, data
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handle_exception(e: Exception, request: HttpRequest):
|
|
112
|
+
if isinstance(e, APIException):
|
|
113
|
+
status = e.status
|
|
114
|
+
status_code = e.status_code
|
|
115
|
+
message = e.message
|
|
116
|
+
data = e.data
|
|
117
|
+
errors = e.errors
|
|
118
|
+
elif isinstance(e, MarshmallowError):
|
|
119
|
+
errors = []
|
|
120
|
+
status, status_code, message, data = handle_marshmallow_exception(e)
|
|
121
|
+
else:
|
|
122
|
+
errors = []
|
|
123
|
+
status = "ERR"
|
|
124
|
+
status_code = 500
|
|
125
|
+
message = "Internal Exception"
|
|
126
|
+
data = {}
|
|
127
|
+
|
|
128
|
+
if status_code >= LOG_HTTP_CODE_GTE:
|
|
129
|
+
# logger.exception is better for sentry than logger.error
|
|
130
|
+
logger.exception(e)
|
|
131
|
+
|
|
132
|
+
return Response(status=status, status_code=status_code, message=message, data=data, errors=errors)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.db.models import QuerySet
|
|
8
|
+
from django_filters.filterset import FilterSetMetaclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def filter_qs(filterset: FilterSetMetaclass, params: dict, queryset: QuerySet, request=None) -> tuple[QuerySet, Any]:
|
|
12
|
+
"""
|
|
13
|
+
Filters queryset according to request params
|
|
14
|
+
|
|
15
|
+
Ex:
|
|
16
|
+
qs = Model.objects.all()
|
|
17
|
+
filtered_qs = filter_queryset(ModelFilter, request.GET, qs)
|
|
18
|
+
"""
|
|
19
|
+
filter_obj = filterset(params, request=request, queryset=queryset)
|
|
20
|
+
return filter_obj.qs, filter_obj
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from django.core.paginator import Paginator
|
|
7
|
+
from django.db.models import QuerySet
|
|
8
|
+
|
|
9
|
+
from .exceptions import BadRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def paginate_qs(params: dict, objs_list: QuerySet) -> tuple[dict, QuerySet]:
|
|
13
|
+
"""
|
|
14
|
+
Paginates list or queryset according to request params
|
|
15
|
+
|
|
16
|
+
Ex:
|
|
17
|
+
qs = Model.objects.all()
|
|
18
|
+
pagination, paginated_qs = paginate(request.GET, qs)
|
|
19
|
+
"""
|
|
20
|
+
page_param = params.get("page", None)
|
|
21
|
+
which_page = int(page_param) if page_param is not None else 1
|
|
22
|
+
limit_param = params.get("limit", None)
|
|
23
|
+
objs_per_page = int(limit_param) if limit_param is not None else 10
|
|
24
|
+
if objs_per_page > 100:
|
|
25
|
+
raise BadRequest(message="Requesting more than 100 records per request is not allowed")
|
|
26
|
+
else:
|
|
27
|
+
paginator = Paginator(objs_list, objs_per_page)
|
|
28
|
+
pages = paginator.num_pages
|
|
29
|
+
records = paginator.count
|
|
30
|
+
pagination_dict = {
|
|
31
|
+
"page": which_page,
|
|
32
|
+
"limit": objs_per_page,
|
|
33
|
+
"pages": pages,
|
|
34
|
+
"records": records,
|
|
35
|
+
}
|
|
36
|
+
page = paginator.page(which_page)
|
|
37
|
+
return pagination_dict, page.object_list
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.http import JsonResponse
|
|
8
|
+
|
|
9
|
+
from .base_response import BaseResponse
|
|
10
|
+
from .errors import ErrorInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Response(BaseResponse):
|
|
14
|
+
language = None
|
|
15
|
+
currency = None
|
|
16
|
+
country = None
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
data: Any,
|
|
21
|
+
status: str = "OK",
|
|
22
|
+
message: str = "",
|
|
23
|
+
status_code: int = 200,
|
|
24
|
+
messages: list[str] | None = None,
|
|
25
|
+
errors: list[ErrorInfo] = None,
|
|
26
|
+
):
|
|
27
|
+
super().__init__(errors)
|
|
28
|
+
self.data = data
|
|
29
|
+
self.status = status
|
|
30
|
+
self.message = message
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
|
|
33
|
+
if messages is None:
|
|
34
|
+
messages = []
|
|
35
|
+
|
|
36
|
+
self.messages = messages
|
|
37
|
+
if message == "" and len(messages) > 0:
|
|
38
|
+
self.message = messages[0]
|
|
39
|
+
|
|
40
|
+
def add_regional(self, language: str | None = None, currency: str | None = None, country: str | None = None):
|
|
41
|
+
self.language = language
|
|
42
|
+
self.currency = currency
|
|
43
|
+
self.country = country
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PaginatedResponse(Response):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
pagination: dict,
|
|
51
|
+
data: list,
|
|
52
|
+
status: str = "OK",
|
|
53
|
+
message: str = "",
|
|
54
|
+
status_code: int = 200,
|
|
55
|
+
messages: list[str] | None = None,
|
|
56
|
+
):
|
|
57
|
+
super().__init__(data, status, message, status_code, messages)
|
|
58
|
+
self.pagination = pagination
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def to_json_response(res: Response):
|
|
62
|
+
base = {
|
|
63
|
+
"meta": {
|
|
64
|
+
"status": res.status,
|
|
65
|
+
"message": res.message,
|
|
66
|
+
"messages": res.messages,
|
|
67
|
+
"errors": [error.to_dict() for error in res.errors],
|
|
68
|
+
"regional": {"language": res.language, "currency": res.currency, "country": res.country},
|
|
69
|
+
},
|
|
70
|
+
"data": res.data,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if isinstance(res, PaginatedResponse):
|
|
74
|
+
body = dict(**base, pagination=res.pagination)
|
|
75
|
+
else:
|
|
76
|
+
body = base
|
|
77
|
+
|
|
78
|
+
response = JsonResponse(body)
|
|
79
|
+
response.status_code = res.status_code
|
|
80
|
+
return response
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from django_utils import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Need to have it here, otherwise django will iterate over a queryset
|
|
10
|
+
def make_media_url(media_path: str) -> str | None:
|
|
11
|
+
media_url = getattr(settings, "MEDIA_URL", "")
|
|
12
|
+
if media_path is not None:
|
|
13
|
+
return "".join([media_url, media_path])
|
|
14
|
+
else:
|
|
15
|
+
return None
|