hunt-framework 0.2.2__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 (189) hide show
  1. hunt/__init__.py +15 -0
  2. hunt/admin/__init__.py +53 -0
  3. hunt/admin/action.py +58 -0
  4. hunt/admin/application.py +211 -0
  5. hunt/admin/console/__init__.py +0 -0
  6. hunt/admin/console/make_admin_resource.py +140 -0
  7. hunt/admin/controllers/__init__.py +0 -0
  8. hunt/admin/controllers/action.py +87 -0
  9. hunt/admin/controllers/dashboard.py +21 -0
  10. hunt/admin/controllers/resource.py +298 -0
  11. hunt/admin/controllers/search.py +53 -0
  12. hunt/admin/field.py +123 -0
  13. hunt/admin/fields/__init__.py +30 -0
  14. hunt/admin/fields/badge.py +25 -0
  15. hunt/admin/fields/belongs_to.py +21 -0
  16. hunt/admin/fields/boolean.py +15 -0
  17. hunt/admin/fields/datetime_.py +58 -0
  18. hunt/admin/fields/has_many.py +22 -0
  19. hunt/admin/fields/image.py +51 -0
  20. hunt/admin/fields/number.py +43 -0
  21. hunt/admin/fields/richtext.py +71 -0
  22. hunt/admin/fields/select.py +30 -0
  23. hunt/admin/fields/text.py +24 -0
  24. hunt/admin/fields/textarea.py +16 -0
  25. hunt/admin/filter.py +86 -0
  26. hunt/admin/metrics/__init__.py +5 -0
  27. hunt/admin/metrics/partition.py +44 -0
  28. hunt/admin/metrics/trend.py +33 -0
  29. hunt/admin/metrics/value.py +64 -0
  30. hunt/admin/middleware/__init__.py +0 -0
  31. hunt/admin/middleware/gate.py +42 -0
  32. hunt/admin/navigation.py +79 -0
  33. hunt/admin/resource.py +163 -0
  34. hunt/admin/templates/admin/dashboard.html +87 -0
  35. hunt/admin/templates/admin/layout.html +268 -0
  36. hunt/admin/templates/admin/resource/_form.html +197 -0
  37. hunt/admin/templates/admin/resource/create.html +79 -0
  38. hunt/admin/templates/admin/resource/edit.html +84 -0
  39. hunt/admin/templates/admin/resource/index.html +388 -0
  40. hunt/admin/templates/admin/resource/show.html +167 -0
  41. hunt/application.py +123 -0
  42. hunt/auth/__init__.py +6 -0
  43. hunt/auth/gate.py +155 -0
  44. hunt/auth/manager.py +261 -0
  45. hunt/auth/passwords.py +125 -0
  46. hunt/auth/verification.py +61 -0
  47. hunt/cache/__init__.py +3 -0
  48. hunt/cache/manager.py +171 -0
  49. hunt/config/__init__.py +4 -0
  50. hunt/config/loader.py +28 -0
  51. hunt/config/repository.py +33 -0
  52. hunt/console/__init__.py +3 -0
  53. hunt/console/command.py +9 -0
  54. hunt/console/commands/__init__.py +0 -0
  55. hunt/console/commands/cache.py +35 -0
  56. hunt/console/commands/config_cache.py +62 -0
  57. hunt/console/commands/db/__init__.py +0 -0
  58. hunt/console/commands/db/seed.py +67 -0
  59. hunt/console/commands/key_generate.py +33 -0
  60. hunt/console/commands/make/__init__.py +11 -0
  61. hunt/console/commands/make/command.py +44 -0
  62. hunt/console/commands/make/controller.py +96 -0
  63. hunt/console/commands/make/event.py +29 -0
  64. hunt/console/commands/make/factory.py +37 -0
  65. hunt/console/commands/make/job.py +37 -0
  66. hunt/console/commands/make/listener.py +48 -0
  67. hunt/console/commands/make/mail.py +52 -0
  68. hunt/console/commands/make/middleware.py +33 -0
  69. hunt/console/commands/make/migration.py +92 -0
  70. hunt/console/commands/make/model.py +54 -0
  71. hunt/console/commands/make/notification.py +45 -0
  72. hunt/console/commands/make/observer.py +70 -0
  73. hunt/console/commands/make/policy.py +59 -0
  74. hunt/console/commands/make/request.py +37 -0
  75. hunt/console/commands/make/resource.py +46 -0
  76. hunt/console/commands/make/rule.py +39 -0
  77. hunt/console/commands/make/seeder.py +30 -0
  78. hunt/console/commands/migrate.py +84 -0
  79. hunt/console/commands/new.py +1029 -0
  80. hunt/console/commands/queue_failed.py +74 -0
  81. hunt/console/commands/queue_table.py +54 -0
  82. hunt/console/commands/queue_work.py +133 -0
  83. hunt/console/commands/route_list.py +32 -0
  84. hunt/console/commands/schedule_list.py +42 -0
  85. hunt/console/commands/schedule_run.py +40 -0
  86. hunt/console/commands/serve.py +27 -0
  87. hunt/console/commands/storage_link.py +28 -0
  88. hunt/console/commands/tinker.py +31 -0
  89. hunt/console/commands/upgrade.py +213 -0
  90. hunt/console/commands/view_cache.py +42 -0
  91. hunt/console/kernel.py +105 -0
  92. hunt/container/__init__.py +5 -0
  93. hunt/container/container.py +118 -0
  94. hunt/container/facade.py +40 -0
  95. hunt/container/provider.py +17 -0
  96. hunt/database/__init__.py +5 -0
  97. hunt/database/connection.py +100 -0
  98. hunt/database/factory.py +82 -0
  99. hunt/database/model.py +394 -0
  100. hunt/database/query_builder.py +545 -0
  101. hunt/database/relations/__init__.py +6 -0
  102. hunt/database/relations/belongs_to.py +45 -0
  103. hunt/database/relations/belongs_to_many.py +134 -0
  104. hunt/database/relations/has_many.py +47 -0
  105. hunt/database/relations/has_one.py +43 -0
  106. hunt/database/schema/__init__.py +5 -0
  107. hunt/database/schema/blueprint.py +360 -0
  108. hunt/database/schema/builder.py +250 -0
  109. hunt/database/schema/migration.py +139 -0
  110. hunt/database/seeder.py +13 -0
  111. hunt/events/__init__.py +5 -0
  112. hunt/events/dispatcher.py +59 -0
  113. hunt/events/provider.py +103 -0
  114. hunt/events/queued.py +89 -0
  115. hunt/exceptions/__init__.py +0 -0
  116. hunt/exceptions/handler.py +96 -0
  117. hunt/http/__init__.py +32 -0
  118. hunt/http/client.py +383 -0
  119. hunt/http/controller.py +37 -0
  120. hunt/http/kernel.py +287 -0
  121. hunt/http/middleware/__init__.py +15 -0
  122. hunt/http/middleware/authenticate.py +21 -0
  123. hunt/http/middleware/cors.py +64 -0
  124. hunt/http/middleware/csrf.py +45 -0
  125. hunt/http/middleware/session.py +79 -0
  126. hunt/http/middleware/throttle.py +55 -0
  127. hunt/http/middleware/verified.py +20 -0
  128. hunt/http/request.py +373 -0
  129. hunt/http/response.py +206 -0
  130. hunt/http/route.py +65 -0
  131. hunt/http/router.py +139 -0
  132. hunt/log/__init__.py +3 -0
  133. hunt/log/manager.py +80 -0
  134. hunt/mail/__init__.py +5 -0
  135. hunt/mail/mailable.py +130 -0
  136. hunt/mail/manager.py +312 -0
  137. hunt/mail/message.py +71 -0
  138. hunt/notifications/__init__.py +5 -0
  139. hunt/notifications/channels/__init__.py +0 -0
  140. hunt/notifications/channels/database.py +50 -0
  141. hunt/notifications/channels/mail.py +35 -0
  142. hunt/notifications/fake.py +81 -0
  143. hunt/notifications/notifiable.py +144 -0
  144. hunt/notifications/notification.py +53 -0
  145. hunt/queue/__init__.py +4 -0
  146. hunt/queue/drivers/__init__.py +0 -0
  147. hunt/queue/drivers/database.py +128 -0
  148. hunt/queue/drivers/redis.py +131 -0
  149. hunt/queue/drivers/sync.py +37 -0
  150. hunt/queue/job.py +40 -0
  151. hunt/queue/manager.py +44 -0
  152. hunt/scheduling/__init__.py +3 -0
  153. hunt/scheduling/cron.py +52 -0
  154. hunt/scheduling/scheduler.py +345 -0
  155. hunt/security/__init__.py +0 -0
  156. hunt/security/signing.py +42 -0
  157. hunt/session/__init__.py +3 -0
  158. hunt/session/store.py +144 -0
  159. hunt/storage/__init__.py +5 -0
  160. hunt/storage/local.py +221 -0
  161. hunt/storage/manager.py +59 -0
  162. hunt/storage/s3.py +121 -0
  163. hunt/support/__init__.py +33 -0
  164. hunt/support/collection.py +171 -0
  165. hunt/support/helpers.py +152 -0
  166. hunt/support/str.py +176 -0
  167. hunt/testing/__init__.py +4 -0
  168. hunt/testing/fakes.py +186 -0
  169. hunt/testing/test_case.py +278 -0
  170. hunt/translation/__init__.py +4 -0
  171. hunt/translation/provider.py +21 -0
  172. hunt/translation/translator.py +210 -0
  173. hunt/validation/__init__.py +4 -0
  174. hunt/validation/form_request.py +38 -0
  175. hunt/validation/rules.py +521 -0
  176. hunt/validation/validator.py +137 -0
  177. hunt/view/__init__.py +4 -0
  178. hunt/view/directives.py +568 -0
  179. hunt/view/factory.py +206 -0
  180. hunt/views/auth/forgot_password.html +25 -0
  181. hunt/views/auth/layout.html +39 -0
  182. hunt/views/auth/login.html +35 -0
  183. hunt/views/auth/register.html +33 -0
  184. hunt/views/auth/reset_password.html +29 -0
  185. hunt_framework-0.2.2.dist-info/METADATA +567 -0
  186. hunt_framework-0.2.2.dist-info/RECORD +189 -0
  187. hunt_framework-0.2.2.dist-info/WHEEL +4 -0
  188. hunt_framework-0.2.2.dist-info/entry_points.txt +2 -0
  189. hunt_framework-0.2.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hunt.http.request import Request
