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,61 @@
1
+ """Define the context processors with global variables about the site."""
2
+
3
+ import datetime
4
+ import logging
5
+ from functools import lru_cache
6
+ from typing import Any, Dict, Iterable, Optional
7
+
8
+ from django.conf import settings
9
+ from django.db.models import Q
10
+ from django.http import HttpRequest
11
+ from django.utils.timezone import now
12
+
13
+ from df_site.models import AlertRibbon
14
+ from df_site.user_settings import get_user_setting
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @lru_cache(maxsize=1)
20
+ def get_current_ribbons(current_date: datetime.datetime) -> Iterable[AlertRibbon]:
21
+ """Get the active ribbons for the current date.
22
+
23
+ The current date will be rounded to the nearest hour.
24
+ """
25
+ return list(
26
+ AlertRibbon.objects.filter(
27
+ Q(Q(end_date__gte=current_date) & Q(end_date__isnull=False)) | Q(end_date__isnull=True),
28
+ Q(Q(start_date__lte=current_date) & Q(start_date__isnull=False)) | Q(start_date__isnull=True),
29
+ is_active=True,
30
+ )
31
+ )
32
+
33
+
34
+ def global_site_infos(request: HttpRequest, current_date: Optional[datetime.datetime] = None) -> Dict[str, Any]:
35
+ """Adds a few values to the request context."""
36
+ try:
37
+ color_theme = get_user_setting("color_theme", request=request)
38
+ except Exception as e:
39
+ logger.error("Error getting color theme: %s", e)
40
+ color_theme = settings.DF_SITE_THEMES[0][0]
41
+ if current_date is None:
42
+ current_date = now()
43
+ current_date = current_date.replace(minute=0, second=0, microsecond=0)
44
+ ribbons = get_current_ribbons(current_date)
45
+ x_url = settings.DF_SITE_SOCIAL_NETWORKS.get("twitter", "")
46
+ __, __, df_site_x_account = x_url.rpartition("/")
47
+ return {
48
+ "DF_SITE_TITLE": settings.DF_SITE_TITLE,
49
+ "DF_SITE_DESCRIPTION": settings.DF_SITE_DESCRIPTION,
50
+ "DF_SITE_KEYWORDS": settings.DF_SITE_KEYWORDS,
51
+ "DF_SITE_AUTHOR": settings.DF_SITE_AUTHOR,
52
+ "DF_SITE_ORGANIZATION": settings.DF_SITE_ORGANIZATION,
53
+ "DF_SITE_X_ACCOUNT": df_site_x_account,
54
+ "DF_SITE_SOCIAL_NETWORKS": settings.DF_SITE_SOCIAL_NETWORKS.items(),
55
+ "DF_COLOR_THEMES": settings.DF_SITE_THEMES,
56
+ "DF_MICROSOFT_BACKGROUND_COLOR": settings.DF_MICROSOFT_BACKGROUND_COLOR,
57
+ "DF_ANDROID_THEME_COLOR": settings.DF_ANDROID_THEME_COLOR,
58
+ "DF_SAFARI_PINNED_COLOR": settings.DF_SAFARI_PINNED_COLOR,
59
+ "COLOR_THEME": color_theme,
60
+ "DF_RIBBONS": ribbons,
61
+ }
df_site/defaults.py ADDED
@@ -0,0 +1,319 @@
1
+ """Django settings for the project."""
2
+
3
+ from typing import List, Tuple
4
+
5
+ from df_config.config.dynamic_settings import CallableSetting, SettingReference
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+ from df_site.dynamic_settings import allauth_signup_form, are_tests_running, load_tox_environment
9
+
10
+ load_tox_environment()
11
+ DF_SITE_TITLE = "More batteries to Django"
12
+ DF_SITE_SECURITY_EMAIL = SettingReference("ADMIN_EMAIL")
13
+ DF_SITE_SECURITY_LANGUAGE_CODE = SettingReference("LANGUAGE_CODE")
14
+ DF_SITE_SECURITY_GPG_CONTENT = None
15
+
16
+ DF_SITE_DESCRIPTION = "A simple Django site with lots of batteries included."
17
+ DF_SITE_KEYWORDS = ["Django", "Bootstrap", "WebSockets", "HTMX", "Django Channels"]
18
+ DF_SITE_AUTHOR = "d9pouces"
19
+ DF_SITE_ORGANIZATION = "d9pouces"
20
+ DF_SITE_SOCIAL_NETWORKS = {
21
+ "instagram": "https://www.instagram.com/d9pouces/",
22
+ "twitter": "https://x.com/d9pouces/",
23
+ "github": "https://github.com/d9pouces/",
24
+ }
25
+
26
+ DF_SITE_THEMES: List[Tuple[str, str, str]] = [ # ('theme name', 'theme label', 'icon name')
27
+ ("auto", _("Auto"), "toggle-on"),
28
+ ("light", _("Light"), "sun"),
29
+ ("dark", _("Dark"), "moon"),
30
+ ]
31
+ DF_ANDROID_THEME_COLOR = "#ffffff"
32
+ DF_ANDROID_BACKGROUND_COLOR = "#ffffff"
33
+ DF_MICROSOFT_BACKGROUND_COLOR = "#da532c"
34
+ DF_SAFARI_PINNED_COLOR = "#5bbad5"
35
+
36
+ DF_IMAGE_THUMBNAILS = {
37
+ "media": {
38
+ "src_storage": "default",
39
+ "dst_storage": "staticfiles",
40
+ "cache": "default",
41
+ "prefix": "T",
42
+ "reversible": True,
43
+ },
44
+ "static": {
45
+ "src_storage": "staticfiles",
46
+ "dst_storage": "staticfiles",
47
+ "cache": "default",
48
+ "prefix": "S",
49
+ "reversible": True,
50
+ },
51
+ }
52
+
53
+ CSP_IMG_SRC = ["'self'", "data: w3.org/svg/2000", "https://log.pinterest.com"]
54
+ # "https://log.pinterest.com" is used for Pinterest
55
+ CSP_STYLE_SRC = ["'self'"]
56
+ CSP_FONT_SRC = ["'self'"]
57
+ CSP_DEFAULT_SRC = ["'none'"]
58
+ CSP_SCRIPT_SRC = [
59
+ "'self'",
60
+ "https://www.google.com",
61
+ "https://www.gstatic.com",
62
+ "https://assets.pinterest.com",
63
+ "'unsafe-inline'",
64
+ ]
65
+ # unsafe-inline is used for django-allauth (inline JS)
66
+ # "https://www.google.com", "https://www.gstatic.com" are used for reCAPTCHA
67
+ # "https://assets.pinterest.com" is used for Pinterest
68
+ CSP_OBJECT_SRC = ["'self'"]
69
+ CSP_MEDIA_SRC = ["'self'"]
70
+ CSP_FRAME_SRC = ["'self'", "https://www.google.com", "https://assets.pinterest.com"]
71
+ # "https://www.google.com" is used for reCAPTCHA
72
+ # "https://assets.pinterest.com" is used for Pinterest
73
+ CSP_CHILD_SRC = ["'self'"]
74
+ CSP_FRAME_ANCESTORS = ["'self'"]
75
+ CSP_FORM_ACTION = ["'self'"]
76
+ CSP_MANIFEST_SRC = ["'self'"]
77
+ CSP_BASE_URI = ["'self'"]
78
+ CSP_REPORT_URI = "/csp-report/"
79
+ CSP_REPORT_TO = None
80
+ DF_TEMPLATE_CONTEXT_PROCESSORS = [
81
+ "df_site.context_processors.global_site_infos",
82
+ "django.template.context_processors.request",
83
+ ]
84
+ TESTING = CallableSetting(are_tests_running)
85
+ RECAPTCHA_PUBLIC_KEY = ""
86
+ RECAPTCHA_PRIVATE_KEY = ""
87
+ DF_INSTALLED_APPS = [
88
+ "django_bootstrap5",
89
+ "df_site.apps.DFSiteApp",
90
+ "cookie_consent",
91
+ "postman",
92
+ "allauth.mfa",
93
+ "allauth.usersessions",
94
+ "django_ckeditor_5",
95
+ "django_recaptcha",
96
+ ]
97
+
98
+ DF_MIDDLEWARE = [
99
+ "allauth.usersessions.middleware.UserSessionsMiddleware",
100
+ "df_websockets.middleware.WebsocketMiddleware",
101
+ "df_site.middleware.websocket_middleware",
102
+ ]
103
+ MIDDLEWARE = [
104
+ "django_prometheus.middleware.PrometheusBeforeMiddleware",
105
+ "django.middleware.cache.UpdateCacheMiddleware",
106
+ "debug_toolbar.middleware.DebugToolbarMiddleware",
107
+ "django.contrib.sessions.middleware.SessionMiddleware",
108
+ "django.middleware.common.CommonMiddleware",
109
+ "django.middleware.csrf.CsrfViewMiddleware",
110
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
111
+ "django.middleware.locale.LocaleMiddleware",
112
+ "django.contrib.messages.middleware.MessageMiddleware",
113
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
114
+ "django.middleware.security.SecurityMiddleware",
115
+ "df_config.apps.middleware.DFConfigMiddleware",
116
+ "allauth.usersessions.middleware.UserSessionsMiddleware",
117
+ "df_websockets.middleware.WebsocketMiddleware",
118
+ "df_site.middleware.websocket_middleware",
119
+ "csp.middleware.CSPMiddleware",
120
+ "allauth.account.middleware.AccountMiddleware",
121
+ "django.middleware.cache.FetchFromCacheMiddleware",
122
+ "django_prometheus.middleware.PrometheusAfterMiddleware",
123
+ ]
124
+ USERSESSIONS_TRACK_ACTIVITY = True
125
+ POSTMAN_DISALLOW_ANONYMOUS = True
126
+ POSTMAN_AUTO_MODERATE_AS = True
127
+ POSTMAN_I18N_URLS = False
128
+ AUTH_USER_MODEL = "df_site.PreferencesUser"
129
+ AUTH_USER_SETTINGS_VIEW = "df_site.users.views.UserSettingsView"
130
+ COOKIE_CONSENT_SECURE = SettingReference("USE_SSL")
131
+ COOKIE_CONSENT_DOMAIN = "{SERVER_NAME}"
132
+ COOKIE_CONSENT_SAMESITE = "Strict"
133
+ PIPELINE = {
134
+ "PIPELINE_ENABLED": SettingReference("PIPELINE_ENABLED"),
135
+ "JAVASCRIPT": {
136
+ "base": {
137
+ "source_filenames": [
138
+ "js/base.js",
139
+ "js/df_websockets.min.js",
140
+ # "django_ckeditor_5/dist/bundle.js"
141
+ ],
142
+ "output_filename": "js/base.min.js",
143
+ # "integrity": "sha384",
144
+ "crossorigin": "anonymous",
145
+ "extra_context": {
146
+ "defer": True,
147
+ },
148
+ },
149
+ "app": {
150
+ "source_filenames": ["js/app.ts"],
151
+ "output_filename": "js/app.min.js",
152
+ # "integrity": "sha384",
153
+ "crossorigin": "anonymous",
154
+ "extra_context": {
155
+ "defer": True,
156
+ },
157
+ },
158
+ },
159
+ "STYLESHEETS": {
160
+ "base": {
161
+ "source_filenames": [
162
+ "django_ckeditor_5/src/override-django.css",
163
+ "css/ckeditor5.css",
164
+ "css/base.css",
165
+ ],
166
+ "output_filename": "css/base.min.css",
167
+ "extra_context": {"media": "all"},
168
+ # "integrity": "sha384",
169
+ "crossorigin": "anonymous",
170
+ },
171
+ "app": {
172
+ "source_filenames": ["css/app.css"],
173
+ "output_filename": "css/app.min.css",
174
+ "extra_context": {"media": "all"},
175
+ # "integrity": "sha384",
176
+ "crossorigin": "anonymous",
177
+ },
178
+ },
179
+ "CSS_COMPRESSOR": "pipeline.compressors.yuglify.YuglifyCompressor",
180
+ "JS_COMPRESSOR": "pipeline.compressors.uglifyjs.UglifyJSCompressor",
181
+ "COMPILERS": [],
182
+ }
183
+ SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
184
+ SOCIALACCOUNT_LOGIN_ON_GET = True
185
+ MFA_SUPPORTED_TYPES = ["totp", "webauthn", "recovery_codes"]
186
+ MFA_PASSKEY_LOGIN_ENABLED = True
187
+ MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = SettingReference("DEBUG")
188
+ ACCOUNT_LOGIN_BY_CODE_ENABLED = True
189
+ ACCOUNT_EMAIL_VERIFICATION = "optional"
190
+ ACCOUNT_SIGNUP_FORM_CLASS = CallableSetting(allauth_signup_form)
191
+
192
+ CKEDITOR_5_USER_LANGUAGE = True
193
+ special_chars = [
194
+ {"title": _("smiley face"), "character": "😊"},
195
+ {"title": _("smiley face"), "character": ":)"},
196
+ ]
197
+ CK_EDITOR_5_UPLOAD_FILE_VIEW = "django_ckeditor_5.views.upload_file"
198
+ CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "upload_file"
199
+ CKEDITOR_5_CONFIGS = {
200
+ "inline": {
201
+ "specialChars": special_chars,
202
+ "toolbar": [
203
+ "bold",
204
+ "italic",
205
+ "underline",
206
+ "strikethrough",
207
+ "subscript",
208
+ "superscript",
209
+ "|",
210
+ "specialCharacters",
211
+ "removeFormat",
212
+ ],
213
+ "plugins": [
214
+ "Essentials",
215
+ "Autoformat",
216
+ "Bold",
217
+ "Italic",
218
+ "Underline",
219
+ "Strikethrough",
220
+ "Code",
221
+ "Subscript",
222
+ "Superscript",
223
+ "Paragraph",
224
+ "Font",
225
+ "PasteFromOffice",
226
+ "RemoveFormat",
227
+ "Highlight",
228
+ "SpecialCharacters",
229
+ "SpecialCharactersEssentials",
230
+ "ShowBlocks",
231
+ "SelectAll",
232
+ ],
233
+ },
234
+ "inline_link": {
235
+ "specialChars": special_chars,
236
+ "toolbar": [
237
+ "bold",
238
+ "italic",
239
+ "underline",
240
+ "strikethrough",
241
+ "subscript",
242
+ "superscript",
243
+ "link",
244
+ "|",
245
+ "specialCharacters",
246
+ "removeFormat",
247
+ ],
248
+ "plugins": [
249
+ "Essentials",
250
+ "Autoformat",
251
+ "Bold",
252
+ "Italic",
253
+ "Underline",
254
+ "Strikethrough",
255
+ "Code",
256
+ "Subscript",
257
+ "Superscript",
258
+ "Link",
259
+ "Paragraph",
260
+ "Font",
261
+ "PasteFromOffice",
262
+ "RemoveFormat",
263
+ "Highlight",
264
+ "SpecialCharacters",
265
+ "SpecialCharactersEssentials",
266
+ "ShowBlocks",
267
+ "SelectAll",
268
+ ],
269
+ },
270
+ "default": {
271
+ "specialChars": special_chars,
272
+ "toolbar": [
273
+ "heading",
274
+ "bulletedList",
275
+ "numberedList",
276
+ "blockQuote",
277
+ "|",
278
+ "bold",
279
+ "italic",
280
+ "underline",
281
+ "strikethrough",
282
+ "subscript",
283
+ "superscript",
284
+ "link",
285
+ "highlight",
286
+ "|",
287
+ "insertTable",
288
+ "insertImage",
289
+ "specialCharacters",
290
+ "removeFormat",
291
+ "undo",
292
+ "redo",
293
+ ],
294
+ "image": {
295
+ "toolbar": [
296
+ "imageTextAlternative",
297
+ "|",
298
+ "imageStyle:full",
299
+ "imageStyle:alignLeft",
300
+ "imageStyle:alignRight",
301
+ "imageStyle:alignCenter",
302
+ "imageStyle:side",
303
+ "|",
304
+ ],
305
+ "styles": [
306
+ "full",
307
+ "side",
308
+ "alignLeft",
309
+ "alignRight",
310
+ "alignCenter",
311
+ ],
312
+ },
313
+ "list": {"properties": {"styles": False, "startIndex": True, "reversed": True}},
314
+ "table": {
315
+ "defaultHeadings": {"rows": 1, "columns": 1},
316
+ "contentToolbar": ["tableColumn", "tableRow", "mergeTableCells"],
317
+ },
318
+ },
319
+ }
@@ -0,0 +1,37 @@
1
+ """Python functions to provide Django settings depending on other settings."""
2
+
3
+ import os
4
+ from typing import Any, Optional
5
+
6
+
7
+ def allauth_signup_form(values: dict[str, Any]) -> Optional[str]:
8
+ """Return the form class to use for signing up."""
9
+ if values.get("RECAPTCHA_PRIVATE_KEY") and values.get("RECAPTCHA_PUBLIC_KEY"):
10
+ return "df_site.users.forms.ReCaptchaForm"
11
+ return None
12
+
13
+
14
+ allauth_signup_form.required_settings = ["RECAPTCHA_PRIVATE_KEY", "RECAPTCHA_PUBLIC_KEY"]
15
+
16
+
17
+ def are_tests_running(values: dict[str, Any]) -> bool:
18
+ """Return True if we are running unit tests."""
19
+ return "testserver" in values.get("ALLOWED_HOSTS", [])
20
+
21
+
22
+ are_tests_running.required_settings = ["ALLOWED_HOSTS"]
23
+
24
+
25
+ def load_tox_environment():
26
+ """Is a workaround for https://github.com/tox-dev/tox-docker/issues/55."""
27
+ if os.environ.get("REDIS_HOST") and os.environ.get("REDIS_6379_TCP_PORT"):
28
+ os.environ["REDIS_URL"] = f'redis://:p_df_site@{os.environ["REDIS_HOST"]}:{os.environ["REDIS_6379_TCP_PORT"]}/1'
29
+ if os.environ.get("POSTGRES_HOST") and os.environ.get("POSTGRES_5432_TCP_PORT"):
30
+ os.environ["DATABASE_URL"] = (
31
+ f'postgresql://u_df_site:p_df_site@{os.environ["POSTGRES_HOST"]}:'
32
+ f'{os.environ["POSTGRES_5432_TCP_PORT"]}/d_df_site'
33
+ )
34
+ if os.environ.get("MINIO_HOST") and os.environ.get("MINIO_9000_TCP_PORT"):
35
+ os.environ["MAIN_STORAGE_DIR"] = (
36
+ f's3:http://u_df_site:p_df_site@127.0.0.1:' f'{os.environ["MINIO_9000_TCP_PORT"]}/f_df_site'
37
+ )
df_site/form_fields.py ADDED
@@ -0,0 +1,138 @@
1
+ """Custom form fields for the df_site app."""
2
+
3
+ import html
4
+ from functools import lru_cache
5
+ from typing import Dict, Iterable, List, Set
6
+
7
+ import nh3
8
+ from django import forms
9
+ from django.conf import settings
10
+ from django.core.exceptions import ValidationError
11
+ from django.utils.html import strip_tags
12
+ from django_ckeditor_5.widgets import CKEditor5Widget
13
+ from lxml_html_clean import autolink_html
14
+
15
+ # list of CKEditor5 plugins and associated HTML tags
16
+ BLOCK = "$block"
17
+ TAGS_BY_PLUGIN: Dict[str, Dict[str, List[str]]] = {
18
+ "Alignment": {BLOCK: ["class"]},
19
+ "BlockQuote": {"blockquote": []},
20
+ "Bold": {"strong": []},
21
+ "Code": {"code": []},
22
+ "CodeBlock": {"pre": ["class"], "code": ["class"]},
23
+ "Essentials": {"br": []},
24
+ "Font": {"span": ["class"]},
25
+ "Heading": {"h1": [], "h2": [], "h3": [], "h4": []},
26
+ "Highlight": {"mark": ["class"]},
27
+ "HorizontalLine": {"hr": []},
28
+ "HtmlEmbed": {"div": ["class"]},
29
+ "Image": {"figure": ["class"], "img": []},
30
+ "ImageCaption": {"figcaption": []},
31
+ "Indent": {BLOCK: ["class"]},
32
+ "Italic": {"i": []},
33
+ "Link": {"a": ["rel", "href"]},
34
+ "List": {"ol": [], "ul": [], "li": []},
35
+ "ListProperties": {"ol": [], "ul": [], "li": []},
36
+ "MediaEmbed": {
37
+ "figure": ["class"],
38
+ "oembed": ["url"],
39
+ "div": ["data-oembed-url"],
40
+ "iframe": ["frameborder", "src", "allow"],
41
+ },
42
+ "Mention": {"span": ["class", "data-mention"]},
43
+ "Paragraph": {"p": []},
44
+ "Strikethrough": {"s": []},
45
+ "Subscript": {"sub": []},
46
+ "Superscript": {"sup": []},
47
+ "Table": {
48
+ "figure": ["class"],
49
+ "table": [],
50
+ "thead": [],
51
+ "tbody": [],
52
+ "tr": [],
53
+ "td": ["colspan", "rowspan"],
54
+ "th": ["colspan", "rowspan"],
55
+ },
56
+ "TableCaption": {"figcaption": ["data-placeholder"]},
57
+ "TableCellProperties": {"td": ["class"]},
58
+ "TableProperties": {"figure": ["class"], "table": ["class"]},
59
+ "TodoList": {"ul": ["class"], "label": ["class"], "span": ["class"], "li": ["class"], "input": ["class"]},
60
+ "Underline": {"u": []},
61
+ }
62
+ BLOCKS = {"blockquote", "pre", "div", "figure", "ol", "ul", "p", "table", "tr", "td", "th"}
63
+
64
+
65
+ @lru_cache
66
+ def get_allowed_tags(config_name: str) -> Set[str]:
67
+ """Return the list of allowed HTML tags for a given CKEditor 5 configuration."""
68
+ return set(get_allowed_attributes(config_name))
69
+
70
+
71
+ def get_allowed_attributes(config_name: str) -> Dict[str, Set[str]]:
72
+ """Return the list of allowed HTML tags and attributes for a given CKEditor 5 configuration."""
73
+ config = settings.CKEDITOR_5_CONFIGS.get(config_name, {})
74
+ plugins: Iterable[str] = config.get("plugins", TAGS_BY_PLUGIN.keys())
75
+ allowed_attributes: Dict[str, Set[str]] = {}
76
+ for plugin in plugins:
77
+ for key, attributes in TAGS_BY_PLUGIN.get(plugin, {}).items():
78
+ if key == BLOCK:
79
+ keys = BLOCKS
80
+ else:
81
+ keys = [key]
82
+ for key_ in keys:
83
+ allowed_attributes.setdefault(key_, set())
84
+ allowed_attributes[key_] |= set(attributes)
85
+ return allowed_attributes
86
+
87
+
88
+ class CKEditor5Field(forms.CharField):
89
+ """A form field using CKEditor 5 as text editor."""
90
+
91
+ default_ckeditor5_config = "default"
92
+
93
+ def __init__(self, *args, config_name=None, **kwargs) -> None:
94
+ """Initialize the field."""
95
+ self.ckeditor5_config = config_name or self.default_ckeditor5_config
96
+ self.required_ = kwargs.get("required", True)
97
+ kwargs["required"] = False
98
+ css_class = "django_ckeditor_5"
99
+ self.is_inline_widget = "inline" in self.ckeditor5_config
100
+ if self.is_inline_widget:
101
+ css_class += " ck-editor__singleline"
102
+ kwargs["widget"] = CKEditor5Widget(attrs={"class": css_class}, config_name=self.ckeditor5_config)
103
+ super().__init__(*args, **kwargs)
104
+
105
+ def to_python(self, value):
106
+ """Convert the value to a string."""
107
+ allowed_tags = get_allowed_tags(self.ckeditor5_config)
108
+ allowed_attributes = get_allowed_attributes(self.ckeditor5_config)
109
+ if value is None:
110
+ raise ValidationError(self.error_messages["required"], code="required")
111
+ elif len(value) <= 50 and self.required_:
112
+ striped = html.unescape(strip_tags(value)).strip()
113
+ if not striped:
114
+ raise ValidationError(self.error_messages["required"], code="required")
115
+ if self.is_inline_widget and "p" in allowed_tags:
116
+ allowed_tags.remove("p")
117
+ value = nh3.clean(value, strip_comments=True, tags=allowed_tags, attributes=allowed_attributes)
118
+ value = autolink_html(value)
119
+ value = value.replace("\n", " ")
120
+ return super().to_python(value)
121
+
122
+ def prepare_value(self, value):
123
+ """Prepare the value for the widget."""
124
+ if value is None:
125
+ return ""
126
+ return value
127
+
128
+
129
+ class InlineCKEditor5Field(CKEditor5Field):
130
+ """A form field using CKEditor 5 as inline text editor."""
131
+
132
+ default_ckeditor5_config = "inline"
133
+
134
+
135
+ class InlineLinkCKEditor5Field(CKEditor5Field):
136
+ """A form field using CKEditor 5 as inline text editor."""
137
+
138
+ default_ckeditor5_config = "inline_link"
@@ -0,0 +1 @@
1
+ """Management module for df_site."""
@@ -0,0 +1 @@
1
+ """Some management commands."""
@@ -0,0 +1,104 @@
1
+ """Add an image to the media storage."""
2
+
3
+ import pathlib
4
+
5
+ from django.core.files.storage import Storage, storages
6
+ from django.core.management import BaseCommand
7
+ from django.urls import reverse
8
+
9
+ from df_site.templatetags.images import AspectPolicy, CachedImage
10
+
11
+
12
+ class Command(BaseCommand):
13
+ """Add an image to the media storage."""
14
+
15
+ help = "Add an image to the media storage."
16
+
17
+ def add_arguments(self, parser):
18
+ """Add arguments to the command."""
19
+ parser.add_argument("path", type=pathlib.Path)
20
+ parser.add_argument("--storage", choices=["default", "staticfiles"], default="default")
21
+ parser.add_argument("--name", type=str, default=None)
22
+ parser.add_argument("--height", type=int, default=None)
23
+ parser.add_argument("--width", type=int, default=None)
24
+ parser.add_argument("--format", type=str, default="webp", choices=CachedImage.formats)
25
+ parser.add_argument("--overwrite", action="store_true", default=False)
26
+ parser.add_argument("--print", action="store_true", default=False)
27
+ parser.add_argument(
28
+ "--policy",
29
+ type=int,
30
+ default=AspectPolicy.FORCE_MAX_TRANSPARENT.value,
31
+ choices=[x.value for x in AspectPolicy],
32
+ )
33
+
34
+ def handle(self, *args, **options):
35
+ """Add an image to the media storage and generate thumbnails."""
36
+ aspect_policy = AspectPolicy.get_policy(options["policy"])
37
+ src_path: pathlib.Path = options["path"]
38
+ storage: str = options["storage"]
39
+ name: str = options["name"] or src_path.name
40
+ # we need to check if the file exists
41
+ if not src_path.exists():
42
+ raise FileNotFoundError(f"File {src_path} does not exist.")
43
+ # we need to check if the file exists in the destination storage
44
+ storage_obj: Storage = storages[storage]
45
+ exists = storage_obj.exists(name)
46
+ cache_engine = "locmem"
47
+ fmt = options["format"]
48
+ if exists and not options["overwrite"]:
49
+ self.stderr.write(f"File {name} already exists in storage {storage}.")
50
+ return
51
+ elif exists:
52
+ storage_obj.delete(name)
53
+ # we save the file to the destination storage
54
+ with src_path.open("rb") as src_file:
55
+ storage_obj.save(name, src_file)
56
+ #
57
+ img = CachedImage.from_target_path(f"T/600x600_4/{name}.{fmt}")
58
+ img.process()
59
+ self.stdout.write(f"Generated thumbnails: {img.created_sizes}.")
60
+ if options["print"]:
61
+ self.stdout.write(img.as_html_tag())
62
+
63
+ def url(p):
64
+ """Return the URL of the thumbnail."""
65
+ return reverse("thumbnails", kwargs={"path": p})
66
+
67
+ img = CachedImage(
68
+ CachedImage.sources["media"]["src_storage"],
69
+ CachedImage.sources["media"]["dst_storage"],
70
+ cache_engine,
71
+ name,
72
+ CachedImage.sources["media"]["prefix"],
73
+ url=url,
74
+ height=options["height"],
75
+ width=options["width"],
76
+ widths=None,
77
+ aspect_policy=aspect_policy,
78
+ fmt=fmt,
79
+ write_thumbnails=True,
80
+ )
81
+ img.process()
82
+ self.stdout.write(f"File {name} saved to storage {storage}.")
83
+ self.stdout.write(f"Generated thumbnails: {img.created_sizes}.")
84
+ if options["print"]:
85
+ self.stdout.write(img.as_html_tag())
86
+ img = CachedImage(
87
+ CachedImage.sources["media"]["src_storage"],
88
+ CachedImage.sources["media"]["dst_storage"],
89
+ cache_engine,
90
+ name,
91
+ CachedImage.sources["media"]["prefix"],
92
+ url=url,
93
+ height=options["height"],
94
+ width=options["width"],
95
+ widths=[320, 480, 640, 1024, 2048],
96
+ aspect_policy=aspect_policy,
97
+ fmt=fmt,
98
+ write_thumbnails=True,
99
+ )
100
+ img.process()
101
+ self.stdout.write(f"File {name} saved to storage {storage}.")
102
+ self.stdout.write(f"Generated thumbnails: {img.created_sizes}.")
103
+ if options["print"]:
104
+ self.stdout.write(img.as_html_tag())