bragi-cms 1.27.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 (357) hide show
  1. bragi/__init__.py +15 -0
  2. bragi/alembic/__init__.py +0 -0
  3. bragi/alembic/env.py +66 -0
  4. bragi/alembic/script.py.mako +29 -0
  5. bragi/alembic/versions/.gitkeep +0 -0
  6. bragi/alembic/versions/2026_05_13_2119-cb48ebc1f1f5_initial_schema.py +99 -0
  7. bragi/alembic/versions/2026_05_13_2138-36a0ec65ddbf_add_redirects.py +56 -0
  8. bragi/alembic/versions/2026_05_13_2145-52631645e3c1_add_local_credentials.py +44 -0
  9. bragi/alembic/versions/2026_05_13_2208-38aa6fec0409_add_sessions.py +52 -0
  10. bragi/alembic/versions/2026_05_14_0526-9dbc52ee17a8_add_audit_log.py +61 -0
  11. bragi/alembic/versions/2026_05_14_0538-9f3fd65db818_add_attachments_and_post_image_fks.py +74 -0
  12. bragi/alembic/versions/2026_05_14_0623-1f966d693a2d_add_analytics_events.py +71 -0
  13. bragi/alembic/versions/2026_05_14_0641-f679d5fc62bb_add_extra_settings_to_sites.py +43 -0
  14. bragi/alembic/versions/2026_05_14_0719-0e57d255f32d_add_site_aliases.py +40 -0
  15. bragi/alembic/versions/2026_05_14_0723-6275dd6feda6_add_pages.py +55 -0
  16. bragi/alembic/versions/2026_05_14_0731-46fe76487384_add_tags.py +50 -0
  17. bragi/alembic/versions/2026_05_14_0745-2ecc724d6dfb_add_user_site_roles.py +48 -0
  18. bragi/alembic/versions/2026_05_14_0752-48e608e60109_add_user_identities.py +49 -0
  19. bragi/alembic/versions/2026_05_14_0904-c9d608f87623_add_revisions.py +69 -0
  20. bragi/alembic/versions/2026_05_14_1123-44fe91537fd5_add_attachment_image_fields.py +42 -0
  21. bragi/alembic/versions/2026_05_14_1154-25c6f95918c4_add_attachment_renditions.py +58 -0
  22. bragi/alembic/versions/2026_05_14_1238-17c7f26e8fde_add_page_source_id.py +36 -0
  23. bragi/alembic/versions/2026_05_14_2049-1eff692ffe2b_add_search_fts.py +71 -0
  24. bragi/alembic/versions/2026_05_14_2307-2a429b18c1d8_add_site_theme.py +39 -0
  25. bragi/alembic/versions/2026_05_15_1547-7fd0ed6fe2df_add_site_owner.py +109 -0
  26. bragi/alembic/versions/2026_05_17_1018-3341c91c55aa_add_site_home_page_id.py +61 -0
  27. bragi/alembic/versions/2026_05_17_1157-88dea48173c6_add_page_kind.py +138 -0
  28. bragi/alembic/versions/2026_05_17_1700-22e5570ca7f5_add_og_image_columns.py +68 -0
  29. bragi/alembic/versions/2026_05_17_1730-ad0c0c05ef40_add_user_bio.py +40 -0
  30. bragi/alembic/versions/2026_05_17_1900-3b4c8d1e0f00_add_personal_access_tokens.py +63 -0
  31. bragi/alembic/versions/2026_05_17_2000-4c5d9e2f1a01_add_webmentions.py +80 -0
  32. bragi/alembic/versions/2026_05_17_2100-5d6eaf3b1b02_add_activitypub.py +88 -0
  33. bragi/alembic/versions/2026_05_17_2304-e4f9edb18c87_extend_fk_ondelete_to_core_tables.py +1036 -0
  34. bragi/alembic/versions/2026_05_18_0900-6e7fbf4c2c03_add_fk_ondelete.py +374 -0
  35. bragi/alembic/versions/2026_05_18_1200-a1b2c3d4e5f6_redirect_source_path_nonempty.py +49 -0
  36. bragi/alembic/versions/2026_05_18_2200-2e99f2f0e525_add_internal_links_table_for_backlinks_.py +57 -0
  37. bragi/alembic/versions/2026_05_25_1909-8f97d76bd501_add_post_pinning.py +47 -0
  38. bragi/alembic/versions/2026_05_25_2255-5e26615ca3d3_add_post_revisions_pinning_columns.py +39 -0
  39. bragi/alembic/versions/2026_05_26_0919-e8e90e4b4afc_merge_og_image_into_featured_image.py +173 -0
  40. bragi/alembic/versions/2026_05_26_2022-f0a2ba28973b_attachment_renditions_v2_multi_format_.py +169 -0
  41. bragi/alembic/versions/2026_05_27_1905-171678f699a1_add_page_kind_resume_resume_data_column.py +30 -0
  42. bragi/alembic/versions/2026_05_29_1432-1a03542dfa5c_add_pages_show_in_nav_menu_order.py +46 -0
  43. bragi/alembic/versions/2026_06_02_1628-b6c01fcf9be2_attachments_external_source_credit_.py +57 -0
  44. bragi/alembic/versions/2026_06_03_2045-a523a7f5366b_attachment_renditions_claimed_at_for_.py +39 -0
  45. bragi/alembic/versions/__init__.py +0 -0
  46. bragi/api.py +790 -0
  47. bragi/apps/__init__.py +10 -0
  48. bragi/apps/admin.py +384 -0
  49. bragi/apps/delivery.py +181 -0
  50. bragi/cli.py +375 -0
  51. bragi/contrib/__init__.py +23 -0
  52. bragi/contrib/activitypub/__init__.py +27 -0
  53. bragi/contrib/activitypub/activities.py +160 -0
  54. bragi/contrib/activitypub/cli.py +63 -0
  55. bragi/contrib/activitypub/keys.py +89 -0
  56. bragi/contrib/activitypub/plugin.py +99 -0
  57. bragi/contrib/activitypub/sender.py +174 -0
  58. bragi/contrib/activitypub/signature.py +322 -0
  59. bragi/contrib/activitypub/views.py +440 -0
  60. bragi/contrib/admin_imports/__init__.py +0 -0
  61. bragi/contrib/admin_imports/admin.py +37 -0
  62. bragi/contrib/admin_imports/plugin.py +115 -0
  63. bragi/contrib/admin_imports/templates/admin/import_index.html +31 -0
  64. bragi/contrib/analytics/__init__.py +16 -0
  65. bragi/contrib/analytics/admin.py +85 -0
  66. bragi/contrib/analytics/plugin.py +129 -0
  67. bragi/contrib/analytics/templates/admin/analytics_dashboard.html +32 -0
  68. bragi/contrib/anchors/__init__.py +13 -0
  69. bragi/contrib/anchors/plugin.py +20 -0
  70. bragi/contrib/anchors/transform.py +74 -0
  71. bragi/contrib/api_tokens/__init__.py +18 -0
  72. bragi/contrib/api_tokens/admin.py +163 -0
  73. bragi/contrib/api_tokens/api.py +357 -0
  74. bragi/contrib/api_tokens/auth.py +261 -0
  75. bragi/contrib/api_tokens/plugin.py +59 -0
  76. bragi/contrib/api_tokens/templates/admin/api_tokens/created.html +16 -0
  77. bragi/contrib/api_tokens/templates/admin/api_tokens/list.html +72 -0
  78. bragi/contrib/api_tokens/tokens.py +151 -0
  79. bragi/contrib/attachments/__init__.py +10 -0
  80. bragi/contrib/attachments/admin.py +716 -0
  81. bragi/contrib/attachments/cli.py +436 -0
  82. bragi/contrib/attachments/delivery.py +132 -0
  83. bragi/contrib/attachments/plugin.py +208 -0
  84. bragi/contrib/attachments/service.py +146 -0
  85. bragi/contrib/attachments/templates/admin/_attachment_row.html +60 -0
  86. bragi/contrib/attachments/templates/admin/_attachments_picker_library.html +84 -0
  87. bragi/contrib/attachments/templates/admin/attachment_edit.html +62 -0
  88. bragi/contrib/attachments/templates/admin/attachments_list.html +110 -0
  89. bragi/contrib/attachments/templates/admin/attachments_new.html +22 -0
  90. bragi/contrib/attachments/templates/admin/attachments_picker.html +142 -0
  91. bragi/contrib/attachments/transforms.py +182 -0
  92. bragi/contrib/audit/__init__.py +8 -0
  93. bragi/contrib/audit/admin.py +79 -0
  94. bragi/contrib/audit/plugin.py +28 -0
  95. bragi/contrib/audit/templates/admin/audit_list.html +69 -0
  96. bragi/contrib/auth_github/__init__.py +15 -0
  97. bragi/contrib/auth_github/client.py +78 -0
  98. bragi/contrib/auth_github/plugin.py +26 -0
  99. bragi/contrib/auth_github/views.py +192 -0
  100. bragi/contrib/auth_local/__init__.py +19 -0
  101. bragi/contrib/auth_local/cli.py +252 -0
  102. bragi/contrib/auth_local/passwords.py +58 -0
  103. bragi/contrib/auth_local/plugin.py +112 -0
  104. bragi/contrib/auth_local/templates/auth_local/change_password.html +65 -0
  105. bragi/contrib/auth_local/templates/auth_local/login.html +61 -0
  106. bragi/contrib/auth_local/views.py +209 -0
  107. bragi/contrib/embeds/__init__.py +15 -0
  108. bragi/contrib/embeds/cli.py +47 -0
  109. bragi/contrib/embeds/directive.py +163 -0
  110. bragi/contrib/embeds/plugin.py +54 -0
  111. bragi/contrib/embeds/providers/__init__.py +49 -0
  112. bragi/contrib/embeds/providers/base.py +42 -0
  113. bragi/contrib/embeds/providers/bluesky.py +65 -0
  114. bragi/contrib/embeds/providers/oembed.py +94 -0
  115. bragi/contrib/embeds/providers/youtube.py +178 -0
  116. bragi/contrib/embeds/rerender.py +143 -0
  117. bragi/contrib/embeds/transforms.py +52 -0
  118. bragi/contrib/highlight/__init__.py +13 -0
  119. bragi/contrib/highlight/plugin.py +43 -0
  120. bragi/contrib/highlight/transform.py +64 -0
  121. bragi/contrib/import_ghost/__init__.py +12 -0
  122. bragi/contrib/import_ghost/admin.py +304 -0
  123. bragi/contrib/import_ghost/cli.py +75 -0
  124. bragi/contrib/import_ghost/importer.py +530 -0
  125. bragi/contrib/import_ghost/loader.py +70 -0
  126. bragi/contrib/import_ghost/plugin.py +63 -0
  127. bragi/contrib/import_ghost/templates/admin/import_ghost_result.html +34 -0
  128. bragi/contrib/import_ghost/templates/admin/import_ghost_review.html +48 -0
  129. bragi/contrib/import_ghost/templates/admin/import_ghost_upload.html +20 -0
  130. bragi/contrib/import_hugo/__init__.py +12 -0
  131. bragi/contrib/import_hugo/cli.py +75 -0
  132. bragi/contrib/import_hugo/importer.py +291 -0
  133. bragi/contrib/import_hugo/parser.py +53 -0
  134. bragi/contrib/import_hugo/plugin.py +36 -0
  135. bragi/contrib/import_linkedin/__init__.py +19 -0
  136. bragi/contrib/import_linkedin/admin.py +198 -0
  137. bragi/contrib/import_linkedin/cli.py +188 -0
  138. bragi/contrib/import_linkedin/importer.py +396 -0
  139. bragi/contrib/import_linkedin/parser.py +377 -0
  140. bragi/contrib/import_linkedin/plugin.py +43 -0
  141. bragi/contrib/import_linkedin/proposals.py +548 -0
  142. bragi/contrib/import_linkedin/templates/admin/_linkedin_import_widget.html +20 -0
  143. bragi/contrib/import_linkedin/templates/admin/linkedin_review.html +52 -0
  144. bragi/contrib/import_wordpress/__init__.py +12 -0
  145. bragi/contrib/import_wordpress/_upsert.py +335 -0
  146. bragi/contrib/import_wordpress/cli.py +88 -0
  147. bragi/contrib/import_wordpress/importer.py +295 -0
  148. bragi/contrib/import_wordpress/loader.py +279 -0
  149. bragi/contrib/import_wordpress/plugin.py +34 -0
  150. bragi/contrib/indexnow/__init__.py +17 -0
  151. bragi/contrib/indexnow/cli.py +59 -0
  152. bragi/contrib/indexnow/client.py +61 -0
  153. bragi/contrib/indexnow/plugin.py +171 -0
  154. bragi/contrib/indexnow/views.py +37 -0
  155. bragi/contrib/internal_links/__init__.py +29 -0
  156. bragi/contrib/internal_links/admin.py +286 -0
  157. bragi/contrib/internal_links/cli.py +71 -0
  158. bragi/contrib/internal_links/delivery.py +146 -0
  159. bragi/contrib/internal_links/index.py +162 -0
  160. bragi/contrib/internal_links/markdown_ext.py +139 -0
  161. bragi/contrib/internal_links/plugin.py +93 -0
  162. bragi/contrib/internal_links/templates/admin/_picker.html +85 -0
  163. bragi/contrib/internal_links/templates/admin/backlinks.html +41 -0
  164. bragi/contrib/markdown_extras/__init__.py +7 -0
  165. bragi/contrib/markdown_extras/plugin.py +88 -0
  166. bragi/contrib/nav/__init__.py +15 -0
  167. bragi/contrib/nav/plugin.py +83 -0
  168. bragi/contrib/nav/templates/delivery/_site_nav.html +104 -0
  169. bragi/contrib/nav/tree.py +58 -0
  170. bragi/contrib/page/__init__.py +7 -0
  171. bragi/contrib/page/admin.py +1535 -0
  172. bragi/contrib/page/archive.py +175 -0
  173. bragi/contrib/page/delivery.py +508 -0
  174. bragi/contrib/page/plugin.py +356 -0
  175. bragi/contrib/page/resume.py +42 -0
  176. bragi/contrib/page/resume_jsonld.py +104 -0
  177. bragi/contrib/page/static/pinned-carousel.js +146 -0
  178. bragi/contrib/page/templates/admin/_page_list_table.html +43 -0
  179. bragi/contrib/page/templates/admin/_page_menu_order_cell.html +16 -0
  180. bragi/contrib/page/templates/admin/_page_show_in_nav_cell.html +12 -0
  181. bragi/contrib/page/templates/admin/_page_slug_cell.html +28 -0
  182. bragi/contrib/page/templates/admin/_page_status_cell.html +18 -0
  183. bragi/contrib/page/templates/admin/_page_title_cell.html +30 -0
  184. bragi/contrib/page/templates/admin/_resume_fieldset.html +534 -0
  185. bragi/contrib/page/templates/admin/page_edit.html +213 -0
  186. bragi/contrib/page/templates/admin/page_list.html +15 -0
  187. bragi/contrib/page/templates/admin/page_revision_detail.html +46 -0
  188. bragi/contrib/page/templates/admin/page_revisions.html +38 -0
  189. bragi/contrib/page/templates/delivery/_pinned_carousel.html +46 -0
  190. bragi/contrib/page/templates/delivery/archive_month.html +20 -0
  191. bragi/contrib/page/templates/delivery/archive_year.html +15 -0
  192. bragi/contrib/page/templates/delivery/archive_years.html +17 -0
  193. bragi/contrib/page/templates/delivery/page.html +38 -0
  194. bragi/contrib/page/templates/delivery/post_index.html +60 -0
  195. bragi/contrib/page/templates/delivery/resume.html +193 -0
  196. bragi/contrib/page/templates/delivery/tag_list.html +25 -0
  197. bragi/contrib/post/__init__.py +11 -0
  198. bragi/contrib/post/admin.py +1080 -0
  199. bragi/contrib/post/cli.py +138 -0
  200. bragi/contrib/post/delivery.py +24 -0
  201. bragi/contrib/post/plugin.py +273 -0
  202. bragi/contrib/post/related.py +73 -0
  203. bragi/contrib/post/templates/admin/_pinned_cell.html +17 -0
  204. bragi/contrib/post/templates/admin/_post_list_table.html +47 -0
  205. bragi/contrib/post/templates/admin/_slug_cell.html +31 -0
  206. bragi/contrib/post/templates/admin/_status_cell.html +19 -0
  207. bragi/contrib/post/templates/admin/_title_cell.html +33 -0
  208. bragi/contrib/post/templates/admin/edit.html +122 -0
  209. bragi/contrib/post/templates/admin/list.html +15 -0
  210. bragi/contrib/post/templates/admin/post_revision_detail.html +45 -0
  211. bragi/contrib/post/templates/admin/post_revisions.html +36 -0
  212. bragi/contrib/post/templates/delivery/post.html +115 -0
  213. bragi/contrib/redirects/__init__.py +15 -0
  214. bragi/contrib/redirects/admin.py +298 -0
  215. bragi/contrib/redirects/plugin.py +396 -0
  216. bragi/contrib/redirects/templates/admin/redirects_edit.html +70 -0
  217. bragi/contrib/redirects/templates/admin/redirects_list.html +62 -0
  218. bragi/contrib/search/__init__.py +13 -0
  219. bragi/contrib/search/backend.py +409 -0
  220. bragi/contrib/search/cli.py +66 -0
  221. bragi/contrib/search/delivery.py +55 -0
  222. bragi/contrib/search/plugin.py +148 -0
  223. bragi/contrib/search/templates/delivery/_search_results.html +49 -0
  224. bragi/contrib/search/templates/delivery/search.html +25 -0
  225. bragi/contrib/search/text.py +31 -0
  226. bragi/contrib/seo/__init__.py +15 -0
  227. bragi/contrib/seo/feed.py +70 -0
  228. bragi/contrib/seo/plugin.py +42 -0
  229. bragi/contrib/seo/robots.py +32 -0
  230. bragi/contrib/seo/security_txt.py +33 -0
  231. bragi/contrib/seo/sitemap.py +106 -0
  232. bragi/contrib/sessions/__init__.py +17 -0
  233. bragi/contrib/sessions/admin.py +161 -0
  234. bragi/contrib/sessions/plugin.py +45 -0
  235. bragi/contrib/sessions/templates/admin/sessions_all_list.html +48 -0
  236. bragi/contrib/sessions/templates/admin/sessions_self_list.html +53 -0
  237. bragi/contrib/sites/__init__.py +11 -0
  238. bragi/contrib/sites/admin.py +951 -0
  239. bragi/contrib/sites/cli.py +240 -0
  240. bragi/contrib/sites/plugin.py +59 -0
  241. bragi/contrib/sites/templates/admin/site_dashboard.html +44 -0
  242. bragi/contrib/sites/templates/admin/sites_edit.html +226 -0
  243. bragi/contrib/sites/templates/admin/sites_list.html +66 -0
  244. bragi/contrib/team/__init__.py +18 -0
  245. bragi/contrib/team/admin.py +194 -0
  246. bragi/contrib/team/plugin.py +35 -0
  247. bragi/contrib/team/templates/admin/team_list.html +76 -0
  248. bragi/contrib/theme_default/__init__.py +14 -0
  249. bragi/contrib/theme_default/plugin.py +68 -0
  250. bragi/contrib/theme_default/static/resume.css +165 -0
  251. bragi/contrib/theme_default/templates/delivery/base.html +185 -0
  252. bragi/contrib/theme_minimal/__init__.py +10 -0
  253. bragi/contrib/theme_minimal/plugin.py +36 -0
  254. bragi/contrib/theme_minimal/static/resume.css +165 -0
  255. bragi/contrib/theme_minimal/templates/delivery/base.html +187 -0
  256. bragi/contrib/theme_serif/__init__.py +8 -0
  257. bragi/contrib/theme_serif/plugin.py +27 -0
  258. bragi/contrib/theme_serif/static/resume.css +165 -0
  259. bragi/contrib/theme_serif/templates/delivery/base.html +193 -0
  260. bragi/contrib/theme_terminal/__init__.py +9 -0
  261. bragi/contrib/theme_terminal/plugin.py +27 -0
  262. bragi/contrib/theme_terminal/static/resume.css +165 -0
  263. bragi/contrib/theme_terminal/templates/delivery/base.html +203 -0
  264. bragi/contrib/themes/__init__.py +22 -0
  265. bragi/contrib/themes/cli.py +32 -0
  266. bragi/contrib/themes/delivery.py +31 -0
  267. bragi/contrib/themes/plugin.py +31 -0
  268. bragi/contrib/unsplash/__init__.py +0 -0
  269. bragi/contrib/unsplash/admin.py +209 -0
  270. bragi/contrib/unsplash/client.py +116 -0
  271. bragi/contrib/unsplash/plugin.py +75 -0
  272. bragi/contrib/unsplash/render.py +226 -0
  273. bragi/contrib/unsplash/templates/admin/_unsplash_results.html +44 -0
  274. bragi/contrib/unsplash/templates/admin/_unsplash_tab.html +127 -0
  275. bragi/contrib/webmentions/__init__.py +24 -0
  276. bragi/contrib/webmentions/admin.py +91 -0
  277. bragi/contrib/webmentions/cli.py +36 -0
  278. bragi/contrib/webmentions/parse.py +200 -0
  279. bragi/contrib/webmentions/plugin.py +233 -0
  280. bragi/contrib/webmentions/receiver.py +267 -0
  281. bragi/contrib/webmentions/sender.py +151 -0
  282. bragi/contrib/webmentions/templates/admin/webmentions/list.html +56 -0
  283. bragi/core/__init__.py +12 -0
  284. bragi/core/audit.py +113 -0
  285. bragi/core/breadcrumbs.py +46 -0
  286. bragi/core/cache.py +144 -0
  287. bragi/core/db.py +69 -0
  288. bragi/core/export.py +421 -0
  289. bragi/core/feed.py +95 -0
  290. bragi/core/healthz.py +52 -0
  291. bragi/core/htmx.py +16 -0
  292. bragi/core/http.py +273 -0
  293. bragi/core/image_processor.py +247 -0
  294. bragi/core/middleware/__init__.py +12 -0
  295. bragi/core/middleware/csrf.py +118 -0
  296. bragi/core/middleware/redirects.py +94 -0
  297. bragi/core/middleware/sessions.py +265 -0
  298. bragi/core/middleware/site_resolver.py +58 -0
  299. bragi/core/models/__init__.py +85 -0
  300. bragi/core/models/_base.py +17 -0
  301. bragi/core/models/_mixins.py +40 -0
  302. bragi/core/models/activitypub.py +102 -0
  303. bragi/core/models/analytics_event.py +39 -0
  304. bragi/core/models/attachment.py +79 -0
  305. bragi/core/models/attachment_rendition.py +77 -0
  306. bragi/core/models/audit_log.py +45 -0
  307. bragi/core/models/internal_link.py +75 -0
  308. bragi/core/models/local_credential.py +31 -0
  309. bragi/core/models/page.py +153 -0
  310. bragi/core/models/page_revision.py +44 -0
  311. bragi/core/models/personal_access_token.py +74 -0
  312. bragi/core/models/post.py +104 -0
  313. bragi/core/models/post_revision.py +64 -0
  314. bragi/core/models/redirect.py +82 -0
  315. bragi/core/models/session.py +41 -0
  316. bragi/core/models/site.py +77 -0
  317. bragi/core/models/site_alias.py +30 -0
  318. bragi/core/models/tag.py +40 -0
  319. bragi/core/models/user.py +32 -0
  320. bragi/core/models/user_identity.py +35 -0
  321. bragi/core/models/user_site_role.py +36 -0
  322. bragi/core/models/webmention.py +108 -0
  323. bragi/core/permissions.py +202 -0
  324. bragi/core/registry.py +215 -0
  325. bragi/core/render/__init__.py +12 -0
  326. bragi/core/render/excerpts.py +69 -0
  327. bragi/core/render/markdown.py +146 -0
  328. bragi/core/render/reading_time.py +36 -0
  329. bragi/core/render/toc.py +101 -0
  330. bragi/core/render/transforms.py +58 -0
  331. bragi/core/renditions.py +178 -0
  332. bragi/core/safe_urls.py +158 -0
  333. bragi/core/security.py +55 -0
  334. bragi/core/seo.py +72 -0
  335. bragi/core/storage.py +249 -0
  336. bragi/core/text.py +110 -0
  337. bragi/core/themes.py +219 -0
  338. bragi/core/time.py +59 -0
  339. bragi/core/url.py +228 -0
  340. bragi/core/useragent.py +71 -0
  341. bragi/hookspecs.py +488 -0
  342. bragi/plugins.py +25 -0
  343. bragi/settings.py +222 -0
  344. bragi/static/admin/admin-chrome.css +254 -0
  345. bragi/static/admin/inline-edit.js +12 -0
  346. bragi/templates/admin/_admin_nav.html +157 -0
  347. bragi/templates/admin/_image_picker_field.html +167 -0
  348. bragi/templates/admin/_repeating_field.html +46 -0
  349. bragi/templates/admin/_tiptap_editor.html +760 -0
  350. bragi/templates/admin/base.html +29 -0
  351. bragi/templates/admin/index.html +33 -0
  352. bragi/templates/delivery/_welcome_fallback.html +17 -0
  353. bragi_cms-1.27.0.dist-info/METADATA +717 -0
  354. bragi_cms-1.27.0.dist-info/RECORD +357 -0
  355. bragi_cms-1.27.0.dist-info/WHEEL +4 -0
  356. bragi_cms-1.27.0.dist-info/entry_points.txt +41 -0
  357. bragi_cms-1.27.0.dist-info/licenses/LICENSE +21 -0
