df_site 0.1.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 (309) hide show
  1. df_site/__init__.py +1 -0
  2. df_site/__main__.py +37 -0
  3. df_site/admin.py +130 -0
  4. df_site/apps.py +57 -0
  5. df_site/components/__init__.py +1 -0
  6. df_site/components/base.py +82 -0
  7. df_site/components/detail.py +191 -0
  8. df_site/components/list.py +446 -0
  9. df_site/components/list_filters.py +74 -0
  10. df_site/components/registry.py +55 -0
  11. df_site/constants.py +71 -0
  12. df_site/context_processors.py +61 -0
  13. df_site/defaults.py +319 -0
  14. df_site/dynamic_settings.py +37 -0
  15. df_site/form_fields.py +138 -0
  16. df_site/management/__init__.py +1 -0
  17. df_site/management/commands/__init__.py +1 -0
  18. df_site/management/commands/add_image.py +104 -0
  19. df_site/management/commands/generate_favicon.py +47 -0
  20. df_site/middleware.py +20 -0
  21. df_site/migrations/0001_initial.py +220 -0
  22. df_site/migrations/0002_alter_alertribbon_message_alter_alertribbon_summary.py +23 -0
  23. df_site/migrations/__init__.py +0 -0
  24. df_site/model_fields.py +35 -0
  25. df_site/models.py +130 -0
  26. df_site/postman/__init__.py +1 -0
  27. df_site/postman/forms.py +38 -0
  28. df_site/postman/urls.py +75 -0
  29. df_site/postman/views.py +65 -0
  30. df_site/static/css/app.css +0 -0
  31. df_site/static/css/base.css +22208 -0
  32. df_site/static/css/ckeditor5.css +422 -0
  33. df_site/static/favicon/android-chrome-192x192.png +0 -0
  34. df_site/static/favicon/android-chrome-512x512.png +0 -0
  35. df_site/static/favicon/apple-touch-icon.png +0 -0
  36. df_site/static/favicon/favicon-16x16.png +0 -0
  37. df_site/static/favicon/favicon-32x32.png +0 -0
  38. df_site/static/favicon/favicon.ico +0 -0
  39. df_site/static/favicon/mstile-150x150.png +0 -0
  40. df_site/static/favicon/safari-pinned-tab.svg +46 -0
  41. df_site/static/images/accessibility.svg +1 -0
  42. df_site/static/images/align-bottom.svg +1 -0
  43. df_site/static/images/align-center.svg +1 -0
  44. df_site/static/images/align-justify.svg +1 -0
  45. df_site/static/images/align-left.svg +1 -0
  46. df_site/static/images/align-middle.svg +1 -0
  47. df_site/static/images/align-right.svg +1 -0
  48. df_site/static/images/align-top.svg +1 -0
  49. df_site/static/images/bold.svg +1 -0
  50. df_site/static/images/browse-files.svg +1 -0
  51. df_site/static/images/bulletedlist.svg +1 -0
  52. df_site/static/images/cancel.svg +1 -0
  53. df_site/static/images/caption.svg +1 -0
  54. df_site/static/images/check.svg +1 -0
  55. df_site/static/images/code.svg +1 -0
  56. df_site/static/images/codeblock.svg +1 -0
  57. df_site/static/images/cog.svg +1 -0
  58. df_site/static/images/color-palette.svg +1 -0
  59. df_site/static/images/color-tile-check.svg +1 -0
  60. df_site/static/images/drag-handle.svg +1 -0
  61. df_site/static/images/drag-indicator.svg +1 -0
  62. df_site/static/images/dropdown-arrow.svg +1 -0
  63. df_site/static/images/eraser.svg +1 -0
  64. df_site/static/images/file-arrow-up-solid.svg +1 -0
  65. df_site/static/images/find-replace.svg +1 -0
  66. df_site/static/images/font-background.svg +1 -0
  67. df_site/static/images/font-color.svg +1 -0
  68. df_site/static/images/font-family.svg +1 -0
  69. df_site/static/images/font-size.svg +1 -0
  70. df_site/static/images/heading1.svg +1 -0
  71. df_site/static/images/heading2.svg +1 -0
  72. df_site/static/images/heading3.svg +1 -0
  73. df_site/static/images/heading4.svg +1 -0
  74. df_site/static/images/heading5.svg +1 -0
  75. df_site/static/images/heading6.svg +1 -0
  76. df_site/static/images/history.svg +1 -0
  77. df_site/static/images/horizontalline.svg +1 -0
  78. df_site/static/images/html.svg +1 -0
  79. df_site/static/images/image-asset-manager.svg +1 -0
  80. df_site/static/images/image-upload.svg +1 -0
  81. df_site/static/images/image-url.svg +1 -0
  82. df_site/static/images/image.svg +1 -0
  83. df_site/static/images/importexport.svg +1 -0
  84. df_site/static/images/indent.svg +1 -0
  85. df_site/static/images/italic.svg +1 -0
  86. df_site/static/images/link.svg +1 -0
  87. df_site/static/images/liststylecircle.svg +1 -0
  88. df_site/static/images/liststyledecimal.svg +1 -0
  89. df_site/static/images/liststyledecimalleadingzero.svg +1 -0
  90. df_site/static/images/liststyledisc.svg +1 -0
  91. df_site/static/images/liststylelowerlatin.svg +1 -0
  92. df_site/static/images/liststylelowerroman.svg +1 -0
  93. df_site/static/images/liststylesquare.svg +1 -0
  94. df_site/static/images/liststyleupperlatin.svg +1 -0
  95. df_site/static/images/liststyleupperroman.svg +1 -0
  96. df_site/static/images/loupe.svg +1 -0
  97. df_site/static/images/low-vision.svg +1 -0
  98. df_site/static/images/marker.svg +1 -0
  99. df_site/static/images/media-placeholder.svg +1 -0
  100. df_site/static/images/media.svg +1 -0
  101. df_site/static/images/next-arrow.svg +1 -0
  102. df_site/static/images/numberedlist.svg +1 -0
  103. df_site/static/images/object-center.svg +1 -0
  104. df_site/static/images/object-full-width.svg +1 -0
  105. df_site/static/images/object-inline-left.svg +1 -0
  106. df_site/static/images/object-inline-right.svg +1 -0
  107. df_site/static/images/object-inline.svg +1 -0
  108. df_site/static/images/object-left.svg +1 -0
  109. df_site/static/images/object-right.svg +1 -0
  110. df_site/static/images/object-size-custom.svg +1 -0
  111. df_site/static/images/object-size-full.svg +1 -0
  112. df_site/static/images/object-size-large.svg +1 -0
  113. df_site/static/images/object-size-medium.svg +1 -0
  114. df_site/static/images/object-size-small.svg +1 -0
  115. df_site/static/images/outdent.svg +1 -0
  116. df_site/static/images/paragraph.svg +1 -0
  117. df_site/static/images/pen.svg +1 -0
  118. df_site/static/images/pencil.svg +1 -0
  119. df_site/static/images/pilcrow.svg +1 -0
  120. df_site/static/images/plus.svg +1 -0
  121. df_site/static/images/previous-arrow.svg +1 -0
  122. df_site/static/images/project-logo.svg +1 -0
  123. df_site/static/images/quote.svg +1 -0
  124. df_site/static/images/redo.svg +1 -0
  125. df_site/static/images/remove-format.svg +1 -0
  126. df_site/static/images/return-arrow.svg +1 -0
  127. df_site/static/images/select-all.svg +1 -0
  128. df_site/static/images/show-blocks.svg +1 -0
  129. df_site/static/images/source-editing.svg +1 -0
  130. df_site/static/images/specialcharacters.svg +1 -0
  131. df_site/static/images/strikethrough.svg +1 -0
  132. df_site/static/images/subscript.svg +1 -0
  133. df_site/static/images/superscript.svg +1 -0
  134. df_site/static/images/table-cell-properties.svg +1 -0
  135. df_site/static/images/table-column.svg +1 -0
  136. df_site/static/images/table-merge-cell.svg +1 -0
  137. df_site/static/images/table-properties.svg +1 -0
  138. df_site/static/images/table-row.svg +1 -0
  139. df_site/static/images/table.svg +1 -0
  140. df_site/static/images/text-alternative.svg +1 -0
  141. df_site/static/images/text.svg +1 -0
  142. df_site/static/images/three-vertical-dots.svg +1 -0
  143. df_site/static/images/todolist.svg +1 -0
  144. df_site/static/images/underline.svg +1 -0
  145. df_site/static/images/undo.svg +1 -0
  146. df_site/static/images/unlink.svg +1 -0
  147. df_site/static/js/app.js +98 -0
  148. df_site/static/js/app.js.map +1 -0
  149. df_site/static/js/base.js +161181 -0
  150. df_site/static/js/base.js.map +1 -0
  151. df_site/static/translations/af.js +1 -0
  152. df_site/static/translations/ar.js +1 -0
  153. df_site/static/translations/ast.js +1 -0
  154. df_site/static/translations/az.js +1 -0
  155. df_site/static/translations/bg.js +1 -0
  156. df_site/static/translations/bn.js +1 -0
  157. df_site/static/translations/bs.js +1 -0
  158. df_site/static/translations/ca.js +1 -0
  159. df_site/static/translations/cs.js +1 -0
  160. df_site/static/translations/da.js +1 -0
  161. df_site/static/translations/de-ch.js +1 -0
  162. df_site/static/translations/de.js +1 -0
  163. df_site/static/translations/el.js +1 -0
  164. df_site/static/translations/en-au.js +1 -0
  165. df_site/static/translations/en-gb.js +1 -0
  166. df_site/static/translations/en.js +1 -0
  167. df_site/static/translations/eo.js +1 -0
  168. df_site/static/translations/es-co.js +1 -0
  169. df_site/static/translations/es.js +1 -0
  170. df_site/static/translations/et.js +1 -0
  171. df_site/static/translations/eu.js +1 -0
  172. df_site/static/translations/fa.js +1 -0
  173. df_site/static/translations/fi.js +1 -0
  174. df_site/static/translations/gl.js +1 -0
  175. df_site/static/translations/gu.js +1 -0
  176. df_site/static/translations/he.js +1 -0
  177. df_site/static/translations/hi.js +1 -0
  178. df_site/static/translations/hr.js +1 -0
  179. df_site/static/translations/hu.js +1 -0
  180. df_site/static/translations/hy.js +1 -0
  181. df_site/static/translations/id.js +1 -0
  182. df_site/static/translations/it.js +1 -0
  183. df_site/static/translations/ja.js +1 -0
  184. df_site/static/translations/jv.js +1 -0
  185. df_site/static/translations/kk.js +1 -0
  186. df_site/static/translations/km.js +1 -0
  187. df_site/static/translations/kn.js +1 -0
  188. df_site/static/translations/ko.js +1 -0
  189. df_site/static/translations/ku.js +1 -0
  190. df_site/static/translations/lt.js +1 -0
  191. df_site/static/translations/lv.js +1 -0
  192. df_site/static/translations/ms.js +1 -0
  193. df_site/static/translations/nb.js +1 -0
  194. df_site/static/translations/ne.js +1 -0
  195. df_site/static/translations/nl.js +1 -0
  196. df_site/static/translations/no.js +1 -0
  197. df_site/static/translations/oc.js +1 -0
  198. df_site/static/translations/pl.js +1 -0
  199. df_site/static/translations/pt-br.js +1 -0
  200. df_site/static/translations/pt.js +1 -0
  201. df_site/static/translations/ro.js +1 -0
  202. df_site/static/translations/ru.js +1 -0
  203. df_site/static/translations/si.js +1 -0
  204. df_site/static/translations/sk.js +1 -0
  205. df_site/static/translations/sl.js +1 -0
  206. df_site/static/translations/sq.js +1 -0
  207. df_site/static/translations/sr-latn.js +1 -0
  208. df_site/static/translations/sr.js +1 -0
  209. df_site/static/translations/sv.js +1 -0
  210. df_site/static/translations/th.js +1 -0
  211. df_site/static/translations/ti.js +1 -0
  212. df_site/static/translations/tk.js +1 -0
  213. df_site/static/translations/tr.js +1 -0
  214. df_site/static/translations/tt.js +1 -0
  215. df_site/static/translations/ug.js +1 -0
  216. df_site/static/translations/uk.js +1 -0
  217. df_site/static/translations/ur.js +1 -0
  218. df_site/static/translations/uz.js +1 -0
  219. df_site/static/translations/vi.js +1 -0
  220. df_site/static/translations/zh-cn.js +1 -0
  221. df_site/static/translations/zh.js +1 -0
  222. df_site/static/webfonts/fa-brands-400.ttf +0 -0
  223. df_site/static/webfonts/fa-brands-400.woff2 +0 -0
  224. df_site/static/webfonts/fa-regular-400.ttf +0 -0
  225. df_site/static/webfonts/fa-regular-400.woff2 +0 -0
  226. df_site/static/webfonts/fa-solid-900.ttf +0 -0
  227. df_site/static/webfonts/fa-solid-900.woff2 +0 -0
  228. df_site/static/webfonts/fa-v4compatibility.ttf +0 -0
  229. df_site/static/webfonts/fa-v4compatibility.woff2 +0 -0
  230. df_site/templates/account/email.html +78 -0
  231. df_site/templates/account/password_change.html +28 -0
  232. df_site/templates/account/snippets/warn_no_email.html +6 -0
  233. df_site/templates/allauth/elements/alert.html +6 -0
  234. df_site/templates/allauth/elements/badge.html +4 -0
  235. df_site/templates/allauth/elements/button.html +14 -0
  236. df_site/templates/allauth/elements/button_group.html +5 -0
  237. df_site/templates/allauth/elements/field.html +72 -0
  238. df_site/templates/allauth/elements/fields.html +3 -0
  239. df_site/templates/allauth/elements/form.html +10 -0
  240. df_site/templates/allauth/elements/h1.html +1 -0
  241. df_site/templates/allauth/elements/h2.html +1 -0
  242. df_site/templates/allauth/elements/img.html +4 -0
  243. df_site/templates/allauth/elements/p.html +1 -0
  244. df_site/templates/allauth/elements/panel.html +14 -0
  245. df_site/templates/allauth/elements/provider.html +6 -0
  246. df_site/templates/allauth/elements/provider_list.html +5 -0
  247. df_site/templates/allauth/elements/table.html +5 -0
  248. df_site/templates/allauth/layouts/base.html +14 -0
  249. df_site/templates/allauth/layouts/entrance.html +20 -0
  250. df_site/templates/allauth/layouts/manage.html +1 -0
  251. df_site/templates/cookie_consent/_cookie_group.html +64 -0
  252. df_site/templates/cookie_consent/cookiegroup_list.html +23 -0
  253. df_site/templates/df_components/base.html +0 -0
  254. df_site/templates/df_components/detail.html +12 -0
  255. df_site/templates/df_components/detail_fieldset.html +46 -0
  256. df_site/templates/df_components/list.html +42 -0
  257. df_site/templates/df_components/list_filter.html +13 -0
  258. df_site/templates/df_components/list_filters.html +36 -0
  259. df_site/templates/df_components/list_hierarchy.html +25 -0
  260. df_site/templates/df_components/list_pagination.html +39 -0
  261. df_site/templates/df_components/list_search_form.html +38 -0
  262. df_site/templates/df_components/list_table.html +35 -0
  263. df_site/templates/df_site/app.html +1 -0
  264. df_site/templates/df_site/base.html +221 -0
  265. df_site/templates/df_site/detail.html +8 -0
  266. df_site/templates/df_site/humans.txt +11 -0
  267. df_site/templates/df_site/manage_base.html +51 -0
  268. df_site/templates/df_site/popup_app.html +1 -0
  269. df_site/templates/df_site/popup_base.html +29 -0
  270. df_site/templates/df_site/security.txt +5 -0
  271. df_site/templates/django_bootstrap5/breadcrumb.html +17 -0
  272. df_site/templates/django_bootstrap5/pagination.html +40 -0
  273. df_site/templates/django_ckeditor_5/widget.html +13 -0
  274. df_site/templates/favicon/browserconfig.xml +9 -0
  275. df_site/templates/mfa/index.html +115 -0
  276. df_site/templates/mfa/recovery_codes/index.html +33 -0
  277. df_site/templates/mfa/webauthn/authenticator_list.html +74 -0
  278. df_site/templates/pipeline/css.html +1 -0
  279. df_site/templates/pipeline/js.html +1 -0
  280. df_site/templates/postman/archives.html +8 -0
  281. df_site/templates/postman/base.html +20 -0
  282. df_site/templates/postman/base_folder.html +71 -0
  283. df_site/templates/postman/base_write.html +26 -0
  284. df_site/templates/postman/inbox.html +7 -0
  285. df_site/templates/postman/inc_subject_ex.html +21 -0
  286. df_site/templates/postman/trash.html +12 -0
  287. df_site/templates/postman/view.html +64 -0
  288. df_site/templates/users/settings.html +26 -0
  289. df_site/templates/usersessions/usersession_list.html +70 -0
  290. df_site/templatetags/__init__.py +1 -0
  291. df_site/templatetags/df_site.py +241 -0
  292. df_site/templatetags/images.py +515 -0
  293. df_site/templatetags/pipeline_sri.py +97 -0
  294. df_site/testing/__init__.py +1 -0
  295. df_site/testing/multiple_views.py +369 -0
  296. df_site/testing/requests.py +299 -0
  297. df_site/urls.py +41 -0
  298. df_site/user_settings.py +69 -0
  299. df_site/users/__init__.py +1 -0
  300. df_site/users/forms.py +35 -0
  301. df_site/users/notifications.py +14 -0
  302. df_site/users/urls.py +17 -0
  303. df_site/users/views.py +75 -0
  304. df_site/views.py +122 -0
  305. df_site-0.1.0.dist-info/LICENSE +519 -0
  306. df_site-0.1.0.dist-info/METADATA +217 -0
  307. df_site-0.1.0.dist-info/RECORD +309 -0
  308. df_site-0.1.0.dist-info/WHEEL +4 -0
  309. df_site-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,446 @@
