django-bananas 2.2__py3-none-any.whl → 2.4__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.
- bananas/__init__.py +1 -1
- bananas/admin/api/mixins.py +2 -2
- bananas/admin/api/schemas/base.py +1 -1
- bananas/admin/api/schemas/yasg.py +2 -1
- bananas/admin/api/v1_0/urls.py +2 -2
- bananas/admin/api/versioning.py +2 -3
- bananas/admin/api/views.py +6 -8
- bananas/admin/extension.py +23 -23
- bananas/admin/{api/i18n.py → i18n.py} +0 -1
- bananas/drf/fencing.py +17 -14
- bananas/drf/utils.py +4 -4
- bananas/environment.py +32 -50
- bananas/management/commands/show_urls.py +2 -2
- bananas/management/commands/syncpermissions.py +0 -1
- bananas/models.py +6 -11
- bananas/query.py +13 -10
- bananas/secrets.py +4 -6
- bananas/static/admin/bananas/css/bananas.css +48 -28
- bananas/static/admin/bananas/css/banansive.css +17 -4
- bananas/templates/admin/base_site.html +4 -1
- bananas/url.py +6 -7
- {django_bananas-2.2.dist-info → django_bananas-2.4.dist-info}/METADATA +36 -23
- django_bananas-2.4.dist-info/RECORD +47 -0
- {django_bananas-2.2.dist-info → django_bananas-2.4.dist-info}/WHEEL +1 -1
- {django_bananas-2.2.dist-info → django_bananas-2.4.dist-info/licenses}/LICENSE +1 -1
- django_bananas-2.2.dist-info/RECORD +0 -47
- {django_bananas-2.2.dist-info → django_bananas-2.4.dist-info}/top_level.txt +0 -0
bananas/__init__.py
CHANGED
bananas/admin/api/mixins.py
CHANGED
|
@@ -63,9 +63,9 @@ class BananasAPI:
|
|
|
63
63
|
if admin is not None:
|
|
64
64
|
meta.update(
|
|
65
65
|
{
|
|
66
|
-
key: getattr(admin, key)
|
|
66
|
+
key: getattr(admin, key)
|
|
67
67
|
for key in filter(
|
|
68
|
-
lambda key: key in meta,
|
|
68
|
+
lambda key: key in meta,
|
|
69
69
|
admin.__dict__.keys(),
|
|
70
70
|
)
|
|
71
71
|
}
|
|
@@ -13,7 +13,8 @@ from rest_framework.request import Request
|
|
|
13
13
|
from rest_framework.routers import SimpleRouter
|
|
14
14
|
from rest_framework.schemas.coreapi import is_custom_action
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from bananas.admin.api.versioning import BananasVersioning
|
|
17
|
+
|
|
17
18
|
from .base import BananasBaseRouter
|
|
18
19
|
|
|
19
20
|
|
bananas/admin/api/v1_0/urls.py
CHANGED
bananas/admin/api/versioning.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from types import ModuleType
|
|
2
|
-
from typing import Dict, Sequence
|
|
2
|
+
from typing import ClassVar, Dict, Sequence
|
|
3
3
|
|
|
4
4
|
from rest_framework.request import Request
|
|
5
5
|
from rest_framework.versioning import NamespaceVersioning
|
|
@@ -10,12 +10,11 @@ __versions__ = [v1_0]
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class BananasVersioning(NamespaceVersioning):
|
|
13
|
-
|
|
14
13
|
default_version: str = v1_0.__version__
|
|
15
14
|
allowed_versions: Sequence[str] = tuple(
|
|
16
15
|
version.__version__ for version in __versions__
|
|
17
16
|
)
|
|
18
|
-
version_map: Dict[str, ModuleType] = {
|
|
17
|
+
version_map: ClassVar[Dict[str, ModuleType]] = {
|
|
19
18
|
version.__version__: version for version in __versions__
|
|
20
19
|
}
|
|
21
20
|
|
bananas/admin/api/views.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import ClassVar, List
|
|
2
|
+
|
|
1
3
|
from django.contrib.auth import (
|
|
2
4
|
login as auth_login,
|
|
3
5
|
logout as auth_logout,
|
|
@@ -11,7 +13,8 @@ from rest_framework.permissions import AllowAny
|
|
|
11
13
|
from rest_framework.request import Request
|
|
12
14
|
from rest_framework.response import Response
|
|
13
15
|
|
|
14
|
-
from .i18n import RawTranslationCatalog
|
|
16
|
+
from bananas.admin.i18n import RawTranslationCatalog
|
|
17
|
+
|
|
15
18
|
from .mixins import BananasAPI
|
|
16
19
|
from .permissions import IsAnonymous
|
|
17
20
|
from .schemas import schema
|
|
@@ -29,7 +32,6 @@ class BananasAdminAPI(BananasAPI, viewsets.GenericViewSet):
|
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class LoginAPI(BananasAdminAPI):
|
|
32
|
-
|
|
33
35
|
name = _("Log in") # type: ignore[assignment]
|
|
34
36
|
basename = "login"
|
|
35
37
|
permission_classes = (IsAnonymous,)
|
|
@@ -60,7 +62,6 @@ class LoginAPI(BananasAdminAPI):
|
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
class LogoutAPI(BananasAPI, viewsets.ViewSet):
|
|
63
|
-
|
|
64
65
|
name = _("Log out") # type: ignore[assignment]
|
|
65
66
|
basename = "logout"
|
|
66
67
|
|
|
@@ -77,11 +78,10 @@ class LogoutAPI(BananasAPI, viewsets.ViewSet):
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
class MeAPI(BananasAdminAPI):
|
|
80
|
-
|
|
81
81
|
serializer_class = UserSerializer
|
|
82
82
|
|
|
83
83
|
class Admin:
|
|
84
|
-
exclude_tags = ["navigation"]
|
|
84
|
+
exclude_tags: ClassVar[List[str]] = ["navigation"]
|
|
85
85
|
|
|
86
86
|
@schema(responses={200: UserSerializer})
|
|
87
87
|
def list(self, request: Request) -> Response:
|
|
@@ -93,7 +93,6 @@ class MeAPI(BananasAdminAPI):
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class ChangePasswordAPI(BananasAdminAPI):
|
|
96
|
-
|
|
97
96
|
name = _("Change password") # type: ignore[assignment]
|
|
98
97
|
basename = "change_password"
|
|
99
98
|
serializer_class = PasswordChangeSerializer # Placeholder for schema
|
|
@@ -121,13 +120,12 @@ class ChangePasswordAPI(BananasAdminAPI):
|
|
|
121
120
|
|
|
122
121
|
|
|
123
122
|
class TranslationAPI(BananasAdminAPI):
|
|
124
|
-
|
|
125
123
|
name = _("Translation catalog") # type: ignore[assignment]
|
|
126
124
|
basename = "i18n"
|
|
127
125
|
permission_classes = (AllowAny,)
|
|
128
126
|
|
|
129
127
|
class Admin:
|
|
130
|
-
exclude_tags = ["navigation"]
|
|
128
|
+
exclude_tags: ClassVar[List[str]] = ["navigation"]
|
|
131
129
|
|
|
132
130
|
@schema(responses={200: ""})
|
|
133
131
|
def list(self, request: Request) -> Response:
|
bananas/admin/extension.py
CHANGED
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
from typing import (
|
|
3
3
|
Any,
|
|
4
4
|
Callable,
|
|
5
|
+
ClassVar,
|
|
5
6
|
Dict,
|
|
6
7
|
List,
|
|
7
8
|
Optional,
|
|
@@ -31,7 +32,7 @@ from django.utils.safestring import SafeText, mark_safe
|
|
|
31
32
|
from django.utils.translation import gettext_lazy as _
|
|
32
33
|
from django.views.generic import View
|
|
33
34
|
|
|
34
|
-
from
|
|
35
|
+
from bananas.environment import env
|
|
35
36
|
|
|
36
37
|
__all__ = ["ModelAdminView", "ViewTool", "AdminView", "register", "site"]
|
|
37
38
|
|
|
@@ -41,7 +42,7 @@ MT = TypeVar("MT", bound=Model)
|
|
|
41
42
|
|
|
42
43
|
class ExtendedAdminSite(AdminSite):
|
|
43
44
|
enable_nav_sidebar = False
|
|
44
|
-
default_settings = {
|
|
45
|
+
default_settings: ClassVar[Dict[str, Any]] = {
|
|
45
46
|
"INHERIT_REGISTERED_MODELS": env.get_bool(
|
|
46
47
|
"DJANGO_ADMIN_INHERIT_REGISTERED_MODELS", True
|
|
47
48
|
),
|
|
@@ -84,10 +85,8 @@ class ModelAdminView(ModelAdmin):
|
|
|
84
85
|
@cached_property
|
|
85
86
|
def access_permission(self) -> str:
|
|
86
87
|
meta = self.model._meta
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
codename=meta.permissions[0][0], # First perm codename
|
|
90
|
-
)
|
|
88
|
+
codename = meta.permissions[0][0] # First perm codename
|
|
89
|
+
return f"{meta.app_label}.{codename}"
|
|
91
90
|
|
|
92
91
|
def get_urls(self) -> List[URLPattern]:
|
|
93
92
|
app_label = self.model._meta.app_label
|
|
@@ -122,7 +121,8 @@ class ModelAdminView(ModelAdmin):
|
|
|
122
121
|
|
|
123
122
|
admin_login_url = reverse_lazy("admin:login")
|
|
124
123
|
view = user_passes_test(
|
|
125
|
-
lambda u: u.is_active and hasattr(u, "is_staff") and u.is_staff,
|
|
124
|
+
lambda u: u.is_active and hasattr(u, "is_staff") and u.is_staff,
|
|
125
|
+
login_url=admin_login_url,
|
|
126
126
|
)(view)
|
|
127
127
|
view = permission_required(perm, login_url=admin_login_url)(view)
|
|
128
128
|
return view
|
|
@@ -133,12 +133,12 @@ class ModelAdminView(ModelAdmin):
|
|
|
133
133
|
return perm
|
|
134
134
|
|
|
135
135
|
def has_module_permission(self, request: HttpRequest) -> bool:
|
|
136
|
-
return request.user.has_perm(self.access_permission)
|
|
136
|
+
return bool(request.user.has_perm(self.access_permission))
|
|
137
137
|
|
|
138
138
|
def has_change_permission(
|
|
139
139
|
self, request: HttpRequest, obj: Optional[MT] = None
|
|
140
140
|
) -> bool:
|
|
141
|
-
return request.user.has_perm(self.access_permission)
|
|
141
|
+
return bool(request.user.has_perm(self.access_permission))
|
|
142
142
|
|
|
143
143
|
# TODO: Remove obj?
|
|
144
144
|
def has_add_permission(
|
|
@@ -175,8 +175,7 @@ def register(
|
|
|
175
175
|
*,
|
|
176
176
|
admin_site: Optional[AdminSite] = None,
|
|
177
177
|
admin_class: Type[ModelAdmin] = ModelAdminView,
|
|
178
|
-
) -> Type["AdminView"]:
|
|
179
|
-
...
|
|
178
|
+
) -> Type["AdminView"]: ...
|
|
180
179
|
|
|
181
180
|
|
|
182
181
|
# Call with parenthesis: @register()
|
|
@@ -186,8 +185,7 @@ def register(
|
|
|
186
185
|
*,
|
|
187
186
|
admin_site: Optional[AdminSite] = None,
|
|
188
187
|
admin_class: Type[ModelAdmin] = ModelAdminView,
|
|
189
|
-
) -> Callable[[Type["AdminView"]], Type["AdminView"]]:
|
|
190
|
-
...
|
|
188
|
+
) -> Callable[[Type["AdminView"]], Type["AdminView"]]: ...
|
|
191
189
|
|
|
192
190
|
|
|
193
191
|
def register(
|
|
@@ -236,14 +234,14 @@ def register(
|
|
|
236
234
|
inner_view.verbose_name = verbose_name
|
|
237
235
|
|
|
238
236
|
access_perm_codename = "can_access_" + model_name.lower()
|
|
239
|
-
access_perm_name = _("Can access {verbose_name}").format(
|
|
237
|
+
access_perm_name = str(_("Can access {verbose_name}")).format(
|
|
240
238
|
verbose_name=verbose_name
|
|
241
239
|
)
|
|
242
240
|
# The first permission here is expected to be
|
|
243
241
|
# the general access permission.
|
|
244
|
-
permissions =
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
permissions = (
|
|
243
|
+
(access_perm_codename, access_perm_name),
|
|
244
|
+
*list(getattr(inner_view, "permissions", [])),
|
|
247
245
|
)
|
|
248
246
|
|
|
249
247
|
model = type(
|
|
@@ -303,9 +301,11 @@ class ViewTool:
|
|
|
303
301
|
|
|
304
302
|
|
|
305
303
|
class AdminView(View):
|
|
306
|
-
tools:
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
tools: ClassVar[
|
|
305
|
+
Optional[List[Union[Tuple[str, str], Tuple[str, str, str], ViewTool]]]
|
|
306
|
+
] = None
|
|
307
|
+
action: ClassVar[Optional[str]] = None
|
|
308
|
+
admin: ClassVar[Optional[ModelAdminView]] = None
|
|
309
309
|
|
|
310
310
|
label: str
|
|
311
311
|
verbose_name: str
|
|
@@ -354,8 +354,8 @@ class AdminView(View):
|
|
|
354
354
|
# Mypy doesn't change type on a len(...) call
|
|
355
355
|
# See: https://github.com/python/mypy/issues/1178
|
|
356
356
|
if len(tool) == 3:
|
|
357
|
-
tool, perm =
|
|
358
|
-
text, link =
|
|
357
|
+
tool, perm = tool[:-1], tool[-1]
|
|
358
|
+
text, link = tool
|
|
359
359
|
tool = ViewTool(text, link, perm=perm)
|
|
360
360
|
else:
|
|
361
361
|
# Assume ViewTool
|
|
@@ -385,7 +385,7 @@ class AdminView(View):
|
|
|
385
385
|
|
|
386
386
|
def has_permission(self, perm: str) -> bool:
|
|
387
387
|
perm = self.get_permission(perm)
|
|
388
|
-
return self.request.user.has_perm(perm)
|
|
388
|
+
return bool(self.request.user.has_perm(perm))
|
|
389
389
|
|
|
390
390
|
def has_access(self) -> bool:
|
|
391
391
|
assert self.admin is not None
|
bananas/drf/fencing.py
CHANGED
|
@@ -5,6 +5,7 @@ from functools import wraps
|
|
|
5
5
|
from typing import (
|
|
6
6
|
Any,
|
|
7
7
|
Callable,
|
|
8
|
+
Final,
|
|
8
9
|
FrozenSet,
|
|
9
10
|
Generic,
|
|
10
11
|
List,
|
|
@@ -24,7 +25,7 @@ from rest_framework.request import Request
|
|
|
24
25
|
from rest_framework.response import Response
|
|
25
26
|
from rest_framework.serializers import BaseSerializer, ModelSerializer
|
|
26
27
|
from rest_framework.viewsets import GenericViewSet
|
|
27
|
-
from typing_extensions import
|
|
28
|
+
from typing_extensions import Protocol, final
|
|
28
29
|
|
|
29
30
|
from bananas.admin.api.schemas.yasg import BananasSwaggerSchema
|
|
30
31
|
from bananas.models import TimeStampedModel
|
|
@@ -55,14 +56,18 @@ class Fence(abc.ABC, Generic[InstanceType, TokenType]):
|
|
|
55
56
|
compare: Callable[[TokenType, TokenType], bool],
|
|
56
57
|
get_version: Callable[[InstanceType], Optional[TokenType]],
|
|
57
58
|
openapi_parameter: openapi.Parameter,
|
|
58
|
-
rejection: Exception =
|
|
59
|
-
"The resource does not fulfill the given preconditions"
|
|
60
|
-
),
|
|
59
|
+
rejection: Optional[Exception] = None,
|
|
61
60
|
) -> None:
|
|
62
61
|
self._get_token: Final = get_token
|
|
63
62
|
self._compare: Final = compare
|
|
64
63
|
self._get_version: Final = get_version
|
|
65
|
-
self._rejection: Final =
|
|
64
|
+
self._rejection: Final = (
|
|
65
|
+
rejection
|
|
66
|
+
if rejection is not None
|
|
67
|
+
else errors.PreconditionFailed(
|
|
68
|
+
"The resource does not fulfill the given preconditions"
|
|
69
|
+
)
|
|
70
|
+
)
|
|
66
71
|
self.openapi_parameter: Final = openapi_parameter
|
|
67
72
|
|
|
68
73
|
def check(self, request: Request, instance: InstanceType) -> bool:
|
|
@@ -93,7 +98,7 @@ class FenceAwareSwaggerAutoSchema(BananasSwaggerSchema):
|
|
|
93
98
|
isinstance(self.view, FencedUpdateModelMixin)
|
|
94
99
|
and self.method in self.update_methods
|
|
95
100
|
):
|
|
96
|
-
return parameters
|
|
101
|
+
return [*parameters, self.view.fence.openapi_parameter]
|
|
97
102
|
return parameters
|
|
98
103
|
|
|
99
104
|
|
|
@@ -101,15 +106,13 @@ _MT_co = TypeVar("_MT_co", bound=Model, covariant=True)
|
|
|
101
106
|
|
|
102
107
|
|
|
103
108
|
class UsesQuerySet(Protocol[_MT_co]):
|
|
104
|
-
def get_queryset(self) -> "QuerySet[_MT_co]":
|
|
105
|
-
...
|
|
109
|
+
def get_queryset(self) -> "QuerySet[_MT_co]": ...
|
|
106
110
|
|
|
107
111
|
|
|
108
112
|
class FencedUpdateModelMixin(UpdateModelMixin, abc.ABC):
|
|
109
113
|
@property
|
|
110
114
|
@abc.abstractmethod
|
|
111
|
-
def fence(self) -> Fence:
|
|
112
|
-
...
|
|
115
|
+
def fence(self) -> Fence: ...
|
|
113
116
|
|
|
114
117
|
# django-restframework uses an "advanced self-type" on self in
|
|
115
118
|
# perform_update() which subtly breaks subclassing. We try to remedy this by
|
|
@@ -141,7 +144,7 @@ def header_date_parser(header: str) -> Callable[[Request], datetime.datetime]:
|
|
|
141
144
|
try:
|
|
142
145
|
return parse_header_datetime(request, header)
|
|
143
146
|
except HeaderError as e:
|
|
144
|
-
raise e.as_api_error()
|
|
147
|
+
raise e.as_api_error() from e
|
|
145
148
|
|
|
146
149
|
return parse
|
|
147
150
|
|
|
@@ -183,7 +186,7 @@ def header_etag_parser(header: str) -> Callable[[Request], FrozenSet[str]]:
|
|
|
183
186
|
try:
|
|
184
187
|
return parse_header_etags(request, header)
|
|
185
188
|
except HeaderError as e:
|
|
186
|
-
raise e.as_api_error()
|
|
189
|
+
raise e.as_api_error() from e
|
|
187
190
|
|
|
188
191
|
return parse
|
|
189
192
|
|
|
@@ -192,7 +195,7 @@ T = TypeVar("T")
|
|
|
192
195
|
|
|
193
196
|
|
|
194
197
|
def as_set(
|
|
195
|
-
fn: Callable[[InstanceType], Optional[T]]
|
|
198
|
+
fn: Callable[[InstanceType], Optional[T]],
|
|
196
199
|
) -> Callable[[InstanceType], Optional[FrozenSet[T]]]:
|
|
197
200
|
@wraps(fn)
|
|
198
201
|
def wrapper(instance: InstanceType) -> Optional[FrozenSet[T]]:
|
|
@@ -203,7 +206,7 @@ def as_set(
|
|
|
203
206
|
|
|
204
207
|
|
|
205
208
|
def allow_if_match(
|
|
206
|
-
version_getter: Callable[[InstanceType], Optional[str]]
|
|
209
|
+
version_getter: Callable[[InstanceType], Optional[str]],
|
|
207
210
|
) -> Fence[InstanceType, FrozenSet[str]]:
|
|
208
211
|
return Fence(
|
|
209
212
|
get_token=header_etag_parser("If-Match"),
|
bananas/drf/utils.py
CHANGED
|
@@ -39,8 +39,8 @@ class InvalidHeader(HeaderError):
|
|
|
39
39
|
def parse_header_datetime(request: Request, header: str) -> datetime.datetime:
|
|
40
40
|
try:
|
|
41
41
|
value = request.headers[header]
|
|
42
|
-
except KeyError:
|
|
43
|
-
raise MissingHeader(header)
|
|
42
|
+
except KeyError as exc:
|
|
43
|
+
raise MissingHeader(header) from exc
|
|
44
44
|
try:
|
|
45
45
|
return datetime.datetime.fromtimestamp(
|
|
46
46
|
parse_http_date(value), tz=datetime.timezone.utc
|
|
@@ -60,8 +60,8 @@ def clean_tags(tags: Iterable[str]) -> Iterable[str]:
|
|
|
60
60
|
def parse_header_etags(request: Request, header: str) -> FrozenSet[str]:
|
|
61
61
|
try:
|
|
62
62
|
parts = request.headers[header].split(",")
|
|
63
|
-
except KeyError:
|
|
64
|
-
raise MissingHeader(header)
|
|
63
|
+
except KeyError as exc:
|
|
64
|
+
raise MissingHeader(header) from exc
|
|
65
65
|
tags = frozenset(clean_tags(parts))
|
|
66
66
|
if not tags:
|
|
67
67
|
raise InvalidHeader(header)
|
bananas/environment.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import (
|
|
|
5
5
|
Any,
|
|
6
6
|
Callable,
|
|
7
7
|
Dict,
|
|
8
|
+
Final,
|
|
8
9
|
Generic,
|
|
9
10
|
Iterable,
|
|
10
11
|
List,
|
|
@@ -18,7 +19,7 @@ from typing import (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
from django.conf import global_settings
|
|
21
|
-
from typing_extensions import
|
|
22
|
+
from typing_extensions import Protocol, overload
|
|
22
23
|
|
|
23
24
|
__all__ = ["env", "parse_bool", "parse_int", "parse_tuple", "parse_list", "parse_set"]
|
|
24
25
|
|
|
@@ -26,8 +27,7 @@ __all__ = ["env", "parse_bool", "parse_int", "parse_tuple", "parse_list", "parse
|
|
|
26
27
|
log = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
class Undefined:
|
|
30
|
-
...
|
|
30
|
+
class Undefined: ...
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
UNDEFINED: Final = Undefined()
|
|
@@ -94,12 +94,10 @@ Q = TypeVar("Q", covariant=True)
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
class _Instantiable(Protocol[Q]):
|
|
97
|
-
def __init__(self, value: Iterable[Q]) -> None:
|
|
98
|
-
...
|
|
97
|
+
def __init__(self, value: Iterable[Q]) -> None: ...
|
|
99
98
|
|
|
100
99
|
|
|
101
|
-
class _InstantiableIterable(Iterable[Q], _Instantiable[Q], Generic[Q]):
|
|
102
|
-
...
|
|
100
|
+
class _InstantiableIterable(Iterable[Q], _Instantiable[Q], Generic[Q]): ...
|
|
103
101
|
|
|
104
102
|
|
|
105
103
|
T = TypeVar("T", bound=_InstantiableIterable)
|
|
@@ -127,23 +125,19 @@ P = TypeVar("P", bound=Union[Builtin, tuple, list, set])
|
|
|
127
125
|
|
|
128
126
|
|
|
129
127
|
@overload
|
|
130
|
-
def get_parser(typ: Type[B]) -> Callable[[str], B]:
|
|
131
|
-
...
|
|
128
|
+
def get_parser(typ: Type[B]) -> Callable[[str], B]: ...
|
|
132
129
|
|
|
133
130
|
|
|
134
131
|
@overload
|
|
135
|
-
def get_parser(typ: Type[tuple]) -> Callable[[str], Tuple[str, ...]]:
|
|
136
|
-
...
|
|
132
|
+
def get_parser(typ: Type[tuple]) -> Callable[[str], Tuple[str, ...]]: ...
|
|
137
133
|
|
|
138
134
|
|
|
139
135
|
@overload
|
|
140
|
-
def get_parser(typ: Type[list]) -> Callable[[str], List[str]]:
|
|
141
|
-
...
|
|
136
|
+
def get_parser(typ: Type[list]) -> Callable[[str], List[str]]: ...
|
|
142
137
|
|
|
143
138
|
|
|
144
139
|
@overload
|
|
145
|
-
def get_parser(typ: Type[set]) -> Callable[[str], Set[str]]:
|
|
146
|
-
...
|
|
140
|
+
def get_parser(typ: Type[set]) -> Callable[[str], Set[str]]: ...
|
|
147
141
|
|
|
148
142
|
|
|
149
143
|
def get_parser(typ: Type[P]) -> Callable[[str], P]:
|
|
@@ -165,8 +159,8 @@ def get_parser(typ: Type[P]) -> Callable[[str], P]:
|
|
|
165
159
|
set: parse_set,
|
|
166
160
|
}[typ],
|
|
167
161
|
)
|
|
168
|
-
except KeyError:
|
|
169
|
-
raise NotImplementedError("Unsupported setting type: %r", typ)
|
|
162
|
+
except KeyError as exc:
|
|
163
|
+
raise NotImplementedError("Unsupported setting type: %r", typ) from exc
|
|
170
164
|
|
|
171
165
|
|
|
172
166
|
def get_settings() -> Dict[str, Any]:
|
|
@@ -189,8 +183,8 @@ def get_settings() -> Dict[str, Any]:
|
|
|
189
183
|
if key:
|
|
190
184
|
if key in UNSUPPORTED_ENV_SETTINGS:
|
|
191
185
|
raise ValueError(
|
|
192
|
-
'Django setting "{}" can not be '
|
|
193
|
-
"configured through environment."
|
|
186
|
+
f'Django setting "{key}" can not be '
|
|
187
|
+
"configured through environment."
|
|
194
188
|
)
|
|
195
189
|
|
|
196
190
|
default_value = getattr(global_settings, key, UNDEFINED)
|
|
@@ -201,9 +195,9 @@ def get_settings() -> Dict[str, Any]:
|
|
|
201
195
|
parse = get_parser(SETTINGS_TYPES[key])
|
|
202
196
|
else:
|
|
203
197
|
# Determine parser by django setting type
|
|
204
|
-
parse = get_parser(type(default_value))
|
|
198
|
+
parse = get_parser(type(default_value)) # type: ignore[type-var]
|
|
205
199
|
|
|
206
|
-
value = parse(value)
|
|
200
|
+
value = parse(value) # type: ignore[assignment]
|
|
207
201
|
|
|
208
202
|
settings[key] = value
|
|
209
203
|
|
|
@@ -237,12 +231,12 @@ class EnvironWrapper:
|
|
|
237
231
|
get: Callable[..., str]
|
|
238
232
|
|
|
239
233
|
@overload
|
|
240
|
-
def parse(
|
|
241
|
-
|
|
234
|
+
def parse(
|
|
235
|
+
self, parser: Callable[[str], S], key: str, default: None
|
|
236
|
+
) -> Optional[S]: ...
|
|
242
237
|
|
|
243
238
|
@overload
|
|
244
|
-
def parse(self, parser: Callable[[str], S], key: str, default: S) -> S:
|
|
245
|
-
...
|
|
239
|
+
def parse(self, parser: Callable[[str], S], key: str, default: S) -> S: ...
|
|
246
240
|
|
|
247
241
|
def parse(
|
|
248
242
|
self, parser: Callable[[str], S], key: str, default: Optional[S] = None
|
|
@@ -253,64 +247,52 @@ class EnvironWrapper:
|
|
|
253
247
|
try:
|
|
254
248
|
return parser(value)
|
|
255
249
|
except ValueError:
|
|
256
|
-
log.warning(
|
|
257
|
-
"Unable to parse environment variable {key}={value}".format(
|
|
258
|
-
key=key, value=value
|
|
259
|
-
)
|
|
260
|
-
)
|
|
250
|
+
log.warning(f"Unable to parse environment variable {key}={value}")
|
|
261
251
|
return default
|
|
262
252
|
|
|
263
253
|
@overload
|
|
264
|
-
def get_bool(self, key: str, default: U) -> Union[bool, U]:
|
|
265
|
-
...
|
|
254
|
+
def get_bool(self, key: str, default: U) -> Union[bool, U]: ...
|
|
266
255
|
|
|
267
256
|
@overload
|
|
268
|
-
def get_bool(self, key: str, default: None = None) -> Optional[bool]:
|
|
269
|
-
...
|
|
257
|
+
def get_bool(self, key: str, default: None = None) -> Optional[bool]: ...
|
|
270
258
|
|
|
271
259
|
def get_bool(self, key: str, default: object = None) -> object:
|
|
272
260
|
return self.parse(parse_bool, key, default=default)
|
|
273
261
|
|
|
274
262
|
@overload
|
|
275
|
-
def get_int(self, key: str, default: U) -> Union[int, U]:
|
|
276
|
-
...
|
|
263
|
+
def get_int(self, key: str, default: U) -> Union[int, U]: ...
|
|
277
264
|
|
|
278
265
|
@overload
|
|
279
|
-
def get_int(self, key: str, default: None = None) -> Optional[int]:
|
|
280
|
-
...
|
|
266
|
+
def get_int(self, key: str, default: None = None) -> Optional[int]: ...
|
|
281
267
|
|
|
282
268
|
def get_int(self, key: str, default: object = None) -> object:
|
|
283
269
|
return self.parse(parse_int, key, default=default)
|
|
284
270
|
|
|
285
271
|
@overload
|
|
286
|
-
def get_tuple(self, key: str, default: U) -> Union[Tuple[str, ...], U]:
|
|
287
|
-
...
|
|
272
|
+
def get_tuple(self, key: str, default: U) -> Union[Tuple[str, ...], U]: ...
|
|
288
273
|
|
|
289
274
|
@overload
|
|
290
|
-
def get_tuple(
|
|
291
|
-
|
|
275
|
+
def get_tuple(
|
|
276
|
+
self, key: str, default: None = None
|
|
277
|
+
) -> Optional[Tuple[str, ...]]: ...
|
|
292
278
|
|
|
293
279
|
def get_tuple(self, key: str, default: object = None) -> object:
|
|
294
280
|
return self.parse(parse_tuple, key, default=default)
|
|
295
281
|
|
|
296
282
|
@overload
|
|
297
|
-
def get_list(self, key: str, default: U) -> Union[List[str], U]:
|
|
298
|
-
...
|
|
283
|
+
def get_list(self, key: str, default: U) -> Union[List[str], U]: ...
|
|
299
284
|
|
|
300
285
|
@overload
|
|
301
|
-
def get_list(self, key: str, default: None = None) -> Optional[List[str]]:
|
|
302
|
-
...
|
|
286
|
+
def get_list(self, key: str, default: None = None) -> Optional[List[str]]: ...
|
|
303
287
|
|
|
304
288
|
def get_list(self, key: str, default: object = None) -> object:
|
|
305
289
|
return self.parse(parse_list, key, default=default)
|
|
306
290
|
|
|
307
291
|
@overload
|
|
308
|
-
def get_set(self, key: str, default: U) -> Union[Set[str], U]:
|
|
309
|
-
...
|
|
292
|
+
def get_set(self, key: str, default: U) -> Union[Set[str], U]: ...
|
|
310
293
|
|
|
311
294
|
@overload
|
|
312
|
-
def get_set(self, key: str, default: None = None) -> Optional[Set[str]]:
|
|
313
|
-
...
|
|
295
|
+
def get_set(self, key: str, default: None = None) -> Optional[Set[str]]: ...
|
|
314
296
|
|
|
315
297
|
def get_set(self, key: str, default: object = None) -> object:
|
|
316
298
|
return self.parse(parse_set, key, default=default)
|
|
@@ -19,7 +19,7 @@ def collect_urls(
|
|
|
19
19
|
pattern = urls.pattern.regex.pattern
|
|
20
20
|
for x in urls.url_patterns:
|
|
21
21
|
res += collect_urls(
|
|
22
|
-
x, namespace=urls.namespace or namespace, prefix=prefix
|
|
22
|
+
x, namespace=urls.namespace or namespace, prefix=[*prefix, pattern]
|
|
23
23
|
)
|
|
24
24
|
return res
|
|
25
25
|
elif isinstance(urls, URLPattern):
|
|
@@ -30,7 +30,7 @@ def collect_urls(
|
|
|
30
30
|
[
|
|
31
31
|
("namespace", namespace),
|
|
32
32
|
("name", urls.name),
|
|
33
|
-
("pattern", prefix
|
|
33
|
+
("pattern", [*prefix, pattern]),
|
|
34
34
|
("lookup_str", lookup_str),
|
|
35
35
|
("default_args", dict(urls.default_args or {})),
|
|
36
36
|
]
|
bananas/models.py
CHANGED
|
@@ -4,23 +4,20 @@ import math
|
|
|
4
4
|
import os
|
|
5
5
|
import uuid
|
|
6
6
|
from itertools import chain
|
|
7
|
-
from typing import Any, Dict, Mapping, Optional, Sized
|
|
7
|
+
from typing import Any, ClassVar, Dict, Final, Mapping, Optional, Sized
|
|
8
8
|
|
|
9
9
|
from django.core.exceptions import ValidationError
|
|
10
10
|
from django.db import models
|
|
11
11
|
from django.utils.translation import gettext_lazy as _
|
|
12
|
-
from typing_extensions import Final
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class Missing:
|
|
16
|
-
...
|
|
14
|
+
class Missing: ...
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
MISSING: Final = Missing()
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
class ModelDict(Dict[str, Any]):
|
|
23
|
-
|
|
24
21
|
_nested: Optional[Dict[str, "ModelDict"]] = None
|
|
25
22
|
|
|
26
23
|
def __getattr__(self, item: str) -> Any:
|
|
@@ -120,9 +117,7 @@ class ModelDict(Dict[str, Any]):
|
|
|
120
117
|
)
|
|
121
118
|
else:
|
|
122
119
|
raise AttributeError(
|
|
123
|
-
"{!r} does not have {!r} attribute"
|
|
124
|
-
previous_value, _field
|
|
125
|
-
)
|
|
120
|
+
f"{previous_value!r} does not have {_field!r} attribute"
|
|
126
121
|
)
|
|
127
122
|
|
|
128
123
|
elif value is None:
|
|
@@ -180,9 +175,9 @@ class UUIDModel(models.Model):
|
|
|
180
175
|
class SecretField(models.CharField):
|
|
181
176
|
description = _("Generates and stores a random key.")
|
|
182
177
|
|
|
183
|
-
default_error_messages = {
|
|
184
|
-
"random-is-none": _("%(cls)s.get_random_bytes returned None"),
|
|
185
|
-
"random-too-short": _(
|
|
178
|
+
default_error_messages = { # noqa: RUF012
|
|
179
|
+
"random-is-none": _("%(cls)s.get_random_bytes returned None"),
|
|
180
|
+
"random-too-short": _(
|
|
186
181
|
"Too few random bytes received from "
|
|
187
182
|
"get_random_bytes. Number of"
|
|
188
183
|
" bytes=%(num_bytes)s,"
|
bananas/query.py
CHANGED
|
@@ -38,12 +38,16 @@ class ModelDictIterable:
|
|
|
38
38
|
query = queryset.query
|
|
39
39
|
compiler = query.get_compiler(queryset.db)
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if hasattr(query, "selected") and query.selected:
|
|
42
|
+
names = list(query.selected)
|
|
43
|
+
else:
|
|
44
|
+
extra_names: List[str] = list(query.extra_select)
|
|
45
|
+
field_names: List[str] = list(query.values_select)
|
|
46
|
+
annotation_names: List[str] = list(query.annotation_select)
|
|
47
|
+
|
|
48
|
+
# Modified super(); rename fields given in queryset.values() kwargs
|
|
49
|
+
names = extra_names + field_names + annotation_names
|
|
44
50
|
|
|
45
|
-
# Modified super(); rename fields given in queryset.values() kwargs
|
|
46
|
-
names = extra_names + field_names + annotation_names
|
|
47
51
|
if self.named_fields:
|
|
48
52
|
names = self.rename_fields(names)
|
|
49
53
|
|
|
@@ -62,8 +66,7 @@ _MT_co = TypeVar("_MT_co", bound=Model, covariant=True)
|
|
|
62
66
|
class IsQuerySet(Protocol[_MT_co]):
|
|
63
67
|
def values(
|
|
64
68
|
self, *fields: Union[str, Combinable], **expressions: Any
|
|
65
|
-
) -> "_QuerySet[_MT_co, ModelDict]":
|
|
66
|
-
...
|
|
69
|
+
) -> "_QuerySet[_MT_co, ModelDict]": ...
|
|
67
70
|
|
|
68
71
|
|
|
69
72
|
class ModelDictQuerySetMixin:
|
|
@@ -74,7 +77,7 @@ class ModelDictQuerySetMixin:
|
|
|
74
77
|
fields += tuple(named_fields.values())
|
|
75
78
|
|
|
76
79
|
clone = self.values(*fields)
|
|
77
|
-
clone._iterable_class = ModelDictIterable
|
|
80
|
+
clone._iterable_class = ModelDictIterable
|
|
78
81
|
|
|
79
82
|
# QuerySet._hints is a dict object used by db router
|
|
80
83
|
# to aid deciding which db should get a request. Currently
|
|
@@ -82,7 +85,7 @@ class ModelDictQuerySetMixin:
|
|
|
82
85
|
# fine to set a custom key on this dict as it's a guaranteed
|
|
83
86
|
# way that it'll be returned with the QuerySet instance
|
|
84
87
|
# while leaving the queryset intact
|
|
85
|
-
clone._add_hints(**{"_named_fields": named_fields})
|
|
88
|
+
clone._add_hints(**{"_named_fields": named_fields})
|
|
86
89
|
|
|
87
90
|
return clone
|
|
88
91
|
|
|
@@ -92,7 +95,7 @@ _MT = TypeVar("_MT", bound=Model)
|
|
|
92
95
|
|
|
93
96
|
if TYPE_CHECKING:
|
|
94
97
|
|
|
95
|
-
class ModelDictQuerySet(
|
|
98
|
+
class ModelDictQuerySet(
|
|
96
99
|
ModelDictQuerySetMixin,
|
|
97
100
|
QuerySet[_MT],
|
|
98
101
|
IsQuerySet[_MT],
|
bananas/secrets.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Final, Optional
|
|
3
3
|
|
|
4
|
-
from typing_extensions import
|
|
4
|
+
from typing_extensions import overload
|
|
5
5
|
|
|
6
6
|
from .environment import env
|
|
7
7
|
|
|
@@ -9,13 +9,11 @@ BANANAS_SECRETS_DIR_ENV_KEY: Final = "BANANAS_SECRETS_DIR"
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@overload
|
|
12
|
-
def get_secret(secret_name: str, default: str) -> str:
|
|
13
|
-
...
|
|
12
|
+
def get_secret(secret_name: str, default: str) -> str: ...
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
@overload
|
|
17
|
-
def get_secret(secret_name: str) -> Optional[str]:
|
|
18
|
-
...
|
|
16
|
+
def get_secret(secret_name: str) -> Optional[str]: ...
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
def get_secret(secret_name: str, default: Optional[str] = None) -> Optional[str]:
|
|
@@ -75,6 +75,7 @@ body:not(.login) #header {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
#branding {
|
|
78
|
+
width: 100%;
|
|
78
79
|
float: none;
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -113,6 +114,7 @@ body.login #branding a:active {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
#branding a.logo {
|
|
117
|
+
width: 100%;
|
|
116
118
|
padding-top: 15px;
|
|
117
119
|
line-height: calc(70px - 15px * 2) /* fallback */;
|
|
118
120
|
line-height: var(--topbar-height-inner);
|
|
@@ -144,8 +146,8 @@ body.login #branding a:active {
|
|
|
144
146
|
height: 100%;
|
|
145
147
|
}
|
|
146
148
|
#header .searchable nav {
|
|
147
|
-
height: calc(100% - 45px) /* fallback */;
|
|
148
|
-
height: calc(100% - var(--searchbar-height));
|
|
149
|
+
height: calc(100% - 45px - 15px) /* fallback */;
|
|
150
|
+
height: calc(100% - var(--searchbar-height) - 15px);
|
|
149
151
|
}
|
|
150
152
|
#header nav > ul {
|
|
151
153
|
overflow-y: auto;
|
|
@@ -171,7 +173,7 @@ body.login #branding a:active {
|
|
|
171
173
|
margin-top: 15px;
|
|
172
174
|
}
|
|
173
175
|
#header .filtered-results nav > ul:first-of-type > li:first-of-type {
|
|
174
|
-
margin-top:
|
|
176
|
+
margin-top: 15px;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
#header nav > ul > li > a,
|
|
@@ -381,6 +383,11 @@ body.popup #title .back-arrow {
|
|
|
381
383
|
background-color: transparent;
|
|
382
384
|
}
|
|
383
385
|
|
|
386
|
+
#content h2:first-of-type {
|
|
387
|
+
font-size: 22px;
|
|
388
|
+
margin-top: 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
384
391
|
#content .object-tools {
|
|
385
392
|
position: fixed;
|
|
386
393
|
z-index: 3000;
|
|
@@ -527,6 +534,7 @@ input[type="button"]:disabled,
|
|
|
527
534
|
.timelist a:hover,
|
|
528
535
|
.timelist a:focus,
|
|
529
536
|
.calendar td.selected a {
|
|
537
|
+
color: white;
|
|
530
538
|
background-color: #417690 /* fallback */;
|
|
531
539
|
background-color: var(--theme-color);
|
|
532
540
|
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.25)) /* fallback */;
|
|
@@ -572,6 +580,8 @@ textarea:disabled {
|
|
|
572
580
|
background: none;
|
|
573
581
|
color: #333;
|
|
574
582
|
font-size: 22px;
|
|
583
|
+
border: 0;
|
|
584
|
+
text-transform: unset;
|
|
575
585
|
}
|
|
576
586
|
|
|
577
587
|
.module > h2 a.section:link,
|
|
@@ -604,26 +614,6 @@ a.active.selector-clearall:hover {
|
|
|
604
614
|
/* TODO: selector-icons.svg is hardcoded blue on hover */
|
|
605
615
|
}
|
|
606
616
|
|
|
607
|
-
@supports (background-blend-mode: overlay) {
|
|
608
|
-
.datetimeshortcuts .date-icon,
|
|
609
|
-
.datetimeshortcuts .clock-icon {
|
|
610
|
-
background: none;
|
|
611
|
-
background-color: #447e9b /* fallback */;
|
|
612
|
-
background-color: var(--secondary-color);
|
|
613
|
-
background-blend-mode: overlay;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
.datetimeshortcuts .date-icon {
|
|
617
|
-
-webkit-mask-image: url(../../img/icon-calendar.svg);
|
|
618
|
-
mask-image: url(../../img/icon-calendar.svg);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
.datetimeshortcuts .clock-icon {
|
|
622
|
-
-webkit-mask-image: url(../../img/icon-clock.svg);
|
|
623
|
-
mask-image: url(../../img/icon-clock.svg);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
617
|
.calendar caption,
|
|
628
618
|
.calendarbox h2 {
|
|
629
619
|
color: white;
|
|
@@ -634,12 +624,15 @@ a.active.selector-clearall:hover {
|
|
|
634
624
|
filter: invert(1);
|
|
635
625
|
}
|
|
636
626
|
|
|
637
|
-
.
|
|
638
|
-
|
|
627
|
+
.calendar-cancel a {
|
|
628
|
+
color: var(--button-fg);
|
|
639
629
|
}
|
|
640
630
|
|
|
641
|
-
.
|
|
642
|
-
|
|
631
|
+
.clockbox h2 {
|
|
632
|
+
font-size: 1.1em;
|
|
633
|
+
color: white;
|
|
634
|
+
background: var(--theme-color);
|
|
635
|
+
background-image: var(--bg-lighten-25);
|
|
643
636
|
}
|
|
644
637
|
|
|
645
638
|
#header #changelist-filter,
|
|
@@ -685,6 +678,21 @@ a.active.selector-clearall:hover {
|
|
|
685
678
|
margin: 0;
|
|
686
679
|
}
|
|
687
680
|
|
|
681
|
+
#header #changelist-filter h3 .viewlink {
|
|
682
|
+
background: none;
|
|
683
|
+
}
|
|
684
|
+
#header #changelist-filter h3 .viewlink:hover {
|
|
685
|
+
color: var(--lighten-75);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
#changelist-filter #changelist-filter-extra-actions {
|
|
689
|
+
border-bottom: 0;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#changelist-filter details[open] > summary::before {
|
|
693
|
+
color: var(--lighten-25);
|
|
694
|
+
}
|
|
695
|
+
|
|
688
696
|
#header #object-tools h2 {
|
|
689
697
|
margin: 0;
|
|
690
698
|
}
|
|
@@ -735,6 +743,11 @@ body:not(.popup) #changelist #toolbar {
|
|
|
735
743
|
margin-right: 0;
|
|
736
744
|
}
|
|
737
745
|
|
|
746
|
+
.nav-global #toolbar {
|
|
747
|
+
border: 0;
|
|
748
|
+
margin-bottom: 25px;
|
|
749
|
+
}
|
|
750
|
+
|
|
738
751
|
.nav-global #toolbar form {
|
|
739
752
|
display: block;
|
|
740
753
|
}
|
|
@@ -764,7 +777,9 @@ body:not(.popup) #changelist #toolbar {
|
|
|
764
777
|
background-image: var(--bg-lighten-33);
|
|
765
778
|
color: #fff;
|
|
766
779
|
line-height: 25px;
|
|
767
|
-
height:
|
|
780
|
+
height: 45px;
|
|
781
|
+
padding: 10px 0;
|
|
782
|
+
box-sizing: border-box;
|
|
768
783
|
}
|
|
769
784
|
|
|
770
785
|
#changelist .results ~ .actions {
|
|
@@ -778,6 +793,7 @@ body:not(.popup) #changelist #toolbar {
|
|
|
778
793
|
}
|
|
779
794
|
|
|
780
795
|
#changelist .actions label {
|
|
796
|
+
padding-left: 10px;
|
|
781
797
|
vertical-align: top;
|
|
782
798
|
}
|
|
783
799
|
|
|
@@ -806,3 +822,7 @@ body:not(.popup) #changelist #toolbar {
|
|
|
806
822
|
#changelist .actions span.question {
|
|
807
823
|
margin: 0 0 0 10px;
|
|
808
824
|
}
|
|
825
|
+
|
|
826
|
+
.selector-chosen-title {
|
|
827
|
+
background: var(--theme-color);
|
|
828
|
+
}
|
|
@@ -48,9 +48,8 @@ body:not(.popup) #header #object-tools {
|
|
|
48
48
|
padding: 0;
|
|
49
49
|
border-radius: 0;
|
|
50
50
|
border: 0;
|
|
51
|
-
background:
|
|
52
|
-
background:
|
|
53
|
-
background-image: url(../../img/search.svg), linear-gradient(to bottom, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15));
|
|
51
|
+
background: white;
|
|
52
|
+
background-image: url(../../img/search.svg);
|
|
54
53
|
background-position: center center;
|
|
55
54
|
background-repeat: no-repeat;
|
|
56
55
|
box-shadow: none;
|
|
@@ -58,7 +57,7 @@ body:not(.popup) #header #object-tools {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
#header #changelist-search input[type="submit"]:hover {
|
|
61
|
-
background-image: url(../../img/search.svg), linear-gradient(to bottom, rgba(0, 0, 0, 0.
|
|
60
|
+
background-image: url(../../img/search.svg), linear-gradient(to bottom, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15));
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
#changelist-search > div {
|
|
@@ -123,6 +122,12 @@ body:not(.popup) #header #object-tools {
|
|
|
123
122
|
transform: translateX(var(--sidebar-width));
|
|
124
123
|
}
|
|
125
124
|
|
|
125
|
+
html.is-sidebarOpen #header nav {
|
|
126
|
+
position: unset;
|
|
127
|
+
width: var(--sidebar-width);
|
|
128
|
+
transform: translateY(-15px);
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
body:not(.popup) #content .object-tools {
|
|
127
132
|
display: none;
|
|
128
133
|
}
|
|
@@ -140,6 +145,14 @@ body:not(.popup) #header #object-tools {
|
|
|
140
145
|
margin-right: 0;
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
#changelist > .changelist-form-container {
|
|
149
|
+
max-width: 100vw;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
html.is-sidebarOpen #changelist > .changelist-form-container {
|
|
153
|
+
max-width: calc(100vw - var(--sidebar-width));
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
#changelist .actions {
|
|
144
157
|
padding: 10px;
|
|
145
158
|
left: 0;
|
|
@@ -120,7 +120,10 @@
|
|
|
120
120
|
{% endif %}
|
|
121
121
|
|
|
122
122
|
<li>
|
|
123
|
-
<
|
|
123
|
+
<form id="logout-form" method="post" action="{% url 'admin:logout' %}">
|
|
124
|
+
{% csrf_token %}
|
|
125
|
+
</form>
|
|
126
|
+
<a href="" onclick="document.getElementById('logout-form').submit();">
|
|
124
127
|
{% trans 'Log out' %}
|
|
125
128
|
</a>
|
|
126
129
|
</li>
|
bananas/url.py
CHANGED
|
@@ -27,10 +27,9 @@ Currently supported engines are:
|
|
|
27
27
|
You can add your own by running ``register(scheme, module_name)`` before
|
|
28
28
|
parsing.
|
|
29
29
|
"""
|
|
30
|
-
from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union
|
|
31
|
-
from urllib.parse import parse_qs, unquote_plus, urlsplit
|
|
32
30
|
|
|
33
|
-
from
|
|
31
|
+
from typing import Any, Dict, Final, List, Mapping, NamedTuple, Optional, Tuple, Union
|
|
32
|
+
from urllib.parse import parse_qs, unquote_plus, urlsplit
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
class Alias:
|
|
@@ -97,8 +96,8 @@ def resolve(
|
|
|
97
96
|
), "Multiple levels of aliases are not supported"
|
|
98
97
|
|
|
99
98
|
return result
|
|
100
|
-
except KeyError:
|
|
101
|
-
raise KeyError("No matches for engine %s" % key)
|
|
99
|
+
except KeyError as exc:
|
|
100
|
+
raise KeyError("No matches for engine %s" % key) from exc
|
|
102
101
|
|
|
103
102
|
|
|
104
103
|
def get_engine(scheme: str) -> str:
|
|
@@ -128,12 +127,12 @@ def get_engine(scheme: str) -> str:
|
|
|
128
127
|
|
|
129
128
|
try:
|
|
130
129
|
engine, extra = engine
|
|
131
|
-
except ValueError:
|
|
130
|
+
except ValueError as exc:
|
|
132
131
|
# engine was not a list of length 2
|
|
133
132
|
raise ValueError(
|
|
134
133
|
"django-bananas.url' engine "
|
|
135
134
|
"configuration is invalid: %r" % ENGINE_MAPPING
|
|
136
|
-
)
|
|
135
|
+
) from exc
|
|
137
136
|
|
|
138
137
|
assert isinstance(
|
|
139
138
|
extra, Mapping
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: django-bananas
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4
|
|
4
4
|
Summary: Django Bananas - Django extensions the monkey way
|
|
5
5
|
Home-page: https://github.com/5monkeys/django-bananas
|
|
6
6
|
License: MIT License
|
|
7
7
|
Classifier: Development Status :: 5 - Production/Stable
|
|
8
8
|
Classifier: Programming Language :: Python
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.8
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 3.2
|
|
18
|
+
Classifier: Framework :: Django :: 4.0
|
|
19
|
+
Classifier: Framework :: Django :: 4.1
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Framework :: Django :: 5.1
|
|
23
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
24
|
Classifier: Intended Audience :: Developers
|
|
16
25
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
26
|
Classifier: Operating System :: OS Independent
|
|
@@ -19,21 +28,24 @@ Classifier: Framework :: Django
|
|
|
19
28
|
Requires-Python: >=3.6
|
|
20
29
|
Description-Content-Type: text/x-rst; charset=UTF-8
|
|
21
30
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: Django
|
|
23
|
-
Requires-Dist: typing-extensions
|
|
24
|
-
Requires-Dist: drf-yasg (>=1.20.0)
|
|
25
|
-
Provides-Extra: dev
|
|
26
|
-
Requires-Dist: mypy ; extra == 'dev'
|
|
27
|
-
Requires-Dist: django-stubs ; extra == 'dev'
|
|
28
|
-
Requires-Dist: djangorestframework-stubs ; extra == 'dev'
|
|
29
|
-
Requires-Dist: pytest-mypy-plugins ; extra == 'dev'
|
|
30
|
-
Requires-Dist: pre-commit ; extra == 'dev'
|
|
31
|
+
Requires-Dist: Django>=2.2
|
|
32
|
+
Requires-Dist: typing-extensions>=3.7.4.3
|
|
31
33
|
Provides-Extra: drf
|
|
32
|
-
Requires-Dist: djangorestframework
|
|
33
|
-
Requires-Dist: drf-yasg
|
|
34
|
+
Requires-Dist: djangorestframework>=3.10; extra == "drf"
|
|
35
|
+
Requires-Dist: drf-yasg>=1.20.0; extra == "drf"
|
|
34
36
|
Provides-Extra: test
|
|
35
|
-
Requires-Dist: tox
|
|
36
|
-
Requires-Dist: coverage
|
|
37
|
+
Requires-Dist: tox; extra == "test"
|
|
38
|
+
Requires-Dist: coverage[toml]; extra == "test"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: mypy; extra == "dev"
|
|
41
|
+
Requires-Dist: types-setuptools; extra == "dev"
|
|
42
|
+
Requires-Dist: django-stubs; extra == "dev"
|
|
43
|
+
Requires-Dist: djangorestframework-stubs; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-django; extra == "dev"
|
|
47
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
48
|
+
Dynamic: license-file
|
|
37
49
|
|
|
38
50
|
================================================================================
|
|
39
51
|
:banana: Django Bananas - Django extensions the monkey way
|
|
@@ -69,10 +81,13 @@ with the ``drf`` extra to keep those in sync:
|
|
|
69
81
|
|
|
70
82
|
Currently tested only for
|
|
71
83
|
|
|
72
|
-
- Django 3.2 under Python 3.
|
|
84
|
+
- Django 3.2 under Python 3.8-3.10
|
|
73
85
|
- Django 4.0 under Python 3.8-3.10
|
|
74
|
-
- Django 4.1 under Python 3.8-3.
|
|
75
|
-
- Django 4.2 under Python 3.8-3.
|
|
86
|
+
- Django 4.1 under Python 3.8-3.13
|
|
87
|
+
- Django 4.2 under Python 3.8-3.13
|
|
88
|
+
- Django 5.0 under Python 3.10-3.13
|
|
89
|
+
- Django 5.1 under Python 3.10-3.13
|
|
90
|
+
- Django 5.2 under Python 3.10-3.13
|
|
76
91
|
|
|
77
92
|
Pull requests welcome!
|
|
78
93
|
|
|
@@ -275,7 +290,6 @@ feature requires installation with the ``drf`` extra.
|
|
|
275
290
|
|
|
276
291
|
|
|
277
292
|
class CustomAdminAPI(BananasAdminAPI):
|
|
278
|
-
|
|
279
293
|
name = lazy_title(_("custom"))
|
|
280
294
|
|
|
281
295
|
@schema(query_serializer=SomeSerializer, responses={200: SomeSerializer})
|
|
@@ -284,7 +298,6 @@ feature requires installation with the ``drf`` extra.
|
|
|
284
298
|
|
|
285
299
|
|
|
286
300
|
class SomeModelAdminAPI(BananasAPI, viewsets.ModelViewSet):
|
|
287
|
-
|
|
288
301
|
serializer_class = SomeModelSerializer
|
|
289
302
|
|
|
290
303
|
def list(self, request):
|
|
@@ -593,4 +606,4 @@ and select specific tests with the ``test`` argument to ``make test``:
|
|
|
593
606
|
|
|
594
607
|
.. code-block:: bash
|
|
595
608
|
|
|
596
|
-
make test test='
|
|
609
|
+
make test test='-k test_logout'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
bananas/__init__.py,sha256=HBTRN6OCuRJ35rOq9Scw0Q_tH_VrxXXcWvRjUDjLtpc,686
|
|
2
|
+
bananas/environment.py,sha256=1UwUNjSbnLlWh0XN41_AdEc0TVXQbbswkM34KNPzNwk,7744
|
|
3
|
+
bananas/lazy.py,sha256=XFaqACJ6K5s_MZcxK_5OLQU35F7c7jB3KsdAtHuYQ6k,140
|
|
4
|
+
bananas/models.py,sha256=upapLi3Zvk073G4oMXCobh7RRXDhBLQ0kjPTWWAeYCM,8797
|
|
5
|
+
bananas/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
bananas/query.py,sha256=vDvbY3_KSri_bUZflmUDb2zo46kuZJpuWVBL6s2lX4Y,3615
|
|
7
|
+
bananas/secrets.py,sha256=WDBZapuPzDGHUwO1J_UjFuoljFkgbLKYy0uFTIl53Fk,1021
|
|
8
|
+
bananas/settings.py,sha256=HGZBxm5BMwHJc9PpQpvkzAuasXvePa-q_4tgeup9DPA,139
|
|
9
|
+
bananas/url.py,sha256=63vR_bK0Py9X-8dA7seEPVrQaU8FRSIOLwUb8XIUDAk,7907
|
|
10
|
+
bananas/admin/__init__.py,sha256=0Dzu51GjYjbpnNf5ByaKTx5xiYZ0vxfhW1EssFuiQ90,173
|
|
11
|
+
bananas/admin/extension.py,sha256=pfAjvKrGpnHZoX5YQSXM9-6GojFvLtUkwgN9OhHIekI,13632
|
|
12
|
+
bananas/admin/i18n.py,sha256=WxK_LKnPjoFgoNiNuZdr5wk3LoMXdu7acaIsZNsOxc0,301
|
|
13
|
+
bananas/admin/api/__init__.py,sha256=86dXxHQKnQk-2ZOT2znTBasX-k7nPKlxslwYVZlzu4g,54
|
|
14
|
+
bananas/admin/api/mixins.py,sha256=NroiouccAD6MORoFdS-I3Xnjsmm69_JhdQRT5Roe240,5287
|
|
15
|
+
bananas/admin/api/permissions.py,sha256=sIIHfuJERYvdSJQ8YwNV4W3tj1DPlICi1snAe5RGq2g,375
|
|
16
|
+
bananas/admin/api/router.py,sha256=VWn6O97sbsoPZtKPLPxPDn_raO7Gy8B62N1s7-mGHhU,570
|
|
17
|
+
bananas/admin/api/serializers.py,sha256=XziZ1-6h4hn1qgts3qB8W18ctpl6h8vP3dkqa7WxLe8,2964
|
|
18
|
+
bananas/admin/api/urls.py,sha256=fch3C5BhI6d12U9un8I28fFiHZWTWFH5XAhAzXPURxY,398
|
|
19
|
+
bananas/admin/api/versioning.py,sha256=ciyelTzGZtJDPnigRZAJ0Gk1ZHfGxM9FXctS-OoyJ4Y,901
|
|
20
|
+
bananas/admin/api/views.py,sha256=at_iF0Q860srlsyqjuQq9D274PItTSrXp0uURgegqrE,4169
|
|
21
|
+
bananas/admin/api/schemas/__init__.py,sha256=n4vZ3Sd1H23krxIIUWvktHimmdfh1n0K-J4z2-QsZxI,336
|
|
22
|
+
bananas/admin/api/schemas/base.py,sha256=XFfSQE_VeUraB_LIge1xEtVYOaTGfx6aDq6eahd6D2o,449
|
|
23
|
+
bananas/admin/api/schemas/decorators.py,sha256=Zwp7AwV_6Y0iWVcsO8VjNHHttYMcdGnrfr7ElwEzJzE,492
|
|
24
|
+
bananas/admin/api/schemas/yasg.py,sha256=FpdsUSeuOuH_Rgm_C0H4Q3Tb6rG3McvJ7B43R1ALfbw,6036
|
|
25
|
+
bananas/admin/api/v1_0/__init__.py,sha256=1M909E2Ei8-GI0Xox9t5NfO3xpPG2F13znjlx_jXGZc,59
|
|
26
|
+
bananas/admin/api/v1_0/urls.py,sha256=8N-fFqycB-SnzTMQQk-TTM1b7GuRGXlPtnRFyv6J2rk,695
|
|
27
|
+
bananas/drf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
bananas/drf/errors.py,sha256=JDZYblAio27rh-nAOtC_4CoVw_lFYIdmmd4bTrgxaQc,427
|
|
29
|
+
bananas/drf/fencing.py,sha256=XyCNiPqx0eKMk1emjp7kvyvj7P9JvDArIWgKeQrCRD4,7264
|
|
30
|
+
bananas/drf/utils.py,sha256=Cg7oBNNPuFwIHwHldSk3za87oesK6sivCbiRWP4zj0s,1824
|
|
31
|
+
bananas/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
bananas/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
bananas/management/commands/show_urls.py,sha256=X6ipP04XN1pdrT2Ne5ozbMx068SQ8Io-HpO6Xi7v7d0,2220
|
|
34
|
+
bananas/management/commands/syncpermissions.py,sha256=fmsJpikAPAwvK8Yfkj4A53fdUOz-y0rHEF7xsRiy8W0,1565
|
|
35
|
+
bananas/static/admin/bananas/css/bananas.css,sha256=h7IuijjLYePQ5vwMWNP_Piyxn7IDBMWBM5Of0n6knNE,17882
|
|
36
|
+
bananas/static/admin/bananas/css/banansive.css,sha256=P2EuItR7n0l_hn3X0mIMKhj4cQc4hnjg4PvQjyknLBg,3984
|
|
37
|
+
bananas/static/admin/bananas/img/django.svg,sha256=4S1Cjntojkcoj17oTNufVtnlflhoKsRuQ9O7hTqyx6Q,5291
|
|
38
|
+
bananas/static/admin/bananas/img/search.svg,sha256=59UwyGYVJ4EJ26d5AAIdjtCHR3CnO94in6eQIbkYTu8,458
|
|
39
|
+
bananas/static/admin/bananas/js/bananas.js,sha256=1Xv6JEiTZYeaJSTxkKyMLwIRJ40SnRDPi2N-prpWoU0,4596
|
|
40
|
+
bananas/static/admin/css/responsive.css,sha256=UAMABM7h5rRZ8FocJw4bfx36Q5XtIDm4ddXN8lMC4SQ,17894
|
|
41
|
+
bananas/templates/admin/base_site.html,sha256=RU67gxvx1ozx3-uCyXWf2jC9H6c4adm0BjwmKF8EHEo,7977
|
|
42
|
+
bananas/templates/admin/view.html,sha256=WezwD9qGPch5S29intLVzbY1zVRop4zdIdm4pf7usVw,114
|
|
43
|
+
django_bananas-2.4.dist-info/licenses/LICENSE,sha256=ezKzVU8KUSkiuP6I5I1PEfOUn33aerXQ_fU8_4Qavos,1081
|
|
44
|
+
django_bananas-2.4.dist-info/METADATA,sha256=13IP-MgvJRUUqgKRwtv6Lag1f5vhsq4bnzhfCPj2UCM,18680
|
|
45
|
+
django_bananas-2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
46
|
+
django_bananas-2.4.dist-info/top_level.txt,sha256=5JKVcC99qGcHeOm5WigbcrQbMcsWr6N2A0tF9ZoY6j8,8
|
|
47
|
+
django_bananas-2.4.dist-info/RECORD,,
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
bananas/__init__.py,sha256=WG_GvpIljTnCx45le_2Xq_hIeHEheJKdVd8UwWpyv6U,686
|
|
2
|
-
bananas/environment.py,sha256=oBxSsFEMeTsizpe_qB3kkTOIvkxqImmRiv1WhZGHw4I,7874
|
|
3
|
-
bananas/lazy.py,sha256=XFaqACJ6K5s_MZcxK_5OLQU35F7c7jB3KsdAtHuYQ6k,140
|
|
4
|
-
bananas/models.py,sha256=Fd__soRYoyPEaClO7vmdIDzx78mIrtOnpfJKTlyrtNw,8931
|
|
5
|
-
bananas/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
bananas/query.py,sha256=ZtLtJZZVt5_JUDaFxkstpwaHNnIcjfDM98EmCOCuzJY,3569
|
|
7
|
-
bananas/secrets.py,sha256=JlTM2mecnvwRmUANTbxMgtyTnRoW3TCrb5uY9KFuW24,1029
|
|
8
|
-
bananas/settings.py,sha256=HGZBxm5BMwHJc9PpQpvkzAuasXvePa-q_4tgeup9DPA,139
|
|
9
|
-
bananas/url.py,sha256=SlnGoHRGnh4j6lvGuNniVz7U5eFYMyDlN_9PWvQX9mQ,7904
|
|
10
|
-
bananas/admin/__init__.py,sha256=0Dzu51GjYjbpnNf5ByaKTx5xiYZ0vxfhW1EssFuiQ90,173
|
|
11
|
-
bananas/admin/extension.py,sha256=6t2rNN_NRcIS6sveuGhmyKSW2UtBOlz8PhVe3V_QM4k,13664
|
|
12
|
-
bananas/admin/api/__init__.py,sha256=86dXxHQKnQk-2ZOT2znTBasX-k7nPKlxslwYVZlzu4g,54
|
|
13
|
-
bananas/admin/api/i18n.py,sha256=ZcZj3PeP2q4O5uZisK7Qk9TMVA6MQKCE2RKMZSwFHCU,302
|
|
14
|
-
bananas/admin/api/mixins.py,sha256=F2-DPkVCqgtqMNZT8jeP_ODm5TjhgzSUKmDd046yicA,5350
|
|
15
|
-
bananas/admin/api/permissions.py,sha256=sIIHfuJERYvdSJQ8YwNV4W3tj1DPlICi1snAe5RGq2g,375
|
|
16
|
-
bananas/admin/api/router.py,sha256=VWn6O97sbsoPZtKPLPxPDn_raO7Gy8B62N1s7-mGHhU,570
|
|
17
|
-
bananas/admin/api/serializers.py,sha256=XziZ1-6h4hn1qgts3qB8W18ctpl6h8vP3dkqa7WxLe8,2964
|
|
18
|
-
bananas/admin/api/urls.py,sha256=fch3C5BhI6d12U9un8I28fFiHZWTWFH5XAhAzXPURxY,398
|
|
19
|
-
bananas/admin/api/versioning.py,sha256=ATCoCf1uuY69lse-c0qNaBUOrQGIadAMk16UslyeUgo,882
|
|
20
|
-
bananas/admin/api/views.py,sha256=9EYe1VdcF9NjPUSSElMxQ_9rDjOaYIDmzk7swQ02S1I,4083
|
|
21
|
-
bananas/admin/api/schemas/__init__.py,sha256=n4vZ3Sd1H23krxIIUWvktHimmdfh1n0K-J4z2-QsZxI,336
|
|
22
|
-
bananas/admin/api/schemas/base.py,sha256=EjYWpLmb1q1y2qbkhNcAFaiq-acT07PsVn89IKAIdp8,433
|
|
23
|
-
bananas/admin/api/schemas/decorators.py,sha256=Zwp7AwV_6Y0iWVcsO8VjNHHttYMcdGnrfr7ElwEzJzE,492
|
|
24
|
-
bananas/admin/api/schemas/yasg.py,sha256=Af5QKnc-UUMP6AqdGBgcVyfERw_70WnNs-Ho1LUCUzc,6019
|
|
25
|
-
bananas/admin/api/v1_0/__init__.py,sha256=1M909E2Ei8-GI0Xox9t5NfO3xpPG2F13znjlx_jXGZc,59
|
|
26
|
-
bananas/admin/api/v1_0/urls.py,sha256=nfH-6G3qzg3e0AJXxZLfEV0zyXqkNh8fJw_Tb5ALDpg,664
|
|
27
|
-
bananas/drf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
bananas/drf/errors.py,sha256=JDZYblAio27rh-nAOtC_4CoVw_lFYIdmmd4bTrgxaQc,427
|
|
29
|
-
bananas/drf/fencing.py,sha256=WJ_Hw5XgI3XrianquYO2ggtAB9tyRFqeCqm_7-0OTzM,7159
|
|
30
|
-
bananas/drf/utils.py,sha256=cHLKsWl_yjpR3lO0gPvapKXg9M5_JpJEKi5wcswpwJg,1792
|
|
31
|
-
bananas/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
-
bananas/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
bananas/management/commands/show_urls.py,sha256=Ng63v7YM5tw1M00mzJc6xdlcCS0w7NMg2TFG1djLGRU,2220
|
|
34
|
-
bananas/management/commands/syncpermissions.py,sha256=WtVzNrEQl1tCVQKq9pPW1QwcIftbrwbhmN5WMPayR8k,1566
|
|
35
|
-
bananas/static/admin/bananas/css/bananas.css,sha256=5WOuhE-vhrqKaXuwat56DTq7MPb79q7ajym0Nx-3vcU,17740
|
|
36
|
-
bananas/static/admin/bananas/css/banansive.css,sha256=u9aYmL_ccDO9rDevOtckqhERDNs0HSazQ-4HJ9TukXI,3783
|
|
37
|
-
bananas/static/admin/bananas/img/django.svg,sha256=4S1Cjntojkcoj17oTNufVtnlflhoKsRuQ9O7hTqyx6Q,5291
|
|
38
|
-
bananas/static/admin/bananas/img/search.svg,sha256=59UwyGYVJ4EJ26d5AAIdjtCHR3CnO94in6eQIbkYTu8,458
|
|
39
|
-
bananas/static/admin/bananas/js/bananas.js,sha256=1Xv6JEiTZYeaJSTxkKyMLwIRJ40SnRDPi2N-prpWoU0,4596
|
|
40
|
-
bananas/static/admin/css/responsive.css,sha256=UAMABM7h5rRZ8FocJw4bfx36Q5XtIDm4ddXN8lMC4SQ,17894
|
|
41
|
-
bananas/templates/admin/base_site.html,sha256=Hq4n99zOuGnzsHl1v1xmYI6uFKk1oaDVcY_bsaMFlMA,7793
|
|
42
|
-
bananas/templates/admin/view.html,sha256=WezwD9qGPch5S29intLVzbY1zVRop4zdIdm4pf7usVw,114
|
|
43
|
-
django_bananas-2.2.dist-info/LICENSE,sha256=bsI-BPMDuol2ZykVXKKQiwufT0JexcSLsU2Nlx3D-zc,1081
|
|
44
|
-
django_bananas-2.2.dist-info/METADATA,sha256=AdpRMTzNJuPL9dqY_ggwG1Qwns4Bq50xC4v2FrcngQ0,18137
|
|
45
|
-
django_bananas-2.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
46
|
-
django_bananas-2.2.dist-info/top_level.txt,sha256=5JKVcC99qGcHeOm5WigbcrQbMcsWr6N2A0tF9ZoY6j8,8
|
|
47
|
-
django_bananas-2.2.dist-info/RECORD,,
|
|
File without changes
|