bragi/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """bragi: a multisite CMS with htmx, plugins, and SEO baked in."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ try:
8
+ # Distribution name is `bragi-cms` (the `bragi` name on PyPI is held
9
+ # by an unrelated project). Import path and this package name stay
10
+ # `bragi`; the distribution name change only affects pip install.
11
+ __version__ = version("bragi-cms")
12
+ except PackageNotFoundError: # pragma: no cover - not installed
13
+ __version__ = "0.0.0+unknown"
14
+
15
+ __all__ = ["__version__"]
File without changes
bragi/alembic/env.py ADDED
@@ -0,0 +1,66 @@
1
+ """Alembic environment for bragi.
2
+
3
+ Reads the DB URL from BRAGI_DATABASE_URL when set, otherwise falls
4
+ back to the value in alembic.ini. Target metadata is
5
+ `bragi.core.models.Base.metadata` so autogenerate sees every model.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from logging.config import fileConfig
12
+
13
+ from alembic import context
14
+ from sqlalchemy import engine_from_config, pool
15
+
16
+ # Alembic Config object; gives access to values in alembic.ini.
17
+ config = context.config
18
+
19
+ # Override DB URL from environment when present.
20
+ if db_url := os.environ.get("BRAGI_DATABASE_URL"):
21
+ config.set_main_option("sqlalchemy.url", db_url)
22
+
23
+ # Configure loggers from alembic.ini.
24
+ if config.config_file_name is not None:
25
+ fileConfig(config.config_file_name)
26
+
27
+ # Target metadata for autogenerate. Imported lazily so the alembic
28
+ # CLI still works during early scaffolding before models exist.
29
+ try:
30
+ from bragi.core.models import Base
31
+
32
+ target_metadata = Base.metadata
33
+ except ImportError:
34
+ target_metadata = None
35
+
36
+
37
+ def run_migrations_offline() -> None:
38
+ """Run migrations in 'offline' mode, emitting SQL to a script."""
39
+ url = config.get_main_option("sqlalchemy.url")
40
+ context.configure(
41
+ url=url,
42
+ target_metadata=target_metadata,
43
+ literal_binds=True,
44
+ dialect_opts={"paramstyle": "named"},
45
+ )
46
+ with context.begin_transaction():
47
+ context.run_migrations()
48
+
49
+
50
+ def run_migrations_online() -> None:
51
+ """Run migrations in 'online' mode against a live DB."""
52
+ connectable = engine_from_config(
53
+ config.get_section(config.config_ini_section, {}),
54
+ prefix="sqlalchemy.",
55
+ poolclass=pool.NullPool,
56
+ )
57
+ with connectable.connect() as connection:
58
+ context.configure(connection=connection, target_metadata=target_metadata)
59
+ with context.begin_transaction():
60
+ context.run_migrations()
61
+
62
+
63
+ if context.is_offline_mode():
64
+ run_migrations_offline()
65
+ else:
66
+ run_migrations_online()
@@ -0,0 +1,29 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+ ${imports if imports else ""}
16
+
17
+ # revision identifiers, used by Alembic.
18
+ revision: str = ${repr(up_revision)}
19
+ down_revision: str | None = ${repr(down_revision)}
20
+ branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
21
+ depends_on: str | Sequence[str] | None = ${repr(depends_on)}
22
+
23
+
24
+ def upgrade() -> None:
25
+ ${upgrades if upgrades else "pass"}
26
+
27
+
28
+ def downgrade() -> None:
29
+ ${downgrades if downgrades else "pass"}
File without changes
@@ -0,0 +1,99 @@
1
+ """initial_schema
2
+
3
+ Revision ID: cb48ebc1f1f5
4
+ Revises:
5
+ Create Date: 2026-05-13 21:19:28.794005+00:00
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "cb48ebc1f1f5"
16
+ down_revision: str | None = None
17
+ branch_labels: str | Sequence[str] | None = None
18
+ depends_on: str | Sequence[str] | None = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table(
24
+ "sites",
25
+ sa.Column("slug", sa.String(length=64), nullable=False),
26
+ sa.Column("hostname", sa.String(length=255), nullable=False),
27
+ sa.Column("title", sa.String(length=255), nullable=False),
28
+ sa.Column("locale", sa.String(length=16), nullable=False),
29
+ sa.Column("timezone", sa.String(length=64), nullable=False),
30
+ sa.Column("canonical_url", sa.String(length=255), nullable=False),
31
+ sa.Column("active", sa.Boolean(), nullable=False),
32
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
33
+ sa.Column("created_at", sa.DateTime(), nullable=False),
34
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
35
+ sa.PrimaryKeyConstraint("id"),
36
+ sa.UniqueConstraint("hostname"),
37
+ sa.UniqueConstraint("slug"),
38
+ )
39
+ op.create_table(
40
+ "users",
41
+ sa.Column("email", sa.String(length=255), nullable=False),
42
+ sa.Column("display_name", sa.String(length=255), nullable=False),
43
+ sa.Column("is_superuser", sa.Boolean(), nullable=False),
44
+ sa.Column("is_active", sa.Boolean(), nullable=False),
45
+ sa.Column("last_login_at", sa.DateTime(), nullable=True),
46
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
47
+ sa.Column("created_at", sa.DateTime(), nullable=False),
48
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
49
+ sa.PrimaryKeyConstraint("id"),
50
+ sa.UniqueConstraint("email"),
51
+ )
52
+ op.create_table(
53
+ "posts",
54
+ sa.Column("site_id", sa.Integer(), nullable=False),
55
+ sa.Column("slug", sa.String(length=255), nullable=False),
56
+ sa.Column("title", sa.String(length=255), nullable=False),
57
+ sa.Column("subtitle", sa.String(length=255), nullable=True),
58
+ sa.Column("body_markdown", sa.Text(), nullable=False),
59
+ sa.Column("body_html", sa.Text(), nullable=False),
60
+ sa.Column("body_excerpt", sa.Text(), nullable=False),
61
+ sa.Column("author_id", sa.Integer(), nullable=False),
62
+ sa.Column("status", sa.String(length=16), nullable=False),
63
+ sa.Column("published_at", sa.DateTime(), nullable=True),
64
+ sa.Column("scheduled_for", sa.DateTime(), nullable=True),
65
+ sa.Column("meta_title", sa.String(length=255), nullable=True),
66
+ sa.Column("meta_description", sa.Text(), nullable=True),
67
+ sa.Column("canonical_url", sa.String(length=255), nullable=True),
68
+ sa.Column("noindex", sa.Boolean(), nullable=False),
69
+ sa.Column("source_id", sa.String(length=255), nullable=True),
70
+ sa.Column("source_meta", sa.JSON(), nullable=True),
71
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
72
+ sa.Column("created_at", sa.DateTime(), nullable=False),
73
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
74
+ sa.ForeignKeyConstraint(
75
+ ["author_id"],
76
+ ["users.id"],
77
+ ),
78
+ sa.ForeignKeyConstraint(
79
+ ["site_id"],
80
+ ["sites.id"],
81
+ ),
82
+ sa.PrimaryKeyConstraint("id"),
83
+ sa.UniqueConstraint("site_id", "slug", name="uq_posts_site_slug"),
84
+ )
85
+ op.create_index(op.f("ix_posts_published_at"), "posts", ["published_at"], unique=False)
86
+ op.create_index(op.f("ix_posts_scheduled_for"), "posts", ["scheduled_for"], unique=False)
87
+ op.create_index(op.f("ix_posts_source_id"), "posts", ["source_id"], unique=False)
88
+ # ### end Alembic commands ###
89
+
90
+
91
+ def downgrade() -> None:
92
+ # ### commands auto generated by Alembic - please adjust! ###
93
+ op.drop_index(op.f("ix_posts_source_id"), table_name="posts")
94
+ op.drop_index(op.f("ix_posts_scheduled_for"), table_name="posts")
95
+ op.drop_index(op.f("ix_posts_published_at"), table_name="posts")
96
+ op.drop_table("posts")
97
+ op.drop_table("users")
98
+ op.drop_table("sites")
99
+ # ### end Alembic commands ###
@@ -0,0 +1,56 @@
1
+ """add_redirects
2
+
3
+ Revision ID: 36a0ec65ddbf
4
+ Revises: cb48ebc1f1f5
5
+ Create Date: 2026-05-13 21:38:41.962916+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "36a0ec65ddbf"
18
+ down_revision: str | None = "cb48ebc1f1f5"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table(
26
+ "redirects",
27
+ sa.Column("site_id", sa.Integer(), nullable=False),
28
+ sa.Column("source_path", sa.String(length=1024), nullable=False),
29
+ sa.Column("target", sa.String(length=1024), nullable=False),
30
+ sa.Column("status_code", sa.Integer(), nullable=False),
31
+ sa.Column("match_type", sa.String(length=16), nullable=False),
32
+ sa.Column("active", sa.Boolean(), nullable=False),
33
+ sa.Column("hit_count", sa.Integer(), nullable=False),
34
+ sa.Column("last_hit_at", sa.DateTime(), nullable=True),
35
+ sa.Column("source", sa.String(length=64), nullable=False),
36
+ sa.Column("note", sa.Text(), nullable=True),
37
+ sa.Column("broken", sa.Boolean(), nullable=False),
38
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
39
+ sa.Column("created_at", sa.DateTime(), nullable=False),
40
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
41
+ sa.ForeignKeyConstraint(
42
+ ["site_id"],
43
+ ["sites.id"],
44
+ ),
45
+ sa.PrimaryKeyConstraint("id"),
46
+ sa.UniqueConstraint(
47
+ "site_id", "source_path", "match_type", name="uq_redirects_site_source_match"
48
+ ),
49
+ )
50
+ # ### end Alembic commands ###
51
+
52
+
53
+ def downgrade() -> None:
54
+ # ### commands auto generated by Alembic - please adjust! ###
55
+ op.drop_table("redirects")
56
+ # ### end Alembic commands ###
@@ -0,0 +1,44 @@
1
+ """add_local_credentials
2
+
3
+ Revision ID: 52631645e3c1
4
+ Revises: 36a0ec65ddbf
5
+ Create Date: 2026-05-13 21:45:47.264899+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "52631645e3c1"
18
+ down_revision: str | None = "36a0ec65ddbf"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table(
26
+ "local_credentials",
27
+ sa.Column("user_id", sa.Integer(), nullable=False),
28
+ sa.Column("password_hash", sa.String(length=255), nullable=False),
29
+ sa.Column("must_change", sa.Boolean(), nullable=False),
30
+ sa.Column("created_at", sa.DateTime(), nullable=False),
31
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
32
+ sa.ForeignKeyConstraint(
33
+ ["user_id"],
34
+ ["users.id"],
35
+ ),
36
+ sa.PrimaryKeyConstraint("user_id"),
37
+ )
38
+ # ### end Alembic commands ###
39
+
40
+
41
+ def downgrade() -> None:
42
+ # ### commands auto generated by Alembic - please adjust! ###
43
+ op.drop_table("local_credentials")
44
+ # ### end Alembic commands ###
@@ -0,0 +1,52 @@
1
+ """add_sessions
2
+
3
+ Revision ID: 38aa6fec0409
4
+ Revises: 52631645e3c1
5
+ Create Date: 2026-05-13 22:08:11.125671+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "38aa6fec0409"
18
+ down_revision: str | None = "52631645e3c1"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table(
26
+ "sessions",
27
+ sa.Column("id", sa.String(length=64), nullable=False),
28
+ sa.Column("user_id", sa.Integer(), nullable=True),
29
+ sa.Column("expires_at", sa.DateTime(), nullable=False),
30
+ sa.Column("last_seen_at", sa.DateTime(), nullable=False),
31
+ sa.Column("ip", sa.String(length=64), nullable=True),
32
+ sa.Column("user_agent", sa.String(length=512), nullable=True),
33
+ sa.Column("data", sa.JSON(), nullable=False),
34
+ sa.Column("created_at", sa.DateTime(), nullable=False),
35
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
36
+ sa.ForeignKeyConstraint(
37
+ ["user_id"],
38
+ ["users.id"],
39
+ ),
40
+ sa.PrimaryKeyConstraint("id"),
41
+ )
42
+ op.create_index(op.f("ix_sessions_expires_at"), "sessions", ["expires_at"], unique=False)
43
+ op.create_index(op.f("ix_sessions_user_id"), "sessions", ["user_id"], unique=False)
44
+ # ### end Alembic commands ###
45
+
46
+
47
+ def downgrade() -> None:
48
+ # ### commands auto generated by Alembic - please adjust! ###
49
+ op.drop_index(op.f("ix_sessions_user_id"), table_name="sessions")
50
+ op.drop_index(op.f("ix_sessions_expires_at"), table_name="sessions")
51
+ op.drop_table("sessions")
52
+ # ### end Alembic commands ###
@@ -0,0 +1,61 @@
1
+ """add_audit_log
2
+
3
+ Revision ID: 9dbc52ee17a8
4
+ Revises: 38aa6fec0409
5
+ Create Date: 2026-05-14 05:26:36.147437+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "9dbc52ee17a8"
18
+ down_revision: str | None = "38aa6fec0409"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table(
26
+ "audit_log",
27
+ sa.Column("actor_user_id", sa.Integer(), nullable=True),
28
+ sa.Column("action", sa.String(length=64), nullable=False),
29
+ sa.Column("target_type", sa.String(length=32), nullable=True),
30
+ sa.Column("target_id", sa.Integer(), nullable=True),
31
+ sa.Column("site_id", sa.Integer(), nullable=True),
32
+ sa.Column("occurred_at", sa.DateTime(), nullable=False),
33
+ sa.Column("ip", sa.String(length=64), nullable=True),
34
+ sa.Column("user_agent", sa.String(length=512), nullable=True),
35
+ sa.Column("extra", sa.JSON(), nullable=False),
36
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
37
+ sa.ForeignKeyConstraint(
38
+ ["actor_user_id"],
39
+ ["users.id"],
40
+ ),
41
+ sa.ForeignKeyConstraint(
42
+ ["site_id"],
43
+ ["sites.id"],
44
+ ),
45
+ sa.PrimaryKeyConstraint("id"),
46
+ )
47
+ op.create_index(op.f("ix_audit_log_action"), "audit_log", ["action"], unique=False)
48
+ op.create_index(
49
+ op.f("ix_audit_log_actor_user_id"), "audit_log", ["actor_user_id"], unique=False
50
+ )
51
+ op.create_index(op.f("ix_audit_log_occurred_at"), "audit_log", ["occurred_at"], unique=False)
52
+ # ### end Alembic commands ###
53
+
54
+
55
+ def downgrade() -> None:
56
+ # ### commands auto generated by Alembic - please adjust! ###
57
+ op.drop_index(op.f("ix_audit_log_occurred_at"), table_name="audit_log")
58
+ op.drop_index(op.f("ix_audit_log_actor_user_id"), table_name="audit_log")
59
+ op.drop_index(op.f("ix_audit_log_action"), table_name="audit_log")
60
+ op.drop_table("audit_log")
61
+ # ### end Alembic commands ###
@@ -0,0 +1,74 @@
1
+ """add_attachments_and_post_image_fks
2
+
3
+ Revision ID: 9f3fd65db818
4
+ Revises: 9dbc52ee17a8
5
+ Create Date: 2026-05-14 05:38:23.956762+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "9f3fd65db818"
18
+ down_revision: str | None = "9dbc52ee17a8"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.create_table(
25
+ "attachments",
26
+ sa.Column("site_id", sa.Integer(), nullable=False),
27
+ sa.Column("filename", sa.String(length=255), nullable=False),
28
+ sa.Column("content_type", sa.String(length=127), nullable=False),
29
+ sa.Column("size_bytes", sa.Integer(), nullable=False),
30
+ sa.Column("storage_key", sa.String(length=128), nullable=False),
31
+ sa.Column("uploaded_by", sa.Integer(), nullable=True),
32
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
33
+ sa.Column("created_at", sa.DateTime(), nullable=False),
34
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
35
+ sa.ForeignKeyConstraint(
36
+ ["site_id"],
37
+ ["sites.id"],
38
+ ),
39
+ sa.ForeignKeyConstraint(
40
+ ["uploaded_by"],
41
+ ["users.id"],
42
+ ),
43
+ sa.PrimaryKeyConstraint("id"),
44
+ sa.UniqueConstraint("site_id", "storage_key", name="uq_attachments_site_key"),
45
+ )
46
+ op.create_index(op.f("ix_attachments_site_id"), "attachments", ["site_id"], unique=False)
47
+
48
+ # SQLite can't ALTER TABLE ... ADD CONSTRAINT; batch mode does
49
+ # the copy-and-move dance. Pass-through on PostgreSQL.
50
+ with op.batch_alter_table("posts") as batch_op:
51
+ batch_op.add_column(sa.Column("featured_image_id", sa.Integer(), nullable=True))
52
+ batch_op.add_column(sa.Column("og_image_id", sa.Integer(), nullable=True))
53
+ batch_op.create_foreign_key(
54
+ "fk_posts_featured_image_id_attachments",
55
+ "attachments",
56
+ ["featured_image_id"],
57
+ ["id"],
58
+ )
59
+ batch_op.create_foreign_key(
60
+ "fk_posts_og_image_id_attachments",
61
+ "attachments",
62
+ ["og_image_id"],
63
+ ["id"],
64
+ )
65
+
66
+
67
+ def downgrade() -> None:
68
+ with op.batch_alter_table("posts") as batch_op:
69
+ batch_op.drop_constraint("fk_posts_og_image_id_attachments", type_="foreignkey")
70
+ batch_op.drop_constraint("fk_posts_featured_image_id_attachments", type_="foreignkey")
71
+ batch_op.drop_column("og_image_id")
72
+ batch_op.drop_column("featured_image_id")
73
+ op.drop_index(op.f("ix_attachments_site_id"), table_name="attachments")
74
+ op.drop_table("attachments")
@@ -0,0 +1,71 @@
1
+ """add_analytics_events
2
+
3
+ Revision ID: 1f966d693a2d
4
+ Revises: 9f3fd65db818
5
+ Create Date: 2026-05-14 06:23:04.536210+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "1f966d693a2d"
18
+ down_revision: str | None = "9f3fd65db818"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table(
26
+ "analytics_events",
27
+ sa.Column("site_id", sa.Integer(), nullable=False),
28
+ sa.Column("event_type", sa.String(length=32), nullable=False),
29
+ sa.Column("path", sa.String(length=1024), nullable=True),
30
+ sa.Column("referrer", sa.String(length=1024), nullable=True),
31
+ sa.Column("user_agent_class", sa.String(length=32), nullable=True),
32
+ sa.Column("user_id", sa.Integer(), nullable=True),
33
+ sa.Column("occurred_at", sa.DateTime(), nullable=False),
34
+ sa.Column("extra", sa.JSON(), nullable=False),
35
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
36
+ sa.ForeignKeyConstraint(
37
+ ["site_id"],
38
+ ["sites.id"],
39
+ ),
40
+ sa.ForeignKeyConstraint(
41
+ ["user_id"],
42
+ ["users.id"],
43
+ ),
44
+ sa.PrimaryKeyConstraint("id"),
45
+ )
46
+ op.create_index(
47
+ op.f("ix_analytics_events_event_type"), "analytics_events", ["event_type"], unique=False
48
+ )
49
+ op.create_index(
50
+ op.f("ix_analytics_events_occurred_at"), "analytics_events", ["occurred_at"], unique=False
51
+ )
52
+ op.create_index(
53
+ op.f("ix_analytics_events_site_id"), "analytics_events", ["site_id"], unique=False
54
+ )
55
+ op.create_index(
56
+ op.f("ix_analytics_events_user_agent_class"),
57
+ "analytics_events",
58
+ ["user_agent_class"],
59
+ unique=False,
60
+ )
61
+ # ### end Alembic commands ###
62
+
63
+
64
+ def downgrade() -> None:
65
+ # ### commands auto generated by Alembic - please adjust! ###
66
+ op.drop_index(op.f("ix_analytics_events_user_agent_class"), table_name="analytics_events")
67
+ op.drop_index(op.f("ix_analytics_events_site_id"), table_name="analytics_events")
68
+ op.drop_index(op.f("ix_analytics_events_occurred_at"), table_name="analytics_events")
69
+ op.drop_index(op.f("ix_analytics_events_event_type"), table_name="analytics_events")
70
+ op.drop_table("analytics_events")
71
+ # ### end Alembic commands ###
@@ -0,0 +1,43 @@
1
+ """add extra_settings to sites
2
+
3
+ Revision ID: f679d5fc62bb
4
+ Revises: 1f966d693a2d
5
+ Create Date: 2026-05-14 06:41:17.203719+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "f679d5fc62bb"
18
+ down_revision: str | None = "1f966d693a2d"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ # server_default makes the migration safe against a populated
25
+ # `sites` table: existing rows get an empty JSON object on add.
26
+ # We don't keep the server_default on the live column because
27
+ # the Python-side `default=dict` is the source of truth at the
28
+ # ORM layer.
29
+ op.add_column(
30
+ "sites",
31
+ sa.Column(
32
+ "extra_settings",
33
+ sa.JSON(),
34
+ nullable=False,
35
+ server_default="{}",
36
+ ),
37
+ )
38
+ with op.batch_alter_table("sites") as batch:
39
+ batch.alter_column("extra_settings", server_default=None)
40
+
41
+
42
+ def downgrade() -> None:
43
+ op.drop_column("sites", "extra_settings")
@@ -0,0 +1,40 @@
1
+ """add_site_aliases
2
+
3
+ Revision ID: 0e57d255f32d
4
+ Revises: f679d5fc62bb
5
+ Create Date: 2026-05-14 07:19:37.827827+00:00
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+
13
+ import sqlalchemy as sa
14
+ from alembic import op
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "0e57d255f32d"
18
+ down_revision: str | None = "f679d5fc62bb"
19
+ branch_labels: str | Sequence[str] | None = None
20
+ depends_on: str | Sequence[str] | None = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.create_table(
25
+ "site_aliases",
26
+ sa.Column("site_id", sa.Integer(), nullable=False),
27
+ sa.Column("hostname", sa.String(length=255), nullable=False),
28
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
29
+ sa.Column("created_at", sa.DateTime(), nullable=False),
30
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
31
+ sa.ForeignKeyConstraint(["site_id"], ["sites.id"]),
32
+ sa.PrimaryKeyConstraint("id"),
33
+ sa.UniqueConstraint("hostname"),
34
+ )
35
+ op.create_index(op.f("ix_site_aliases_site_id"), "site_aliases", ["site_id"], unique=False)
36
+
37
+
38
+ def downgrade() -> None:
39
+ op.drop_index(op.f("ix_site_aliases_site_id"), table_name="site_aliases")
40
+ op.drop_table("site_aliases")