6
+ from hunt.http.response import HttpException, RedirectResponse, Response
7
+ from hunt.validation.validator import Validator
8
+
9
+
10
+ def _get_resource(resource_key: str) -> Any:
11
+ from hunt.admin.application import Admin
12
+
13
+ resource_cls = Admin.find_resource(resource_key)
14
+ if resource_cls is None:
15
+ raise HttpException(404, "Resource not found.")
16
+ return resource_cls()
17
+
18
+
19
+ def _get_instance(resource: Any, id: str) -> Any:
20
+ try:
21
+ instance = resource.model.find(id)
22
+ except Exception:
23
+ instance = None
24
+ if instance is None:
25
+ raise HttpException(404, "Record not found.")
26
+ return instance
27
+
28
+
29
+ def _flash_and_redirect(request: Request, key: str, message: str, url: str) -> RedirectResponse:
30
+ store = getattr(request, "_session", None)
31
+ if store is not None:
32
+ store.flash(key, message)
33
+ return RedirectResponse(url)
34
+
35
+
36
+ def index(request: Request, resource_key: str) -> Response:
37
+ from hunt.admin.application import Admin
38
+
39
+ resource = _get_resource(resource_key)
40
+ if not resource.can_view_any(request):
41
+ raise HttpException(403, "Forbidden.")
42
+
43
+ try:
44
+ page = int(request.query("page", "1"))
45
+ except (ValueError, TypeError):
46
+ page = 1
47
+
48
+ _options = resource.per_page_options or [10, 25, 50, 100]
49
+ _default = resource.per_page if resource.per_page in _options else _options[0]
50
+ try:
51
+ per_page = int(request.query("per_page", str(_default)))
52
+ if per_page not in _options:
53
+ per_page = _default
54
+ except (ValueError, TypeError):
55
+ per_page = _default
56
+
57
+ query = resource.index_query(request)
58
+ paginate_result = query.paginate(per_page, page)
59
+
60
+ index_fields = [f for f in resource.fields() if f._show_on_index]
61
+ ctx = Admin._base_context(request)
62
+ ctx.update(
63
+ {
64
+ "title": resource.get_label_plural(),
65
+ "resource": resource,
66
+ "resource_key": resource_key,
67
+ "items": paginate_result["data"],
68
+ "pagination": paginate_result,
69
+ "fields": index_fields,
70
+ "search": request.query("search", ""),
71
+ "sort": request.query("sort", ""),
72
+ "dir": request.query("dir", "desc"),
73
+ "filters": resource.filters(),
74
+ "actions": resource.actions(),
75
+ "metrics": [],
76
+ "per_page": per_page,
77
+ "per_page_options": _options,
78
+ "can_create": resource.can_create(request),
79
+ "can_update": resource.can_update(request),
80
+ "can_delete": resource.can_delete(request),
81
+ }
82
+ )
83
+
84
+ # Calculate per-resource metrics
85
+ for card in resource.metrics():
86
+ try:
87
+ data = card.calculate()
88
+ data["metric_type"] = card.metric_type
89
+ ctx["metrics"].append(data)
90
+ except Exception:
91
+ pass
92
+
93
+ return Admin._render("admin/resource/index.html", ctx)
94
+
95
+
96
+ def show(request: Request, resource_key: str, id: str) -> Response:
97
+ from hunt.admin.application import Admin
98
+ from hunt.admin.fields.has_many import HasMany
99
+
100
+ resource = _get_resource(resource_key)
101
+ if not resource.can_view_any(request):
102
+ raise HttpException(403, "Forbidden.")
103
+
104
+ instance = _get_instance(resource, id)
105
+ detail_fields = [f for f in resource.fields() if f._show_on_detail]
106
+ has_many_panels = [f for f in resource.fields() if isinstance(f, HasMany)]
107
+
108
+ related_data = {}
109
+ for panel in has_many_panels:
110
+ try:
111
+ related_resource_inst = panel.related_resource_class()
112
+ fk = panel.foreign_key or f"{type(instance).__name__.lower()}_id"
113
+ rel_query = related_resource_inst.model.query().where(fk, instance._attributes.get("id"))
114
+ related_data[panel.attribute] = rel_query.limit(20).get()
115
+ except Exception:
116
+ related_data[panel.attribute] = []
117
+
118
+ ctx = Admin._base_context(request)
119
+ ctx.update(
120
+ {
121
+ "title": resource.title(instance),
122
+ "resource": resource,
123
+ "resource_key": resource_key,
124
+ "instance": instance,
125
+ "fields": detail_fields,
126
+ "has_many_panels": has_many_panels,
127
+ "related_data": related_data,
128
+ "record_id": id,
129
+ "can_update": resource.can_update(request, instance),
130
+ "can_delete": resource.can_delete(request, instance),
131
+ }
132
+ )
133
+ return Admin._render("admin/resource/show.html", ctx)
134
+
135
+
136
+ def create(request: Request, resource_key: str) -> Response:
137
+ from hunt.admin.application import Admin
138
+
139
+ resource = _get_resource(resource_key)
140
+ if not resource.can_create(request):
141
+ raise HttpException(403, "Forbidden.")
142
+
143
+ create_fields = [f for f in resource.fields() if f._show_on_create]
144
+ ctx = Admin._base_context(request)
145
+ ctx.update(
146
+ {
147
+ "title": f"Create {resource.get_label()}",
148
+ "resource": resource,
149
+ "resource_key": resource_key,
150
+ "fields": create_fields,
151
+ }
152
+ )
153
+ return Admin._render("admin/resource/create.html", ctx)
154
+
155
+
156
+ def _collect_data(request: Request, fields: list) -> dict:
157
+ """Merge form text values with uploaded files for the given field list."""
158
+ from hunt.admin.fields.image import Image as ImageField
159
+ from hunt.validation.validator import ValidationException
160
+
161
+ allowed_attrs = {f.attribute for f in fields if not f._readonly}
162
+ raw_data = request.all()
163
+ data = {k: v for k, v in raw_data.items() if k in allowed_attrs}
164
+ size_errors: dict[str, list[str]] = {}
165
+ for field in fields:
166
+ if isinstance(field, ImageField) and not field._readonly:
167
+ uploaded = request.file(field.attribute)
168
+ if uploaded is not None and uploaded.size > 0:
169
+ if uploaded.size > field._max_kb * 1024:
170
+ size_errors[field.attribute] = [f"The file may not be greater than {field._max_kb} KB."]
171
+ else:
172
+ data[field.attribute] = uploaded
173
+ if size_errors:
174
+ raise ValidationException(size_errors)
175
+ return data
176
+
177
+
178
+ def _store_image_fields(data: dict, fields: list) -> dict:
179
+ """Replace UploadedFile values with stored paths; skip if no file submitted."""
180
+ from hunt.admin.fields.image import Image as ImageField
181
+ from hunt.http.request import UploadedFile
182
+
183
+ result = {}
184
+ field_map = {f.attribute: f for f in fields if not f._readonly}
185
+ for key, value in data.items():
186
+ if isinstance(value, UploadedFile):
187
+ field = field_map.get(key)
188
+ disk = field._disk if field and isinstance(field, ImageField) else "public"
189
+ path = field._path if field and isinstance(field, ImageField) else "uploads"
190
+ result[key] = value.store(path, disk)
191
+ else:
192
+ result[key] = value
193
+ return result
194
+
195
+
196
+ def store(request: Request, resource_key: str) -> Response:
197
+ from hunt.admin.application import Admin
198
+
199
+ resource = _get_resource(resource_key)
200
+ if not resource.can_create(request):
201
+ raise HttpException(403, "Forbidden.")
202
+
203
+ create_fields = [f for f in resource.fields() if f._show_on_create]
204
+ rules = {f.attribute: "|".join(f._rules) for f in create_fields if f._rules}
205
+ data = _collect_data(request, create_fields)
206
+
207
+ if rules:
208
+ Validator.make(data, rules).validate()
209
+
210
+ stored = _store_image_fields(data, create_fields)
211
+ resource.model.create(stored)
212
+
213
+ return _flash_and_redirect(
214
+ request,
215
+ "admin_success",
216
+ f"{resource.get_label()} created successfully.",
217
+ f"{Admin.prefix}/resources/{resource_key}",
218
+ )
219
+
220
+
221
+ def edit(request: Request, resource_key: str, id: str) -> Response:
222
+ from hunt.admin.application import Admin
223
+
224
+ resource = _get_resource(resource_key)
225
+ instance = _get_instance(resource, id)
226
+ if not resource.can_update(request, instance):
227
+ raise HttpException(403, "Forbidden.")
228
+
229
+ edit_fields = [f for f in resource.fields() if f._show_on_edit]
230
+ ctx = Admin._base_context(request)
231
+ ctx.update(
232
+ {
233
+ "title": f"Edit {resource.title(instance)}",
234
+ "resource": resource,
235
+ "resource_key": resource_key,
236
+ "instance": instance,
237
+ "fields": edit_fields,
238
+ "old": ctx["old"] or instance._attributes,
239
+ "record_id": id,
240
+ }
241
+ )
242
+ return Admin._render("admin/resource/edit.html", ctx)
243
+
244
+
245
+ def update(request: Request, resource_key: str, id: str) -> Response:
246
+ from hunt.admin.application import Admin
247
+
248
+ resource = _get_resource(resource_key)
249
+ instance = _get_instance(resource, id)
250
+ if not resource.can_update(request, instance):
251
+ raise HttpException(403, "Forbidden.")
252
+
253
+ edit_fields = [f for f in resource.fields() if f._show_on_edit]
254
+ rules = {f.attribute: "|".join(f._rules) for f in edit_fields if f._rules}
255
+ data = _collect_data(request, edit_fields)
256
+
257
+ if rules:
258
+ Validator.make(data, rules).validate()
259
+
260
+ stored = _store_image_fields(data, edit_fields)
261
+ from hunt.http.request import UploadedFile
262
+
263
+ final = {k: v for k, v in stored.items() if not isinstance(v, UploadedFile)}
264
+ instance.fill(final)
265
+ instance.save()
266
+
267
+ return _flash_and_redirect(
268
+ request,
269
+ "admin_success",
270
+ f"{resource.get_label()} updated successfully.",
271
+ f"{Admin.prefix}/resources/{resource_key}/{id}",
272
+ )
273
+
274
+
275
+ def destroy(request: Request, resource_key: str, id: str) -> Response:
276
+ from hunt.admin.application import Admin
277
+
278
+ resource = _get_resource(resource_key)
279
+ instance = _get_instance(resource, id)
280
+ if not resource.can_delete(request, instance):
281
+ raise HttpException(403, "Forbidden.")
282
+
283
+ try:
284
+ instance.delete()
285
+ except Exception:
286
+ return _flash_and_redirect(
287
+ request,
288
+ "admin_error",
289
+ "Could not delete record. Please try again.",
290
+ f"{Admin.prefix}/resources/{resource_key}",
291
+ )
292
+
293
+ return _flash_and_redirect(
294
+ request,
295
+ "admin_success",
296
+ f"{resource.get_label()} deleted successfully.",
297
+ f"{Admin.prefix}/resources/{resource_key}",
298
+ )
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from hunt.http.request import Request
4
+ from hunt.http.response import JsonResponse
5
+
6
+
7
+ def index(request: Request) -> JsonResponse:
8
+ from hunt.admin.application import Admin
9
+
10
+ q = str(request.query("q", "") or "").strip()
11
+ if not q:
12
+ return JsonResponse({"groups": []})
13
+ q = q[:200] # cap to avoid pathological LIKE patterns
14
+ groups = []
15
+
16
+ for resource_cls in Admin._resources:
17
+ resource = resource_cls()
18
+ if not resource.search_columns:
19
+ continue
20
+
21
+ try:
22
+ query = resource.model.query()
23
+ first_col = resource.search_columns[0]
24
+ query = query.where(first_col, "LIKE", f"%{q}%")
25
+ for col in resource.search_columns[1:]:
26
+ try:
27
+ query = query.or_where(col, "LIKE", f"%{q}%")
28
+ except ValueError:
29
+ pass
30
+ items = query.limit(5).get()
31
+
32
+ if not items:
33
+ continue
34
+
35
+ groups.append(
36
+ {
37
+ "resource": resource_cls.get_label(),
38
+ "resource_key": resource_cls.slug(),
39
+ "resource_url": f"{Admin.prefix}/resources/{resource_cls.slug()}",
40
+ "items": [
41
+ {
42
+ "id": item._attributes.get("id"),
43
+ "title": resource.title(item),
44
+ "url": f"{Admin.prefix}/resources/{resource_cls.slug()}/{item._attributes.get('id')}",
45
+ }
46
+ for item in items
47
+ ],
48
+ }
49
+ )
50
+ except Exception:
51
+ continue
52
+
53
+ return JsonResponse({"groups": groups})
hunt/admin/field.py ADDED
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hunt.support.str import Str
6
+
7
+
8
+ class Field:
9
+ """Base field for AdminResource definitions."""
10
+
11
+ field_type: str = "text"
12
+
13
+ def __init__(self, name: str, attribute: str | None = None) -> None:
14
+ self.name = name
15
+ self.attribute = attribute if attribute is not None else Str.snake(name)
16
+ self.label = name
17
+ self.field_type = self.__class__.field_type
18
+
19
+ self._show_on_index: bool = True
20
+ self._show_on_detail: bool = True
21
+ self._show_on_create: bool = True
22
+ self._show_on_edit: bool = True
23
+ self._sortable: bool = False
24
+ self._readonly: bool = False
25
+ self._help_text: str = ""
26
+ self._nullable: bool = False
27
+ self._rules: list[str] = []
28
+ self._panel: str | None = None
29
+
30
+ # ------------------------------------------------------------------
31
+ # Fluent visibility / behaviour setters
32
+ # ------------------------------------------------------------------
33
+
34
+ def hide_from_index(self) -> Field:
35
+ self._show_on_index = False
36
+ return self
37
+
38
+ def hide_from_detail(self) -> Field:
39
+ self._show_on_detail = False
40
+ return self
41
+
42
+ def hide_from_forms(self) -> Field:
43
+ self._show_on_create = False
44
+ self._show_on_edit = False
45
+ return self
46
+
47
+ def hide_from_create(self) -> Field:
48
+ self._show_on_create = False
49
+ return self
50
+
51
+ def hide_from_edit(self) -> Field:
52
+ self._show_on_edit = False
53
+ return self
54
+
55
+ def only_on_index(self) -> Field:
56
+ self._show_on_detail = False
57
+ self._show_on_create = False
58
+ self._show_on_edit = False
59
+ return self
60
+
61
+ def only_on_forms(self) -> Field:
62
+ self._show_on_index = False
63
+ self._show_on_detail = False
64
+ return self
65
+
66
+ def only_on_detail(self) -> Field:
67
+ self._show_on_index = False
68
+ self._show_on_create = False
69
+ self._show_on_edit = False
70
+ return self
71
+
72
+ def show_on_index(self) -> Field:
73
+ self._show_on_index = True
74
+ return self
75
+
76
+ def show_on_detail(self) -> Field:
77
+ self._show_on_detail = True
78
+ return self
79
+
80
+ def show_on_create(self) -> Field:
81
+ self._show_on_create = True
82
+ return self
83
+
84
+ def show_on_edit(self) -> Field:
85
+ self._show_on_edit = True
86
+ return self
87
+
88
+ def sortable(self) -> Field:
89
+ self._sortable = True
90
+ return self
91
+
92
+ def readonly(self) -> Field:
93
+ self._readonly = True
94
+ return self
95
+
96
+ def help(self, text: str) -> Field:
97
+ self._help_text = text
98
+ return self
99
+
100
+ def rules(self, *r: str) -> Field:
101
+ self._rules = list(r)
102
+ return self
103
+
104
+ def nullable(self) -> Field:
105
+ self._nullable = True
106
+ return self
107
+
108
+ def panel(self, name: str) -> Field:
109
+ self._panel = name
110
+ return self
111
+
112
+ # ------------------------------------------------------------------
113
+ # Value resolution
114
+ # ------------------------------------------------------------------
115
+
116
+ def value_for(self, instance: Any) -> Any:
117
+ return instance._attributes.get(self.attribute)
118
+
119
+ def display_value(self, instance: Any) -> str:
120
+ val = self.value_for(instance)
121
+ if val is None:
122
+ return ""
123
+ return str(val)
@@ -0,0 +1,30 @@
1
+ from hunt.admin.fields.badge import Badge
2
+ from hunt.admin.fields.belongs_to import BelongsTo
3
+ from hunt.admin.fields.boolean import Boolean
4
+ from hunt.admin.fields.datetime_ import Date, DateTime
5
+ from hunt.admin.fields.has_many import HasMany
6
+ from hunt.admin.fields.image import Image
7
+ from hunt.admin.fields.number import Currency, Number
8
+ from hunt.admin.fields.richtext import RichText
9
+ from hunt.admin.fields.select import Select
10
+ from hunt.admin.fields.text import Email, Password, Slug, Text
11
+ from hunt.admin.fields.textarea import Textarea
12
+
13
+ __all__ = [
14
+ "Badge",
15
+ "BelongsTo",
16
+ "Boolean",
17
+ "Currency",
18
+ "Date",
19
+ "DateTime",
20
+ "Email",
21
+ "HasMany",
22
+ "Image",
23
+ "Number",
24
+ "Password",
25
+ "RichText",
26
+ "Select",
27
+ "Slug",
28
+ "Text",
29
+ "Textarea",
30
+ ]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hunt.admin.field import Field
6
+
7
+ _DEFAULT_COLOUR = "gray"
8
+
9
+
10
+ class Badge(Field):
11
+ field_type: str = "badge"
12
+
13
+ def __init__(
14
+ self,
15
+ name: str,
16
+ colour_map: dict | None = None,
17
+ attribute: str | None = None,
18
+ ) -> None:
19
+ super().__init__(name, attribute)
20
+ self._colour_map: dict = colour_map or {}
21
+
22
+ def get_colour(self, value: Any) -> str:
23
+ if value is None:
24
+ return _DEFAULT_COLOUR
25
+ return self._colour_map.get(str(value), _DEFAULT_COLOUR)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from hunt.admin.field import Field
4
+
5
+
6
+ class BelongsTo(Field):
7
+ field_type: str = "belongs_to"
8
+
9
+ def __init__(
10
+ self,
11
+ name: str,
12
+ related_resource_class: type,
13
+ attribute: str | None = None,
14
+ ) -> None:
15
+ super().__init__(name, attribute)
16
+ self.related_resource_class = related_resource_class
17
+ self._searchable: bool = False
18
+
19
+ def searchable(self) -> BelongsTo:
20
+ self._searchable = True
21
+ return self
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hunt.admin.field import Field
6
+
7
+
8
+ class Boolean(Field):
9
+ field_type: str = "boolean"
10
+
11
+ def display_value(self, instance: Any) -> str:
12
+ val = self.value_for(instance)
13
+ if val is None:
14
+ return ""
15
+ return "Yes" if val else "No"
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import Any
5
+
6
+ from hunt.admin.field import Field
7
+
8
+
9
+ def _parse_and_format(value: Any, fmt: str) -> str:
10
+ if value is None:
11
+ return ""
12
+ if isinstance(value, (datetime.datetime, datetime.date)):
13
+ return value.strftime(fmt)
14
+ if isinstance(value, (int, float)):
15
+ # Unix timestamp
16
+ try:
17
+ return datetime.datetime.fromtimestamp(value).strftime(fmt)
18
+ except (OSError, OverflowError, ValueError):
19
+ return str(value)
20
+ if isinstance(value, str):
21
+ # Try common ISO formats
22
+ for parse_fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
23
+ try:
24
+ return datetime.datetime.strptime(value, parse_fmt).strftime(fmt)
25
+ except ValueError:
26
+ continue
27
+ return value
28
+ return str(value)
29
+
30
+
31
+ class DateTime(Field):
32
+ field_type: str = "datetime"
33
+
34
+ def __init__(self, name: str, attribute: str | None = None) -> None:
35
+ super().__init__(name, attribute)
36
+ self._format: str = "%Y-%m-%d %H:%M"
37
+
38
+ def format(self, fmt: str) -> DateTime:
39
+ self._format = fmt
40
+ return self
41
+
42
+ def display_value(self, instance: Any) -> str:
43
+ return _parse_and_format(self.value_for(instance), self._format)
44
+
45
+
46
+ class Date(Field):
47
+ field_type: str = "date"
48
+
49
+ def __init__(self, name: str, attribute: str | None = None) -> None:
50
+ super().__init__(name, attribute)
51
+ self._format: str = "%Y-%m-%d"
52
+
53
+ def format(self, fmt: str) -> Date:
54
+ self._format = fmt
55
+ return self
56
+
57
+ def display_value(self, instance: Any) -> str:
58
+ return _parse_and_format(self.value_for(instance), self._format)
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from hunt.admin.field import Field
4
+
5
+
6
+ class HasMany(Field):
7
+ field_type: str = "has_many"
8
+
9
+ def __init__(
10
+ self,
11
+ name: str,
12
+ related_resource_class: type,
13
+ foreign_key: str | None = None,
14
+ attribute: str | None = None,
15
+ ) -> None:
16
+ super().__init__(name, attribute)
17
+ self.related_resource_class = related_resource_class
18
+ self.foreign_key = foreign_key
19
+ # HasMany panels are never displayed in these views
20
+ self.hide_from_index()
21
+ self.hide_from_create()
22
+ self.hide_from_edit()