1
+ """Components for displaying a list of items in a table."""
2
+
3
+ from typing import Callable, List, Optional, Tuple, Type, Union
4
+
5
+ from django.contrib.admin import ListFilter, ShowFacets, SimpleListFilter, widgets
6
+ from django.contrib.admin.options import IS_FACETS_VAR, IS_POPUP_VAR
7
+ from django.contrib.admin.templatetags.admin_list import date_hierarchy as raw_date_hierarchy
8
+ from django.contrib.admin.templatetags.admin_list import result_headers, results
9
+ from django.contrib.admin.utils import get_fields_from_path, lookup_spawns_duplicates
10
+ from django.contrib.admin.views.main import ALL_VAR, ORDER_VAR, PAGE_VAR, SEARCH_VAR, ChangeList
11
+ from django.core.exceptions import FieldDoesNotExist
12
+ from django.core.paginator import Paginator
13
+ from django.db import models
14
+ from django.db.models.constants import LOOKUP_SEP
15
+ from django.db.models.functions.text import Substr, Upper
16
+ from django.http import HttpRequest
17
+ from django.utils.http import urlencode
18
+ from django.utils.text import smart_split, unescape_string_literal
19
+ from django.utils.translation import gettext as _
20
+
21
+ from df_site.components.base import ModelComponent
22
+
23
+
24
+ class ModelListChangeList(ChangeList):
25
+ """Customized ChangeList for ModelListComponent."""
26
+
27
+ formset = None
28
+ paginator: Paginator
29
+
30
+ def __init__(self, request: HttpRequest, *args, **kwargs):
31
+ """Initialize the ChangeList."""
32
+ super().__init__(request, *args, **kwargs)
33
+ self.request: HttpRequest = request
34
+
35
+ def url_for_result(self, result: models.Model):
36
+ """Return the URL for a result, trying to use the get_absolute_url method."""
37
+ if hasattr(result, "get_absolute_url"):
38
+ url = result.get_absolute_url()
39
+ else:
40
+ url = f"/{self.opts.app_label}/{self.opts.model_name}/{result.pk}/show/"
41
+ return url
42
+
43
+
44
+ class ModelListComponent(ModelComponent):
45
+ """A component that can display a list as table, with filters and a search bar."""
46
+
47
+ page_var = PAGE_VAR
48
+ all_var = ALL_VAR
49
+ order_var = ORDER_VAR
50
+ search_var = SEARCH_VAR
51
+ changelist_filters_var = "_changelist_filters"
52
+ list_editable: List[str] = []
53
+
54
+ def __init__(
55
+ self,
56
+ model: Type[models.Model],
57
+ base_template: str = "list.html",
58
+ list_select_related: Optional[List[str]] = None,
59
+ list_display: List[str] = None,
60
+ list_display_links: List[str] = None,
61
+ list_filter: List[Union[str, Tuple[str, Type[ListFilter]], Callable]] = None,
62
+ date_hierarchy: Optional[str] = None,
63
+ search_fields: List[str] = None,
64
+ list_per_page: int = 20,
65
+ list_max_show_all: int = 200,
66
+ sortable_by: List[str] = None,
67
+ search_help_text: Optional[str] = None,
68
+ show_facets: ShowFacets = ShowFacets.ALLOW,
69
+ show_full_result_count: bool = True,
70
+ ordering: List[str] = None,
71
+ pagination_on_top: bool = True,
72
+ pagination_on_bottom: bool = True,
73
+ filters_on_right: bool = True,
74
+ filters_title: str = _("Filters"),
75
+ ):
76
+ """Create a new list component."""
77
+ super().__init__(model, base_template)
78
+ self.list_select_related: Optional[List[str]] = list_select_related
79
+ self.list_display: List[str] = list_display or ["__str__"]
80
+ self.list_display_links: List[str] = list_display_links or []
81
+ self.list_filter: List[Union[str, Tuple[str, Type[ListFilter], Callable]]] = list_filter or []
82
+ self.date_hierarchy: Optional[str] = date_hierarchy
83
+ self.search_fields: List[str] = search_fields or []
84
+ self.list_per_page: int = list_per_page
85
+ self.list_max_show_all: int = list_max_show_all
86
+ self.sortable_by: List[str] = sortable_by or []
87
+ self.search_help_text: Optional[str] = search_help_text
88
+ self.show_facets: ShowFacets = show_facets
89
+ self.show_full_result_count: bool = show_full_result_count
90
+ self.ordering: List[str] = ordering or []
91
+ self.pagination_on_top: bool = pagination_on_top
92
+ self.pagination_on_bottom: bool = pagination_on_bottom
93
+ self.filters_on_right: bool = filters_on_right
94
+ self.filters_title: str = filters_title
95
+
96
+ # noinspection PyMethodMayBeStatic,PyUnusedLocal
97
+ def get_change_list_class(self, request: HttpRequest, **kwargs) -> Type[ChangeList]:
98
+ """Return the ChangeList class to use for this component."""
99
+ return ModelListChangeList
100
+
101
+ # noinspection PyUnusedLocal
102
+ def get_change_list(self, request: HttpRequest, **kwargs) -> ChangeList:
103
+ """Return the ChangeList instance to use for this component."""
104
+ cls = self.get_change_list_class(request, **kwargs)
105
+ return cls(
106
+ request,
107
+ self.model,
108
+ self.get_list_display(request, **kwargs),
109
+ self.get_list_display_links(request, **kwargs),
110
+ self.get_list_filter(request, **kwargs),
111
+ self.get_date_hierarchy(request, **kwargs),
112
+ self.get_search_fields(request, **kwargs),
113
+ self.list_select_related,
114
+ self.list_per_page,
115
+ self.list_max_show_all,
116
+ self.list_editable,
117
+ ModelAdminWrapper(self, kwargs),
118
+ self.sortable_by,
119
+ self.get_search_help_text(request, **kwargs),
120
+ )
121
+
122
+ # noinspection PyUnusedLocal
123
+ def get_date_hierarchy(self, request: HttpRequest, **kwargs):
124
+ """Return the date hierarchy, depending on the request."""
125
+ return self.date_hierarchy
126
+
127
+ # noinspection PyUnusedLocal
128
+ def get_search_help_text(self, request: HttpRequest, **kwargs):
129
+ """Return the text displayed before the search input."""
130
+ return self.search_help_text
131
+
132
+ # noinspection PyUnusedLocal
133
+ def get_list_display_links(self, request: HttpRequest, **kwargs):
134
+ """Return the list of fields with the link to the object."""
135
+ return self.list_display_links
136
+
137
+ # noinspection PyUnusedLocal
138
+ def get_list_display(self, request: HttpRequest, **kwargs):
139
+ """Return the list of fields to display."""
140
+ return self.list_display
141
+
142
+ def get_queryset(self, request: HttpRequest, **kwargs) -> models.QuerySet:
143
+ """Return the queryset to use for this component."""
144
+ qs = self.get_base_queryset(request, **kwargs)
145
+ if self.list_select_related is True:
146
+ qs = qs.select_related()
147
+ elif self.list_select_related:
148
+ qs = qs.select_related(*self.list_select_related)
149
+ return qs
150
+
151
+ def get_preserved_filters(self, request: HttpRequest):
152
+ """Return the preserved filters querystring."""
153
+ # noinspection PyArgumentList
154
+ preserved_filters = request.GET.urlencode()
155
+ if preserved_filters:
156
+ return urlencode({self.changelist_filters_var: preserved_filters})
157
+ return ""
158
+
159
+ # noinspection PyUnusedLocal
160
+ def to_field_allowed(self, request, to_field):
161
+ """Mimic the behavior of the ModelAdmin."""
162
+ try:
163
+ field = self.opts.get_field(to_field)
164
+ except FieldDoesNotExist:
165
+ return False
166
+
167
+ return field.primary_key
168
+
169
+ # noinspection PyMethodMayBeStatic,PyUnusedLocal
170
+ def has_change_permission(self, request: HttpRequest, obj=None):
171
+ """Mimic the behavior of the ModelAdmin."""
172
+ return False
173
+
174
+ def lookup_allowed(self, lookup, value, request: HttpRequest = None, **kwargs):
175
+ """Mimic the behavior of the ModelAdmin."""
176
+ model = self.model
177
+ # Check FKey lookups that are allowed, so that popups produced by
178
+ # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
179
+ # are allowed to work.
180
+ for fk_lookup in model._meta.related_fkey_lookups:
181
+ # As ``limit_choices_to`` can be a callable, invoke it here.
182
+ if callable(fk_lookup):
183
+ fk_lookup = fk_lookup()
184
+ if (lookup, value) in widgets.url_params_from_lookup_dict(fk_lookup).items():
185
+ return True
186
+
187
+ relation_parts = []
188
+ prev_field = None
189
+ part = ""
190
+ parts = lookup.split(LOOKUP_SEP)
191
+ for part in parts:
192
+ try:
193
+ field = model._meta.get_field(part)
194
+ except FieldDoesNotExist:
195
+ # Lookups on nonexistent fields are ok, since they're ignored
196
+ # later.
197
+ break
198
+ if not prev_field or (
199
+ prev_field.is_relation
200
+ and field not in model._meta.parents.values()
201
+ and field is not model._meta.auto_field
202
+ and (model._meta.auto_field is None or part not in getattr(prev_field, "to_fields", []))
203
+ and (field.is_relation or not field.primary_key)
204
+ ):
205
+ relation_parts.append(part)
206
+ if not getattr(field, "path_infos", None):
207
+ # This is not a relational field, so further parts
208
+ # must be transforms.
209
+ break
210
+ prev_field = field
211
+ model = field.path_infos[-1].to_opts.model
212
+
213
+ if len(relation_parts) <= 1:
214
+ # Either a local field filter, or no fields at all.
215
+ return True
216
+ valid_lookups = {self.get_date_hierarchy(request, **kwargs)}
217
+ # RemovedInDjango60Warning: when the deprecation ends, replace with:
218
+ # for filter_item in self.get_list_filter(request):
219
+ list_filter = self.get_list_filter(request, **kwargs) if request is not None else self.list_filter
220
+ for filter_item in list_filter:
221
+ if isinstance(filter_item, type) and issubclass(filter_item, SimpleListFilter):
222
+ valid_lookups.add(filter_item.parameter_name)
223
+ elif isinstance(filter_item, (list, tuple)):
224
+ valid_lookups.add(filter_item[0])
225
+ else:
226
+ valid_lookups.add(filter_item)
227
+
228
+ # Is it a valid relational lookup?
229
+ return not {
230
+ LOOKUP_SEP.join(relation_parts),
231
+ LOOKUP_SEP.join(relation_parts + [part]),
232
+ }.isdisjoint(valid_lookups)
233
+
234
+ # noinspection PyUnusedLocal
235
+ def get_list_filter(self, request, **kwargs):
236
+ """Return a sequence containing the fields to be displayed as filters."""
237
+ return self.list_filter
238
+
239
+ # noinspection PyUnusedLocal,PyMethodMayBeStatic
240
+ def get_paginator(self, request: HttpRequest, queryset, per_page, orphans=0, allow_empty_first_page=True):
241
+ """Return the paginator to use for this component."""
242
+ return Paginator(queryset, per_page, orphans, allow_empty_first_page)
243
+
244
+ # noinspection PyUnusedLocal
245
+ def get_ordering(self, request: HttpRequest, **kwargs):
246
+ """Return the ordering to use for this component."""
247
+ return self.ordering or ()
248
+
249
+ # noinspection PyUnusedLocal
250
+ def get_search_fields(self, request: HttpRequest, **kwargs):
251
+ """Return a sequence containing the fields to be searched."""
252
+ return self.search_fields
253
+
254
+ def get_search_results(
255
+ self, request: HttpRequest, queryset: models.QuerySet, search_term: str
256
+ ) -> Tuple[models.QuerySet, bool]:
257
+ """Return the search result.
258
+
259
+ Return a tuple containing a queryset to implement the search
260
+ and a boolean indicating if the results may contain duplicates.
261
+ """
262
+
263
+ # Apply keyword searches.
264
+ def construct_search(field_name):
265
+ if field_name.startswith("^"):
266
+ removeprefix = field_name.removeprefix("^")
267
+ return f"{removeprefix}__istartswith"
268
+ elif field_name.startswith("="):
269
+ removeprefix = field_name.removeprefix("=")
270
+ return f"{removeprefix}__iexact"
271
+ elif field_name.startswith("@"):
272
+ removeprefix = field_name.removeprefix("@")
273
+ return f"{removeprefix}__search"
274
+ # Use field_name if it includes a lookup.
275
+ opts = self.opts
276
+ lookup_fields = field_name.split(LOOKUP_SEP)
277
+ # Go through the fields, following all relations.
278
+ prev_field = None
279
+ for path_part in lookup_fields:
280
+ if path_part == "pk":
281
+ path_part = opts.pk.name
282
+ try:
283
+ field = opts.get_field(path_part)
284
+ except FieldDoesNotExist:
285
+ # Use valid query lookups.
286
+ if prev_field and prev_field.get_lookup(path_part):
287
+ return field_name
288
+ else:
289
+ prev_field = field
290
+ if hasattr(field, "path_infos"):
291
+ # Update opts to follow the relation.
292
+ opts = field.path_infos[-1].to_opts
293
+ # Otherwise, use the field with icontains.
294
+ return f"{field_name}__icontains"
295
+
296
+ may_have_duplicates = False
297
+ search_fields = self.get_search_fields(request)
298
+ if search_fields and search_term:
299
+ orm_lookups = [construct_search(str(search_field)) for search_field in search_fields]
300
+ term_queries = []
301
+ for bit in smart_split(search_term):
302
+ if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
303
+ bit = unescape_string_literal(bit)
304
+ or_queries = models.Q.create(
305
+ [(orm_lookup, bit) for orm_lookup in orm_lookups],
306
+ connector=models.Q.OR,
307
+ )
308
+ term_queries.append(or_queries)
309
+ queryset = queryset.filter(models.Q.create(term_queries))
310
+ may_have_duplicates |= any(lookup_spawns_duplicates(self.opts, search_spec) for search_spec in orm_lookups)
311
+ return queryset, may_have_duplicates
312
+
313
+ def update_render_context(self, context, **kwargs):
314
+ """Update the context before rendering the component."""
315
+ request = context["request"]
316
+ cl = self.get_change_list(request, **kwargs)
317
+ context.update({"cl": cl, "opts": self.opts})
318
+ context.update(self.get_pagination_context(request, cl, **kwargs))
319
+ context.update(self.get_search_context(request, cl, **kwargs))
320
+ context.update(self.get_hierarchy_context(request, cl, **kwargs))
321
+
322
+ # noinspection PyUnusedLocal
323
+ def get_pagination_context(self, request: HttpRequest, cl: ChangeList, **kwargs):
324
+ """Return the context for the pagination."""
325
+ headers = list(result_headers(cl))
326
+ num_sorted_fields = 0
327
+ for h in headers:
328
+ if h["sortable"] and h["sorted"]:
329
+ num_sorted_fields += 1
330
+ pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page
331
+ page_range = cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else []
332
+ need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page
333
+ return {
334
+ "pagination_required": pagination_required,
335
+ "show_all_url": need_show_all_link and cl.get_query_string({self.all_var: ""}),
336
+ "page_range": list(page_range),
337
+ "ALL_VAR": self.all_var,
338
+ "1": 1,
339
+ "results": list(results(cl)),
340
+ "result_headers": headers,
341
+ "num_sorted_fields": num_sorted_fields,
342
+ }
343
+
344
+ # noinspection PyUnusedLocal
345
+ def get_search_context(self, request: HttpRequest, cl: ChangeList, **kwargs):
346
+ """Return the context for the search bar."""
347
+ return {
348
+ "show_result_count": cl.result_count != cl.full_result_count,
349
+ "search_var": self.search_var,
350
+ "is_popup_var": IS_POPUP_VAR,
351
+ "is_facets_var": IS_FACETS_VAR,
352
+ }
353
+
354
+ # noinspection PyMethodMayBeStatic,PyUnusedLocal
355
+ def get_hierarchy_context(self, request: HttpRequest, cl: ChangeList, **kwargs):
356
+ """Return the context for the hierarchy of results.
357
+
358
+ Currently only works for date fields.
359
+ """
360
+ # noinspection PyTestUnpassedFixture
361
+ field_name = cl.date_hierarchy
362
+ if not field_name:
363
+ return {"show_hierarchy": False}
364
+ field = get_fields_from_path(cl.model, field_name)[-1]
365
+ if isinstance(field, models.CharField) or isinstance(field, models.TextField):
366
+ hierarchy_data = first_letter_hierarchy(cl)
367
+ else:
368
+ hierarchy_data = raw_date_hierarchy(cl)
369
+ if hierarchy_data is not None:
370
+ return {
371
+ "show_hierarchy": True,
372
+ "hierarchy_back": hierarchy_data["back"],
373
+ "hierarchy_choices": hierarchy_data["choices"],
374
+ "hierarchy_title": field.verbose_name,
375
+ }
376
+ return {"show_hierarchy": False}
377
+
378
+
379
+ class ModelAdminWrapper:
380
+ """Wrapper around a ModelListComponent to mimic a ModelAdmin.
381
+
382
+ This wrapper allows to add kwargs to the get_queryset method.
383
+ ChangeList expects a ModelAdmin instance but calls the get_queryset method with
384
+ only request arg, so we need to wrap the ModelListComponent.
385
+ """
386
+
387
+ def __init__(self, model_admin: ModelListComponent, kwargs):
388
+ """Create a new wrapper."""
389
+ self.model_admin: ModelListComponent = model_admin
390
+ self.kwargs = kwargs
391
+
392
+ def get_queryset(self, request):
393
+ """Call the get_queryset method with kwargs."""
394
+ return self.model_admin.get_queryset(request, **self.kwargs)
395
+
396
+ def get_ordering(self, request):
397
+ """Call the get_ordering method with kwargs."""
398
+ return self.model_admin.get_ordering(request, **self.kwargs)
399
+
400
+ def get_list_filter(self, request):
401
+ """Call the get_list_filter method with kwargs."""
402
+ return self.model_admin.get_list_filter(request, **self.kwargs)
403
+
404
+ def get_date_hierarchy(self, request):
405
+ """Call the get_date_hierarchy method with kwargs."""
406
+ return self.model_admin.get_date_hierarchy(request, **self.kwargs)
407
+
408
+ def lookup_allowed(self, lookup, value, request: HttpRequest):
409
+ """Call the lookup_allowed method with kwargs."""
410
+ return self.model_admin.lookup_allowed(lookup, value, request=request, **self.kwargs)
411
+
412
+ def __getattr__(self, item):
413
+ """Delegate all other calls to the ModelListComponent."""
414
+ return getattr(self.model_admin, item)
415
+
416
+
417
+ def first_letter_hierarchy(cl: ChangeList):
418
+ """Fetch all initials of a CharField."""
419
+ # noinspection PyTestUnpassedFixture
420
+ qs = (
421
+ cl.queryset.annotate(hierarchy_initial=Upper(Substr(cl.date_hierarchy, 1, 1)))
422
+ .values_list("hierarchy_initial", flat=True)
423
+ .distinct()
424
+ )
425
+ values = set(qs)
426
+ if not values:
427
+ return None
428
+ # noinspection PyTestUnpassedFixture
429
+ initial_field = f"{cl.date_hierarchy}__istartswith"
430
+ initial_value = cl.params.get(initial_field)
431
+ if initial_value:
432
+ cl.get_query_string(remove=[initial_field])
433
+ back = {"link": cl.get_query_string(remove=[initial_field]), "title": _("All")}
434
+ else:
435
+ back = None
436
+ return {
437
+ "show": True,
438
+ "back": back,
439
+ "choices": [
440
+ {
441
+ "title": initial,
442
+ "link": cl.get_query_string({initial_field: initial}) if initial != initial_value else None,
443
+ }
444
+ for initial in sorted(values)
445
+ ],
446
+ }
@@ -0,0 +1,74 @@
1
+ """Override default admin filters with a custom template."""
2
+
3
+ from django.contrib.admin import AllValuesFieldListFilter as AllValuesFieldListFilterBase
4
+ from django.contrib.admin import BooleanFieldListFilter as BooleanFieldListFilterBase
5
+ from django.contrib.admin import ChoicesFieldListFilter as ChoicesFieldListFilterBase
6
+ from django.contrib.admin import DateFieldListFilter as DateFieldListFilterBase
7
+ from django.contrib.admin import EmptyFieldListFilter as EmptyFieldListFilterBase
8
+ from django.contrib.admin import FieldListFilter as FieldListFilterBase
9
+ from django.contrib.admin import RelatedFieldListFilter as RelatedFieldListFilterBase
10
+ from django.contrib.admin import RelatedOnlyFieldListFilter as RelatedOnlyFieldListFilterBase
11
+ from django.contrib.admin import SimpleListFilter as SimpleListFilterBase
12
+
13
+
14
+ class ListFilterMixin:
15
+ """A mixin for list filters, replacing the default template."""
16
+
17
+ template = "df_components/list_filter.html"
18
+
19
+
20
+ class SimpleListFilter(ListFilterMixin, SimpleListFilterBase):
21
+ """A filter for simple fields."""
22
+
23
+ pass
24
+
25
+
26
+ class FieldListFilter(ListFilterMixin, FieldListFilterBase):
27
+ """A filter for list fields."""
28
+
29
+ pass
30
+
31
+
32
+ class RelatedFieldListFilter(ListFilterMixin, RelatedFieldListFilterBase):
33
+ """A filter for related fields."""
34
+
35
+ def field_admin_ordering(self, field, request, model_admin):
36
+ """Return the default ordering for related field, skipping the use of the admin site."""
37
+ # noinspection PyProtectedMember
38
+ return field.remote_field.model._meta.ordering
39
+
40
+
41
+ class BooleanFieldListFilter(ListFilterMixin, BooleanFieldListFilterBase):
42
+ """A filter for boolean fields."""
43
+
44
+ pass
45
+
46
+
47
+ class ChoicesFieldListFilter(ListFilterMixin, ChoicesFieldListFilterBase):
48
+ """A filter for choice fields."""
49
+
50
+ pass
51
+
52
+
53
+ class DateFieldListFilter(ListFilterMixin, DateFieldListFilterBase):
54
+ """A filter for date fields."""
55
+
56
+ pass
57
+
58
+
59
+ class AllValuesFieldListFilter(ListFilterMixin, AllValuesFieldListFilterBase):
60
+ """A filter for all values of a field."""
61
+
62
+ pass
63
+
64
+
65
+ class RelatedOnlyFieldListFilter(ListFilterMixin, RelatedOnlyFieldListFilterBase):
66
+ """A filter for related fields."""
67
+
68
+ pass
69
+
70
+
71
+ class EmptyFieldListFilter(ListFilterMixin, EmptyFieldListFilterBase):
72
+ """A filter for empty fields."""
73
+
74
+ pass
@@ -0,0 +1,55 @@
1
+ """Simulate a classical Django admin_site with similare URLs."""
2
+
3
+ import warnings
4
+ from collections import defaultdict
5
+ from typing import Optional, Type
6
+
7
+ from django.contrib.admin import ModelAdmin
8
+ from django.db import models
9
+
10
+
11
+ class ModelURLRegistry:
12
+ """A registry for models and their URLs."""
13
+
14
+ def __init__(self):
15
+ """Initialize the registry."""
16
+ self.all_models = defaultdict(dict)
17
+
18
+ def register_model(self, app_label, model):
19
+ """Register a model in the registry."""
20
+ # Since this method is called when models are imported, it cannot
21
+ # perform imports because of the risk of import loops. It mustn't
22
+ # call get_app_config().
23
+ # noinspection PyProtectedMember
24
+ model_name = model._meta.model_name
25
+ app_models = self.all_models[app_label]
26
+ if model_name in app_models:
27
+ existing = app_models[model_name]
28
+ if model.__name__ == existing.__name__ and model.__module__ == existing.__module__:
29
+ warnings.warn(
30
+ f"Model '{app_label}.{model_name}' was already registered. Reloading models is not "
31
+ "advised as it can lead to inconsistencies, most notably with "
32
+ "related models.",
33
+ RuntimeWarning,
34
+ stacklevel=2,
35
+ )
36
+ else:
37
+ msg = f"Conflicting '{model_name}' models in application '{app_label}': {existing} and {model}."
38
+ raise RuntimeError(msg)
39
+ app_models[model_name] = model
40
+
41
+ def unregister_model(self, app_label, model_name):
42
+ """Unregister a model from the registry."""
43
+ if app_label in self.all_models:
44
+ if model_name in self.all_models[app_label]:
45
+ del self.all_models[app_label][model_name]
46
+ if len(self.all_models[app_label]) == 0:
47
+ del self.all_models[app_label]
48
+
49
+ # noinspection PyMethodMayBeStatic
50
+ def get_model_admin(self, model: Type[models.Model]) -> Optional[ModelAdmin]:
51
+ """Return an empty model admin, for the sake of compatibility."""
52
+ return None
53
+
54
+
55
+ default_registry = ModelURLRegistry()
df_site/constants.py ADDED
@@ -0,0 +1,71 @@
1
+ """Contains constants for the users app."""
2
+
3
+ import re
4
+
5
+ BRAND_ICONS = {
6
+ "amazon": "amazon",
7
+ "amazon_cognito": "amazon",
8
+ "angellist": "angellist",
9
+ "apple": "apple",
10
+ "atlassian": "atlassian",
11
+ "battlenet": "battle-net",
12
+ "bitbucket_oauth2": "bitbucket",
13
+ "digitalocean": "digital-ocean",
14
+ "discord": "discord",
15
+ "dropbox": "dropbox",
16
+ "evernote": "evernote",
17
+ "facebook": "facebook",
18
+ "figma": "figma",
19
+ "fivehundredpx": "500px",
20
+ "flickr": "flickr",
21
+ "foursquare": "foursquare",
22
+ "github": "github",
23
+ "gitlab": "gitlab",
24
+ "google": "google",
25
+ "hubspot": "hubspot",
26
+ "instagram": "instagram",
27
+ "line": "line",
28
+ "linkedin_oauth2": "linkedin",
29
+ "mailchimp": "mailchimp",
30
+ "mailru": "",
31
+ "meetup": "meetup",
32
+ "microsoft": "microsoft",
33
+ "odnoklassniki": "odnoklassniki",
34
+ "openid": "openid",
35
+ "openid_connect": "openid",
36
+ "orcid": "orcid",
37
+ "patreon": "patreon",
38
+ "paypal": "paypal",
39
+ "pinterest": "pinterest",
40
+ "pocket": "get-pocket",
41
+ "reddit": "reddit",
42
+ "salesforce": "salesforce",
43
+ "shopify": "shopify",
44
+ "slack": "slack",
45
+ "snapchat": "snapchat",
46
+ "soundcloud": "soundcloud",
47
+ "spotify": "spotify",
48
+ "stackexchange": "stack-exchange",
49
+ "steam": "steam",
50
+ "strava": "strava",
51
+ "stripe": "stripe",
52
+ "telegram": "telegram",
53
+ "tiktok": "tiktok",
54
+ "trello": "trello",
55
+ "tumblr": "tumblr",
56
+ "twitch": "twitch",
57
+ "twitter": "twitter",
58
+ "twitter_oauth2": "twitter",
59
+ "untappd": "untappd",
60
+ "vimeo": "vimeo",
61
+ "vimeo_oauth2": "vimeo",
62
+ "vk": "vk",
63
+ "weibo": "weibo",
64
+ "weixin": "weixin",
65
+ "windowslive": "windows",
66
+ "xing": "xing",
67
+ "yahoo": "yahoo",
68
+ "yandex": "yandex",
69
+ }
70
+ INT_RE = re.compile(r"^(0|[1-9]\d*)$")
71
+ SIZE_RE = re.compile(r"^([1-9]\d*)x([1-9]\d*)$")