djcrud 0.2.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.
- djcrud/__init__.py +145 -0
- djcrud/admin.py +3 -0
- djcrud/apps.py +5 -0
- djcrud/backends.py +159 -0
- djcrud/clonable.py +15 -0
- djcrud/errors.py +81 -0
- djcrud/handlers.py +23 -0
- djcrud/introspection.py +95 -0
- djcrud/locale/en/LC_MESSAGES/django.mo +0 -0
- djcrud/locale/en/LC_MESSAGES/django.po +150 -0
- djcrud/locale/fr/LC_MESSAGES/django.mo +0 -0
- djcrud/locale/fr/LC_MESSAGES/django.po +150 -0
- djcrud/management/commands/show_urls.py +218 -0
- djcrud/model.py +70 -0
- djcrud/models.py +3 -0
- djcrud/redirect.py +39 -0
- djcrud/registry.py +54 -0
- djcrud/route.py +56 -0
- djcrud/router.py +132 -0
- djcrud/settings.py +16 -0
- djcrud/templatetags/djcrud.py +199 -0
- djcrud/tests.py +3 -0
- djcrud/view.py +236 -0
- djcrud/views/action.py +18 -0
- djcrud/views/create.py +49 -0
- djcrud/views/delete.py +250 -0
- djcrud/views/detail.py +115 -0
- djcrud/views/filter.py +141 -0
- djcrud/views/form.py +95 -0
- djcrud/views/generic.py +17 -0
- djcrud/views/json.py +146 -0
- djcrud/views/list.py +290 -0
- djcrud/views/list_action.py +71 -0
- djcrud/views/log.py +190 -0
- djcrud/views/modelform.py +37 -0
- djcrud/views/object.py +78 -0
- djcrud/views/objectform.py +31 -0
- djcrud/views/pagination.py +136 -0
- djcrud/views/search.py +54 -0
- djcrud/views/swagger.py +64 -0
- djcrud/views/tables2.py +228 -0
- djcrud/views/template.py +62 -0
- djcrud/views/update.py +54 -0
- djcrud-0.2.0.dist-info/METADATA +111 -0
- djcrud-0.2.0.dist-info/RECORD +212 -0
- djcrud-0.2.0.dist-info/WHEEL +5 -0
- djcrud-0.2.0.dist-info/scm_file_list.json +341 -0
- djcrud-0.2.0.dist-info/scm_version.json +8 -0
- djcrud-0.2.0.dist-info/top_level.txt +10 -0
- djcrud_api/__init__.py +0 -0
- djcrud_api/apps.py +10 -0
- djcrud_api/djcrud.py +5 -0
- djcrud_api/middleware.py +53 -0
- djcrud_api/migrations/0001_initial.py +50 -0
- djcrud_api/migrations/0002_rename_from_djmvc_swagger.py +24 -0
- djcrud_api/migrations/0003_rename_from_djmvc_api.py +37 -0
- djcrud_api/migrations/__init__.py +0 -0
- djcrud_api/models.py +71 -0
- djcrud_api/static/djcrud_api/swagger-ui-bundle.js +3 -0
- djcrud_api/static/djcrud_api/swagger-ui-standalone-preset.js +3 -0
- djcrud_api/static/djcrud_api/swagger-ui.css +4 -0
- djcrud_api/templates/djcrud_api/api.html +54 -0
- djcrud_api/views.py +239 -0
- djcrud_auth/__init__.py +0 -0
- djcrud_auth/admin.py +3 -0
- djcrud_auth/apps.py +5 -0
- djcrud_auth/djcrud.py +8 -0
- djcrud_auth/locale/en/LC_MESSAGES/django.mo +0 -0
- djcrud_auth/locale/en/LC_MESSAGES/django.po +75 -0
- djcrud_auth/locale/fr/LC_MESSAGES/django.mo +0 -0
- djcrud_auth/locale/fr/LC_MESSAGES/django.po +75 -0
- djcrud_auth/models.py +3 -0
- djcrud_auth/tests.py +3 -0
- djcrud_auth/views.py +312 -0
- djcrud_bulma/__init__.py +0 -0
- djcrud_bulma/admin.py +3 -0
- djcrud_bulma/apps.py +14 -0
- djcrud_bulma/locale/en/LC_MESSAGES/django.mo +0 -0
- djcrud_bulma/locale/en/LC_MESSAGES/django.po +123 -0
- djcrud_bulma/locale/fr/LC_MESSAGES/django.mo +0 -0
- djcrud_bulma/locale/fr/LC_MESSAGES/django.po +123 -0
- djcrud_bulma/models.py +3 -0
- djcrud_bulma/static/djcrud_bulma/css/style.css +259 -0
- djcrud_bulma/static/djcrud_bulma/js/filter-sidebar.js +121 -0
- djcrud_bulma/static/djcrud_bulma/js/filter-sidebar.test.js +147 -0
- djcrud_bulma/static/djcrud_bulma/js/form-focus.js +45 -0
- djcrud_bulma/static/djcrud_bulma/js/form-focus.test.js +93 -0
- djcrud_bulma/static/djcrud_bulma/js/hamburger.js +73 -0
- djcrud_bulma/static/djcrud_bulma/js/hamburger.test.js +208 -0
- djcrud_bulma/static/djcrud_bulma/js/list-action-bar.js +278 -0
- djcrud_bulma/static/djcrud_bulma/js/list-action-bar.test.js +187 -0
- djcrud_bulma/static/djcrud_bulma/js/nav-config.js +9 -0
- djcrud_bulma/static/djcrud_bulma/js/nav-config.test.js +32 -0
- djcrud_bulma/static/djcrud_bulma/js/toast.js +19 -0
- djcrud_bulma/static/djcrud_bulma/js/toast.test.js +89 -0
- djcrud_bulma/templates/djcrud/400.html +1 -0
- djcrud_bulma/templates/djcrud/403.html +1 -0
- djcrud_bulma/templates/djcrud/404.html +1 -0
- djcrud_bulma/templates/djcrud/500.html +1 -0
- djcrud_bulma/templates/djcrud/_actions_column.html +40 -0
- djcrud_bulma/templates/djcrud/_checkbox_column.html +3 -0
- djcrud_bulma/templates/djcrud/_checkbox_header.html +3 -0
- djcrud_bulma/templates/djcrud/_deletion_preview.html +29 -0
- djcrud_bulma/templates/djcrud/_error.html +6 -0
- djcrud_bulma/templates/djcrud/_filter.html +26 -0
- djcrud_bulma/templates/djcrud/_flashes.html +15 -0
- djcrud_bulma/templates/djcrud/_navbar_burger.html +6 -0
- djcrud_bulma/templates/djcrud/_object_list.html +21 -0
- djcrud_bulma/templates/djcrud/_pagination.html +83 -0
- djcrud_bulma/templates/djcrud/_tables2.html +81 -0
- djcrud_bulma/templates/djcrud/base.html +111 -0
- djcrud_bulma/templates/djcrud/base_immersive.html +9 -0
- djcrud_bulma/templates/djcrud/detail.html +46 -0
- djcrud_bulma/templates/djcrud/detaillist.html +69 -0
- djcrud_bulma/templates/djcrud/error.html +7 -0
- djcrud_bulma/templates/djcrud/form.html +74 -0
- djcrud_bulma/templates/djcrud/form_delete.html +40 -0
- djcrud_bulma/templates/djcrud/home.html +32 -0
- djcrud_bulma/templates/djcrud/list.html +67 -0
- djcrud_bulma/templates/djcrud/test_immersive.html +3 -0
- djcrud_bulma/tests.py +3 -0
- djcrud_bulma/views.py +3 -0
- djcrud_dal/__init__.py +0 -0
- djcrud_dal/apps.py +26 -0
- djcrud_dal/hooks.py +62 -0
- djcrud_dal/lookup.py +23 -0
- djcrud_dal/models.py +5 -0
- djcrud_dal/views.py +31 -0
- djcrud_dal_topbar/__init__.py +0 -0
- djcrud_dal_topbar/apps.py +20 -0
- djcrud_dal_topbar/djcrud.py +5 -0
- djcrud_dal_topbar/lookup.py +87 -0
- djcrud_dal_topbar/static/djcrud_dal_topbar/css/site-search.css +27 -0
- djcrud_dal_topbar/static/djcrud_dal_topbar/js/site-search.js +6 -0
- djcrud_dal_topbar/templates/djcrud/_site_search.html +17 -0
- djcrud_dal_topbar/views.py +75 -0
- djcrud_debug/__init__.py +0 -0
- djcrud_debug/apps.py +5 -0
- djcrud_debug/djcrud.py +5 -0
- djcrud_debug/models.py +56 -0
- djcrud_debug/routing_debug.py +160 -0
- djcrud_debug/views.py +44 -0
- djcrud_example/__init__.py +0 -0
- djcrud_example/asgi.py +16 -0
- djcrud_example/example_urls.py +35 -0
- djcrud_example/migrations/0001_initial.py +132 -0
- djcrud_example/migrations/0002_test_users.py +27 -0
- djcrud_example/migrations/__init__.py +0 -0
- djcrud_example/models.py +5 -0
- djcrud_example/settings.py +164 -0
- djcrud_example/stage0/__init__.py +0 -0
- djcrud_example/stage0/admin.py +3 -0
- djcrud_example/stage0/apps.py +5 -0
- djcrud_example/stage0/djcrud.py +14 -0
- djcrud_example/stage0/migrations/0001_initial.py +26 -0
- djcrud_example/stage0/migrations/__init__.py +0 -0
- djcrud_example/stage0/models.py +8 -0
- djcrud_example/stage0/tests.py +3 -0
- djcrud_example/stage1/__init__.py +0 -0
- djcrud_example/stage1/admin.py +0 -0
- djcrud_example/stage1/apps.py +5 -0
- djcrud_example/stage1/djcrud.py +22 -0
- djcrud_example/stage1/migrations/__init__.py +0 -0
- djcrud_example/stage1/models.py +0 -0
- djcrud_example/stage1/tests.py +0 -0
- djcrud_example/stage2/__init__.py +0 -0
- djcrud_example/stage2/admin.py +0 -0
- djcrud_example/stage2/apps.py +5 -0
- djcrud_example/stage2/djcrud.py +18 -0
- djcrud_example/stage2/migrations/0001_initial.py +40 -0
- djcrud_example/stage2/migrations/__init__.py +0 -0
- djcrud_example/stage2/models.py +14 -0
- djcrud_example/stage2/tests.py +0 -0
- djcrud_example/stage3/__init__.py +0 -0
- djcrud_example/stage3/admin.py +5 -0
- djcrud_example/stage3/apps.py +6 -0
- djcrud_example/stage3/backends.py +35 -0
- djcrud_example/stage3/djcrud.py +28 -0
- djcrud_example/stage3/migrations/0001_initial.py +44 -0
- djcrud_example/stage3/migrations/__init__.py +0 -0
- djcrud_example/stage3/models.py +21 -0
- djcrud_example/stage3/tests.py +0 -0
- djcrud_example/stage4/__init__.py +0 -0
- djcrud_example/stage4/admin.py +0 -0
- djcrud_example/stage4/apps.py +5 -0
- djcrud_example/stage4/djcrud.py +29 -0
- djcrud_example/stage4/migrations/0001_initial.py +30 -0
- djcrud_example/stage4/migrations/__init__.py +0 -0
- djcrud_example/stage4/models.py +10 -0
- djcrud_example/stage4/tests.py +0 -0
- djcrud_example/stage5/__init__.py +0 -0
- djcrud_example/stage5/admin.py +0 -0
- djcrud_example/stage5/apps.py +5 -0
- djcrud_example/stage5/djcrud.py +45 -0
- djcrud_example/stage5/migrations/0001_initial.py +29 -0
- djcrud_example/stage5/migrations/__init__.py +0 -0
- djcrud_example/stage5/models.py +9 -0
- djcrud_example/stage5/tests.py +0 -0
- djcrud_example/urls.py +13 -0
- djcrud_example/wsgi.py +16 -0
- djcrud_history/__init__.py +0 -0
- djcrud_history/apps.py +15 -0
- djcrud_history/djcrud.py +5 -0
- djcrud_history/log.py +21 -0
- djcrud_history/models.py +5 -0
- djcrud_history/views.py +76 -0
- djcrud_triggers/__init__.py +1 -0
- djcrud_triggers/apps.py +22 -0
- djcrud_triggers/hooks/__init__.py +12 -0
- djcrud_triggers/hooks/events.py +40 -0
- djcrud_triggers/hooks/registry.py +135 -0
- djcrud_triggers/hooks/runner.py +52 -0
djcrud/__init__.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""djcrud public API.
|
|
2
|
+
|
|
3
|
+
:data:`site` is the root :class:`Site` router. Append routers to
|
|
4
|
+
``site.routes`` in each app's ``djcrud.py`` module; :meth:`Site.build` imports
|
|
5
|
+
those modules via autodiscovery (like Django admin's ``admin.py``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.db import models as db_models
|
|
9
|
+
from django.utils.module_loading import autodiscover_modules
|
|
10
|
+
|
|
11
|
+
from .backends import ViewPermissionBackend, resolve_permission_backend
|
|
12
|
+
from .router import Router
|
|
13
|
+
from .view import View
|
|
14
|
+
from .model import ModelMixin
|
|
15
|
+
from .views import generic
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModelRouter(ModelMixin, Router):
|
|
19
|
+
"""CRUD router for a single Django model.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
routes: Default list, detail, create, update, delete, and bulk-delete
|
|
23
|
+
views. Extend with ``ModelRouter.routes + [MyView]`` or
|
|
24
|
+
replace entries by codename (see :class:`~djcrud.registry.Registry`).
|
|
25
|
+
model: Django model class managed by this router.
|
|
26
|
+
icon (str): Bootstrap Icons name for the navigation list view.
|
|
27
|
+
color (str): Bulma color for the navigation list icon.
|
|
28
|
+
permission_backend: Optional :class:`~djcrud.backends.ViewPermissionBackend`
|
|
29
|
+
subclass, dotted path, or instance. Also settable on
|
|
30
|
+
:class:`django.apps.AppConfig`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
permission_backend = None
|
|
34
|
+
|
|
35
|
+
routes = [
|
|
36
|
+
generic.ListView,
|
|
37
|
+
generic.DetailView,
|
|
38
|
+
generic.UpdateView,
|
|
39
|
+
generic.DeleteView,
|
|
40
|
+
generic.DeleteObjectsView,
|
|
41
|
+
generic.CreateView,
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def codename(self):
|
|
46
|
+
"""URL segment from :attr:`model` name (lowercase)."""
|
|
47
|
+
return self.model.__name__.lower()
|
|
48
|
+
|
|
49
|
+
def has_permission(self, view):
|
|
50
|
+
"""Check Django permissions for *view* via the permission backend."""
|
|
51
|
+
return view.has_permission_backend()
|
|
52
|
+
|
|
53
|
+
def get_queryset(self, view):
|
|
54
|
+
"""Return rows visible to *view* via the permission backend."""
|
|
55
|
+
return view.get_queryset_backend()
|
|
56
|
+
|
|
57
|
+
json_fields = None
|
|
58
|
+
|
|
59
|
+
def get_json_fields(self):
|
|
60
|
+
"""Field names exposed in JSON responses."""
|
|
61
|
+
if self.json_fields is not None:
|
|
62
|
+
return list(self.json_fields)
|
|
63
|
+
return [f.name for f in self.model._meta.fields]
|
|
64
|
+
|
|
65
|
+
def get_FIELD_json(self, obj, field):
|
|
66
|
+
value = getattr(obj, field)
|
|
67
|
+
if callable(value):
|
|
68
|
+
value = value()
|
|
69
|
+
if value is not None and not isinstance(value, (str, int, float, bool)):
|
|
70
|
+
value = str(value)
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def serialize(self, obj, fields=None):
|
|
74
|
+
"""Map a model instance to a JSON-serializable dict."""
|
|
75
|
+
fields = fields or self.get_json_fields()
|
|
76
|
+
return {
|
|
77
|
+
field: getattr(
|
|
78
|
+
self,
|
|
79
|
+
f"get_{field}_json",
|
|
80
|
+
self.get_FIELD_json,
|
|
81
|
+
)(obj, field)
|
|
82
|
+
for field in fields
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def get_swagger_model_name(self, request):
|
|
86
|
+
return self.model.__name__
|
|
87
|
+
|
|
88
|
+
def get_swagger_model_definition(self, request):
|
|
89
|
+
result = {"properties": {}, "type": "object"}
|
|
90
|
+
int_fields = (
|
|
91
|
+
db_models.IntegerField,
|
|
92
|
+
db_models.BigIntegerField,
|
|
93
|
+
db_models.AutoField,
|
|
94
|
+
db_models.PositiveIntegerField,
|
|
95
|
+
db_models.PositiveSmallIntegerField,
|
|
96
|
+
db_models.SmallIntegerField,
|
|
97
|
+
)
|
|
98
|
+
for field in self.model._meta.fields:
|
|
99
|
+
custom = getattr(self, f"swagger_field_{field.name}_definition", None)
|
|
100
|
+
if custom:
|
|
101
|
+
result["properties"][field.name] = custom
|
|
102
|
+
continue
|
|
103
|
+
field_def = {}
|
|
104
|
+
if isinstance(field, int_fields):
|
|
105
|
+
field_def["type"] = "integer"
|
|
106
|
+
elif isinstance(field, db_models.BooleanField):
|
|
107
|
+
field_def["type"] = "boolean"
|
|
108
|
+
elif isinstance(field, db_models.JSONField):
|
|
109
|
+
field_def["type"] = "object"
|
|
110
|
+
else:
|
|
111
|
+
field_def["type"] = "string"
|
|
112
|
+
result["properties"][field.name] = field_def
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Home(generic.TemplateView):
|
|
117
|
+
"""Site root page at ``/``."""
|
|
118
|
+
|
|
119
|
+
urlpath = ""
|
|
120
|
+
|
|
121
|
+
def has_permission(self):
|
|
122
|
+
"""Allow anonymous access to the home page."""
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Site(Router):
|
|
127
|
+
"""Root site router; autodiscovers ``djcrud.py`` in installed apps."""
|
|
128
|
+
|
|
129
|
+
urlpath = ""
|
|
130
|
+
routes = [
|
|
131
|
+
Home,
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def autodiscover(self):
|
|
135
|
+
"""Import ``djcrud`` modules from every installed app."""
|
|
136
|
+
autodiscover_modules("djcrud")
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def build(self):
|
|
140
|
+
"""Autodiscover app routes, then build the route registry."""
|
|
141
|
+
self.autodiscover()
|
|
142
|
+
return super().build()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
site = Site()
|
djcrud/admin.py
ADDED
djcrud/apps.py
ADDED
djcrud/backends.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""View-aware permission backends for djcrud.
|
|
2
|
+
|
|
3
|
+
Default :attr:`~djcrud.view.ViewMixin.permission_shortcode` values on generic
|
|
4
|
+
views (Django permission codenames are ``{shortcode}_{model}``):
|
|
5
|
+
|
|
6
|
+
- ``view`` — :class:`~djcrud.views.list.ListView`,
|
|
7
|
+
:class:`~djcrud.views.detail.DetailView`,
|
|
8
|
+
:class:`~djcrud_dal.views.AutocompleteView`
|
|
9
|
+
- ``add`` — :class:`~djcrud.views.create.CreateView`
|
|
10
|
+
- ``change`` — :class:`~djcrud.views.update.UpdateView`
|
|
11
|
+
- ``delete`` — :class:`~djcrud.views.delete.DeleteView`,
|
|
12
|
+
:class:`~djcrud.views.delete.DeleteObjectsView`
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from django.apps import apps
|
|
16
|
+
from django.contrib.auth import get_backends
|
|
17
|
+
from django.utils.module_loading import import_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ViewPermissionBackend:
|
|
21
|
+
"""Base permission backend with convention-based handler dispatch.
|
|
22
|
+
|
|
23
|
+
Implement handlers named ``has_perm_{model}_{shortcode}``,
|
|
24
|
+
``has_perm_{model}``, ``get_queryset_{model}_{shortcode}``, or
|
|
25
|
+
``get_queryset_{model}`` where *shortcode* is the view's
|
|
26
|
+
:attr:`~djcrud.view.ViewMixin.permission_shortcode`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def handler(self, prefix, view):
|
|
30
|
+
"""Return the handler callable for *prefix* and *view*, or None."""
|
|
31
|
+
if view is None or view.router is None:
|
|
32
|
+
return None
|
|
33
|
+
model_name = view.router.model_router.model._meta.model_name
|
|
34
|
+
shortcode = view.permission_shortcode
|
|
35
|
+
for name in (
|
|
36
|
+
f"{prefix}_{model_name}_{shortcode}",
|
|
37
|
+
f"{prefix}_{model_name}",
|
|
38
|
+
prefix,
|
|
39
|
+
):
|
|
40
|
+
method = getattr(self, name, None)
|
|
41
|
+
if method is not None:
|
|
42
|
+
return method
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def has_perm(self, user_obj, perm, obj=None):
|
|
46
|
+
handler = self.handler("has_perm", obj)
|
|
47
|
+
if handler is None:
|
|
48
|
+
return False
|
|
49
|
+
return handler(user_obj, perm=perm, obj=obj)
|
|
50
|
+
|
|
51
|
+
def get_queryset(self, user_obj, perm, obj=None, **kwargs):
|
|
52
|
+
handler = self.handler("get_queryset", obj)
|
|
53
|
+
if handler is None:
|
|
54
|
+
return None
|
|
55
|
+
return handler(user_obj, perm=perm, obj=obj, **kwargs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_permission_backend(value):
|
|
59
|
+
"""Return a backend instance from a class, path string, or instance."""
|
|
60
|
+
if value is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(value, ViewPermissionBackend):
|
|
63
|
+
return value
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
value = import_string(value)
|
|
66
|
+
if isinstance(value, type) and issubclass(value, ViewPermissionBackend):
|
|
67
|
+
return value()
|
|
68
|
+
raise TypeError(
|
|
69
|
+
"permission_backend must be a ViewPermissionBackend subclass, "
|
|
70
|
+
"dotted path, or instance — got %r" % (value,)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def permission_backend_from_router(router):
|
|
75
|
+
current = router
|
|
76
|
+
while current is not None:
|
|
77
|
+
value = type(current).__dict__.get("permission_backend")
|
|
78
|
+
if value is not None:
|
|
79
|
+
return load_permission_backend(value)
|
|
80
|
+
current = getattr(current, "router", None)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def permission_backend_from_app(model):
|
|
85
|
+
app_config = apps.get_app_config(model._meta.app_label)
|
|
86
|
+
value = getattr(app_config, "permission_backend", None)
|
|
87
|
+
if value is None:
|
|
88
|
+
return None
|
|
89
|
+
return load_permission_backend(value)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_permission_backend(*, view=None, router=None, model=None):
|
|
93
|
+
"""Return the permission backend for *view*, *router*, and/or *model*."""
|
|
94
|
+
if view is not None:
|
|
95
|
+
if "permission_backend" in view.__dict__:
|
|
96
|
+
return view.__dict__["permission_backend"]
|
|
97
|
+
if router is None and view.router is not None:
|
|
98
|
+
router = view.router.model_router
|
|
99
|
+
if model is None and router is not None:
|
|
100
|
+
model = router.model
|
|
101
|
+
|
|
102
|
+
if router is not None:
|
|
103
|
+
backend = permission_backend_from_router(router)
|
|
104
|
+
if backend is not None:
|
|
105
|
+
return backend
|
|
106
|
+
|
|
107
|
+
if model is not None:
|
|
108
|
+
backend = permission_backend_from_app(model)
|
|
109
|
+
if backend is not None:
|
|
110
|
+
return backend
|
|
111
|
+
|
|
112
|
+
from . import site
|
|
113
|
+
|
|
114
|
+
site_backend = type(site).__dict__.get("permission_backend")
|
|
115
|
+
if site_backend is not None:
|
|
116
|
+
return load_permission_backend(site_backend)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def default_has_perm(user_obj, perm):
|
|
121
|
+
"""Check global DB permissions (superuser or ModelBackend)."""
|
|
122
|
+
if user_obj.is_active and user_obj.is_superuser:
|
|
123
|
+
return True
|
|
124
|
+
from django.contrib.auth.backends import ModelBackend
|
|
125
|
+
|
|
126
|
+
return ModelBackend().has_perm(user_obj, perm, None)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def detail_view_for(router, request):
|
|
130
|
+
"""Return a detail-route view instance for *router*, or None."""
|
|
131
|
+
detail_route = router.model_router.find_route("detail")
|
|
132
|
+
if detail_route is None:
|
|
133
|
+
return None
|
|
134
|
+
view = type(detail_route)(request=request)
|
|
135
|
+
view.router = router
|
|
136
|
+
return view
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def queryset_from_backends(user, view):
|
|
140
|
+
"""Return a queryset from permission backends for *view*, or None.
|
|
141
|
+
|
|
142
|
+
Checks the resolved :class:`ViewPermissionBackend` first, then each entry
|
|
143
|
+
in ``AUTHENTICATION_BACKENDS`` that implements ``get_queryset``.
|
|
144
|
+
"""
|
|
145
|
+
model = view.router.model_router.model
|
|
146
|
+
perm = view.permission_fullcode
|
|
147
|
+
backend = resolve_permission_backend(view=view)
|
|
148
|
+
if backend is not None:
|
|
149
|
+
qs = backend.get_queryset(user, perm, view, model=model)
|
|
150
|
+
if qs is not None:
|
|
151
|
+
return qs
|
|
152
|
+
for auth_backend in get_backends():
|
|
153
|
+
get_qs = getattr(auth_backend, "get_queryset", None)
|
|
154
|
+
if get_qs is None:
|
|
155
|
+
continue
|
|
156
|
+
qs = get_qs(user, perm, view)
|
|
157
|
+
if qs is not None:
|
|
158
|
+
return qs
|
|
159
|
+
return None
|
djcrud/clonable.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Clonable:
|
|
2
|
+
"""Mixin for declaring specialized subclasses at runtime.
|
|
3
|
+
|
|
4
|
+
Controllers and views use :meth:`clone` to override attributes without
|
|
5
|
+
defining a new module-level class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def clone(cls, *mixins, **attributes):
|
|
10
|
+
"""Return a subclass with the given attributes.
|
|
11
|
+
|
|
12
|
+
If a model is found, it will prefix the class name with the model.
|
|
13
|
+
"""
|
|
14
|
+
name = cls.__name__
|
|
15
|
+
return type(name, (cls,) + mixins, attributes)
|
djcrud/errors.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from django.template.response import TemplateResponse
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
DEFAULT_ERROR_PAGES = {
|
|
5
|
+
400: (
|
|
6
|
+
_("Bad request"),
|
|
7
|
+
_("The request could not be understood by the server."),
|
|
8
|
+
),
|
|
9
|
+
403: (
|
|
10
|
+
_("Forbidden"),
|
|
11
|
+
_("You don't have permission to view or edit anything."),
|
|
12
|
+
),
|
|
13
|
+
404: (
|
|
14
|
+
_("Page not found"),
|
|
15
|
+
_("We're sorry, but the requested page could not be found."),
|
|
16
|
+
),
|
|
17
|
+
500: (
|
|
18
|
+
_("Server error"),
|
|
19
|
+
_(
|
|
20
|
+
"There's been an error. It's been reported to the site "
|
|
21
|
+
"administrators via email and should be fixed shortly. "
|
|
22
|
+
"Thanks for your patience."
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def layout_view_for_request(request):
|
|
29
|
+
import djcrud
|
|
30
|
+
from .views.template import TemplateView
|
|
31
|
+
|
|
32
|
+
if not hasattr(djcrud.site, "registry"):
|
|
33
|
+
djcrud.site.build()
|
|
34
|
+
|
|
35
|
+
class LayoutView(TemplateView):
|
|
36
|
+
def has_permission(self):
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
view_class = LayoutView.clone(router=djcrud.site)
|
|
40
|
+
view = view_class()
|
|
41
|
+
view.request = request
|
|
42
|
+
view.kwargs = {}
|
|
43
|
+
view.args = ()
|
|
44
|
+
return view
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def error_response(request, status, view=None, title=None, message=None):
|
|
48
|
+
view = view or layout_view_for_request(request)
|
|
49
|
+
default_title, default_message = DEFAULT_ERROR_PAGES[status]
|
|
50
|
+
context = {
|
|
51
|
+
"view": view,
|
|
52
|
+
"error_status": status,
|
|
53
|
+
"error_title": title or default_title,
|
|
54
|
+
"error_message": message or default_message,
|
|
55
|
+
}
|
|
56
|
+
return TemplateResponse(
|
|
57
|
+
request,
|
|
58
|
+
[
|
|
59
|
+
f"djcrud/{status}.html",
|
|
60
|
+
"djcrud/error.html",
|
|
61
|
+
f"{status}.html",
|
|
62
|
+
],
|
|
63
|
+
context,
|
|
64
|
+
status=status,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def bad_request_response(request, view=None, title=None, message=None):
|
|
69
|
+
return error_response(request, 400, view=view, title=title, message=message)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def forbidden_response(request, view=None, title=None, message=None):
|
|
73
|
+
return error_response(request, 403, view=view, title=title, message=message)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def not_found_response(request, view=None, title=None, message=None):
|
|
77
|
+
return error_response(request, 404, view=view, title=title, message=message)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def server_error_response(request, view=None, title=None, message=None):
|
|
81
|
+
return error_response(request, 500, view=view, title=title, message=message)
|
djcrud/handlers.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .errors import (
|
|
2
|
+
bad_request_response,
|
|
3
|
+
forbidden_response,
|
|
4
|
+
layout_view_for_request,
|
|
5
|
+
not_found_response,
|
|
6
|
+
server_error_response,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handler400(request, exception=None):
|
|
11
|
+
return bad_request_response(request, view=layout_view_for_request(request))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handler403(request, exception=None):
|
|
15
|
+
return forbidden_response(request, view=layout_view_for_request(request))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handler404(request, exception=None):
|
|
19
|
+
return not_found_response(request, view=layout_view_for_request(request))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handler500(request):
|
|
23
|
+
return server_error_response(request, view=layout_view_for_request(request))
|
djcrud/introspection.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import djcrud
|
|
4
|
+
|
|
5
|
+
from .router import Router
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_fullurlpath(route):
|
|
9
|
+
"""Return the URL path for *route* by walking router ancestors."""
|
|
10
|
+
try:
|
|
11
|
+
return route.url
|
|
12
|
+
except Exception:
|
|
13
|
+
parts = []
|
|
14
|
+
current = route
|
|
15
|
+
while current is not None:
|
|
16
|
+
segment = getattr(current, "urlpath", "")
|
|
17
|
+
if segment:
|
|
18
|
+
parts.insert(0, segment.strip("/"))
|
|
19
|
+
current = getattr(current, "router", None)
|
|
20
|
+
if not parts:
|
|
21
|
+
return "/"
|
|
22
|
+
return "/" + "/".join(parts) + "/"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def swagger_path(fullurlpath):
|
|
26
|
+
"""Convert Django URL path converters to Swagger brace syntax."""
|
|
27
|
+
return "/" + re.sub(r"<(?:[^:>]+:)?([^>]+)>", r"{\1}", fullurlpath.strip("/"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def walk_leaf_views(router):
|
|
31
|
+
"""Yield built view route instances under *router*."""
|
|
32
|
+
routes = getattr(router, "routes", None)
|
|
33
|
+
if routes is None:
|
|
34
|
+
return
|
|
35
|
+
for route in routes:
|
|
36
|
+
if isinstance(route, Router):
|
|
37
|
+
yield from walk_leaf_views(route)
|
|
38
|
+
else:
|
|
39
|
+
yield route
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def walk_model_routers(router):
|
|
43
|
+
"""Yield :class:`~djcrud.ModelRouter` instances in the tree."""
|
|
44
|
+
if getattr(type(router), "model", None) is not None:
|
|
45
|
+
yield router
|
|
46
|
+
routes = getattr(router, "routes", None)
|
|
47
|
+
if routes is None:
|
|
48
|
+
return
|
|
49
|
+
for route in routes:
|
|
50
|
+
if isinstance(route, Router):
|
|
51
|
+
yield from walk_model_routers(route)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_built_site(site):
|
|
55
|
+
"""Return *site* after :meth:`~djcrud.Site.build` if needed."""
|
|
56
|
+
if not hasattr(site, "registry"):
|
|
57
|
+
site.build()
|
|
58
|
+
return site
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def has_json_handler(view_class):
|
|
62
|
+
"""Return whether *view_class* defines any ``json_*`` handler."""
|
|
63
|
+
return any(
|
|
64
|
+
hasattr(view_class, f"json_{method}")
|
|
65
|
+
for method in ("get", "post", "put", "patch", "delete")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def instantiate_view(route, request):
|
|
70
|
+
"""Return a view instance bound to *request* from a built route."""
|
|
71
|
+
view = type(route)(request=request)
|
|
72
|
+
view.args = ()
|
|
73
|
+
view.kwargs = {}
|
|
74
|
+
return view
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def iter_schema_json_views(request, site=None):
|
|
78
|
+
"""Yield every JSON-capable view for Swagger schema (no permission filter)."""
|
|
79
|
+
site = get_built_site(site or djcrud.site)
|
|
80
|
+
for route in walk_leaf_views(site):
|
|
81
|
+
if has_json_handler(type(route)):
|
|
82
|
+
yield instantiate_view(route, request)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def iter_schema_model_routers(request, site=None):
|
|
86
|
+
"""Yield model controllers referenced by JSON-capable views in the schema."""
|
|
87
|
+
seen = set()
|
|
88
|
+
for view in iter_schema_json_views(request, site):
|
|
89
|
+
mc = view.router.model_router
|
|
90
|
+
if getattr(type(mc), "model", None) is None:
|
|
91
|
+
continue
|
|
92
|
+
key = id(mc)
|
|
93
|
+
if key not in seen:
|
|
94
|
+
seen.add(key)
|
|
95
|
+
yield mc
|
|
Binary file
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
#, fuzzy
|
|
7
|
+
msgid ""
|
|
8
|
+
msgstr ""
|
|
9
|
+
"Project-Id-Version: PACKAGE VERSION\n"
|
|
10
|
+
"Report-Msgid-Bugs-To: \n"
|
|
11
|
+
"POT-Creation-Date: 2026-06-28 12:49+0000\n"
|
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
15
|
+
"Language: \n"
|
|
16
|
+
"MIME-Version: 1.0\n"
|
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
19
|
+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
20
|
+
|
|
21
|
+
#: src/djcrud/errors.py:7
|
|
22
|
+
msgid "Bad request"
|
|
23
|
+
msgstr ""
|
|
24
|
+
|
|
25
|
+
#: src/djcrud/errors.py:8
|
|
26
|
+
msgid "The request could not be understood by the server."
|
|
27
|
+
msgstr ""
|
|
28
|
+
|
|
29
|
+
#: src/djcrud/errors.py:11
|
|
30
|
+
msgid "Forbidden"
|
|
31
|
+
msgstr ""
|
|
32
|
+
|
|
33
|
+
#: src/djcrud/errors.py:12
|
|
34
|
+
msgid "You don't have permission to view or edit anything."
|
|
35
|
+
msgstr ""
|
|
36
|
+
|
|
37
|
+
#: src/djcrud/errors.py:15
|
|
38
|
+
msgid "Page not found"
|
|
39
|
+
msgstr ""
|
|
40
|
+
|
|
41
|
+
#: src/djcrud/errors.py:16
|
|
42
|
+
msgid "We're sorry, but the requested page could not be found."
|
|
43
|
+
msgstr ""
|
|
44
|
+
|
|
45
|
+
#: src/djcrud/errors.py:19
|
|
46
|
+
msgid "Server error"
|
|
47
|
+
msgstr ""
|
|
48
|
+
|
|
49
|
+
#: src/djcrud/errors.py:21
|
|
50
|
+
msgid ""
|
|
51
|
+
"There's been an error. It's been reported to the site administrators via "
|
|
52
|
+
"email and should be fixed shortly. Thanks for your patience."
|
|
53
|
+
msgstr ""
|
|
54
|
+
|
|
55
|
+
#: src/djcrud/views/create.py:19
|
|
56
|
+
#, python-format
|
|
57
|
+
msgid "Add %(name)s"
|
|
58
|
+
msgstr ""
|
|
59
|
+
|
|
60
|
+
#: src/djcrud/views/delete.py:19
|
|
61
|
+
#, python-format
|
|
62
|
+
msgid ""
|
|
63
|
+
"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? "
|
|
64
|
+
"All of the following related items will be deleted:"
|
|
65
|
+
msgstr ""
|
|
66
|
+
|
|
67
|
+
#: src/djcrud/views/delete.py:24
|
|
68
|
+
#, python-format
|
|
69
|
+
msgid ""
|
|
70
|
+
"Are you sure you want to delete the selected %(objects_name)s? All of the "
|
|
71
|
+
"following objects and their related items will be deleted:"
|
|
72
|
+
msgstr ""
|
|
73
|
+
|
|
74
|
+
#: src/djcrud/views/delete.py:81 src/djcrud/views/delete.py:158
|
|
75
|
+
#: src/djcrud/views/delete.py:191
|
|
76
|
+
msgid "Delete"
|
|
77
|
+
msgstr ""
|
|
78
|
+
|
|
79
|
+
#: src/djcrud/views/delete.py:130
|
|
80
|
+
#, python-format
|
|
81
|
+
msgid ""
|
|
82
|
+
"Deleting the selected %(objects_name)s would require deleting the following "
|
|
83
|
+
"protected related objects:"
|
|
84
|
+
msgstr ""
|
|
85
|
+
|
|
86
|
+
#: src/djcrud/views/delete.py:137
|
|
87
|
+
#, python-format
|
|
88
|
+
msgid ""
|
|
89
|
+
"Deleting the %(object_name)s \"%(escaped_object)s\" would require deleting "
|
|
90
|
+
"the following protected related objects:"
|
|
91
|
+
msgstr ""
|
|
92
|
+
|
|
93
|
+
#: src/djcrud/views/delete.py:170
|
|
94
|
+
#, python-format
|
|
95
|
+
msgid "The %(name)s \"%(obj)s\" was deleted successfully."
|
|
96
|
+
msgstr ""
|
|
97
|
+
|
|
98
|
+
#: src/djcrud/views/delete.py:199
|
|
99
|
+
#, python-format
|
|
100
|
+
msgid "Successfully deleted %(count)d %(items)s."
|
|
101
|
+
msgstr ""
|
|
102
|
+
|
|
103
|
+
#: src/djcrud/views/form.py:49
|
|
104
|
+
msgid "Please correct the error below."
|
|
105
|
+
msgid_plural "Please correct the errors below."
|
|
106
|
+
msgstr[0] ""
|
|
107
|
+
msgstr[1] ""
|
|
108
|
+
|
|
109
|
+
#: src/djcrud/views/list.py:32
|
|
110
|
+
#, python-format
|
|
111
|
+
msgid "No %(verbose_name)s found matching the query"
|
|
112
|
+
msgstr ""
|
|
113
|
+
|
|
114
|
+
#: src/djcrud/views/list.py:39 src/djcrud/views/list.py:47
|
|
115
|
+
#, python-format
|
|
116
|
+
msgid "%(total_count)s selected"
|
|
117
|
+
msgid_plural "All %(total_count)s selected"
|
|
118
|
+
msgstr[0] ""
|
|
119
|
+
msgstr[1] ""
|
|
120
|
+
|
|
121
|
+
#: src/djcrud/views/filter.py:33
|
|
122
|
+
msgid "Apply"
|
|
123
|
+
msgstr ""
|
|
124
|
+
|
|
125
|
+
#: src/djcrud/views/modelform.py:14
|
|
126
|
+
#, python-brace-format
|
|
127
|
+
msgid "The {name} \"{obj}\" was added successfully."
|
|
128
|
+
msgstr ""
|
|
129
|
+
|
|
130
|
+
#: src/djcrud/views/objectform.py:13 src/djcrud/views/objectform.py:24
|
|
131
|
+
#, python-brace-format
|
|
132
|
+
msgid "The {name} \"{obj}\" was changed successfully."
|
|
133
|
+
msgstr ""
|
|
134
|
+
|
|
135
|
+
#: src/djcrud/views/pagination.py:94
|
|
136
|
+
msgid "Page is not “last”, nor can it be converted to an int."
|
|
137
|
+
msgstr ""
|
|
138
|
+
|
|
139
|
+
#: src/djcrud/views/search.py:38
|
|
140
|
+
msgid "Search"
|
|
141
|
+
msgstr ""
|
|
142
|
+
|
|
143
|
+
#: src/djcrud/views/tables2.py:18
|
|
144
|
+
msgid "Actions"
|
|
145
|
+
msgstr ""
|
|
146
|
+
|
|
147
|
+
#: src/djcrud/views/update.py:17
|
|
148
|
+
#, python-format
|
|
149
|
+
msgid "Change %(name)s"
|
|
150
|
+
msgstr ""
|
|
Binary file
|