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.
Files changed (212) hide show
  1. djcrud/__init__.py +145 -0
  2. djcrud/admin.py +3 -0
  3. djcrud/apps.py +5 -0
  4. djcrud/backends.py +159 -0
  5. djcrud/clonable.py +15 -0
  6. djcrud/errors.py +81 -0
  7. djcrud/handlers.py +23 -0
  8. djcrud/introspection.py +95 -0
  9. djcrud/locale/en/LC_MESSAGES/django.mo +0 -0
  10. djcrud/locale/en/LC_MESSAGES/django.po +150 -0
  11. djcrud/locale/fr/LC_MESSAGES/django.mo +0 -0
  12. djcrud/locale/fr/LC_MESSAGES/django.po +150 -0
  13. djcrud/management/commands/show_urls.py +218 -0
  14. djcrud/model.py +70 -0
  15. djcrud/models.py +3 -0
  16. djcrud/redirect.py +39 -0
  17. djcrud/registry.py +54 -0
  18. djcrud/route.py +56 -0
  19. djcrud/router.py +132 -0
  20. djcrud/settings.py +16 -0
  21. djcrud/templatetags/djcrud.py +199 -0
  22. djcrud/tests.py +3 -0
  23. djcrud/view.py +236 -0
  24. djcrud/views/action.py +18 -0
  25. djcrud/views/create.py +49 -0
  26. djcrud/views/delete.py +250 -0
  27. djcrud/views/detail.py +115 -0
  28. djcrud/views/filter.py +141 -0
  29. djcrud/views/form.py +95 -0
  30. djcrud/views/generic.py +17 -0
  31. djcrud/views/json.py +146 -0
  32. djcrud/views/list.py +290 -0
  33. djcrud/views/list_action.py +71 -0
  34. djcrud/views/log.py +190 -0
  35. djcrud/views/modelform.py +37 -0
  36. djcrud/views/object.py +78 -0
  37. djcrud/views/objectform.py +31 -0
  38. djcrud/views/pagination.py +136 -0
  39. djcrud/views/search.py +54 -0
  40. djcrud/views/swagger.py +64 -0
  41. djcrud/views/tables2.py +228 -0
  42. djcrud/views/template.py +62 -0
  43. djcrud/views/update.py +54 -0
  44. djcrud-0.2.0.dist-info/METADATA +111 -0
  45. djcrud-0.2.0.dist-info/RECORD +212 -0
  46. djcrud-0.2.0.dist-info/WHEEL +5 -0
  47. djcrud-0.2.0.dist-info/scm_file_list.json +341 -0
  48. djcrud-0.2.0.dist-info/scm_version.json +8 -0
  49. djcrud-0.2.0.dist-info/top_level.txt +10 -0
  50. djcrud_api/__init__.py +0 -0
  51. djcrud_api/apps.py +10 -0
  52. djcrud_api/djcrud.py +5 -0
  53. djcrud_api/middleware.py +53 -0
  54. djcrud_api/migrations/0001_initial.py +50 -0
  55. djcrud_api/migrations/0002_rename_from_djmvc_swagger.py +24 -0
  56. djcrud_api/migrations/0003_rename_from_djmvc_api.py +37 -0
  57. djcrud_api/migrations/__init__.py +0 -0
  58. djcrud_api/models.py +71 -0
  59. djcrud_api/static/djcrud_api/swagger-ui-bundle.js +3 -0
  60. djcrud_api/static/djcrud_api/swagger-ui-standalone-preset.js +3 -0
  61. djcrud_api/static/djcrud_api/swagger-ui.css +4 -0
  62. djcrud_api/templates/djcrud_api/api.html +54 -0
  63. djcrud_api/views.py +239 -0
  64. djcrud_auth/__init__.py +0 -0
  65. djcrud_auth/admin.py +3 -0
  66. djcrud_auth/apps.py +5 -0
  67. djcrud_auth/djcrud.py +8 -0
  68. djcrud_auth/locale/en/LC_MESSAGES/django.mo +0 -0
  69. djcrud_auth/locale/en/LC_MESSAGES/django.po +75 -0
  70. djcrud_auth/locale/fr/LC_MESSAGES/django.mo +0 -0
  71. djcrud_auth/locale/fr/LC_MESSAGES/django.po +75 -0
  72. djcrud_auth/models.py +3 -0
  73. djcrud_auth/tests.py +3 -0
  74. djcrud_auth/views.py +312 -0
  75. djcrud_bulma/__init__.py +0 -0
  76. djcrud_bulma/admin.py +3 -0
  77. djcrud_bulma/apps.py +14 -0
  78. djcrud_bulma/locale/en/LC_MESSAGES/django.mo +0 -0
  79. djcrud_bulma/locale/en/LC_MESSAGES/django.po +123 -0
  80. djcrud_bulma/locale/fr/LC_MESSAGES/django.mo +0 -0
  81. djcrud_bulma/locale/fr/LC_MESSAGES/django.po +123 -0
  82. djcrud_bulma/models.py +3 -0
  83. djcrud_bulma/static/djcrud_bulma/css/style.css +259 -0
  84. djcrud_bulma/static/djcrud_bulma/js/filter-sidebar.js +121 -0
  85. djcrud_bulma/static/djcrud_bulma/js/filter-sidebar.test.js +147 -0
  86. djcrud_bulma/static/djcrud_bulma/js/form-focus.js +45 -0
  87. djcrud_bulma/static/djcrud_bulma/js/form-focus.test.js +93 -0
  88. djcrud_bulma/static/djcrud_bulma/js/hamburger.js +73 -0
  89. djcrud_bulma/static/djcrud_bulma/js/hamburger.test.js +208 -0
  90. djcrud_bulma/static/djcrud_bulma/js/list-action-bar.js +278 -0
  91. djcrud_bulma/static/djcrud_bulma/js/list-action-bar.test.js +187 -0
  92. djcrud_bulma/static/djcrud_bulma/js/nav-config.js +9 -0
  93. djcrud_bulma/static/djcrud_bulma/js/nav-config.test.js +32 -0
  94. djcrud_bulma/static/djcrud_bulma/js/toast.js +19 -0
  95. djcrud_bulma/static/djcrud_bulma/js/toast.test.js +89 -0
  96. djcrud_bulma/templates/djcrud/400.html +1 -0
  97. djcrud_bulma/templates/djcrud/403.html +1 -0
  98. djcrud_bulma/templates/djcrud/404.html +1 -0
  99. djcrud_bulma/templates/djcrud/500.html +1 -0
  100. djcrud_bulma/templates/djcrud/_actions_column.html +40 -0
  101. djcrud_bulma/templates/djcrud/_checkbox_column.html +3 -0
  102. djcrud_bulma/templates/djcrud/_checkbox_header.html +3 -0
  103. djcrud_bulma/templates/djcrud/_deletion_preview.html +29 -0
  104. djcrud_bulma/templates/djcrud/_error.html +6 -0
  105. djcrud_bulma/templates/djcrud/_filter.html +26 -0
  106. djcrud_bulma/templates/djcrud/_flashes.html +15 -0
  107. djcrud_bulma/templates/djcrud/_navbar_burger.html +6 -0
  108. djcrud_bulma/templates/djcrud/_object_list.html +21 -0
  109. djcrud_bulma/templates/djcrud/_pagination.html +83 -0
  110. djcrud_bulma/templates/djcrud/_tables2.html +81 -0
  111. djcrud_bulma/templates/djcrud/base.html +111 -0
  112. djcrud_bulma/templates/djcrud/base_immersive.html +9 -0
  113. djcrud_bulma/templates/djcrud/detail.html +46 -0
  114. djcrud_bulma/templates/djcrud/detaillist.html +69 -0
  115. djcrud_bulma/templates/djcrud/error.html +7 -0
  116. djcrud_bulma/templates/djcrud/form.html +74 -0
  117. djcrud_bulma/templates/djcrud/form_delete.html +40 -0
  118. djcrud_bulma/templates/djcrud/home.html +32 -0
  119. djcrud_bulma/templates/djcrud/list.html +67 -0
  120. djcrud_bulma/templates/djcrud/test_immersive.html +3 -0
  121. djcrud_bulma/tests.py +3 -0
  122. djcrud_bulma/views.py +3 -0
  123. djcrud_dal/__init__.py +0 -0
  124. djcrud_dal/apps.py +26 -0
  125. djcrud_dal/hooks.py +62 -0
  126. djcrud_dal/lookup.py +23 -0
  127. djcrud_dal/models.py +5 -0
  128. djcrud_dal/views.py +31 -0
  129. djcrud_dal_topbar/__init__.py +0 -0
  130. djcrud_dal_topbar/apps.py +20 -0
  131. djcrud_dal_topbar/djcrud.py +5 -0
  132. djcrud_dal_topbar/lookup.py +87 -0
  133. djcrud_dal_topbar/static/djcrud_dal_topbar/css/site-search.css +27 -0
  134. djcrud_dal_topbar/static/djcrud_dal_topbar/js/site-search.js +6 -0
  135. djcrud_dal_topbar/templates/djcrud/_site_search.html +17 -0
  136. djcrud_dal_topbar/views.py +75 -0
  137. djcrud_debug/__init__.py +0 -0
  138. djcrud_debug/apps.py +5 -0
  139. djcrud_debug/djcrud.py +5 -0
  140. djcrud_debug/models.py +56 -0
  141. djcrud_debug/routing_debug.py +160 -0
  142. djcrud_debug/views.py +44 -0
  143. djcrud_example/__init__.py +0 -0
  144. djcrud_example/asgi.py +16 -0
  145. djcrud_example/example_urls.py +35 -0
  146. djcrud_example/migrations/0001_initial.py +132 -0
  147. djcrud_example/migrations/0002_test_users.py +27 -0
  148. djcrud_example/migrations/__init__.py +0 -0
  149. djcrud_example/models.py +5 -0
  150. djcrud_example/settings.py +164 -0
  151. djcrud_example/stage0/__init__.py +0 -0
  152. djcrud_example/stage0/admin.py +3 -0
  153. djcrud_example/stage0/apps.py +5 -0
  154. djcrud_example/stage0/djcrud.py +14 -0
  155. djcrud_example/stage0/migrations/0001_initial.py +26 -0
  156. djcrud_example/stage0/migrations/__init__.py +0 -0
  157. djcrud_example/stage0/models.py +8 -0
  158. djcrud_example/stage0/tests.py +3 -0
  159. djcrud_example/stage1/__init__.py +0 -0
  160. djcrud_example/stage1/admin.py +0 -0
  161. djcrud_example/stage1/apps.py +5 -0
  162. djcrud_example/stage1/djcrud.py +22 -0
  163. djcrud_example/stage1/migrations/__init__.py +0 -0
  164. djcrud_example/stage1/models.py +0 -0
  165. djcrud_example/stage1/tests.py +0 -0
  166. djcrud_example/stage2/__init__.py +0 -0
  167. djcrud_example/stage2/admin.py +0 -0
  168. djcrud_example/stage2/apps.py +5 -0
  169. djcrud_example/stage2/djcrud.py +18 -0
  170. djcrud_example/stage2/migrations/0001_initial.py +40 -0
  171. djcrud_example/stage2/migrations/__init__.py +0 -0
  172. djcrud_example/stage2/models.py +14 -0
  173. djcrud_example/stage2/tests.py +0 -0
  174. djcrud_example/stage3/__init__.py +0 -0
  175. djcrud_example/stage3/admin.py +5 -0
  176. djcrud_example/stage3/apps.py +6 -0
  177. djcrud_example/stage3/backends.py +35 -0
  178. djcrud_example/stage3/djcrud.py +28 -0
  179. djcrud_example/stage3/migrations/0001_initial.py +44 -0
  180. djcrud_example/stage3/migrations/__init__.py +0 -0
  181. djcrud_example/stage3/models.py +21 -0
  182. djcrud_example/stage3/tests.py +0 -0
  183. djcrud_example/stage4/__init__.py +0 -0
  184. djcrud_example/stage4/admin.py +0 -0
  185. djcrud_example/stage4/apps.py +5 -0
  186. djcrud_example/stage4/djcrud.py +29 -0
  187. djcrud_example/stage4/migrations/0001_initial.py +30 -0
  188. djcrud_example/stage4/migrations/__init__.py +0 -0
  189. djcrud_example/stage4/models.py +10 -0
  190. djcrud_example/stage4/tests.py +0 -0
  191. djcrud_example/stage5/__init__.py +0 -0
  192. djcrud_example/stage5/admin.py +0 -0
  193. djcrud_example/stage5/apps.py +5 -0
  194. djcrud_example/stage5/djcrud.py +45 -0
  195. djcrud_example/stage5/migrations/0001_initial.py +29 -0
  196. djcrud_example/stage5/migrations/__init__.py +0 -0
  197. djcrud_example/stage5/models.py +9 -0
  198. djcrud_example/stage5/tests.py +0 -0
  199. djcrud_example/urls.py +13 -0
  200. djcrud_example/wsgi.py +16 -0
  201. djcrud_history/__init__.py +0 -0
  202. djcrud_history/apps.py +15 -0
  203. djcrud_history/djcrud.py +5 -0
  204. djcrud_history/log.py +21 -0
  205. djcrud_history/models.py +5 -0
  206. djcrud_history/views.py +76 -0
  207. djcrud_triggers/__init__.py +1 -0
  208. djcrud_triggers/apps.py +22 -0
  209. djcrud_triggers/hooks/__init__.py +12 -0
  210. djcrud_triggers/hooks/events.py +40 -0
  211. djcrud_triggers/hooks/registry.py +135 -0
  212. 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
@@ -0,0 +1,3 @@
1
+ from django.contrib import admin
2
+
3
+ # Register your models here.
djcrud/apps.py ADDED
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjcrudConfig(AppConfig):
5
+ name = "djcrud"
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))
@@ -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