From f42042ccb7953637a3fab218ffcc4ede786edfa4 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 24 Feb 2026 19:44:17 +0000 Subject: [PATCH] Monorepo: consolidate 7 repos into one Combines shared, blog, market, cart, events, federation, and account into a single repository. Eliminates submodule sync, sibling model copying at build time, and per-app CI orchestration. Changes: - Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs - Remove stale sibling model copies from each app - Update all 6 Dockerfiles for monorepo build context (root = .) - Add build directives to docker-compose.yml - Add single .gitea/workflows/ci.yml with change detection - Add .dockerignore for monorepo build context - Create __init__.py for federation and account (cross-app imports) --- .dockerignore | 13 + .gitea/workflows/ci.yml | 72 + .gitignore | 12 + _config/app-config.yaml | 83 + account/Dockerfile | 50 + account/__init__.py | 0 account/app.py | 65 + account/bp/__init__.py | 3 + account/bp/account/__init__.py | 0 account/bp/account/routes.py | 168 + account/bp/auth/__init__.py | 0 account/bp/auth/routes.py | 486 +++ account/bp/auth/services/__init__.py | 24 + account/bp/auth/services/auth_operations.py | 156 + account/bp/auth/services/login_redirect.py | 45 + account/bp/fragments/__init__.py | 1 + account/bp/fragments/routes.py | 52 + account/entrypoint.sh | 26 + account/models/__init__.py | 0 account/path_setup.py | 9 + account/services/__init__.py | 27 + account/templates/_email/magic_link.html | 33 + account/templates/_email/magic_link.txt | 8 + .../_types/auth/_bookings_panel.html | 44 + .../_types/auth/_fragment_panel.html | 1 + .../templates/_types/auth/_main_panel.html | 49 + account/templates/_types/auth/_nav.html | 7 + .../_types/auth/_newsletter_toggle.html | 17 + .../_types/auth/_newsletters_panel.html | 46 + .../templates/_types/auth/_oob_elements.html | 29 + .../templates/_types/auth/_tickets_panel.html | 44 + .../templates/_types/auth/check_email.html | 33 + .../templates/_types/auth/header/_header.html | 12 + account/templates/_types/auth/index copy.html | 18 + account/templates/_types/auth/index.html | 18 + account/templates/_types/auth/login.html | 46 + account/templates/auth/check_email.html | 19 + account/templates/auth/login.html | 36 + account/templates/fragments/auth_menu.html | 36 + blog/.gitignore | 9 + blog/Dockerfile | 61 + blog/README.md | 60 + blog/__init__.py | 0 blog/app.py | 138 + blog/bp/__init__.py | 5 + blog/bp/admin/routes.py | 67 + blog/bp/blog/__init__.py | 7 + blog/bp/blog/admin/__init__.py | 0 blog/bp/blog/admin/routes.py | 173 ++ blog/bp/blog/filters/qs.py | 120 + blog/bp/blog/ghost/editor_api.py | 256 ++ blog/bp/blog/ghost/ghost_admin_token.py | 46 + blog/bp/blog/ghost/ghost_posts.py | 204 ++ blog/bp/blog/ghost/ghost_sync.py | 1240 ++++++++ blog/bp/blog/ghost/lexical_renderer.py | 668 ++++ blog/bp/blog/ghost/lexical_validator.py | 86 + blog/bp/blog/ghost_db.py | 632 ++++ blog/bp/blog/routes.py | 369 +++ blog/bp/blog/services/pages_data.py | 18 + blog/bp/blog/services/posts_data.py | 142 + blog/bp/blog/web_hooks/routes.py | 120 + blog/bp/fragments/__init__.py | 1 + blog/bp/fragments/routes.py | 52 + blog/bp/menu_items/__init__.py | 3 + blog/bp/menu_items/routes.py | 213 ++ blog/bp/menu_items/services/menu_items.py | 209 ++ blog/bp/post/admin/routes.py | 688 +++++ blog/bp/post/routes.py | 180 ++ blog/bp/post/services/entry_associations.py | 60 + blog/bp/post/services/markets.py | 61 + blog/bp/post/services/post_data.py | 42 + blog/bp/post/services/post_operations.py | 58 + blog/bp/snippets/__init__.py | 3 + blog/bp/snippets/routes.py | 107 + blog/config/app-config.yaml | 84 + blog/entrypoint.sh | 32 + blog/models/__init__.py | 14 + blog/models/ghost_content.py | 3 + blog/models/ghost_membership_entities.py | 12 + blog/models/kv.py | 4 + blog/models/magic_link.py | 4 + blog/models/menu_item.py | 4 + blog/models/snippet.py | 32 + blog/models/tag_group.py | 52 + blog/models/user.py | 4 + blog/path_setup.py | 9 + blog/services/__init__.py | 28 + blog/templates/_email/magic_link.html | 33 + blog/templates/_email/magic_link.txt | 8 + .../_types/blog/_action_buttons.html | 64 + blog/templates/_types/blog/_card.html | 80 + blog/templates/_types/blog/_card/at_bar.html | 19 + blog/templates/_types/blog/_card/author.html | 21 + blog/templates/_types/blog/_card/authors.html | 32 + blog/templates/_types/blog/_card/tag.html | 19 + .../_types/blog/_card/tag_group.html | 22 + blog/templates/_types/blog/_card/tags.html | 17 + blog/templates/_types/blog/_card_tile.html | 59 + blog/templates/_types/blog/_cards.html | 111 + blog/templates/_types/blog/_main_panel.html | 84 + blog/templates/_types/blog/_oob_elements.html | 40 + blog/templates/_types/blog/_page_card.html | 56 + blog/templates/_types/blog/_page_cards.html | 19 + .../blog/admin/tag_groups/_edit_header.html | 9 + .../admin/tag_groups/_edit_main_panel.html | 79 + .../blog/admin/tag_groups/_edit_oob.html | 17 + .../_types/blog/admin/tag_groups/_header.html | 9 + .../blog/admin/tag_groups/_main_panel.html | 73 + .../blog/admin/tag_groups/_oob_elements.html | 16 + .../_types/blog/admin/tag_groups/edit.html | 13 + .../_types/blog/admin/tag_groups/index.html | 20 + blog/templates/_types/blog/desktop/menu.html | 19 + .../_types/blog/desktop/menu/authors.html | 62 + .../_types/blog/desktop/menu/tag_groups.html | 70 + .../_types/blog/desktop/menu/tags.html | 59 + .../templates/_types/blog/header/_header.html | 7 + blog/templates/_types/blog/index.html | 37 + .../blog/mobile/_filter/_hamburger.html | 13 + .../_types/blog/mobile/_filter/summary.html | 14 + .../blog/mobile/_filter/summary/authors.html | 31 + .../mobile/_filter/summary/tag_groups.html | 33 + .../blog/mobile/_filter/summary/tags.html | 31 + blog/templates/_types/blog/not_found.html | 22 + .../_types/blog_drafts/_main_panel.html | 55 + .../_types/blog_drafts/_oob_elements.html | 12 + blog/templates/_types/blog_drafts/index.html | 11 + .../_types/blog_new/_main_panel.html | 259 ++ .../_types/blog_new/_oob_elements.html | 12 + blog/templates/_types/blog_new/index.html | 11 + blog/templates/_types/home/_oob_elements.html | 19 + blog/templates/_types/home/index.html | 14 + blog/templates/_types/menu_items/_form.html | 125 + blog/templates/_types/menu_items/_list.html | 68 + .../_types/menu_items/_main_panel.html | 20 + .../templates/_types/menu_items/_nav_oob.html | 31 + .../_types/menu_items/_oob_elements.html | 23 + .../menu_items/_page_search_results.html | 44 + .../_types/menu_items/header/_header.html | 9 + blog/templates/_types/menu_items/index.html | 20 + .../_types/post/_entry_container.html | 24 + blog/templates/_types/post/_entry_items.html | 38 + blog/templates/_types/post/_main_panel.html | 65 + blog/templates/_types/post/_meta.html | 124 + blog/templates/_types/post/_nav.html | 15 + blog/templates/_types/post/_oob_elements.html | 36 + .../post/admin/_associated_entries.html | 50 + .../_types/post/admin/_calendar_view.html | 88 + .../_types/post/admin/_features_panel.html | 112 + .../_types/post/admin/_main_panel.html | 7 + .../_types/post/admin/_markets_panel.html | 44 + blog/templates/_types/post/admin/_nav.html | 28 + .../_types/post/admin/_nav_entries.html | 50 + .../_types/post/admin/_nav_entries_oob.html | 80 + .../_types/post/admin/_oob_elements.html | 22 + .../_types/post/admin/header/_header.html | 13 + blog/templates/_types/post/admin/index.html | 18 + .../templates/_types/post/header/_header.html | 28 + blog/templates/_types/post/index.html | 25 + .../_types/post_data/_main_panel.html | 137 + blog/templates/_types/post_data/_nav.html | 2 + .../_types/post_data/_oob_elements.html | 28 + .../_types/post_data/header/_header.html | 15 + blog/templates/_types/post_data/index.html | 24 + .../_types/post_edit/_main_panel.html | 352 +++ blog/templates/_types/post_edit/_nav.html | 5 + .../_types/post_edit/_oob_elements.html | 19 + .../_types/post_edit/header/_header.html | 14 + blog/templates/_types/post_edit/index.html | 17 + .../_types/post_entries/_main_panel.html | 48 + blog/templates/_types/post_entries/_nav.html | 2 + .../_types/post_entries/_oob_elements.html | 28 + .../_types/post_entries/header/_header.html | 17 + blog/templates/_types/post_entries/index.html | 19 + .../_types/post_settings/_main_panel.html | 198 ++ blog/templates/_types/post_settings/_nav.html | 5 + .../_types/post_settings/_oob_elements.html | 19 + .../_types/post_settings/header/_header.html | 14 + .../templates/_types/post_settings/index.html | 17 + .../templates/_types/root/header/_header.html | 42 + .../_types/root/settings/_main_panel.html | 2 + blog/templates/_types/root/settings/_nav.html | 5 + .../_types/root/settings/_oob_elements.html | 26 + .../_types/root/settings/cache/_header.html | 9 + .../root/settings/cache/_main_panel.html | 14 + .../root/settings/cache/_oob_elements.html | 16 + .../_types/root/settings/cache/index.html | 20 + .../_types/root/settings/header/_header.html | 11 + .../templates/_types/root/settings/index.html | 18 + blog/templates/_types/snippets/_list.html | 73 + .../_types/snippets/_main_panel.html | 9 + .../_types/snippets/_oob_elements.html | 18 + .../_types/snippets/header/_header.html | 9 + blog/templates/_types/snippets/index.html | 20 + blog/templates/fragments/nav_tree.html | 32 + blog/templates/macros/admin_nav.html | 21 + blog/templates/macros/scrolling_menu.html | 68 + blog/templates/macros/stickers.html | 24 + cart/.gitignore | 8 + cart/Dockerfile | 50 + cart/README.md | 76 + cart/__init__.py | 0 cart/app.py | 235 ++ cart/bp/__init__.py | 6 + cart/bp/cart/global_routes.py | 294 ++ cart/bp/cart/overview_routes.py | 31 + cart/bp/cart/page_routes.py | 129 + cart/bp/cart/services/__init__.py | 13 + cart/bp/cart/services/calendar_cart.py | 45 + cart/bp/cart/services/check_sumup_status.py | 43 + cart/bp/cart/services/checkout.py | 248 ++ cart/bp/cart/services/clear_cart_for_order.py | 37 + cart/bp/cart/services/get_cart.py | 25 + cart/bp/cart/services/identity.py | 4 + cart/bp/cart/services/page_cart.py | 212 ++ cart/bp/cart/services/ticket_groups.py | 43 + cart/bp/cart/services/total.py | 13 + cart/bp/fragments/__init__.py | 1 + cart/bp/fragments/routes.py | 70 + cart/bp/order/filters/qs.py | 74 + cart/bp/order/routes.py | 137 + cart/bp/orders/filters/qs.py | 77 + cart/bp/orders/routes.py | 151 + cart/config/app-config.yaml | 84 + cart/entrypoint.sh | 29 + cart/models/__init__.py | 2 + cart/models/order.py | 1 + cart/models/page_config.py | 1 + cart/path_setup.py | 9 + cart/services/__init__.py | 28 + .../templates/_types/auth/header/_header.html | 12 + cart/templates/_types/auth/index.html | 18 + cart/templates/_types/cart/_cart.html | 260 ++ cart/templates/_types/cart/_main_panel.html | 4 + cart/templates/_types/cart/_mini.html | 45 + cart/templates/_types/cart/_nav.html | 2 + cart/templates/_types/cart/_oob_elements.html | 28 + .../templates/_types/cart/checkout_error.html | 38 + .../_types/cart/checkout_return.html | 68 + .../templates/_types/cart/header/_header.html | 12 + cart/templates/_types/cart/index.html | 22 + .../_types/cart/overview/_main_panel.html | 147 + .../_types/cart/overview/_oob_elements.html | 24 + .../templates/_types/cart/overview/index.html | 22 + .../_types/cart/page/_main_panel.html | 4 + .../_types/cart/page/_oob_elements.html | 27 + .../_types/cart/page/header/_header.html | 25 + cart/templates/_types/cart/page/index.html | 24 + .../_types/order/_calendar_items.html | 43 + cart/templates/_types/order/_items.html | 51 + cart/templates/_types/order/_main_panel.html | 7 + cart/templates/_types/order/_nav.html | 2 + .../templates/_types/order/_oob_elements.html | 30 + cart/templates/_types/order/_summary.html | 52 + .../templates/_types/order/_ticket_items.html | 49 + .../_types/order/header/_header.html | 17 + cart/templates/_types/order/index.html | 68 + cart/templates/_types/orders/_main_panel.html | 26 + cart/templates/_types/orders/_nav.html | 2 + .../_types/orders/_oob_elements.html | 38 + cart/templates/_types/orders/_rows.html | 164 + cart/templates/_types/orders/_summary.html | 11 + .../_types/orders/header/_header.html | 14 + cart/templates/_types/orders/index.html | 29 + cart/templates/_types/product/_cart.html | 250 ++ cart/templates/fragments/cart_mini.html | 27 + docker-compose.yml | 175 ++ events/.gitignore | 4 + events/Dockerfile | 49 + events/README.md | 78 + events/__init__.py | 0 events/app.py | 154 + events/bp/__init__.py | 6 + events/bp/all_events/__init__.py | 0 events/bp/all_events/routes.py | 143 + events/bp/calendar/admin/routes.py | 76 + events/bp/calendar/routes.py | 251 ++ events/bp/calendar/services/__init__.py | 1 + .../adopt_session_entries_for_user.py | 25 + events/bp/calendar/services/calendar.py | 28 + events/bp/calendar/services/calendar_view.py | 109 + events/bp/calendar/services/slots.py | 118 + events/bp/calendar/services/visiblity.py | 116 + events/bp/calendar_entries/routes.py | 257 ++ .../bp/calendar_entries/services/entries.py | 278 ++ events/bp/calendar_entry/admin/routes.py | 28 + events/bp/calendar_entry/routes.py | 626 ++++ .../services/post_associations.py | 121 + .../services/ticket_operations.py | 87 + events/bp/calendars/routes.py | 99 + events/bp/calendars/services/calendars.py | 115 + events/bp/day/admin/routes.py | 28 + events/bp/day/routes.py | 154 + events/bp/fragments/__init__.py | 1 + events/bp/fragments/routes.py | 130 + events/bp/markets/__init__.py | 0 events/bp/markets/routes.py | 65 + events/bp/markets/services/__init__.py | 0 events/bp/markets/services/markets.py | 57 + events/bp/page/__init__.py | 0 events/bp/page/routes.py | 129 + events/bp/payments/__init__.py | 0 events/bp/payments/routes.py | 81 + events/bp/slot/routes.py | 182 ++ events/bp/slot/services/slot.py | 91 + events/bp/slots/routes.py | 152 + events/bp/slots/services/slots.py | 65 + events/bp/ticket_admin/__init__.py | 0 events/bp/ticket_admin/routes.py | 166 + events/bp/ticket_admin/services/__init__.py | 0 events/bp/ticket_type/routes.py | 159 + events/bp/ticket_type/services/ticket.py | 57 + events/bp/ticket_types/routes.py | 132 + events/bp/ticket_types/services/tickets.py | 48 + events/bp/tickets/__init__.py | 0 events/bp/tickets/routes.py | 308 ++ events/bp/tickets/services/__init__.py | 0 events/bp/tickets/services/tickets.py | 313 ++ events/config/app-config.yaml | 84 + events/entrypoint.sh | 29 + events/models/__init__.py | 4 + events/models/calendars.py | 4 + events/path_setup.py | 9 + events/services/__init__.py | 29 + events/templates/_types/all_events/_card.html | 62 + .../_types/all_events/_card_tile.html | 60 + .../templates/_types/all_events/_cards.html | 31 + .../_types/all_events/_main_panel.html | 54 + events/templates/_types/all_events/index.html | 7 + .../_types/calendar/_description.html | 12 + .../_types/calendar/_main_panel.html | 170 + events/templates/_types/calendar/_nav.html | 18 + .../_types/calendar/_oob_elements.html | 22 + .../_types/calendar/admin/_description.html | 32 + .../calendar/admin/_description_edit.html | 41 + .../_types/calendar/admin/_main_panel.html | 45 + .../templates/_types/calendar/admin/_nav.html | 2 + .../_types/calendar/admin/_oob_elements.html | 25 + .../_types/calendar/admin/header/_header.html | 13 + .../_types/calendar/admin/index.html | 24 + .../_types/calendar/header/_header.html | 23 + events/templates/_types/calendar/index.html | 26 + .../_types/calendars/_calendars_list.html | 44 + .../_types/calendars/_main_panel.html | 27 + events/templates/_types/calendars/_nav.html | 2 + .../_types/calendars/_oob_elements.html | 28 + .../_types/calendars/header/_header.html | 14 + events/templates/_types/calendars/index.html | 26 + events/templates/_types/day/_add.html | 299 ++ events/templates/_types/day/_add_button.html | 16 + events/templates/_types/day/_main_panel.html | 28 + events/templates/_types/day/_nav.html | 39 + .../templates/_types/day/_oob_elements.html | 18 + events/templates/_types/day/_row.html | 74 + .../_types/day/admin/_main_panel.html | 2 + events/templates/_types/day/admin/_nav.html | 2 + .../_types/day/admin/_nav_entries_oob.html | 33 + .../_types/day/admin/_oob_elements.html | 25 + .../_types/day/admin/header/_header.html | 20 + events/templates/_types/day/admin/index.html | 24 + .../templates/_types/day/header/_header.html | 26 + events/templates/_types/day/index.html | 18 + events/templates/_types/entry/_edit.html | 332 ++ .../templates/_types/entry/_main_panel.html | 128 + events/templates/_types/entry/_nav.html | 39 + .../templates/_types/entry/_oob_elements.html | 18 + events/templates/_types/entry/_optioned.html | 9 + events/templates/_types/entry/_options.html | 95 + .../_types/entry/_post_search_results.html | 105 + events/templates/_types/entry/_posts.html | 72 + events/templates/_types/entry/_state.html | 15 + events/templates/_types/entry/_tickets.html | 104 + events/templates/_types/entry/_times.html | 5 + events/templates/_types/entry/_title.html | 3 + .../_types/entry/admin/_main_panel.html | 2 + events/templates/_types/entry/admin/_nav.html | 17 + .../_types/entry/admin/_nav_posts_oob.html | 31 + .../_types/entry/admin/_oob_elements.html | 25 + .../_types/entry/admin/header/_header.html | 21 + .../templates/_types/entry/admin/index.html | 24 + .../_types/entry/header/_header.html | 27 + events/templates/_types/entry/index.html | 20 + .../templates/_types/markets/_main_panel.html | 25 + .../_types/markets/_markets_list.html | 37 + events/templates/_types/markets/_nav.html | 2 + .../_types/markets/_oob_elements.html | 19 + .../_types/markets/header/_header.html | 14 + events/templates/_types/markets/index.html | 23 + .../templates/_types/page_summary/_card.html | 49 + .../_types/page_summary/_card_tile.html | 48 + .../templates/_types/page_summary/_cards.html | 31 + .../_types/page_summary/_main_panel.html | 54 + .../_types/page_summary/_ticket_widget.html | 63 + .../templates/_types/page_summary/index.html | 15 + .../_types/payments/_main_panel.html | 70 + events/templates/_types/payments/_nav.html | 2 + .../_types/payments/_oob_elements.html | 19 + .../_types/payments/header/_header.html | 14 + events/templates/_types/payments/index.html | 23 + events/templates/_types/post/_nav.html | 14 + .../post/admin/_associated_entries.html | 50 + events/templates/_types/post/admin/_nav.html | 36 + .../_types/post/admin/header/_header.html | 12 + .../templates/_types/post/header/_header.html | 28 + .../_types/post_entries/_main_panel.html | 47 + .../templates/_types/post_entries/_nav.html | 2 + .../_types/post_entries/header/_header.html | 17 + .../templates/_types/slot/__description.html | 13 + .../templates/_types/slot/_description.html | 5 + events/templates/_types/slot/_edit.html | 180 ++ events/templates/_types/slot/_main_panel.html | 72 + .../templates/_types/slot/_oob_elements.html | 15 + .../templates/_types/slot/header/_header.html | 25 + events/templates/_types/slot/index.html | 20 + events/templates/_types/slots/_add.html | 123 + .../templates/_types/slots/_add_button.html | 11 + .../templates/_types/slots/_main_panel.html | 26 + .../templates/_types/slots/_oob_elements.html | 15 + events/templates/_types/slots/_row.html | 61 + .../_types/slots/header/_header.html | 18 + events/templates/_types/slots/index.html | 19 + .../_types/ticket_admin/_checkin_result.html | 39 + .../_types/ticket_admin/_entry_tickets.html | 75 + .../_types/ticket_admin/_lookup_result.html | 82 + .../_types/ticket_admin/_main_panel.html | 148 + .../templates/_types/ticket_admin/index.html | 8 + .../templates/_types/ticket_type/_edit.html | 101 + .../_types/ticket_type/_main_panel.html | 49 + events/templates/_types/ticket_type/_nav.html | 2 + .../_types/ticket_type/_oob_elements.html | 18 + .../_types/ticket_type/header/_header.html | 32 + .../templates/_types/ticket_type/index.html | 19 + .../templates/_types/ticket_types/_add.html | 85 + .../_types/ticket_types/_add_button.html | 15 + .../_types/ticket_types/_main_panel.html | 24 + .../templates/_types/ticket_types/_nav.html | 2 + .../_types/ticket_types/_oob_elements.html | 18 + .../templates/_types/ticket_types/_row.html | 55 + .../_types/ticket_types/header/_header.html | 24 + .../templates/_types/ticket_types/index.html | 20 + .../_types/tickets/_adjust_response.html | 4 + .../templates/_types/tickets/_buy_form.html | 206 ++ .../templates/_types/tickets/_buy_result.html | 43 + .../_types/tickets/_detail_panel.html | 124 + .../templates/_types/tickets/_main_panel.html | 65 + events/templates/_types/tickets/detail.html | 8 + events/templates/_types/tickets/index.html | 8 + .../fragments/account_nav_items.html | 23 + .../fragments/account_page_bookings.html | 44 + .../fragments/account_page_tickets.html | 44 + .../fragments/container_cards_entries.html | 33 + .../fragments/container_nav_calendars.html | 10 + .../fragments/container_nav_entries.html | 28 + events/templates/macros/date.html | 7 + federation/.gitignore | 9 + federation/Dockerfile | 50 + federation/__init__.py | 0 federation/app.py | 84 + federation/bp/__init__.py | 3 + federation/bp/auth/__init__.py | 0 federation/bp/auth/routes.py | 232 ++ federation/bp/auth/services/__init__.py | 24 + .../bp/auth/services/auth_operations.py | 157 + federation/bp/auth/services/login_redirect.py | 45 + federation/bp/fragments/__init__.py | 1 + federation/bp/fragments/routes.py | 34 + federation/bp/identity/__init__.py | 0 federation/bp/identity/routes.py | 108 + federation/bp/social/__init__.py | 0 federation/bp/social/routes.py | 499 +++ federation/config/app-config.yaml | 84 + federation/entrypoint.sh | 32 + federation/models/__init__.py | 9 + federation/path_setup.py | 9 + federation/services/__init__.py | 27 + federation/templates/_email/magic_link.html | 33 + federation/templates/_email/magic_link.txt | 8 + .../templates/_types/federation/index.html | 3 + .../_types/social/header/_header.html | 52 + federation/templates/_types/social/index.html | 10 + federation/templates/auth/check_email.html | 19 + federation/templates/auth/login.html | 36 + .../federation/_actor_list_items.html | 63 + .../federation/_interaction_buttons.html | 61 + .../templates/federation/_notification.html | 42 + .../templates/federation/_post_card.html | 52 + .../templates/federation/_search_results.html | 61 + .../templates/federation/_timeline_items.html | 18 + federation/templates/federation/account.html | 27 + .../templates/federation/actor_card.html | 45 + .../templates/federation/actor_timeline.html | 53 + .../templates/federation/choose_username.html | 54 + federation/templates/federation/compose.html | 34 + .../templates/federation/followers.html | 12 + .../templates/federation/following.html | 13 + .../templates/federation/notifications.html | 17 + federation/templates/federation/profile.html | 32 + federation/templates/federation/search.html | 32 + federation/templates/federation/timeline.html | 19 + market/.gitignore | 12 + market/Dockerfile | 50 + market/README.md | 56 + market/__init__.py | 0 market/app.py | 188 ++ market/bp/__init__.py | 5 + market/bp/all_markets/__init__.py | 0 market/bp/all_markets/routes.py | 74 + market/bp/api/__init__.py | 0 market/bp/api/routes.py | 432 +++ market/bp/browse/__init__.py | 7 + market/bp/browse/routes.py | 163 + market/bp/browse/services/__init__.py | 13 + .../bp/browse/services/blacklist/category.py | 12 + .../bp/browse/services/blacklist/product.py | 15 + .../services/blacklist/product_details.py | 11 + market/bp/browse/services/cache_backend.py | 367 +++ market/bp/browse/services/db_backend.py | 714 +++++ market/bp/browse/services/nav.py | 163 + market/bp/browse/services/products.py | 118 + market/bp/browse/services/services.py | 185 ++ market/bp/browse/services/slugs.py | 24 + market/bp/browse/services/state.py | 21 + market/bp/cart/__init__.py | 0 market/bp/cart/services/__init__.py | 2 + market/bp/cart/services/identity.py | 4 + market/bp/cart/services/total.py | 6 + market/bp/fragments/__init__.py | 1 + market/bp/fragments/routes.py | 54 + market/bp/market/__init__.py | 7 + market/bp/market/admin/__init__.py | 0 market/bp/market/admin/routes.py | 28 + market/bp/market/filters/__init__.py | 0 market/bp/market/filters/qs.py | 101 + market/bp/market/routes.py | 49 + market/bp/page_markets/__init__.py | 0 market/bp/page_markets/routes.py | 65 + market/bp/product/routes.py | 269 ++ market/bp/product/services/__init__.py | 3 + .../bp/product/services/product_operations.py | 95 + market/config/app-config.yaml | 84 + market/entrypoint.sh | 29 + market/models/__init__.py | 8 + market/models/market.py | 7 + market/models/market_place.py | 1 + market/path_setup.py | 9 + market/scrape-test.sh | 6 + market/scrape.sh | 5 + market/scrape/__init__.py | 0 market/scrape/build_snapshot/__init__.py | 1 + .../scrape/build_snapshot/build_snapshot.py | 104 + .../tools/APP_ROOT_PLACEHOLDER.py | 1 + .../scrape/build_snapshot/tools/__init__.py | 1 + .../build_snapshot/tools/_anchor_text.py | 6 + .../tools/_collect_html_img_srcs.py | 16 + .../tools/_dedupe_preserve_order.py | 14 + .../tools/_product_dict_is_cf.py | 32 + .../tools/_resolve_sub_redirects.py | 34 + .../tools/_rewrite_links_fragment.py | 100 + .../build_snapshot/tools/candidate_subs.py | 14 + .../build_snapshot/tools/capture_category.py | 18 + .../tools/capture_product_slugs.py | 25 + .../build_snapshot/tools/capture_sub.py | 22 + .../tools/fetch_and_upsert_product.py | 106 + .../tools/fetch_and_upsert_products.py | 49 + .../build_snapshot/tools/rewrite_nav.py | 24 + .../scrape/build_snapshot/tools/valid_subs.py | 16 + market/scrape/get_auth.py | 244 ++ market/scrape/html_utils.py | 44 + market/scrape/http_client.py | 220 ++ market/scrape/listings.py | 289 ++ market/scrape/nav.py | 104 + market/scrape/persist_api/__init__.py | 6 + market/scrape/persist_api/capture_listing.py | 27 + .../scrape/persist_api/log_product_result.py | 24 + market/scrape/persist_api/save_nav.py | 19 + .../persist_api/save_subcategory_redirects.py | 15 + market/scrape/persist_api/upsert_product.py | 256 ++ market/scrape/persist_snapshot/__init__.py | 7 + market/scrape/persist_snapshot/_get.py | 3 + .../persist_snapshot/capture_listing.py | 137 + .../persist_snapshot/log_product_result.py | 35 + .../persist_snapshot/save_link_reports.py | 29 + market/scrape/persist_snapshot/save_nav.py | 110 + .../save_subcategory_redirects.py | 32 + .../scrape/persist_snapshot/upsert_product.py | 237 ++ market/scrape/product/__init__.py | 1 + market/scrape/product/extractors/__init__.py | 13 + .../scrape/product/extractors/breadcrumbs.py | 68 + .../extractors/description_sections.py | 43 + market/scrape/product/extractors/images.py | 89 + .../scrape/product/extractors/info_table.py | 76 + market/scrape/product/extractors/labels.py | 41 + .../scrape/product/extractors/nutrition_ex.py | 129 + .../product/extractors/oe_list_price.py | 56 + .../extractors/regular_price_fallback.py | 33 + .../product/extractors/short_description.py | 19 + market/scrape/product/extractors/stickers.py | 30 + market/scrape/product/extractors/title.py | 17 + market/scrape/product/helpers/desc.py | 165 + market/scrape/product/helpers/html.py | 53 + market/scrape/product/helpers/price.py | 42 + market/scrape/product/helpers/text.py | 16 + market/scrape/product/product_core.py | 48 + market/scrape/product/product_detail.py | 4 + market/scrape/product/registry.py | 20 + market/services/__init__.py | 29 + .../templates/_types/all_markets/_card.html | 33 + .../templates/_types/all_markets/_cards.html | 18 + .../_types/all_markets/_main_panel.html | 12 + .../templates/_types/all_markets/index.html | 7 + market/templates/_types/browse/_admin.html | 7 + .../templates/_types/browse/_main_panel.html | 5 + .../_types/browse/_oob_elements.html | 37 + .../_types/browse/_product_card.html | 104 + .../_types/browse/_product_cards.html | 107 + .../browse/desktop/_category_selector.html | 40 + .../_types/browse/desktop/_filter/brand.html | 40 + .../_types/browse/desktop/_filter/labels.html | 44 + .../_types/browse/desktop/_filter/like.html | 38 + .../_types/browse/desktop/_filter/search.html | 44 + .../_types/browse/desktop/_filter/sort.html | 34 + .../browse/desktop/_filter/stickers.html | 46 + .../templates/_types/browse/desktop/menu.html | 37 + market/templates/_types/browse/index.html | 13 + .../templates/_types/browse/like/button.html | 20 + .../browse/mobile/_filter/brand_ul.html | 40 + .../_types/browse/mobile/_filter/index.html | 30 + .../_types/browse/mobile/_filter/labels.html | 47 + .../_types/browse/mobile/_filter/like.html | 40 + .../_types/browse/mobile/_filter/search.html | 40 + .../_types/browse/mobile/_filter/sort_ul.html | 33 + .../browse/mobile/_filter/stickers.html | 50 + .../_types/browse/mobile/_filter/summary.html | 120 + market/templates/_types/market/_admin.html | 7 + .../templates/_types/market/_main_panel.html | 23 + .../_types/market/_oob_elements.html | 30 + market/templates/_types/market/_title.html | 17 + .../_types/market/admin/_main_panel.html | 1 + .../templates/_types/market/admin/_nav.html | 2 + .../_types/market/admin/_oob_elements.html | 29 + .../_types/market/admin/header/_header.html | 11 + .../templates/_types/market/admin/index.html | 19 + .../templates/_types/market/desktop/_nav.html | 38 + .../_types/market/header/_header.html | 11 + market/templates/_types/market/index.html | 27 + .../_types/market/markets_listing.html | 23 + .../_types/market/mobile/_nav_panel.html | 110 + .../templates/_types/market/mobile/menu.html | 6 + .../templates/_types/page_markets/_card.html | 13 + .../templates/_types/page_markets/_cards.html | 18 + .../_types/page_markets/_main_panel.html | 12 + .../templates/_types/page_markets/index.html | 15 + market/templates/_types/post/_nav.html | 15 + .../_types/post/admin/_nav_entries.html | 50 + .../templates/_types/post/header/_header.html | 28 + market/templates/_types/product/_added.html | 17 + market/templates/_types/product/_cart.html | 250 ++ .../templates/_types/product/_main_panel.html | 131 + market/templates/_types/product/_meta.html | 106 + .../_types/product/_oob_elements.html | 49 + market/templates/_types/product/_prices.html | 33 + market/templates/_types/product/_title.html | 2 + .../templates/_types/product/admin/_nav.html | 2 + .../_types/product/admin/_oob_elements.html | 40 + .../_types/product/admin/header/_header.html | 11 + .../templates/_types/product/admin/index.html | 39 + .../_types/product/header/_header.html | 15 + market/templates/_types/product/index.html | 61 + market/templates/_types/product/prices.html | 66 + market/templates/aside_clear.html | 7 + market/templates/filter_clear.html | 5 + .../fragments/container_nav_markets.html | 9 + market/templates/macros/filters.html | 117 + schema.sql | 2741 +++++++++++++++++ shared/.gitignore | 2 + shared/README.md | 91 + shared/__init__.py | 1 + shared/alembic.ini | 35 + shared/alembic/env.py | 69 + shared/alembic/script.py.mako | 24 + shared/alembic/versions/0001_initial_schem.py | 33 + .../alembic/versions/0002_add_cart_items.py | 78 + shared/alembic/versions/0003_add_orders.py | 118 + .../versions/0004_add_sumup_reference.py | 27 + .../alembic/versions/0005_add_description.py | 27 + .../versions/0006_update_calendar_entries.py | 28 + .../alembic/versions/0007_add_oid_entries.py | 50 + .../versions/0008_add_flexible_to_slots.py | 33 + .../versions/0009_add_slot_id_to_entries.py | 54 + .../alembic/versions/0010_add_post_likes.py | 64 + .../versions/0011_add_entry_tickets.py | 43 + .../47fc53fc0d2b_add_ticket_types_table.py | 41 + .../versions/6cb124491c9d_entry_posts.py | 36 + .../a1b2c3d4e5f6_add_page_configs_table.py | 74 + .../a9f54e4eaf02_add_menu_items_table.py | 37 + .../b2c3d4e5f6a7_add_market_places_table.py | 97 + .../c3a1f7b9d4e5_add_snippets_table.py | 35 + ...3d4e5f6a7b8_add_page_tracking_to_orders.py | 55 + ...1a3c7_add_post_user_id_and_author_email.py | 45 + ...2b1d6_add_tag_groups_and_tag_group_tags.py | 45 + .../f6d4a0b2c3e7_add_domain_events_table.py | 40 + .../f6d4a1b2c3e7_add_tickets_table.py | 47 + .../g7e5b1c3d4f8_generic_containers.py | 115 + .../versions/h8f6c2d4e5a9_merge_heads.py | 23 + .../i9g7d3e5f6_add_glue_layer_tables.py | 98 + .../j0h8e4f6g7_drop_cross_domain_fks.py | 51 + .../k1i9f5g7h8_add_federation_tables.py | 142 + .../l2j0g6h8i9_add_fediverse_tables.py | 138 + .../m3k1h7i9j0_add_activity_bus_columns.py | 113 + .../n4l2i8j0k1_drop_domain_events_table.py | 46 + .../o5m3j9k1l2_add_origin_app_column.py | 35 + .../p6n4k0l2m3_add_oauth_codes_table.py | 37 + .../q7o5l1m3n4_add_oauth_grants_table.py | 41 + ...8p6m2n4o5_add_device_id_to_oauth_grants.py | 29 + .../s9q7n3o5p6_add_ap_delivery_log_table.py | 30 + ...r8n4o6p7_add_app_domain_to_ap_followers.py | 51 + ...s9o5p7q8_add_app_domain_to_delivery_log.py | 33 + shared/browser/__init__.py | 1 + shared/browser/app/__init__.py | 12 + shared/browser/app/authz.py | 152 + shared/browser/app/csrf.py | 99 + shared/browser/app/errors.py | 126 + shared/browser/app/filters/__init__.py | 17 + shared/browser/app/filters/combine.py | 25 + shared/browser/app/filters/currency.py | 12 + shared/browser/app/filters/getattr.py | 6 + shared/browser/app/filters/highlight.py | 21 + shared/browser/app/filters/qs.py | 13 + shared/browser/app/filters/qs_base.py | 78 + shared/browser/app/filters/query_types.py | 33 + shared/browser/app/filters/truncate.py | 22 + shared/browser/app/filters/url_join.py | 19 + shared/browser/app/middleware.py | 58 + shared/browser/app/payments/__init__.py | 1 + shared/browser/app/payments/sumup.py | 133 + shared/browser/app/redis_cacher.py | 346 +++ shared/browser/app/utils/__init__.py | 12 + shared/browser/app/utils/htmx.py | 46 + shared/browser/app/utils/parse.py | 36 + shared/browser/app/utils/utc.py | 6 + shared/browser/app/utils/utils.py | 51 + shared/browser/templates/_oob_elements.html | 33 + .../templates/_types/root/_full_user.html | 11 + .../templates/_types/root/_hamburger.html | 13 + .../browser/templates/_types/root/_head.html | 67 + .../browser/templates/_types/root/_index.html | 13 + .../templates/_types/root/_n/macros.html | 35 + .../browser/templates/_types/root/_nav.html | 29 + .../templates/_types/root/_nav_panel.html | 7 + .../templates/_types/root/_oob_menu.html | 46 + .../templates/_types/root/_sign_in.html | 10 + .../_types/root/exceptions/403/img.html | 1 + .../_types/root/exceptions/403/message.html | 1 + .../_types/root/exceptions/404/img.html | 1 + .../_types/root/exceptions/404/message.html | 1 + .../templates/_types/root/exceptions/_.html | 12 + .../_types/root/exceptions/app_error.html | 42 + .../_types/root/exceptions/base.html | 17 + .../_types/root/exceptions/error.html | 12 + .../_types/root/exceptions/hx/_.html | 8 + .../templates/_types/root/header/_header.html | 41 + .../templates/_types/root/header/_oob.html | 67 + .../templates/_types/root/header/_oob_.html | 38 + .../browser/templates/_types/root/index.html | 84 + .../_types/root/mobile/_full_user.html | 10 + .../_types/root/mobile/_sign_in.html | 8 + .../browser/templates/macros/admin_nav.html | 21 + .../browser/templates/macros/cart_icon.html | 31 + shared/browser/templates/macros/glyphs.html | 17 + shared/browser/templates/macros/layout.html | 61 + shared/browser/templates/macros/links.html | 59 + .../templates/macros/scrolling_menu.html | 68 + shared/browser/templates/macros/search.html | 83 + shared/browser/templates/macros/stickers.html | 24 + shared/browser/templates/macros/title.html | 10 + shared/browser/templates/mobile/menu.html | 5 + shared/browser/templates/oob_elements.html | 38 + .../templates/sentinel/desktop_content.html | 9 + .../templates/sentinel/mobile_content.html | 11 + .../templates/sentinel/wireless_error.svg | 20 + .../browser/templates/social/meta_base.html | 54 + .../browser/templates/social/meta_site.html | 25 + shared/config.py | 84 + shared/containers.py | 20 + shared/contracts/__init__.py | 31 + shared/contracts/dtos.py | 255 ++ shared/contracts/protocols.py | 368 +++ shared/contracts/widgets.py | 49 + shared/db/__init__.py | 0 shared/db/base.py | 4 + shared/db/session.py | 82 + shared/editor/build.mjs | 45 + shared/editor/package-lock.json | 512 +++ shared/editor/package.json | 18 + shared/editor/src/Editor.jsx | 81 + shared/editor/src/index.jsx | 49 + shared/editor/src/useFileUpload.js | 99 + shared/events/__init__.py | 9 + shared/events/bus.py | 126 + shared/events/handlers/__init__.py | 10 + shared/events/handlers/ap_delivery_handler.py | 250 ++ shared/events/handlers/container_handlers.py | 19 + .../handlers/external_delivery_handler.py | 101 + shared/events/handlers/login_handlers.py | 23 + shared/events/handlers/order_handlers.py | 22 + shared/events/processor.py | 243 ++ shared/infrastructure/__init__.py | 1 + shared/infrastructure/activitypub.py | 454 +++ shared/infrastructure/ap_inbox_handlers.py | 564 ++++ shared/infrastructure/cart_identity.py | 34 + shared/infrastructure/context.py | 58 + shared/infrastructure/factory.py | 289 ++ shared/infrastructure/fragments.py | 193 ++ shared/infrastructure/http_utils.py | 49 + shared/infrastructure/jinja_setup.py | 120 + shared/infrastructure/oauth.py | 183 ++ shared/infrastructure/urls.py | 97 + shared/infrastructure/user_loader.py | 35 + shared/log_config/__init__.py | 3 + shared/log_config/setup.py | 66 + shared/models/__init__.py | 33 + shared/models/calendars.py | 297 ++ shared/models/container_relation.py | 38 + shared/models/federation.py | 466 +++ shared/models/ghost_content.py | 216 ++ shared/models/ghost_membership_entities.py | 122 + shared/models/kv.py | 12 + shared/models/magic_link.py | 25 + shared/models/market.py | 441 +++ shared/models/market_place.py | 52 + shared/models/menu_item.py | 37 + shared/models/menu_node.py | 50 + shared/models/oauth_code.py | 26 + shared/models/oauth_grant.py | 32 + shared/models/order.py | 114 + shared/models/page_config.py | 39 + shared/models/user.py | 46 + shared/requirements.txt | 49 + shared/services/__init__.py | 5 + shared/services/blog_impl.py | 65 + shared/services/calendar_impl.py | 669 ++++ shared/services/cart_impl.py | 162 + shared/services/federation_impl.py | 1654 ++++++++++ shared/services/federation_publish.py | 92 + shared/services/market_impl.py | 128 + shared/services/navigation.py | 32 + shared/services/registry.py | 105 + shared/services/relationships.py | 161 + shared/services/stubs.py | 314 ++ shared/services/widget_registry.py | 90 + shared/services/widgets/__init__.py | 22 + shared/services/widgets/calendar_widgets.py | 10 + shared/services/widgets/cart_widgets.py | 10 + shared/services/widgets/market_widgets.py | 10 + shared/static/errors/403.gif | Bin 0 -> 103278 bytes shared/static/errors/404.gif | Bin 0 -> 21684 bytes shared/static/errors/error.gif | Bin 0 -> 661992 bytes shared/static/favicon.ico | Bin 0 -> 15406 bytes shared/static/fontawesome/css/all.min.css | 9 + .../static/fontawesome/css/v4-shims.min.css | 6 + .../fontawesome/webfonts/fa-brands-400.ttf | Bin 0 -> 207972 bytes .../fontawesome/webfonts/fa-brands-400.woff2 | Bin 0 -> 117372 bytes .../fontawesome/webfonts/fa-regular-400.ttf | Bin 0 -> 68004 bytes .../fontawesome/webfonts/fa-regular-400.woff2 | Bin 0 -> 25452 bytes .../fontawesome/webfonts/fa-solid-900.ttf | Bin 0 -> 419720 bytes .../fontawesome/webfonts/fa-solid-900.woff2 | Bin 0 -> 156496 bytes .../webfonts/fa-v4compatibility.ttf | Bin 0 -> 10832 bytes .../webfonts/fa-v4compatibility.woff2 | Bin 0 -> 4792 bytes shared/static/img/filter.svg | 2 + shared/static/img/logo.jpg | Bin 0 -> 366136 bytes shared/static/img/search.svg | 4 + shared/static/labels/_blank.svg | 17 + shared/static/labels/new.svg | 17 + shared/static/labels/offer.svg | 19 + shared/static/nav-labels/new.svg | 14 + shared/static/nav-labels/offer.svg | 16 + shared/static/order/a-z.svg | 10 + shared/static/order/h-l.svg | 10 + shared/static/order/l-h.svg | 10 + shared/static/order/z-a.svg | 10 + shared/static/scripts/body.js | 822 +++++ shared/static/stickers/biodynamic.svg | 13 + shared/static/stickers/fairtrade.svg | 13 + shared/static/stickers/glutenfree.svg | 14 + shared/static/stickers/organic.svg | 13 + shared/static/stickers/sugarfree.svg | 13 + shared/static/stickers/vegan.svg | 14 + shared/static/styles/basics.css | 32 + shared/static/styles/blog-content.css | 165 + shared/static/styles/cards.css | 2637 ++++++++++++++++ shared/utils/__init__.py | 101 + shared/utils/anchoring.py | 236 ++ shared/utils/calendar_helpers.py | 54 + shared/utils/http_signatures.py | 181 ++ shared/utils/ipfs_client.py | 141 + shared/utils/webfinger.py | 68 + 895 files changed, 61147 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 _config/app-config.yaml create mode 100644 account/Dockerfile create mode 100644 account/__init__.py create mode 100644 account/app.py create mode 100644 account/bp/__init__.py create mode 100644 account/bp/account/__init__.py create mode 100644 account/bp/account/routes.py create mode 100644 account/bp/auth/__init__.py create mode 100644 account/bp/auth/routes.py create mode 100644 account/bp/auth/services/__init__.py create mode 100644 account/bp/auth/services/auth_operations.py create mode 100644 account/bp/auth/services/login_redirect.py create mode 100644 account/bp/fragments/__init__.py create mode 100644 account/bp/fragments/routes.py create mode 100644 account/entrypoint.sh create mode 100644 account/models/__init__.py create mode 100644 account/path_setup.py create mode 100644 account/services/__init__.py create mode 100644 account/templates/_email/magic_link.html create mode 100644 account/templates/_email/magic_link.txt create mode 100644 account/templates/_types/auth/_bookings_panel.html create mode 100644 account/templates/_types/auth/_fragment_panel.html create mode 100644 account/templates/_types/auth/_main_panel.html create mode 100644 account/templates/_types/auth/_nav.html create mode 100644 account/templates/_types/auth/_newsletter_toggle.html create mode 100644 account/templates/_types/auth/_newsletters_panel.html create mode 100644 account/templates/_types/auth/_oob_elements.html create mode 100644 account/templates/_types/auth/_tickets_panel.html create mode 100644 account/templates/_types/auth/check_email.html create mode 100644 account/templates/_types/auth/header/_header.html create mode 100644 account/templates/_types/auth/index copy.html create mode 100644 account/templates/_types/auth/index.html create mode 100644 account/templates/_types/auth/login.html create mode 100644 account/templates/auth/check_email.html create mode 100644 account/templates/auth/login.html create mode 100644 account/templates/fragments/auth_menu.html create mode 100644 blog/.gitignore create mode 100644 blog/Dockerfile create mode 100644 blog/README.md create mode 100644 blog/__init__.py create mode 100644 blog/app.py create mode 100644 blog/bp/__init__.py create mode 100644 blog/bp/admin/routes.py create mode 100644 blog/bp/blog/__init__.py create mode 100644 blog/bp/blog/admin/__init__.py create mode 100644 blog/bp/blog/admin/routes.py create mode 100644 blog/bp/blog/filters/qs.py create mode 100644 blog/bp/blog/ghost/editor_api.py create mode 100644 blog/bp/blog/ghost/ghost_admin_token.py create mode 100644 blog/bp/blog/ghost/ghost_posts.py create mode 100644 blog/bp/blog/ghost/ghost_sync.py create mode 100644 blog/bp/blog/ghost/lexical_renderer.py create mode 100644 blog/bp/blog/ghost/lexical_validator.py create mode 100644 blog/bp/blog/ghost_db.py create mode 100644 blog/bp/blog/routes.py create mode 100644 blog/bp/blog/services/pages_data.py create mode 100644 blog/bp/blog/services/posts_data.py create mode 100644 blog/bp/blog/web_hooks/routes.py create mode 100644 blog/bp/fragments/__init__.py create mode 100644 blog/bp/fragments/routes.py create mode 100644 blog/bp/menu_items/__init__.py create mode 100644 blog/bp/menu_items/routes.py create mode 100644 blog/bp/menu_items/services/menu_items.py create mode 100644 blog/bp/post/admin/routes.py create mode 100644 blog/bp/post/routes.py create mode 100644 blog/bp/post/services/entry_associations.py create mode 100644 blog/bp/post/services/markets.py create mode 100644 blog/bp/post/services/post_data.py create mode 100644 blog/bp/post/services/post_operations.py create mode 100644 blog/bp/snippets/__init__.py create mode 100644 blog/bp/snippets/routes.py create mode 100644 blog/config/app-config.yaml create mode 100644 blog/entrypoint.sh create mode 100644 blog/models/__init__.py create mode 100644 blog/models/ghost_content.py create mode 100644 blog/models/ghost_membership_entities.py create mode 100644 blog/models/kv.py create mode 100644 blog/models/magic_link.py create mode 100644 blog/models/menu_item.py create mode 100644 blog/models/snippet.py create mode 100644 blog/models/tag_group.py create mode 100644 blog/models/user.py create mode 100644 blog/path_setup.py create mode 100644 blog/services/__init__.py create mode 100644 blog/templates/_email/magic_link.html create mode 100644 blog/templates/_email/magic_link.txt create mode 100644 blog/templates/_types/blog/_action_buttons.html create mode 100644 blog/templates/_types/blog/_card.html create mode 100644 blog/templates/_types/blog/_card/at_bar.html create mode 100644 blog/templates/_types/blog/_card/author.html create mode 100644 blog/templates/_types/blog/_card/authors.html create mode 100644 blog/templates/_types/blog/_card/tag.html create mode 100644 blog/templates/_types/blog/_card/tag_group.html create mode 100644 blog/templates/_types/blog/_card/tags.html create mode 100644 blog/templates/_types/blog/_card_tile.html create mode 100644 blog/templates/_types/blog/_cards.html create mode 100644 blog/templates/_types/blog/_main_panel.html create mode 100644 blog/templates/_types/blog/_oob_elements.html create mode 100644 blog/templates/_types/blog/_page_card.html create mode 100644 blog/templates/_types/blog/_page_cards.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/_edit_header.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/_edit_main_panel.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/_edit_oob.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/_header.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/_main_panel.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/_oob_elements.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/edit.html create mode 100644 blog/templates/_types/blog/admin/tag_groups/index.html create mode 100644 blog/templates/_types/blog/desktop/menu.html create mode 100644 blog/templates/_types/blog/desktop/menu/authors.html create mode 100644 blog/templates/_types/blog/desktop/menu/tag_groups.html create mode 100644 blog/templates/_types/blog/desktop/menu/tags.html create mode 100644 blog/templates/_types/blog/header/_header.html create mode 100644 blog/templates/_types/blog/index.html create mode 100644 blog/templates/_types/blog/mobile/_filter/_hamburger.html create mode 100644 blog/templates/_types/blog/mobile/_filter/summary.html create mode 100644 blog/templates/_types/blog/mobile/_filter/summary/authors.html create mode 100644 blog/templates/_types/blog/mobile/_filter/summary/tag_groups.html create mode 100644 blog/templates/_types/blog/mobile/_filter/summary/tags.html create mode 100644 blog/templates/_types/blog/not_found.html create mode 100644 blog/templates/_types/blog_drafts/_main_panel.html create mode 100644 blog/templates/_types/blog_drafts/_oob_elements.html create mode 100644 blog/templates/_types/blog_drafts/index.html create mode 100644 blog/templates/_types/blog_new/_main_panel.html create mode 100644 blog/templates/_types/blog_new/_oob_elements.html create mode 100644 blog/templates/_types/blog_new/index.html create mode 100644 blog/templates/_types/home/_oob_elements.html create mode 100644 blog/templates/_types/home/index.html create mode 100644 blog/templates/_types/menu_items/_form.html create mode 100644 blog/templates/_types/menu_items/_list.html create mode 100644 blog/templates/_types/menu_items/_main_panel.html create mode 100644 blog/templates/_types/menu_items/_nav_oob.html create mode 100644 blog/templates/_types/menu_items/_oob_elements.html create mode 100644 blog/templates/_types/menu_items/_page_search_results.html create mode 100644 blog/templates/_types/menu_items/header/_header.html create mode 100644 blog/templates/_types/menu_items/index.html create mode 100644 blog/templates/_types/post/_entry_container.html create mode 100644 blog/templates/_types/post/_entry_items.html create mode 100644 blog/templates/_types/post/_main_panel.html create mode 100644 blog/templates/_types/post/_meta.html create mode 100644 blog/templates/_types/post/_nav.html create mode 100644 blog/templates/_types/post/_oob_elements.html create mode 100644 blog/templates/_types/post/admin/_associated_entries.html create mode 100644 blog/templates/_types/post/admin/_calendar_view.html create mode 100644 blog/templates/_types/post/admin/_features_panel.html create mode 100644 blog/templates/_types/post/admin/_main_panel.html create mode 100644 blog/templates/_types/post/admin/_markets_panel.html create mode 100644 blog/templates/_types/post/admin/_nav.html create mode 100644 blog/templates/_types/post/admin/_nav_entries.html create mode 100644 blog/templates/_types/post/admin/_nav_entries_oob.html create mode 100644 blog/templates/_types/post/admin/_oob_elements.html create mode 100644 blog/templates/_types/post/admin/header/_header.html create mode 100644 blog/templates/_types/post/admin/index.html create mode 100644 blog/templates/_types/post/header/_header.html create mode 100644 blog/templates/_types/post/index.html create mode 100644 blog/templates/_types/post_data/_main_panel.html create mode 100644 blog/templates/_types/post_data/_nav.html create mode 100644 blog/templates/_types/post_data/_oob_elements.html create mode 100644 blog/templates/_types/post_data/header/_header.html create mode 100644 blog/templates/_types/post_data/index.html create mode 100644 blog/templates/_types/post_edit/_main_panel.html create mode 100644 blog/templates/_types/post_edit/_nav.html create mode 100644 blog/templates/_types/post_edit/_oob_elements.html create mode 100644 blog/templates/_types/post_edit/header/_header.html create mode 100644 blog/templates/_types/post_edit/index.html create mode 100644 blog/templates/_types/post_entries/_main_panel.html create mode 100644 blog/templates/_types/post_entries/_nav.html create mode 100644 blog/templates/_types/post_entries/_oob_elements.html create mode 100644 blog/templates/_types/post_entries/header/_header.html create mode 100644 blog/templates/_types/post_entries/index.html create mode 100644 blog/templates/_types/post_settings/_main_panel.html create mode 100644 blog/templates/_types/post_settings/_nav.html create mode 100644 blog/templates/_types/post_settings/_oob_elements.html create mode 100644 blog/templates/_types/post_settings/header/_header.html create mode 100644 blog/templates/_types/post_settings/index.html create mode 100644 blog/templates/_types/root/header/_header.html create mode 100644 blog/templates/_types/root/settings/_main_panel.html create mode 100644 blog/templates/_types/root/settings/_nav.html create mode 100644 blog/templates/_types/root/settings/_oob_elements.html create mode 100644 blog/templates/_types/root/settings/cache/_header.html create mode 100644 blog/templates/_types/root/settings/cache/_main_panel.html create mode 100644 blog/templates/_types/root/settings/cache/_oob_elements.html create mode 100644 blog/templates/_types/root/settings/cache/index.html create mode 100644 blog/templates/_types/root/settings/header/_header.html create mode 100644 blog/templates/_types/root/settings/index.html create mode 100644 blog/templates/_types/snippets/_list.html create mode 100644 blog/templates/_types/snippets/_main_panel.html create mode 100644 blog/templates/_types/snippets/_oob_elements.html create mode 100644 blog/templates/_types/snippets/header/_header.html create mode 100644 blog/templates/_types/snippets/index.html create mode 100644 blog/templates/fragments/nav_tree.html create mode 100644 blog/templates/macros/admin_nav.html create mode 100644 blog/templates/macros/scrolling_menu.html create mode 100644 blog/templates/macros/stickers.html create mode 100644 cart/.gitignore create mode 100644 cart/Dockerfile create mode 100644 cart/README.md create mode 100644 cart/__init__.py create mode 100644 cart/app.py create mode 100644 cart/bp/__init__.py create mode 100644 cart/bp/cart/global_routes.py create mode 100644 cart/bp/cart/overview_routes.py create mode 100644 cart/bp/cart/page_routes.py create mode 100644 cart/bp/cart/services/__init__.py create mode 100644 cart/bp/cart/services/calendar_cart.py create mode 100644 cart/bp/cart/services/check_sumup_status.py create mode 100644 cart/bp/cart/services/checkout.py create mode 100644 cart/bp/cart/services/clear_cart_for_order.py create mode 100644 cart/bp/cart/services/get_cart.py create mode 100644 cart/bp/cart/services/identity.py create mode 100644 cart/bp/cart/services/page_cart.py create mode 100644 cart/bp/cart/services/ticket_groups.py create mode 100644 cart/bp/cart/services/total.py create mode 100644 cart/bp/fragments/__init__.py create mode 100644 cart/bp/fragments/routes.py create mode 100644 cart/bp/order/filters/qs.py create mode 100644 cart/bp/order/routes.py create mode 100644 cart/bp/orders/filters/qs.py create mode 100644 cart/bp/orders/routes.py create mode 100644 cart/config/app-config.yaml create mode 100644 cart/entrypoint.sh create mode 100644 cart/models/__init__.py create mode 100644 cart/models/order.py create mode 100644 cart/models/page_config.py create mode 100644 cart/path_setup.py create mode 100644 cart/services/__init__.py create mode 100644 cart/templates/_types/auth/header/_header.html create mode 100644 cart/templates/_types/auth/index.html create mode 100644 cart/templates/_types/cart/_cart.html create mode 100644 cart/templates/_types/cart/_main_panel.html create mode 100644 cart/templates/_types/cart/_mini.html create mode 100644 cart/templates/_types/cart/_nav.html create mode 100644 cart/templates/_types/cart/_oob_elements.html create mode 100644 cart/templates/_types/cart/checkout_error.html create mode 100644 cart/templates/_types/cart/checkout_return.html create mode 100644 cart/templates/_types/cart/header/_header.html create mode 100644 cart/templates/_types/cart/index.html create mode 100644 cart/templates/_types/cart/overview/_main_panel.html create mode 100644 cart/templates/_types/cart/overview/_oob_elements.html create mode 100644 cart/templates/_types/cart/overview/index.html create mode 100644 cart/templates/_types/cart/page/_main_panel.html create mode 100644 cart/templates/_types/cart/page/_oob_elements.html create mode 100644 cart/templates/_types/cart/page/header/_header.html create mode 100644 cart/templates/_types/cart/page/index.html create mode 100644 cart/templates/_types/order/_calendar_items.html create mode 100644 cart/templates/_types/order/_items.html create mode 100644 cart/templates/_types/order/_main_panel.html create mode 100644 cart/templates/_types/order/_nav.html create mode 100644 cart/templates/_types/order/_oob_elements.html create mode 100644 cart/templates/_types/order/_summary.html create mode 100644 cart/templates/_types/order/_ticket_items.html create mode 100644 cart/templates/_types/order/header/_header.html create mode 100644 cart/templates/_types/order/index.html create mode 100644 cart/templates/_types/orders/_main_panel.html create mode 100644 cart/templates/_types/orders/_nav.html create mode 100644 cart/templates/_types/orders/_oob_elements.html create mode 100644 cart/templates/_types/orders/_rows.html create mode 100644 cart/templates/_types/orders/_summary.html create mode 100644 cart/templates/_types/orders/header/_header.html create mode 100644 cart/templates/_types/orders/index.html create mode 100644 cart/templates/_types/product/_cart.html create mode 100644 cart/templates/fragments/cart_mini.html create mode 100644 docker-compose.yml create mode 100644 events/.gitignore create mode 100644 events/Dockerfile create mode 100644 events/README.md create mode 100644 events/__init__.py create mode 100644 events/app.py create mode 100644 events/bp/__init__.py create mode 100644 events/bp/all_events/__init__.py create mode 100644 events/bp/all_events/routes.py create mode 100644 events/bp/calendar/admin/routes.py create mode 100644 events/bp/calendar/routes.py create mode 100644 events/bp/calendar/services/__init__.py create mode 100644 events/bp/calendar/services/adopt_session_entries_for_user.py create mode 100644 events/bp/calendar/services/calendar.py create mode 100644 events/bp/calendar/services/calendar_view.py create mode 100644 events/bp/calendar/services/slots.py create mode 100644 events/bp/calendar/services/visiblity.py create mode 100644 events/bp/calendar_entries/routes.py create mode 100644 events/bp/calendar_entries/services/entries.py create mode 100644 events/bp/calendar_entry/admin/routes.py create mode 100644 events/bp/calendar_entry/routes.py create mode 100644 events/bp/calendar_entry/services/post_associations.py create mode 100644 events/bp/calendar_entry/services/ticket_operations.py create mode 100644 events/bp/calendars/routes.py create mode 100644 events/bp/calendars/services/calendars.py create mode 100644 events/bp/day/admin/routes.py create mode 100644 events/bp/day/routes.py create mode 100644 events/bp/fragments/__init__.py create mode 100644 events/bp/fragments/routes.py create mode 100644 events/bp/markets/__init__.py create mode 100644 events/bp/markets/routes.py create mode 100644 events/bp/markets/services/__init__.py create mode 100644 events/bp/markets/services/markets.py create mode 100644 events/bp/page/__init__.py create mode 100644 events/bp/page/routes.py create mode 100644 events/bp/payments/__init__.py create mode 100644 events/bp/payments/routes.py create mode 100644 events/bp/slot/routes.py create mode 100644 events/bp/slot/services/slot.py create mode 100644 events/bp/slots/routes.py create mode 100644 events/bp/slots/services/slots.py create mode 100644 events/bp/ticket_admin/__init__.py create mode 100644 events/bp/ticket_admin/routes.py create mode 100644 events/bp/ticket_admin/services/__init__.py create mode 100644 events/bp/ticket_type/routes.py create mode 100644 events/bp/ticket_type/services/ticket.py create mode 100644 events/bp/ticket_types/routes.py create mode 100644 events/bp/ticket_types/services/tickets.py create mode 100644 events/bp/tickets/__init__.py create mode 100644 events/bp/tickets/routes.py create mode 100644 events/bp/tickets/services/__init__.py create mode 100644 events/bp/tickets/services/tickets.py create mode 100644 events/config/app-config.yaml create mode 100644 events/entrypoint.sh create mode 100644 events/models/__init__.py create mode 100644 events/models/calendars.py create mode 100644 events/path_setup.py create mode 100644 events/services/__init__.py create mode 100644 events/templates/_types/all_events/_card.html create mode 100644 events/templates/_types/all_events/_card_tile.html create mode 100644 events/templates/_types/all_events/_cards.html create mode 100644 events/templates/_types/all_events/_main_panel.html create mode 100644 events/templates/_types/all_events/index.html create mode 100644 events/templates/_types/calendar/_description.html create mode 100644 events/templates/_types/calendar/_main_panel.html create mode 100644 events/templates/_types/calendar/_nav.html create mode 100644 events/templates/_types/calendar/_oob_elements.html create mode 100644 events/templates/_types/calendar/admin/_description.html create mode 100644 events/templates/_types/calendar/admin/_description_edit.html create mode 100644 events/templates/_types/calendar/admin/_main_panel.html create mode 100644 events/templates/_types/calendar/admin/_nav.html create mode 100644 events/templates/_types/calendar/admin/_oob_elements.html create mode 100644 events/templates/_types/calendar/admin/header/_header.html create mode 100644 events/templates/_types/calendar/admin/index.html create mode 100644 events/templates/_types/calendar/header/_header.html create mode 100644 events/templates/_types/calendar/index.html create mode 100644 events/templates/_types/calendars/_calendars_list.html create mode 100644 events/templates/_types/calendars/_main_panel.html create mode 100644 events/templates/_types/calendars/_nav.html create mode 100644 events/templates/_types/calendars/_oob_elements.html create mode 100644 events/templates/_types/calendars/header/_header.html create mode 100644 events/templates/_types/calendars/index.html create mode 100644 events/templates/_types/day/_add.html create mode 100644 events/templates/_types/day/_add_button.html create mode 100644 events/templates/_types/day/_main_panel.html create mode 100644 events/templates/_types/day/_nav.html create mode 100644 events/templates/_types/day/_oob_elements.html create mode 100644 events/templates/_types/day/_row.html create mode 100644 events/templates/_types/day/admin/_main_panel.html create mode 100644 events/templates/_types/day/admin/_nav.html create mode 100644 events/templates/_types/day/admin/_nav_entries_oob.html create mode 100644 events/templates/_types/day/admin/_oob_elements.html create mode 100644 events/templates/_types/day/admin/header/_header.html create mode 100644 events/templates/_types/day/admin/index.html create mode 100644 events/templates/_types/day/header/_header.html create mode 100644 events/templates/_types/day/index.html create mode 100644 events/templates/_types/entry/_edit.html create mode 100644 events/templates/_types/entry/_main_panel.html create mode 100644 events/templates/_types/entry/_nav.html create mode 100644 events/templates/_types/entry/_oob_elements.html create mode 100644 events/templates/_types/entry/_optioned.html create mode 100644 events/templates/_types/entry/_options.html create mode 100644 events/templates/_types/entry/_post_search_results.html create mode 100644 events/templates/_types/entry/_posts.html create mode 100644 events/templates/_types/entry/_state.html create mode 100644 events/templates/_types/entry/_tickets.html create mode 100644 events/templates/_types/entry/_times.html create mode 100644 events/templates/_types/entry/_title.html create mode 100644 events/templates/_types/entry/admin/_main_panel.html create mode 100644 events/templates/_types/entry/admin/_nav.html create mode 100644 events/templates/_types/entry/admin/_nav_posts_oob.html create mode 100644 events/templates/_types/entry/admin/_oob_elements.html create mode 100644 events/templates/_types/entry/admin/header/_header.html create mode 100644 events/templates/_types/entry/admin/index.html create mode 100644 events/templates/_types/entry/header/_header.html create mode 100644 events/templates/_types/entry/index.html create mode 100644 events/templates/_types/markets/_main_panel.html create mode 100644 events/templates/_types/markets/_markets_list.html create mode 100644 events/templates/_types/markets/_nav.html create mode 100644 events/templates/_types/markets/_oob_elements.html create mode 100644 events/templates/_types/markets/header/_header.html create mode 100644 events/templates/_types/markets/index.html create mode 100644 events/templates/_types/page_summary/_card.html create mode 100644 events/templates/_types/page_summary/_card_tile.html create mode 100644 events/templates/_types/page_summary/_cards.html create mode 100644 events/templates/_types/page_summary/_main_panel.html create mode 100644 events/templates/_types/page_summary/_ticket_widget.html create mode 100644 events/templates/_types/page_summary/index.html create mode 100644 events/templates/_types/payments/_main_panel.html create mode 100644 events/templates/_types/payments/_nav.html create mode 100644 events/templates/_types/payments/_oob_elements.html create mode 100644 events/templates/_types/payments/header/_header.html create mode 100644 events/templates/_types/payments/index.html create mode 100644 events/templates/_types/post/_nav.html create mode 100644 events/templates/_types/post/admin/_associated_entries.html create mode 100644 events/templates/_types/post/admin/_nav.html create mode 100644 events/templates/_types/post/admin/header/_header.html create mode 100644 events/templates/_types/post/header/_header.html create mode 100644 events/templates/_types/post_entries/_main_panel.html create mode 100644 events/templates/_types/post_entries/_nav.html create mode 100644 events/templates/_types/post_entries/header/_header.html create mode 100644 events/templates/_types/slot/__description.html create mode 100644 events/templates/_types/slot/_description.html create mode 100644 events/templates/_types/slot/_edit.html create mode 100644 events/templates/_types/slot/_main_panel.html create mode 100644 events/templates/_types/slot/_oob_elements.html create mode 100644 events/templates/_types/slot/header/_header.html create mode 100644 events/templates/_types/slot/index.html create mode 100644 events/templates/_types/slots/_add.html create mode 100644 events/templates/_types/slots/_add_button.html create mode 100644 events/templates/_types/slots/_main_panel.html create mode 100644 events/templates/_types/slots/_oob_elements.html create mode 100644 events/templates/_types/slots/_row.html create mode 100644 events/templates/_types/slots/header/_header.html create mode 100644 events/templates/_types/slots/index.html create mode 100644 events/templates/_types/ticket_admin/_checkin_result.html create mode 100644 events/templates/_types/ticket_admin/_entry_tickets.html create mode 100644 events/templates/_types/ticket_admin/_lookup_result.html create mode 100644 events/templates/_types/ticket_admin/_main_panel.html create mode 100644 events/templates/_types/ticket_admin/index.html create mode 100644 events/templates/_types/ticket_type/_edit.html create mode 100644 events/templates/_types/ticket_type/_main_panel.html create mode 100644 events/templates/_types/ticket_type/_nav.html create mode 100644 events/templates/_types/ticket_type/_oob_elements.html create mode 100644 events/templates/_types/ticket_type/header/_header.html create mode 100644 events/templates/_types/ticket_type/index.html create mode 100644 events/templates/_types/ticket_types/_add.html create mode 100644 events/templates/_types/ticket_types/_add_button.html create mode 100644 events/templates/_types/ticket_types/_main_panel.html create mode 100644 events/templates/_types/ticket_types/_nav.html create mode 100644 events/templates/_types/ticket_types/_oob_elements.html create mode 100644 events/templates/_types/ticket_types/_row.html create mode 100644 events/templates/_types/ticket_types/header/_header.html create mode 100644 events/templates/_types/ticket_types/index.html create mode 100644 events/templates/_types/tickets/_adjust_response.html create mode 100644 events/templates/_types/tickets/_buy_form.html create mode 100644 events/templates/_types/tickets/_buy_result.html create mode 100644 events/templates/_types/tickets/_detail_panel.html create mode 100644 events/templates/_types/tickets/_main_panel.html create mode 100644 events/templates/_types/tickets/detail.html create mode 100644 events/templates/_types/tickets/index.html create mode 100644 events/templates/fragments/account_nav_items.html create mode 100644 events/templates/fragments/account_page_bookings.html create mode 100644 events/templates/fragments/account_page_tickets.html create mode 100644 events/templates/fragments/container_cards_entries.html create mode 100644 events/templates/fragments/container_nav_calendars.html create mode 100644 events/templates/fragments/container_nav_entries.html create mode 100644 events/templates/macros/date.html create mode 100644 federation/.gitignore create mode 100644 federation/Dockerfile create mode 100644 federation/__init__.py create mode 100644 federation/app.py create mode 100644 federation/bp/__init__.py create mode 100644 federation/bp/auth/__init__.py create mode 100644 federation/bp/auth/routes.py create mode 100644 federation/bp/auth/services/__init__.py create mode 100644 federation/bp/auth/services/auth_operations.py create mode 100644 federation/bp/auth/services/login_redirect.py create mode 100644 federation/bp/fragments/__init__.py create mode 100644 federation/bp/fragments/routes.py create mode 100644 federation/bp/identity/__init__.py create mode 100644 federation/bp/identity/routes.py create mode 100644 federation/bp/social/__init__.py create mode 100644 federation/bp/social/routes.py create mode 100644 federation/config/app-config.yaml create mode 100755 federation/entrypoint.sh create mode 100644 federation/models/__init__.py create mode 100644 federation/path_setup.py create mode 100644 federation/services/__init__.py create mode 100644 federation/templates/_email/magic_link.html create mode 100644 federation/templates/_email/magic_link.txt create mode 100644 federation/templates/_types/federation/index.html create mode 100644 federation/templates/_types/social/header/_header.html create mode 100644 federation/templates/_types/social/index.html create mode 100644 federation/templates/auth/check_email.html create mode 100644 federation/templates/auth/login.html create mode 100644 federation/templates/federation/_actor_list_items.html create mode 100644 federation/templates/federation/_interaction_buttons.html create mode 100644 federation/templates/federation/_notification.html create mode 100644 federation/templates/federation/_post_card.html create mode 100644 federation/templates/federation/_search_results.html create mode 100644 federation/templates/federation/_timeline_items.html create mode 100644 federation/templates/federation/account.html create mode 100644 federation/templates/federation/actor_card.html create mode 100644 federation/templates/federation/actor_timeline.html create mode 100644 federation/templates/federation/choose_username.html create mode 100644 federation/templates/federation/compose.html create mode 100644 federation/templates/federation/followers.html create mode 100644 federation/templates/federation/following.html create mode 100644 federation/templates/federation/notifications.html create mode 100644 federation/templates/federation/profile.html create mode 100644 federation/templates/federation/search.html create mode 100644 federation/templates/federation/timeline.html create mode 100644 market/.gitignore create mode 100644 market/Dockerfile create mode 100644 market/README.md create mode 100644 market/__init__.py create mode 100644 market/app.py create mode 100644 market/bp/__init__.py create mode 100644 market/bp/all_markets/__init__.py create mode 100644 market/bp/all_markets/routes.py create mode 100644 market/bp/api/__init__.py create mode 100644 market/bp/api/routes.py create mode 100644 market/bp/browse/__init__.py create mode 100644 market/bp/browse/routes.py create mode 100644 market/bp/browse/services/__init__.py create mode 100644 market/bp/browse/services/blacklist/category.py create mode 100644 market/bp/browse/services/blacklist/product.py create mode 100644 market/bp/browse/services/blacklist/product_details.py create mode 100644 market/bp/browse/services/cache_backend.py create mode 100644 market/bp/browse/services/db_backend.py create mode 100644 market/bp/browse/services/nav.py create mode 100644 market/bp/browse/services/products.py create mode 100644 market/bp/browse/services/services.py create mode 100644 market/bp/browse/services/slugs.py create mode 100644 market/bp/browse/services/state.py create mode 100644 market/bp/cart/__init__.py create mode 100644 market/bp/cart/services/__init__.py create mode 100644 market/bp/cart/services/identity.py create mode 100644 market/bp/cart/services/total.py create mode 100644 market/bp/fragments/__init__.py create mode 100644 market/bp/fragments/routes.py create mode 100644 market/bp/market/__init__.py create mode 100644 market/bp/market/admin/__init__.py create mode 100644 market/bp/market/admin/routes.py create mode 100644 market/bp/market/filters/__init__.py create mode 100644 market/bp/market/filters/qs.py create mode 100644 market/bp/market/routes.py create mode 100644 market/bp/page_markets/__init__.py create mode 100644 market/bp/page_markets/routes.py create mode 100644 market/bp/product/routes.py create mode 100644 market/bp/product/services/__init__.py create mode 100644 market/bp/product/services/product_operations.py create mode 100644 market/config/app-config.yaml create mode 100644 market/entrypoint.sh create mode 100644 market/models/__init__.py create mode 100644 market/models/market.py create mode 100644 market/models/market_place.py create mode 100644 market/path_setup.py create mode 100644 market/scrape-test.sh create mode 100644 market/scrape.sh create mode 100644 market/scrape/__init__.py create mode 100644 market/scrape/build_snapshot/__init__.py create mode 100644 market/scrape/build_snapshot/build_snapshot.py create mode 100644 market/scrape/build_snapshot/tools/APP_ROOT_PLACEHOLDER.py create mode 100644 market/scrape/build_snapshot/tools/__init__.py create mode 100644 market/scrape/build_snapshot/tools/_anchor_text.py create mode 100644 market/scrape/build_snapshot/tools/_collect_html_img_srcs.py create mode 100644 market/scrape/build_snapshot/tools/_dedupe_preserve_order.py create mode 100644 market/scrape/build_snapshot/tools/_product_dict_is_cf.py create mode 100644 market/scrape/build_snapshot/tools/_resolve_sub_redirects.py create mode 100644 market/scrape/build_snapshot/tools/_rewrite_links_fragment.py create mode 100644 market/scrape/build_snapshot/tools/candidate_subs.py create mode 100644 market/scrape/build_snapshot/tools/capture_category.py create mode 100644 market/scrape/build_snapshot/tools/capture_product_slugs.py create mode 100644 market/scrape/build_snapshot/tools/capture_sub.py create mode 100644 market/scrape/build_snapshot/tools/fetch_and_upsert_product.py create mode 100644 market/scrape/build_snapshot/tools/fetch_and_upsert_products.py create mode 100644 market/scrape/build_snapshot/tools/rewrite_nav.py create mode 100644 market/scrape/build_snapshot/tools/valid_subs.py create mode 100644 market/scrape/get_auth.py create mode 100644 market/scrape/html_utils.py create mode 100644 market/scrape/http_client.py create mode 100644 market/scrape/listings.py create mode 100644 market/scrape/nav.py create mode 100644 market/scrape/persist_api/__init__.py create mode 100644 market/scrape/persist_api/capture_listing.py create mode 100644 market/scrape/persist_api/log_product_result.py create mode 100644 market/scrape/persist_api/save_nav.py create mode 100644 market/scrape/persist_api/save_subcategory_redirects.py create mode 100644 market/scrape/persist_api/upsert_product.py create mode 100644 market/scrape/persist_snapshot/__init__.py create mode 100644 market/scrape/persist_snapshot/_get.py create mode 100644 market/scrape/persist_snapshot/capture_listing.py create mode 100644 market/scrape/persist_snapshot/log_product_result.py create mode 100644 market/scrape/persist_snapshot/save_link_reports.py create mode 100644 market/scrape/persist_snapshot/save_nav.py create mode 100644 market/scrape/persist_snapshot/save_subcategory_redirects.py create mode 100644 market/scrape/persist_snapshot/upsert_product.py create mode 100644 market/scrape/product/__init__.py create mode 100644 market/scrape/product/extractors/__init__.py create mode 100644 market/scrape/product/extractors/breadcrumbs.py create mode 100644 market/scrape/product/extractors/description_sections.py create mode 100644 market/scrape/product/extractors/images.py create mode 100644 market/scrape/product/extractors/info_table.py create mode 100644 market/scrape/product/extractors/labels.py create mode 100644 market/scrape/product/extractors/nutrition_ex.py create mode 100644 market/scrape/product/extractors/oe_list_price.py create mode 100644 market/scrape/product/extractors/regular_price_fallback.py create mode 100644 market/scrape/product/extractors/short_description.py create mode 100644 market/scrape/product/extractors/stickers.py create mode 100644 market/scrape/product/extractors/title.py create mode 100644 market/scrape/product/helpers/desc.py create mode 100644 market/scrape/product/helpers/html.py create mode 100644 market/scrape/product/helpers/price.py create mode 100644 market/scrape/product/helpers/text.py create mode 100644 market/scrape/product/product_core.py create mode 100644 market/scrape/product/product_detail.py create mode 100644 market/scrape/product/registry.py create mode 100644 market/services/__init__.py create mode 100644 market/templates/_types/all_markets/_card.html create mode 100644 market/templates/_types/all_markets/_cards.html create mode 100644 market/templates/_types/all_markets/_main_panel.html create mode 100644 market/templates/_types/all_markets/index.html create mode 100644 market/templates/_types/browse/_admin.html create mode 100644 market/templates/_types/browse/_main_panel.html create mode 100644 market/templates/_types/browse/_oob_elements.html create mode 100644 market/templates/_types/browse/_product_card.html create mode 100644 market/templates/_types/browse/_product_cards.html create mode 100644 market/templates/_types/browse/desktop/_category_selector.html create mode 100644 market/templates/_types/browse/desktop/_filter/brand.html create mode 100644 market/templates/_types/browse/desktop/_filter/labels.html create mode 100644 market/templates/_types/browse/desktop/_filter/like.html create mode 100644 market/templates/_types/browse/desktop/_filter/search.html create mode 100644 market/templates/_types/browse/desktop/_filter/sort.html create mode 100644 market/templates/_types/browse/desktop/_filter/stickers.html create mode 100644 market/templates/_types/browse/desktop/menu.html create mode 100644 market/templates/_types/browse/index.html create mode 100644 market/templates/_types/browse/like/button.html create mode 100644 market/templates/_types/browse/mobile/_filter/brand_ul.html create mode 100644 market/templates/_types/browse/mobile/_filter/index.html create mode 100644 market/templates/_types/browse/mobile/_filter/labels.html create mode 100644 market/templates/_types/browse/mobile/_filter/like.html create mode 100644 market/templates/_types/browse/mobile/_filter/search.html create mode 100644 market/templates/_types/browse/mobile/_filter/sort_ul.html create mode 100644 market/templates/_types/browse/mobile/_filter/stickers.html create mode 100644 market/templates/_types/browse/mobile/_filter/summary.html create mode 100644 market/templates/_types/market/_admin.html create mode 100644 market/templates/_types/market/_main_panel.html create mode 100644 market/templates/_types/market/_oob_elements.html create mode 100644 market/templates/_types/market/_title.html create mode 100644 market/templates/_types/market/admin/_main_panel.html create mode 100644 market/templates/_types/market/admin/_nav.html create mode 100644 market/templates/_types/market/admin/_oob_elements.html create mode 100644 market/templates/_types/market/admin/header/_header.html create mode 100644 market/templates/_types/market/admin/index.html create mode 100644 market/templates/_types/market/desktop/_nav.html create mode 100644 market/templates/_types/market/header/_header.html create mode 100644 market/templates/_types/market/index.html create mode 100644 market/templates/_types/market/markets_listing.html create mode 100644 market/templates/_types/market/mobile/_nav_panel.html create mode 100644 market/templates/_types/market/mobile/menu.html create mode 100644 market/templates/_types/page_markets/_card.html create mode 100644 market/templates/_types/page_markets/_cards.html create mode 100644 market/templates/_types/page_markets/_main_panel.html create mode 100644 market/templates/_types/page_markets/index.html create mode 100644 market/templates/_types/post/_nav.html create mode 100644 market/templates/_types/post/admin/_nav_entries.html create mode 100644 market/templates/_types/post/header/_header.html create mode 100644 market/templates/_types/product/_added.html create mode 100644 market/templates/_types/product/_cart.html create mode 100644 market/templates/_types/product/_main_panel.html create mode 100644 market/templates/_types/product/_meta.html create mode 100644 market/templates/_types/product/_oob_elements.html create mode 100644 market/templates/_types/product/_prices.html create mode 100644 market/templates/_types/product/_title.html create mode 100644 market/templates/_types/product/admin/_nav.html create mode 100644 market/templates/_types/product/admin/_oob_elements.html create mode 100644 market/templates/_types/product/admin/header/_header.html create mode 100644 market/templates/_types/product/admin/index.html create mode 100644 market/templates/_types/product/header/_header.html create mode 100644 market/templates/_types/product/index.html create mode 100644 market/templates/_types/product/prices.html create mode 100644 market/templates/aside_clear.html create mode 100644 market/templates/filter_clear.html create mode 100644 market/templates/fragments/container_nav_markets.html create mode 100644 market/templates/macros/filters.html create mode 100644 schema.sql create mode 100644 shared/.gitignore create mode 100644 shared/README.md create mode 100644 shared/__init__.py create mode 100644 shared/alembic.ini create mode 100644 shared/alembic/env.py create mode 100644 shared/alembic/script.py.mako create mode 100644 shared/alembic/versions/0001_initial_schem.py create mode 100644 shared/alembic/versions/0002_add_cart_items.py create mode 100644 shared/alembic/versions/0003_add_orders.py create mode 100644 shared/alembic/versions/0004_add_sumup_reference.py create mode 100644 shared/alembic/versions/0005_add_description.py create mode 100644 shared/alembic/versions/0006_update_calendar_entries.py create mode 100644 shared/alembic/versions/0007_add_oid_entries.py create mode 100644 shared/alembic/versions/0008_add_flexible_to_slots.py create mode 100644 shared/alembic/versions/0009_add_slot_id_to_entries.py create mode 100644 shared/alembic/versions/0010_add_post_likes.py create mode 100644 shared/alembic/versions/0011_add_entry_tickets.py create mode 100644 shared/alembic/versions/47fc53fc0d2b_add_ticket_types_table.py create mode 100644 shared/alembic/versions/6cb124491c9d_entry_posts.py create mode 100644 shared/alembic/versions/a1b2c3d4e5f6_add_page_configs_table.py create mode 100644 shared/alembic/versions/a9f54e4eaf02_add_menu_items_table.py create mode 100644 shared/alembic/versions/b2c3d4e5f6a7_add_market_places_table.py create mode 100644 shared/alembic/versions/c3a1f7b9d4e5_add_snippets_table.py create mode 100644 shared/alembic/versions/c3d4e5f6a7b8_add_page_tracking_to_orders.py create mode 100644 shared/alembic/versions/d4b2e8f1a3c7_add_post_user_id_and_author_email.py create mode 100644 shared/alembic/versions/e5c3f9a2b1d6_add_tag_groups_and_tag_group_tags.py create mode 100644 shared/alembic/versions/f6d4a0b2c3e7_add_domain_events_table.py create mode 100644 shared/alembic/versions/f6d4a1b2c3e7_add_tickets_table.py create mode 100644 shared/alembic/versions/g7e5b1c3d4f8_generic_containers.py create mode 100644 shared/alembic/versions/h8f6c2d4e5a9_merge_heads.py create mode 100644 shared/alembic/versions/i9g7d3e5f6_add_glue_layer_tables.py create mode 100644 shared/alembic/versions/j0h8e4f6g7_drop_cross_domain_fks.py create mode 100644 shared/alembic/versions/k1i9f5g7h8_add_federation_tables.py create mode 100644 shared/alembic/versions/l2j0g6h8i9_add_fediverse_tables.py create mode 100644 shared/alembic/versions/m3k1h7i9j0_add_activity_bus_columns.py create mode 100644 shared/alembic/versions/n4l2i8j0k1_drop_domain_events_table.py create mode 100644 shared/alembic/versions/o5m3j9k1l2_add_origin_app_column.py create mode 100644 shared/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py create mode 100644 shared/alembic/versions/q7o5l1m3n4_add_oauth_grants_table.py create mode 100644 shared/alembic/versions/r8p6m2n4o5_add_device_id_to_oauth_grants.py create mode 100644 shared/alembic/versions/s9q7n3o5p6_add_ap_delivery_log_table.py create mode 100644 shared/alembic/versions/t0r8n4o6p7_add_app_domain_to_ap_followers.py create mode 100644 shared/alembic/versions/u1s9o5p7q8_add_app_domain_to_delivery_log.py create mode 100644 shared/browser/__init__.py create mode 100644 shared/browser/app/__init__.py create mode 100644 shared/browser/app/authz.py create mode 100644 shared/browser/app/csrf.py create mode 100644 shared/browser/app/errors.py create mode 100644 shared/browser/app/filters/__init__.py create mode 100644 shared/browser/app/filters/combine.py create mode 100644 shared/browser/app/filters/currency.py create mode 100644 shared/browser/app/filters/getattr.py create mode 100644 shared/browser/app/filters/highlight.py create mode 100644 shared/browser/app/filters/qs.py create mode 100644 shared/browser/app/filters/qs_base.py create mode 100644 shared/browser/app/filters/query_types.py create mode 100644 shared/browser/app/filters/truncate.py create mode 100644 shared/browser/app/filters/url_join.py create mode 100644 shared/browser/app/middleware.py create mode 100644 shared/browser/app/payments/__init__.py create mode 100644 shared/browser/app/payments/sumup.py create mode 100644 shared/browser/app/redis_cacher.py create mode 100644 shared/browser/app/utils/__init__.py create mode 100644 shared/browser/app/utils/htmx.py create mode 100644 shared/browser/app/utils/parse.py create mode 100644 shared/browser/app/utils/utc.py create mode 100644 shared/browser/app/utils/utils.py create mode 100644 shared/browser/templates/_oob_elements.html create mode 100644 shared/browser/templates/_types/root/_full_user.html create mode 100644 shared/browser/templates/_types/root/_hamburger.html create mode 100644 shared/browser/templates/_types/root/_head.html create mode 100644 shared/browser/templates/_types/root/_index.html create mode 100644 shared/browser/templates/_types/root/_n/macros.html create mode 100644 shared/browser/templates/_types/root/_nav.html create mode 100644 shared/browser/templates/_types/root/_nav_panel.html create mode 100644 shared/browser/templates/_types/root/_oob_menu.html create mode 100644 shared/browser/templates/_types/root/_sign_in.html create mode 100644 shared/browser/templates/_types/root/exceptions/403/img.html create mode 100644 shared/browser/templates/_types/root/exceptions/403/message.html create mode 100644 shared/browser/templates/_types/root/exceptions/404/img.html create mode 100644 shared/browser/templates/_types/root/exceptions/404/message.html create mode 100644 shared/browser/templates/_types/root/exceptions/_.html create mode 100644 shared/browser/templates/_types/root/exceptions/app_error.html create mode 100644 shared/browser/templates/_types/root/exceptions/base.html create mode 100644 shared/browser/templates/_types/root/exceptions/error.html create mode 100644 shared/browser/templates/_types/root/exceptions/hx/_.html create mode 100644 shared/browser/templates/_types/root/header/_header.html create mode 100644 shared/browser/templates/_types/root/header/_oob.html create mode 100644 shared/browser/templates/_types/root/header/_oob_.html create mode 100644 shared/browser/templates/_types/root/index.html create mode 100644 shared/browser/templates/_types/root/mobile/_full_user.html create mode 100644 shared/browser/templates/_types/root/mobile/_sign_in.html create mode 100644 shared/browser/templates/macros/admin_nav.html create mode 100644 shared/browser/templates/macros/cart_icon.html create mode 100644 shared/browser/templates/macros/glyphs.html create mode 100644 shared/browser/templates/macros/layout.html create mode 100644 shared/browser/templates/macros/links.html create mode 100644 shared/browser/templates/macros/scrolling_menu.html create mode 100644 shared/browser/templates/macros/search.html create mode 100644 shared/browser/templates/macros/stickers.html create mode 100644 shared/browser/templates/macros/title.html create mode 100644 shared/browser/templates/mobile/menu.html create mode 100644 shared/browser/templates/oob_elements.html create mode 100644 shared/browser/templates/sentinel/desktop_content.html create mode 100644 shared/browser/templates/sentinel/mobile_content.html create mode 100644 shared/browser/templates/sentinel/wireless_error.svg create mode 100644 shared/browser/templates/social/meta_base.html create mode 100644 shared/browser/templates/social/meta_site.html create mode 100644 shared/config.py create mode 100644 shared/containers.py create mode 100644 shared/contracts/__init__.py create mode 100644 shared/contracts/dtos.py create mode 100644 shared/contracts/protocols.py create mode 100644 shared/contracts/widgets.py create mode 100644 shared/db/__init__.py create mode 100644 shared/db/base.py create mode 100644 shared/db/session.py create mode 100644 shared/editor/build.mjs create mode 100644 shared/editor/package-lock.json create mode 100644 shared/editor/package.json create mode 100644 shared/editor/src/Editor.jsx create mode 100644 shared/editor/src/index.jsx create mode 100644 shared/editor/src/useFileUpload.js create mode 100644 shared/events/__init__.py create mode 100644 shared/events/bus.py create mode 100644 shared/events/handlers/__init__.py create mode 100644 shared/events/handlers/ap_delivery_handler.py create mode 100644 shared/events/handlers/container_handlers.py create mode 100644 shared/events/handlers/external_delivery_handler.py create mode 100644 shared/events/handlers/login_handlers.py create mode 100644 shared/events/handlers/order_handlers.py create mode 100644 shared/events/processor.py create mode 100644 shared/infrastructure/__init__.py create mode 100644 shared/infrastructure/activitypub.py create mode 100644 shared/infrastructure/ap_inbox_handlers.py create mode 100644 shared/infrastructure/cart_identity.py create mode 100644 shared/infrastructure/context.py create mode 100644 shared/infrastructure/factory.py create mode 100644 shared/infrastructure/fragments.py create mode 100644 shared/infrastructure/http_utils.py create mode 100644 shared/infrastructure/jinja_setup.py create mode 100644 shared/infrastructure/oauth.py create mode 100644 shared/infrastructure/urls.py create mode 100644 shared/infrastructure/user_loader.py create mode 100644 shared/log_config/__init__.py create mode 100644 shared/log_config/setup.py create mode 100644 shared/models/__init__.py create mode 100644 shared/models/calendars.py create mode 100644 shared/models/container_relation.py create mode 100644 shared/models/federation.py create mode 100644 shared/models/ghost_content.py create mode 100644 shared/models/ghost_membership_entities.py create mode 100644 shared/models/kv.py create mode 100644 shared/models/magic_link.py create mode 100644 shared/models/market.py create mode 100644 shared/models/market_place.py create mode 100644 shared/models/menu_item.py create mode 100644 shared/models/menu_node.py create mode 100644 shared/models/oauth_code.py create mode 100644 shared/models/oauth_grant.py create mode 100644 shared/models/order.py create mode 100644 shared/models/page_config.py create mode 100644 shared/models/user.py create mode 100644 shared/requirements.txt create mode 100644 shared/services/__init__.py create mode 100644 shared/services/blog_impl.py create mode 100644 shared/services/calendar_impl.py create mode 100644 shared/services/cart_impl.py create mode 100644 shared/services/federation_impl.py create mode 100644 shared/services/federation_publish.py create mode 100644 shared/services/market_impl.py create mode 100644 shared/services/navigation.py create mode 100644 shared/services/registry.py create mode 100644 shared/services/relationships.py create mode 100644 shared/services/stubs.py create mode 100644 shared/services/widget_registry.py create mode 100644 shared/services/widgets/__init__.py create mode 100644 shared/services/widgets/calendar_widgets.py create mode 100644 shared/services/widgets/cart_widgets.py create mode 100644 shared/services/widgets/market_widgets.py create mode 100644 shared/static/errors/403.gif create mode 100644 shared/static/errors/404.gif create mode 100644 shared/static/errors/error.gif create mode 100644 shared/static/favicon.ico create mode 100644 shared/static/fontawesome/css/all.min.css create mode 100644 shared/static/fontawesome/css/v4-shims.min.css create mode 100644 shared/static/fontawesome/webfonts/fa-brands-400.ttf create mode 100644 shared/static/fontawesome/webfonts/fa-brands-400.woff2 create mode 100644 shared/static/fontawesome/webfonts/fa-regular-400.ttf create mode 100644 shared/static/fontawesome/webfonts/fa-regular-400.woff2 create mode 100644 shared/static/fontawesome/webfonts/fa-solid-900.ttf create mode 100644 shared/static/fontawesome/webfonts/fa-solid-900.woff2 create mode 100644 shared/static/fontawesome/webfonts/fa-v4compatibility.ttf create mode 100644 shared/static/fontawesome/webfonts/fa-v4compatibility.woff2 create mode 100644 shared/static/img/filter.svg create mode 100644 shared/static/img/logo.jpg create mode 100644 shared/static/img/search.svg create mode 100644 shared/static/labels/_blank.svg create mode 100644 shared/static/labels/new.svg create mode 100644 shared/static/labels/offer.svg create mode 100644 shared/static/nav-labels/new.svg create mode 100644 shared/static/nav-labels/offer.svg create mode 100644 shared/static/order/a-z.svg create mode 100644 shared/static/order/h-l.svg create mode 100644 shared/static/order/l-h.svg create mode 100644 shared/static/order/z-a.svg create mode 100644 shared/static/scripts/body.js create mode 100644 shared/static/stickers/biodynamic.svg create mode 100644 shared/static/stickers/fairtrade.svg create mode 100644 shared/static/stickers/glutenfree.svg create mode 100644 shared/static/stickers/organic.svg create mode 100644 shared/static/stickers/sugarfree.svg create mode 100644 shared/static/stickers/vegan.svg create mode 100644 shared/static/styles/basics.css create mode 100644 shared/static/styles/blog-content.css create mode 100644 shared/static/styles/cards.css create mode 100644 shared/utils/__init__.py create mode 100644 shared/utils/anchoring.py create mode 100644 shared/utils/calendar_helpers.py create mode 100644 shared/utils/http_signatures.py create mode 100644 shared/utils/ipfs_client.py create mode 100644 shared/utils/webfinger.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..397600b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.gitea +.env +_snapshot +docs +schema.sql +**/.gitmodules +**/.gitignore +**/README.md +**/__pycache__ +**/.pytest_cache +**/node_modules +**/*.pyc diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..852020c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,72 @@ +name: Build and Deploy + +on: + push: + branches: [main, decoupling] + +env: + REGISTRY: registry.rose-ash.com:5000 + COOP_DIR: /root/rose-ash + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install tools + run: | + apt-get update && apt-get install -y --no-install-recommends openssh-client + + - name: Set up SSH + env: + SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true + + - name: Build and deploy changed apps + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + run: | + ssh "root@$DEPLOY_HOST" " + cd ${{ env.COOP_DIR }} + git fetch origin ${{ github.ref_name }} + + # Detect what changed since current HEAD + CHANGED=\$(git diff --name-only HEAD origin/${{ github.ref_name }}) + git reset --hard origin/${{ github.ref_name }} + + REBUILD_ALL=false + if echo \"\$CHANGED\" | grep -q '^shared/'; then + REBUILD_ALL=true + fi + if echo \"\$CHANGED\" | grep -q '^docker-compose.yml'; then + REBUILD_ALL=true + fi + + for app in blog market cart events federation account; do + if [ \"\$REBUILD_ALL\" = true ] || echo \"\$CHANGED\" | grep -q \"^\$app/\"; then + echo \"Building \$app...\" + docker build \ + --build-arg CACHEBUST=\$(date +%s) \ + -f \$app/Dockerfile \ + -t ${{ env.REGISTRY }}/\$app:latest \ + -t ${{ env.REGISTRY }}/\$app:${{ github.sha }} \ + . + docker push ${{ env.REGISTRY }}/\$app:latest + docker push ${{ env.REGISTRY }}/\$app:${{ github.sha }} + else + echo \"Skipping \$app (no changes)\" + fi + done + + source .env + docker stack deploy -c docker-compose.yml coop + echo 'Waiting for services to update...' + sleep 10 + docker stack services coop + " diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e2e7ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.env +node_modules/ +*.egg-info/ +dist/ +build/ +.venv/ +venv/ +_snapshot/ +_debug/ diff --git a/_config/app-config.yaml b/_config/app-config.yaml new file mode 100644 index 0000000..dabb4c3 --- /dev/null +++ b/_config/app-config.yaml @@ -0,0 +1,83 @@ +root: "/rose-ash-wholefood-coop" # no trailing slash needed (we normalize it) +host: "https://rose-ash.com" +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: ROSE-ASH 2.0 +market_root: /market +market_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + blog: "https://blog.rose-ash.com" + market: "https://market.rose-ash.com" + cart: "https://cart.rose-ash.com" + events: "https://events.rose-ash.com" + federation: "https://federation.rose-ash.com" + account: "https://account.rose-ash.com" +cache: + fs_root: /app/_snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/ciders + - branded-goods/wines + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + product-details: + - General Information + - A Note About Prices +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "jfwlekjfwef798ewf769ew8f679ew8f7weflwef" + + diff --git a/account/Dockerfile b/account/Dockerfile new file mode 100644 index 0000000..6131e2d --- /dev/null +++ b/account/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# ---------- Python application ---------- +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + PIP_NO_CACHE_DIR=1 \ + APP_PORT=8000 \ + APP_MODULE=app:app + +WORKDIR /app + +# Install system deps + psql client +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +COPY shared/requirements.txt ./requirements.txt +RUN pip install -r requirements.txt + +# Shared code (replaces submodule) +COPY shared/ ./shared/ + +# App code +COPY account/ ./ + +# Sibling models for cross-domain SQLAlchemy imports +COPY blog/__init__.py ./blog/__init__.py +COPY blog/models/ ./blog/models/ +COPY market/__init__.py ./market/__init__.py +COPY market/models/ ./market/models/ +COPY cart/__init__.py ./cart/__init__.py +COPY cart/models/ ./cart/models/ +COPY events/__init__.py ./events/__init__.py +COPY events/models/ ./events/models/ +COPY federation/__init__.py ./federation/__init__.py +COPY federation/models/ ./federation/models/ + +# ---------- Runtime setup ---------- +COPY account/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +RUN useradd -m -u 10001 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE ${APP_PORT} +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/account/__init__.py b/account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/account/app.py b/account/app.py new file mode 100644 index 0000000..c23ff3d --- /dev/null +++ b/account/app.py @@ -0,0 +1,65 @@ +from __future__ import annotations +import path_setup # noqa: F401 # adds shared/ to sys.path +from pathlib import Path + +from quart import g, request +from jinja2 import FileSystemLoader, ChoiceLoader + +from shared.infrastructure.factory import create_base_app +from shared.services.registry import services + +from bp import register_account_bp, register_auth_bp, register_fragments + + +async def account_context() -> dict: + """Account app context processor.""" + from shared.infrastructure.context import base_context + from shared.services.navigation import get_navigation_tree + from shared.infrastructure.cart_identity import current_cart_identity + from shared.infrastructure.fragments import fetch_fragment + + ctx = await base_context() + + ctx["nav_tree_html"] = await fetch_fragment( + "blog", "nav-tree", + params={"app_name": "account", "path": request.path}, + ) + # Fallback for _nav.html when nav-tree fragment fetch fails + ctx["menu_items"] = await get_navigation_tree(g.s) + + # Cart data (consistent with all other apps) + ident = current_cart_identity() + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count + ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total) + + return ctx + + +def create_app() -> "Quart": + from services import register_domain_services + + app = create_base_app( + "account", + context_fn=account_context, + domain_services_fn=register_domain_services, + ) + + # App-specific templates override shared templates + app_templates = str(Path(__file__).resolve().parent / "templates") + app.jinja_loader = ChoiceLoader([ + FileSystemLoader(app_templates), + app.jinja_loader, + ]) + + # --- blueprints --- + app.register_blueprint(register_auth_bp()) + app.register_blueprint(register_account_bp()) + app.register_blueprint(register_fragments()) + + return app + + +app = create_app() diff --git a/account/bp/__init__.py b/account/bp/__init__.py new file mode 100644 index 0000000..fe22f4e --- /dev/null +++ b/account/bp/__init__.py @@ -0,0 +1,3 @@ +from .account.routes import register as register_account_bp +from .auth.routes import register as register_auth_bp +from .fragments import register_fragments diff --git a/account/bp/account/__init__.py b/account/bp/account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/account/bp/account/routes.py b/account/bp/account/routes.py new file mode 100644 index 0000000..23b3cae --- /dev/null +++ b/account/bp/account/routes.py @@ -0,0 +1,168 @@ +"""Account pages blueprint. + +Moved from federation/bp/auth — newsletters, fragment pages (tickets, bookings). +Mounted at root /. +""" +from __future__ import annotations + +from quart import ( + Blueprint, + request, + render_template, + make_response, + redirect, + g, +) +from sqlalchemy import select + +from shared.models import UserNewsletter +from shared.models.ghost_membership_entities import GhostNewsletter +from shared.infrastructure.urls import login_url +from shared.infrastructure.fragments import fetch_fragment, fetch_fragments + +oob = { + "oob_extends": "oob_elements.html", + "extends": "_types/root/_index.html", + "parent_id": "root-header-child", + "child_id": "auth-header-child", + "header": "_types/auth/header/_header.html", + "parent_header": "_types/root/header/_header.html", + "nav": "_types/auth/_nav.html", + "main": "_types/auth/_main_panel.html", +} + + +def register(url_prefix="/"): + account_bp = Blueprint("account", __name__, url_prefix=url_prefix) + + @account_bp.context_processor + async def context(): + events_nav, cart_nav = await fetch_fragments([ + ("events", "account-nav-item", {}), + ("cart", "account-nav-item", {}), + ]) + return {"oob": oob, "account_nav_html": events_nav + cart_nav} + + @account_bp.get("/") + async def account(): + from shared.browser.app.utils.htmx import is_htmx_request + + if not g.get("user"): + return redirect(login_url("/")) + + if not is_htmx_request(): + html = await render_template("_types/auth/index.html") + else: + html = await render_template("_types/auth/_oob_elements.html") + + return await make_response(html) + + @account_bp.get("/newsletters/") + async def newsletters(): + from shared.browser.app.utils.htmx import is_htmx_request + + if not g.get("user"): + return redirect(login_url("/newsletters/")) + + result = await g.s.execute( + select(GhostNewsletter).order_by(GhostNewsletter.name) + ) + all_newsletters = result.scalars().all() + + sub_result = await g.s.execute( + select(UserNewsletter).where( + UserNewsletter.user_id == g.user.id, + ) + ) + user_subs = {un.newsletter_id: un for un in sub_result.scalars().all()} + + newsletter_list = [] + for nl in all_newsletters: + un = user_subs.get(nl.id) + newsletter_list.append({ + "newsletter": nl, + "un": un, + "subscribed": un.subscribed if un else False, + }) + + nl_oob = {**oob, "main": "_types/auth/_newsletters_panel.html"} + + if not is_htmx_request(): + html = await render_template( + "_types/auth/index.html", + oob=nl_oob, + newsletter_list=newsletter_list, + ) + else: + html = await render_template( + "_types/auth/_oob_elements.html", + oob=nl_oob, + newsletter_list=newsletter_list, + ) + + return await make_response(html) + + @account_bp.post("/newsletter//toggle/") + async def toggle_newsletter(newsletter_id: int): + if not g.get("user"): + return "", 401 + + result = await g.s.execute( + select(UserNewsletter).where( + UserNewsletter.user_id == g.user.id, + UserNewsletter.newsletter_id == newsletter_id, + ) + ) + un = result.scalar_one_or_none() + + if un: + un.subscribed = not un.subscribed + else: + un = UserNewsletter( + user_id=g.user.id, + newsletter_id=newsletter_id, + subscribed=True, + ) + g.s.add(un) + + await g.s.flush() + + return await render_template( + "_types/auth/_newsletter_toggle.html", + un=un, + ) + + # Catch-all for fragment-provided pages — must be last + @account_bp.get("//") + async def fragment_page(slug): + from shared.browser.app.utils.htmx import is_htmx_request + from quart import abort + + if not g.get("user"): + return redirect(login_url(f"/{slug}/")) + + fragment_html = await fetch_fragment( + "events", "account-page", + params={"slug": slug, "user_id": str(g.user.id)}, + ) + if not fragment_html: + abort(404) + + w_oob = {**oob, "main": "_types/auth/_fragment_panel.html"} + + if not is_htmx_request(): + html = await render_template( + "_types/auth/index.html", + oob=w_oob, + page_fragment_html=fragment_html, + ) + else: + html = await render_template( + "_types/auth/_oob_elements.html", + oob=w_oob, + page_fragment_html=fragment_html, + ) + + return await make_response(html) + + return account_bp diff --git a/account/bp/auth/__init__.py b/account/bp/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/account/bp/auth/routes.py b/account/bp/auth/routes.py new file mode 100644 index 0000000..5d1f334 --- /dev/null +++ b/account/bp/auth/routes.py @@ -0,0 +1,486 @@ +"""Authentication routes for the account app. + +Account is the OAuth authorization server. Owns magic link login/logout, +OAuth2 authorize endpoint, grant verification, and SSO logout. +""" +from __future__ import annotations + +import secrets +from datetime import datetime, timezone, timedelta + +from quart import ( + Blueprint, + request, + render_template, + redirect, + url_for, + session as qsession, + g, + current_app, + jsonify, +) +from sqlalchemy import select, update +from sqlalchemy.exc import SQLAlchemyError + +from shared.db.session import get_session +from shared.models import User +from shared.models.oauth_code import OAuthCode +from shared.models.oauth_grant import OAuthGrant +from shared.infrastructure.urls import account_url, app_url +from shared.infrastructure.cart_identity import current_cart_identity +from shared.events import emit_activity + +from .services import ( + pop_login_redirect_target, + store_login_redirect_target, + send_magic_email, + find_or_create_user, + create_magic_link, + validate_magic_link, + validate_email, +) + +SESSION_USER_KEY = "uid" +ACCOUNT_SESSION_KEY = "account_sid" + +ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "federation", "artdag"} + + +def register(url_prefix="/auth"): + auth_bp = Blueprint("auth", __name__, url_prefix=url_prefix) + + # --- OAuth2 authorize endpoint ------------------------------------------- + + @auth_bp.get("/oauth/authorize") + @auth_bp.get("/oauth/authorize/") + async def oauth_authorize(): + client_id = request.args.get("client_id", "") + redirect_uri = request.args.get("redirect_uri", "") + state = request.args.get("state", "") + device_id = request.args.get("device_id", "") + prompt = request.args.get("prompt", "") + + if client_id not in ALLOWED_CLIENTS: + return "Invalid client_id", 400 + + expected_redirect = app_url(client_id, "/auth/callback") + if redirect_uri != expected_redirect: + return "Invalid redirect_uri", 400 + + # Account's own device id — always available via factory hook + account_did = g.device_id + + # Not logged in + if not g.get("user"): + if prompt == "none": + # Silent check — pass account_did so client can watch for future logins + sep = "&" if "?" in redirect_uri else "?" + return redirect( + f"{redirect_uri}{sep}error=login_required" + f"&state={state}&account_did={account_did}" + ) + authorize_path = request.full_path + store_login_redirect_target() + return redirect(url_for("auth.login_form", next=authorize_path)) + + # Logged in — create grant + authorization code + account_sid = qsession.get(ACCOUNT_SESSION_KEY) + if not account_sid: + account_sid = secrets.token_urlsafe(32) + qsession[ACCOUNT_SESSION_KEY] = account_sid + + grant_token = secrets.token_urlsafe(48) + code = secrets.token_urlsafe(48) + now = datetime.now(timezone.utc) + expires = now + timedelta(minutes=5) + + async with get_session() as s: + async with s.begin(): + grant = OAuthGrant( + token=grant_token, + user_id=g.user.id, + client_id=client_id, + issuer_session=account_sid, + device_id=device_id or None, + ) + s.add(grant) + + oauth_code = OAuthCode( + code=code, + user_id=g.user.id, + client_id=client_id, + redirect_uri=redirect_uri, + expires_at=expires, + grant_token=grant_token, + ) + s.add(oauth_code) + + sep = "&" if "?" in redirect_uri else "?" + return redirect( + f"{redirect_uri}{sep}code={code}&state={state}" + f"&account_did={account_did}" + ) + + # --- OAuth2 token exchange (for external clients like artdag) ------------- + + from shared.browser.app.csrf import csrf_exempt + + @csrf_exempt + @auth_bp.post("/oauth/token") + @auth_bp.post("/oauth/token/") + async def oauth_token(): + """Exchange an authorization code for user info + grant token. + + Used by clients that don't share the coop database (e.g. artdag). + Accepts JSON: {code, client_id, redirect_uri} + Returns JSON: {user_id, username, display_name, grant_token} + """ + data = await request.get_json() + if not data: + return jsonify({"error": "invalid_request"}), 400 + + code = data.get("code", "") + client_id = data.get("client_id", "") + redirect_uri = data.get("redirect_uri", "") + + if client_id not in ALLOWED_CLIENTS: + return jsonify({"error": "invalid_client"}), 400 + + now = datetime.now(timezone.utc) + + async with get_session() as s: + async with s.begin(): + result = await s.execute( + select(OAuthCode) + .where(OAuthCode.code == code) + .with_for_update() + ) + oauth_code = result.scalar_one_or_none() + + if not oauth_code: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.used_at is not None: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.expires_at < now: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.client_id != client_id: + return jsonify({"error": "invalid_grant"}), 400 + + if oauth_code.redirect_uri != redirect_uri: + return jsonify({"error": "invalid_grant"}), 400 + + oauth_code.used_at = now + user_id = oauth_code.user_id + grant_token = oauth_code.grant_token + + user = await s.get(User, user_id) + if not user: + return jsonify({"error": "invalid_grant"}), 400 + + return jsonify({ + "user_id": user_id, + "username": user.email or "", + "display_name": user.name or "", + "grant_token": grant_token, + }) + + # --- Grant verification (internal endpoint) ------------------------------ + + @auth_bp.get("/internal/verify-grant") + async def verify_grant(): + """Called by client apps to check if a grant is still valid.""" + token = request.args.get("token", "") + if not token: + return jsonify({"valid": False}), 200 + + async with get_session() as s: + grant = await s.scalar( + select(OAuthGrant).where(OAuthGrant.token == token) + ) + if not grant or grant.revoked_at is not None: + return jsonify({"valid": False}), 200 + return jsonify({"valid": True}), 200 + + @auth_bp.get("/internal/check-device") + async def check_device(): + """Called by client apps to check if a device has an active auth. + + Looks up the most recent grant for (device_id, client_id). + If the grant is active → {active: true}. + If revoked but user has logged in since → {active: true} (re-auth needed). + Otherwise → {active: false}. + """ + device_id = request.args.get("device_id", "") + app_name = request.args.get("app", "") + if not device_id or not app_name: + return jsonify({"active": False}), 200 + + async with get_session() as s: + # Find the most recent grant for this device + app + result = await s.execute( + select(OAuthGrant) + .where(OAuthGrant.device_id == device_id) + .where(OAuthGrant.client_id == app_name) + .order_by(OAuthGrant.created_at.desc()) + .limit(1) + ) + grant = result.scalar_one_or_none() + + if not grant: + return jsonify({"active": False}), 200 + + # Grant still active + if grant.revoked_at is None: + return jsonify({"active": True}), 200 + + # Grant revoked — check if user logged in since + user = await s.get(User, grant.user_id) + if user and user.last_login_at and user.last_login_at > grant.revoked_at: + return jsonify({"active": True}), 200 + + return jsonify({"active": False}), 200 + + # --- Magic link login flow ----------------------------------------------- + + @auth_bp.get("/login/") + async def login_form(): + store_login_redirect_target() + cross_cart_sid = request.args.get("cart_sid") + if cross_cart_sid: + qsession["cart_sid"] = cross_cart_sid + if g.get("user"): + redirect_url = pop_login_redirect_target() + return redirect(redirect_url) + return await render_template("auth/login.html") + + @auth_bp.post("/start/") + async def start_login(): + form = await request.form + email_input = form.get("email") or "" + + is_valid, email = validate_email(email_input) + if not is_valid: + return ( + await render_template( + "auth/login.html", + error="Please enter a valid email address.", + email=email_input, + ), + 400, + ) + + user = await find_or_create_user(g.s, email) + token, expires = await create_magic_link(g.s, user.id) + + from shared.utils import host_url + magic_url = host_url(url_for("auth.magic", token=token)) + + email_error = None + try: + await send_magic_email(email, magic_url) + except Exception as e: + current_app.logger.error("EMAIL SEND FAILED: %r", e) + email_error = ( + "We couldn't send the email automatically. " + "Please try again in a moment." + ) + + return await render_template( + "auth/check_email.html", + email=email, + email_error=email_error, + ) + + @auth_bp.get("/magic//") + async def magic(token: str): + now = datetime.now(timezone.utc) + user_id: int | None = None + + try: + async with get_session() as s: + async with s.begin(): + user, error = await validate_magic_link(s, token) + + if error: + return ( + await render_template("auth/login.html", error=error), + 400, + ) + user_id = user.id + + except Exception: + return ( + await render_template( + "auth/login.html", + error="Could not sign you in right now. Please try again.", + ), + 502, + ) + + assert user_id is not None + + ident = current_cart_identity() + anon_session_id = ident.get("session_id") + + try: + async with get_session() as s: + async with s.begin(): + u2 = await s.get(User, user_id) + if u2: + u2.last_login_at = now + if anon_session_id: + await emit_activity( + s, + activity_type="rose:Login", + actor_uri="internal:system", + object_type="Person", + object_data={ + "user_id": user_id, + "session_id": anon_session_id, + }, + ) + # Notify external services of device login + await emit_activity( + s, + activity_type="rose:DeviceAuth", + actor_uri="internal:system", + object_type="Device", + object_data={ + "device_id": g.device_id, + "action": "login", + }, + ) + except SQLAlchemyError: + current_app.logger.exception( + "[auth] non-fatal DB update for user_id=%s", user_id + ) + + qsession[SESSION_USER_KEY] = user_id + # Fresh account session ID for grant tracking + qsession[ACCOUNT_SESSION_KEY] = secrets.token_urlsafe(32) + + # Signal login for this device so client apps can detect it + try: + from shared.browser.app.redis_cacher import get_redis + import time as _time + _redis = get_redis() + if _redis: + await _redis.set( + f"did_auth:{g.device_id}", + str(_time.time()).encode(), + ex=30 * 24 * 3600, + ) + except Exception: + current_app.logger.exception("[auth] failed to set did_auth in Redis") + + redirect_url = pop_login_redirect_target() + return redirect(redirect_url, 303) + + @auth_bp.post("/logout/") + async def logout(): + # Revoke all grants issued by this account session + account_sid = qsession.get(ACCOUNT_SESSION_KEY) + if account_sid: + try: + async with get_session() as s: + async with s.begin(): + await s.execute( + update(OAuthGrant) + .where(OAuthGrant.issuer_session == account_sid) + .where(OAuthGrant.revoked_at.is_(None)) + .values(revoked_at=datetime.now(timezone.utc)) + ) + except SQLAlchemyError: + current_app.logger.exception("[auth] failed to revoke grants") + + # Clear login signal for this device + try: + from shared.browser.app.redis_cacher import get_redis + _redis = get_redis() + if _redis: + await _redis.delete(f"did_auth:{g.device_id}") + except Exception: + pass + + # Notify external services of device logout + try: + async with get_session() as s: + async with s.begin(): + await emit_activity( + s, + activity_type="rose:DeviceAuth", + actor_uri="internal:system", + object_type="Device", + object_data={ + "device_id": g.device_id, + "action": "logout", + }, + ) + except Exception: + current_app.logger.exception("[auth] failed to emit DeviceAuth logout") + + qsession.pop(SESSION_USER_KEY, None) + qsession.pop(ACCOUNT_SESSION_KEY, None) + from shared.infrastructure.urls import blog_url + return redirect(blog_url("/")) + + @auth_bp.get("/sso-logout/") + async def sso_logout(): + """SSO logout called by client apps: revoke grants, clear session.""" + account_sid = qsession.get(ACCOUNT_SESSION_KEY) + if account_sid: + try: + async with get_session() as s: + async with s.begin(): + await s.execute( + update(OAuthGrant) + .where(OAuthGrant.issuer_session == account_sid) + .where(OAuthGrant.revoked_at.is_(None)) + .values(revoked_at=datetime.now(timezone.utc)) + ) + except SQLAlchemyError: + current_app.logger.exception("[auth] failed to revoke grants") + + # Clear login signal for this device + try: + from shared.browser.app.redis_cacher import get_redis + _redis = get_redis() + if _redis: + await _redis.delete(f"did_auth:{g.device_id}") + except Exception: + pass + + # Notify external services of device logout + try: + async with get_session() as s: + async with s.begin(): + await emit_activity( + s, + activity_type="rose:DeviceAuth", + actor_uri="internal:system", + object_type="Device", + object_data={ + "device_id": g.device_id, + "action": "logout", + }, + ) + except Exception: + current_app.logger.exception("[auth] failed to emit DeviceAuth logout") + + qsession.pop(SESSION_USER_KEY, None) + qsession.pop(ACCOUNT_SESSION_KEY, None) + from shared.infrastructure.urls import blog_url + return redirect(blog_url("/")) + + @auth_bp.get("/clear/") + async def clear(): + """One-time migration helper: clear all session cookies.""" + qsession.clear() + resp = redirect(account_url("/")) + resp.delete_cookie("blog_session", domain=".rose-ash.com", path="/") + return resp + + return auth_bp diff --git a/account/bp/auth/services/__init__.py b/account/bp/auth/services/__init__.py new file mode 100644 index 0000000..648f87d --- /dev/null +++ b/account/bp/auth/services/__init__.py @@ -0,0 +1,24 @@ +from .login_redirect import pop_login_redirect_target, store_login_redirect_target +from .auth_operations import ( + get_app_host, + get_app_root, + send_magic_email, + load_user_by_id, + find_or_create_user, + create_magic_link, + validate_magic_link, + validate_email, +) + +__all__ = [ + "pop_login_redirect_target", + "store_login_redirect_target", + "get_app_host", + "get_app_root", + "send_magic_email", + "load_user_by_id", + "find_or_create_user", + "create_magic_link", + "validate_magic_link", + "validate_email", +] diff --git a/account/bp/auth/services/auth_operations.py b/account/bp/auth/services/auth_operations.py new file mode 100644 index 0000000..f727c0d --- /dev/null +++ b/account/bp/auth/services/auth_operations.py @@ -0,0 +1,156 @@ +"""Auth operations for the account app. + +Owns magic-link login. Shared models, shared config. +""" +from __future__ import annotations + +import os +import secrets +from datetime import datetime, timedelta, timezone +from typing import Optional, Tuple + +from quart import current_app, render_template, request, g +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from shared.models import User, MagicLink +from shared.config import config + + +def get_app_host() -> str: + host = ( + config().get("host") or os.getenv("APP_HOST") or "http://localhost:8000" + ).rstrip("/") + return host + + +def get_app_root() -> str: + root = (g.root).rstrip("/") + return root + + +async def send_magic_email(to_email: str, link_url: str) -> None: + host = os.getenv("SMTP_HOST") + port = int(os.getenv("SMTP_PORT") or "587") + username = os.getenv("SMTP_USER") + password = os.getenv("SMTP_PASS") + mail_from = os.getenv("MAIL_FROM") or "no-reply@example.com" + + site_name = config().get("title", "Rose Ash") + subject = f"Your sign-in link \u2014 {site_name}" + + tpl_vars = dict(site_name=site_name, link_url=link_url) + text_body = await render_template("_email/magic_link.txt", **tpl_vars) + html_body = await render_template("_email/magic_link.html", **tpl_vars) + + if not host or not username or not password: + current_app.logger.warning( + "SMTP not configured. Printing magic link to console for %s: %s", + to_email, + link_url, + ) + print(f"[DEV] Magic link for {to_email}: {link_url}") + return + + import aiosmtplib + from email.message import EmailMessage + + msg = EmailMessage() + msg["From"] = mail_from + msg["To"] = to_email + msg["Subject"] = subject + msg.set_content(text_body) + msg.add_alternative(html_body, subtype="html") + + is_secure = port == 465 + if is_secure: + smtp = aiosmtplib.SMTP( + hostname=host, port=port, use_tls=True, + username=username, password=password, + ) + else: + smtp = aiosmtplib.SMTP( + hostname=host, port=port, start_tls=True, + username=username, password=password, + ) + + async with smtp: + await smtp.send_message(msg) + + +async def load_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]: + stmt = ( + select(User) + .options(selectinload(User.labels)) + .where(User.id == user_id) + ) + result = await session.execute(stmt) + return result.scalar_one_or_none() + + +async def find_or_create_user(session: AsyncSession, email: str) -> User: + result = await session.execute(select(User).where(User.email == email)) + user = result.scalar_one_or_none() + + if user is None: + user = User(email=email) + session.add(user) + await session.flush() + + return user + + +async def create_magic_link( + session: AsyncSession, + user_id: int, + purpose: str = "signin", + expires_minutes: int = 15, +) -> Tuple[str, datetime]: + token = secrets.token_urlsafe(32) + expires = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes) + + ml = MagicLink( + token=token, + user_id=user_id, + purpose=purpose, + expires_at=expires, + ip=request.headers.get("x-forwarded-for", request.remote_addr), + user_agent=request.headers.get("user-agent"), + ) + session.add(ml) + + return token, expires + + +async def validate_magic_link( + session: AsyncSession, + token: str, +) -> Tuple[Optional[User], Optional[str]]: + now = datetime.now(timezone.utc) + + ml = await session.scalar( + select(MagicLink) + .where(MagicLink.token == token) + .with_for_update() + ) + + if not ml or ml.purpose != "signin": + return None, "Invalid or expired link." + + if ml.used_at or ml.expires_at < now: + return None, "This link has expired. Please request a new one." + + user = await session.get(User, ml.user_id) + if not user: + return None, "User not found." + + ml.used_at = now + return user, None + + +def validate_email(email: str) -> Tuple[bool, str]: + email = email.strip().lower() + if not email or "@" not in email: + return False, email + return True, email diff --git a/account/bp/auth/services/login_redirect.py b/account/bp/auth/services/login_redirect.py new file mode 100644 index 0000000..8382516 --- /dev/null +++ b/account/bp/auth/services/login_redirect.py @@ -0,0 +1,45 @@ +from urllib.parse import urlparse +from quart import session + +from shared.infrastructure.urls import account_url + + +LOGIN_REDIRECT_SESSION_KEY = "login_redirect_to" + + +def store_login_redirect_target() -> None: + from quart import request + + target = request.args.get("next") + if not target: + ref = request.referrer or "" + try: + parsed = urlparse(ref) + target = parsed.path or "" + except Exception: + target = "" + + if not target: + return + + # Accept both relative paths and absolute URLs (cross-app redirects) + if target.startswith("http://") or target.startswith("https://"): + session[LOGIN_REDIRECT_SESSION_KEY] = target + elif target.startswith("/") and not target.startswith("//"): + session[LOGIN_REDIRECT_SESSION_KEY] = target + + +def pop_login_redirect_target() -> str: + path = session.pop(LOGIN_REDIRECT_SESSION_KEY, None) + if not path or not isinstance(path, str): + return account_url("/") + + # Absolute URL: return as-is (cross-app redirect) + if path.startswith("http://") or path.startswith("https://"): + return path + + # Relative path: must start with / and not // + if path.startswith("/") and not path.startswith("//"): + return account_url(path) + + return account_url("/") diff --git a/account/bp/fragments/__init__.py b/account/bp/fragments/__init__.py new file mode 100644 index 0000000..a4af44b --- /dev/null +++ b/account/bp/fragments/__init__.py @@ -0,0 +1 @@ +from .routes import register as register_fragments diff --git a/account/bp/fragments/routes.py b/account/bp/fragments/routes.py new file mode 100644 index 0000000..b21a601 --- /dev/null +++ b/account/bp/fragments/routes.py @@ -0,0 +1,52 @@ +"""Account app fragment endpoints. + +Exposes HTML fragments at ``/internal/fragments/`` for consumption +by other coop apps via the fragment client. + +Fragments: + auth-menu Desktop + mobile auth menu (sign-in or user link) +""" + +from __future__ import annotations + +from quart import Blueprint, Response, request, render_template + +from shared.infrastructure.fragments import FRAGMENT_HEADER + + +def register(): + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + # --------------------------------------------------------------- + # Fragment handlers + # --------------------------------------------------------------- + + async def _auth_menu(): + user_email = request.args.get("email", "") + return await render_template( + "fragments/auth_menu.html", + user_email=user_email, + ) + + _handlers = { + "auth-menu": _auth_menu, + } + + # --------------------------------------------------------------- + # Routing + # --------------------------------------------------------------- + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.get("/") + async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/html") + html = await handler() + return Response(html, status=200, content_type="text/html") + + return bp diff --git a/account/entrypoint.sh b/account/entrypoint.sh new file mode 100644 index 0000000..52b4f51 --- /dev/null +++ b/account/entrypoint.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional: wait for Postgres to be reachable +if [[ -n "${DATABASE_HOST:-}" && -n "${DATABASE_PORT:-}" ]]; then + echo "Waiting for Postgres at ${DATABASE_HOST}:${DATABASE_PORT}..." + for i in {1..60}; do + (echo > /dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}) >/dev/null 2>&1 && break || true + sleep 1 + done +fi + +# Clear Redis page cache on deploy +if [[ -n "${REDIS_URL:-}" && "${REDIS_URL}" != "no" ]]; then + echo "Flushing Redis cache..." + python3 -c " +import redis, os +r = redis.from_url(os.environ['REDIS_URL']) +r.flushall() +print('Redis cache cleared.') +" || echo "Redis flush failed (non-fatal), continuing..." +fi + +# Start the app +echo "Starting Hypercorn (${APP_MODULE:-app:app})..." +PYTHONUNBUFFERED=1 exec hypercorn "${APP_MODULE:-app:app}" --bind 0.0.0.0:${PORT:-8000} diff --git a/account/models/__init__.py b/account/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/account/path_setup.py b/account/path_setup.py new file mode 100644 index 0000000..c7166f7 --- /dev/null +++ b/account/path_setup.py @@ -0,0 +1,9 @@ +import sys +import os + +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p) diff --git a/account/services/__init__.py b/account/services/__init__.py new file mode 100644 index 0000000..299f0ad --- /dev/null +++ b/account/services/__init__.py @@ -0,0 +1,27 @@ +"""Account app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the account app. + + Account needs all domain services since widgets (tickets, bookings) + pull data from blog, calendar, market, cart, and federation. + """ + from shared.services.registry import services + from shared.services.federation_impl import SqlFederationService + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + if not services.has("federation"): + services.federation = SqlFederationService() + if not services.has("blog"): + services.blog = SqlBlogService() + if not services.has("calendar"): + services.calendar = SqlCalendarService() + if not services.has("market"): + services.market = SqlMarketService() + if not services.has("cart"): + services.cart = SqlCartService() diff --git a/account/templates/_email/magic_link.html b/account/templates/_email/magic_link.html new file mode 100644 index 0000000..3c1eac6 --- /dev/null +++ b/account/templates/_email/magic_link.html @@ -0,0 +1,33 @@ + + + + + + +
+ + +
+

{{ site_name }}

+

Sign in to your account

+

+ Click the button below to sign in. This link will expire in 15 minutes. +

+
+ + Sign in + +
+

Or copy and paste this link into your browser:

+

+ {{ link_url }} +

+
+

+ If you did not request this email, you can safely ignore it. +

+
+
+ + diff --git a/account/templates/_email/magic_link.txt b/account/templates/_email/magic_link.txt new file mode 100644 index 0000000..28a2efb --- /dev/null +++ b/account/templates/_email/magic_link.txt @@ -0,0 +1,8 @@ +Hello, + +Click this link to sign in: +{{ link_url }} + +This link will expire in 15 minutes. + +If you did not request this, you can ignore this email. diff --git a/account/templates/_types/auth/_bookings_panel.html b/account/templates/_types/auth/_bookings_panel.html new file mode 100644 index 0000000..28f8280 --- /dev/null +++ b/account/templates/_types/auth/_bookings_panel.html @@ -0,0 +1,44 @@ +
+
+ +

Bookings

+ + {% if bookings %} +
+ {% for booking in bookings %} +
+
+
+

{{ booking.name }}

+
+ {{ booking.start_at.strftime('%d %b %Y, %H:%M') }} + {% if booking.end_at %} + – {{ booking.end_at.strftime('%H:%M') }} + {% endif %} + {% if booking.calendar_name %} + · {{ booking.calendar_name }} + {% endif %} + {% if booking.cost %} + · £{{ booking.cost }} + {% endif %} +
+
+
+ {% if booking.state == 'confirmed' %} + confirmed + {% elif booking.state == 'provisional' %} + provisional + {% else %} + {{ booking.state }} + {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} +

No bookings yet.

+ {% endif %} + +
+
diff --git a/account/templates/_types/auth/_fragment_panel.html b/account/templates/_types/auth/_fragment_panel.html new file mode 100644 index 0000000..f27345c --- /dev/null +++ b/account/templates/_types/auth/_fragment_panel.html @@ -0,0 +1 @@ +{{ page_fragment_html | safe }} diff --git a/account/templates/_types/auth/_main_panel.html b/account/templates/_types/auth/_main_panel.html new file mode 100644 index 0000000..e80fd12 --- /dev/null +++ b/account/templates/_types/auth/_main_panel.html @@ -0,0 +1,49 @@ +
+
+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {# Account header #} +
+
+

Account

+ {% if g.user %} +

{{ g.user.email }}

+ {% if g.user.name %} +

{{ g.user.name }}

+ {% endif %} + {% endif %} +
+
+ + +
+
+ + {# Labels #} + {% set labels = g.user.labels if g.user is defined and g.user.labels is defined else [] %} + {% if labels %} +
+

Labels

+
+ {% for label in labels %} + + {{ label.name }} + + {% endfor %} +
+
+ {% endif %} + +
+
diff --git a/account/templates/_types/auth/_nav.html b/account/templates/_types/auth/_nav.html new file mode 100644 index 0000000..ff5de92 --- /dev/null +++ b/account/templates/_types/auth/_nav.html @@ -0,0 +1,7 @@ +{% import 'macros/links.html' as links %} +{% call links.link(account_url('/newsletters/'), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + newsletters +{% endcall %} +{% if account_nav_html %} + {{ account_nav_html | safe }} +{% endif %} diff --git a/account/templates/_types/auth/_newsletter_toggle.html b/account/templates/_types/auth/_newsletter_toggle.html new file mode 100644 index 0000000..8bb3f69 --- /dev/null +++ b/account/templates/_types/auth/_newsletter_toggle.html @@ -0,0 +1,17 @@ +
+ +
diff --git a/account/templates/_types/auth/_newsletters_panel.html b/account/templates/_types/auth/_newsletters_panel.html new file mode 100644 index 0000000..0f3fdbb --- /dev/null +++ b/account/templates/_types/auth/_newsletters_panel.html @@ -0,0 +1,46 @@ +
+
+ +

Newsletters

+ + {% if newsletter_list %} +
+ {% for item in newsletter_list %} +
+
+

{{ item.newsletter.name }}

+ {% if item.newsletter.description %} +

{{ item.newsletter.description }}

+ {% endif %} +
+
+ {% if item.un %} + {% with un=item.un %} + {% include "_types/auth/_newsletter_toggle.html" %} + {% endwith %} + {% else %} + {# No subscription row yet — show an off toggle that will create one #} +
+ +
+ {% endif %} +
+
+ {% endfor %} +
+ {% else %} +

No newsletters available.

+ {% endif %} + +
+
diff --git a/account/templates/_types/auth/_oob_elements.html b/account/templates/_types/auth/_oob_elements.html new file mode 100644 index 0000000..cafb113 --- /dev/null +++ b/account/templates/_types/auth/_oob_elements.html @@ -0,0 +1,29 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-header-child', 'auth-header-child', '_types/auth/header/_header.html')}} + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/auth/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include oob.main %} +{% endblock %} + + diff --git a/account/templates/_types/auth/_tickets_panel.html b/account/templates/_types/auth/_tickets_panel.html new file mode 100644 index 0000000..69f7596 --- /dev/null +++ b/account/templates/_types/auth/_tickets_panel.html @@ -0,0 +1,44 @@ +
+
+ +

Tickets

+ + {% if tickets %} +
+ {% for ticket in tickets %} +
+
+
+ + {{ ticket.entry_name }} + +
+ {{ ticket.entry_start_at.strftime('%d %b %Y, %H:%M') }} + {% if ticket.calendar_name %} + · {{ ticket.calendar_name }} + {% endif %} + {% if ticket.ticket_type_name %} + · {{ ticket.ticket_type_name }} + {% endif %} +
+
+
+ {% if ticket.state == 'checked_in' %} + checked in + {% elif ticket.state == 'confirmed' %} + confirmed + {% else %} + {{ ticket.state }} + {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} +

No tickets yet.

+ {% endif %} + +
+
diff --git a/account/templates/_types/auth/check_email.html b/account/templates/_types/auth/check_email.html new file mode 100644 index 0000000..e4cea28 --- /dev/null +++ b/account/templates/_types/auth/check_email.html @@ -0,0 +1,33 @@ +{% extends "_types/root/index.html" %} +{% block content %} +
+
+

Check your email

+ +

+ If an account exists for + {{ email }}, + you’ll receive a link to sign in. It expires in 15 minutes. +

+ + {% if email_error %} + + {% endif %} + +

+ + ← Back + +

+
+
+{% endblock %} diff --git a/account/templates/_types/auth/header/_header.html b/account/templates/_types/auth/header/_header.html new file mode 100644 index 0000000..c59a712 --- /dev/null +++ b/account/templates/_types/auth/header/_header.html @@ -0,0 +1,12 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='auth-row', oob=oob) %} + {% call links.link(account_url('/'), hx_select_search ) %} + +
account
+ {% endcall %} + {% call links.desktop_nav() %} + {% include "_types/auth/_nav.html" %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/account/templates/_types/auth/index copy.html b/account/templates/_types/auth/index copy.html new file mode 100644 index 0000000..cd4d6d3 --- /dev/null +++ b/account/templates/_types/auth/index copy.html @@ -0,0 +1,18 @@ +{% extends "_types/root/_index.html" %} + + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('auth-header-child', '_types/auth/header/_header.html') %} + {% block auth_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include "_types/auth/_nav.html" %} +{% endblock %} + +{% block content %} + {% include '_types/auth/_main_panel.html' %} +{% endblock %} diff --git a/account/templates/_types/auth/index.html b/account/templates/_types/auth/index.html new file mode 100644 index 0000000..3c66bf1 --- /dev/null +++ b/account/templates/_types/auth/index.html @@ -0,0 +1,18 @@ +{% extends oob.extends %} + + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row(oob.child_id, oob.header) %} + {% block auth_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include oob.nav %} +{% endblock %} + +{% block content %} + {% include oob.main %} +{% endblock %} diff --git a/account/templates/_types/auth/login.html b/account/templates/_types/auth/login.html new file mode 100644 index 0000000..b55ea99 --- /dev/null +++ b/account/templates/_types/auth/login.html @@ -0,0 +1,46 @@ +{% extends "_types/root/index.html" %} +{% block content %} +
+
+

Sign in

+

+ Enter your email and we’ll email you a one-time sign-in link. +

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+ +
+ + +
+ + +
+
+
+{% endblock %} diff --git a/account/templates/auth/check_email.html b/account/templates/auth/check_email.html new file mode 100644 index 0000000..5eb1b61 --- /dev/null +++ b/account/templates/auth/check_email.html @@ -0,0 +1,19 @@ +{% extends "_types/root/_index.html" %} +{% block meta %}{% endblock %} +{% block title %}Check your email — Rose Ash{% endblock %} +{% block content %} +
+

Check your email

+

+ We sent a sign-in link to {{ email }}. +

+

+ Click the link in the email to sign in. The link expires in 15 minutes. +

+ {% if email_error %} +
+ {{ email_error }} +
+ {% endif %} +
+{% endblock %} diff --git a/account/templates/auth/login.html b/account/templates/auth/login.html new file mode 100644 index 0000000..79031e5 --- /dev/null +++ b/account/templates/auth/login.html @@ -0,0 +1,36 @@ +{% extends "_types/root/_index.html" %} +{% block meta %}{% endblock %} +{% block title %}Login — Rose Ash{% endblock %} +{% block content %} +
+

Sign in

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+ +
+ + +
+ +
+
+{% endblock %} diff --git a/account/templates/fragments/auth_menu.html b/account/templates/fragments/auth_menu.html new file mode 100644 index 0000000..eb68cdc --- /dev/null +++ b/account/templates/fragments/auth_menu.html @@ -0,0 +1,36 @@ +{# Desktop auth menu #} + +{# Mobile auth menu #} + +{% if user_email %} + + + {{ user_email }} + +{% else %} + + + sign in or register + +{% endif %} + diff --git a/blog/.gitignore b/blog/.gitignore new file mode 100644 index 0000000..87d616e --- /dev/null +++ b/blog/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +*.pyo +.env +node_modules/ +*.egg-info/ +dist/ +build/ +.venv/ diff --git a/blog/Dockerfile b/blog/Dockerfile new file mode 100644 index 0000000..585991f --- /dev/null +++ b/blog/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:1 + +# ---------- Stage 1: Build editor JS/CSS ---------- +FROM node:20-slim AS editor-build +WORKDIR /build +COPY shared/editor/package.json shared/editor/package-lock.json* ./ +RUN npm ci --ignore-scripts 2>/dev/null || npm install +COPY shared/editor/ ./ +RUN NODE_ENV=production node build.mjs + +# ---------- Stage 2: Python runtime ---------- +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + PIP_NO_CACHE_DIR=1 \ + APP_PORT=8000 \ + APP_MODULE=app:app + +WORKDIR /app + +# Install system deps + psql client +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +COPY shared/requirements.txt ./requirements.txt +RUN pip install -r requirements.txt + +# Shared code (replaces submodule) +COPY shared/ ./shared/ + +# App code +COPY blog/ ./ + +# Sibling models for cross-domain SQLAlchemy imports +COPY market/__init__.py ./market/__init__.py +COPY market/models/ ./market/models/ +COPY cart/__init__.py ./cart/__init__.py +COPY cart/models/ ./cart/models/ +COPY events/__init__.py ./events/__init__.py +COPY events/models/ ./events/models/ +COPY federation/__init__.py ./federation/__init__.py +COPY federation/models/ ./federation/models/ +COPY account/__init__.py ./account/__init__.py +COPY account/models/ ./account/models/ + +# Copy built editor assets from stage 1 +COPY --from=editor-build /static/scripts/editor.js /static/scripts/editor.css shared/static/scripts/ + +# ---------- Runtime setup ---------- +COPY blog/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +RUN useradd -m -u 10001 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE ${APP_PORT} +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/blog/README.md b/blog/README.md new file mode 100644 index 0000000..ef45943 --- /dev/null +++ b/blog/README.md @@ -0,0 +1,60 @@ +# Blog App (Coop) + +Blog, authentication, and content management service for the Rose Ash cooperative platform. Handles Ghost CMS integration, user auth, and admin settings. + +## Architecture + +One of five Quart microservices sharing a single PostgreSQL database: + +| App | Port | Domain | +|-----|------|--------| +| **blog (coop)** | 8000 | Auth, blog, admin, menus, snippets | +| market | 8001 | Product browsing, Suma scraping | +| cart | 8002 | Shopping cart, checkout, orders | +| events | 8003 | Calendars, bookings, tickets | +| federation | 8004 | ActivityPub, fediverse social | + +## Structure + +``` +app.py # Application factory (create_base_app + blueprints) +path_setup.py # Adds project root + app dir to sys.path +config/app-config.yaml # App URLs, feature flags, SumUp config +models/ # Blog-domain models (+ re-export stubs for shared models) +bp/ # Blueprints + auth/ # Magic link login, account, newsletters + blog/ # Post listing, Ghost CMS sync + post/ # Single post view and admin + admin/ # Settings admin interface + menu_items/ # Navigation menu management + snippets/ # Reusable content snippets +templates/ # Jinja2 templates +services/ # register_domain_services() — wires blog + calendar + market + cart +shared/ # Submodule -> git.rose-ash.com/coop/shared.git +``` + +## Cross-Domain Communication + +All inter-app communication uses typed service contracts (no HTTP APIs): + +- `services.calendar.*` — calendar/entry queries via CalendarService protocol +- `services.market.*` — marketplace queries via MarketService protocol +- `services.cart.*` — cart summary via CartService protocol +- `services.federation.*` — AP publishing via FederationService protocol +- `shared.services.navigation` — site navigation tree + +## Domain Events + +- `auth/routes.py` emits `user.logged_in` via `shared.events.emit_event` +- Ghost sync emits `post.published` / `post.updated` for federation + +## Running + +```bash +export DATABASE_URL_ASYNC=postgresql+asyncpg://user:pass@localhost/coop +export REDIS_URL=redis://localhost:6379/0 +export SECRET_KEY=your-secret-key + +alembic -c shared/alembic.ini upgrade head +hypercorn app:app --bind 0.0.0.0:8000 +``` diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/app.py b/blog/app.py new file mode 100644 index 0000000..e59895f --- /dev/null +++ b/blog/app.py @@ -0,0 +1,138 @@ +from __future__ import annotations +import path_setup # noqa: F401 # adds shared/ to sys.path +from pathlib import Path + +from quart import g, request +from jinja2 import FileSystemLoader, ChoiceLoader +from sqlalchemy import select + +from shared.infrastructure.factory import create_base_app +from shared.config import config +from shared.models import KV + +from bp import ( + register_blog_bp, + register_admin, + register_menu_items, + register_snippets, + register_fragments, +) + + +async def blog_context() -> dict: + """ + Blog app context processor. + + - cart_count/cart_total: via cart service (shared DB) + - cart_mini_html / auth_menu_html / nav_tree_html: pre-fetched fragments + """ + from shared.infrastructure.context import base_context + from shared.services.navigation import get_navigation_tree + from shared.services.registry import services + from shared.infrastructure.cart_identity import current_cart_identity + from shared.infrastructure.fragments import fetch_fragments + + ctx = await base_context() + + # Fallback for _nav.html when nav-tree fragment fetch fails + ctx["menu_items"] = await get_navigation_tree(g.s) + + # Cart data via service (replaces cross-app HTTP API) + ident = current_cart_identity() + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count + ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total) + + # Pre-fetch cross-app HTML fragments concurrently + # (fetch_fragment auto-skips when inside a fragment request to prevent circular deps) + user = getattr(g, "user", None) + cart_params = {} + if ident["user_id"] is not None: + cart_params["user_id"] = ident["user_id"] + if ident["session_id"] is not None: + cart_params["session_id"] = ident["session_id"] + + auth_params = {"email": user.email} if user else {} + nav_params = {"app_name": "blog", "path": request.path} + + cart_mini_html, auth_menu_html, nav_tree_html = await fetch_fragments([ + ("cart", "cart-mini", cart_params or None), + ("account", "auth-menu", auth_params or None), + ("blog", "nav-tree", nav_params), + ]) + ctx["cart_mini_html"] = cart_mini_html + ctx["auth_menu_html"] = auth_menu_html + ctx["nav_tree_html"] = nav_tree_html + + return ctx + + +def create_app() -> "Quart": + from services import register_domain_services + + app = create_base_app( + "blog", + context_fn=blog_context, + domain_services_fn=register_domain_services, + ) + + # App-specific templates override shared templates + app_templates = str(Path(__file__).resolve().parent / "templates") + app.jinja_loader = ChoiceLoader([ + FileSystemLoader(app_templates), + app.jinja_loader, + ]) + + # --- blueprints --- + app.register_blueprint( + register_blog_bp( + url_prefix=config()["blog_root"], + title=config()["blog_title"], + ), + url_prefix=config()["blog_root"], + ) + + app.register_blueprint(register_admin("/settings")) + app.register_blueprint(register_menu_items()) + app.register_blueprint(register_snippets()) + app.register_blueprint(register_fragments()) + + # --- KV admin endpoints --- + @app.get("/settings/kv/") + async def kv_get(key: str): + row = ( + await g.s.execute(select(KV).where(KV.key == key)) + ).scalar_one_or_none() + return {"key": key, "value": (row.value if row else None)} + + @app.post("/settings/kv/") + async def kv_set(key: str): + data = await request.get_json() or {} + val = data.get("value", "") + obj = await g.s.get(KV, key) + if obj is None: + obj = KV(key=key, value=val) + g.s.add(obj) + else: + obj.value = val + return {"ok": True, "key": key, "value": val} + + # --- debug: url rules --- + @app.get("/__rules") + async def dump_rules(): + rules = [] + for r in app.url_map.iter_rules(): + rules.append({ + "endpoint": r.endpoint, + "rule": repr(r.rule), + "methods": sorted(r.methods - {"HEAD", "OPTIONS"}), + "strict_slashes": r.strict_slashes, + }) + return {"rules": rules} + + return app + + +app = create_app() diff --git a/blog/bp/__init__.py b/blog/bp/__init__.py new file mode 100644 index 0000000..59bc262 --- /dev/null +++ b/blog/bp/__init__.py @@ -0,0 +1,5 @@ +from .blog.routes import register as register_blog_bp +from .admin.routes import register as register_admin +from .menu_items.routes import register as register_menu_items +from .snippets.routes import register as register_snippets +from .fragments import register_fragments diff --git a/blog/bp/admin/routes.py b/blog/bp/admin/routes.py new file mode 100644 index 0000000..e387c17 --- /dev/null +++ b/blog/bp/admin/routes.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +#from quart import Blueprint, g + +from quart import ( + render_template, + make_response, + Blueprint, + redirect, + url_for, + request, + jsonify +) +from shared.browser.app.redis_cacher import clear_all_cache +from shared.browser.app.authz import require_admin +from shared.browser.app.utils.htmx import is_htmx_request +from shared.config import config +from datetime import datetime + +def register(url_prefix): + bp = Blueprint("settings", __name__, url_prefix = url_prefix) + + @bp.context_processor + async def inject_root(): + return { + "base_title": f"{config()['title']} settings", + } + + @bp.get("/") + @require_admin + async def home(): + + # Determine which template to use based on request type and pagination + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/root/settings/index.html", + ) + + else: + html = await render_template("_types/root/settings/_oob_elements.html") + + + return await make_response(html) + + @bp.get("/cache/") + @require_admin + async def cache(): + if not is_htmx_request(): + html = await render_template("_types/root/settings/cache/index.html") + else: + html = await render_template("_types/root/settings/cache/_oob_elements.html") + return await make_response(html) + + @bp.post("/cache_clear/") + @require_admin + async def cache_clear(): + await clear_all_cache() + if is_htmx_request(): + now = datetime.now() + html = f'Cache cleared at {now.strftime("%H:%M:%S")}' + return html + + return redirect(url_for("settings.cache")) + return bp + + diff --git a/blog/bp/blog/__init__.py b/blog/bp/blog/__init__.py new file mode 100644 index 0000000..85fd1a5 --- /dev/null +++ b/blog/bp/blog/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +# create the blueprint at package import time +from .routes import register # = Blueprint("browse_bp", __name__) + +# import routes AFTER browse_bp is defined so routes can attach to it +from . import routes # noqa: F401 diff --git a/blog/bp/blog/admin/__init__.py b/blog/bp/blog/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/bp/blog/admin/routes.py b/blog/bp/blog/admin/routes.py new file mode 100644 index 0000000..4bf8139 --- /dev/null +++ b/blog/bp/blog/admin/routes.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import re +from quart import ( + render_template, + make_response, + Blueprint, + redirect, + url_for, + request, + g, +) +from sqlalchemy import select, delete + +from shared.browser.app.authz import require_admin +from shared.browser.app.utils.htmx import is_htmx_request +from shared.browser.app.redis_cacher import invalidate_tag_cache + +from models.tag_group import TagGroup, TagGroupTag +from models.ghost_content import Tag + + +def _slugify(name: str) -> str: + s = name.strip().lower() + s = re.sub(r"[^\w\s-]", "", s) + s = re.sub(r"[\s_]+", "-", s) + return s.strip("-") + + +async def _unassigned_tags(session): + """Return public, non-deleted tags not assigned to any group.""" + assigned_sq = select(TagGroupTag.tag_id).subquery() + q = ( + select(Tag) + .where( + Tag.deleted_at.is_(None), + (Tag.visibility == "public") | (Tag.visibility.is_(None)), + Tag.id.notin_(select(assigned_sq)), + ) + .order_by(Tag.name) + ) + return list((await session.execute(q)).scalars()) + + +def register(): + bp = Blueprint("tag_groups_admin", __name__, url_prefix="/settings/tag-groups") + + @bp.get("/") + @require_admin + async def index(): + groups = list( + (await g.s.execute( + select(TagGroup).order_by(TagGroup.sort_order, TagGroup.name) + )).scalars() + ) + unassigned = await _unassigned_tags(g.s) + + ctx = {"groups": groups, "unassigned_tags": unassigned} + + if not is_htmx_request(): + return await render_template("_types/blog/admin/tag_groups/index.html", **ctx) + else: + return await render_template("_types/blog/admin/tag_groups/_oob_elements.html", **ctx) + + @bp.post("/") + @require_admin + async def create(): + form = await request.form + name = (form.get("name") or "").strip() + if not name: + return redirect(url_for("blog.tag_groups_admin.index")) + + slug = _slugify(name) + feature_image = (form.get("feature_image") or "").strip() or None + colour = (form.get("colour") or "").strip() or None + sort_order = int(form.get("sort_order") or 0) + + tg = TagGroup( + name=name, slug=slug, + feature_image=feature_image, colour=colour, + sort_order=sort_order, + ) + g.s.add(tg) + await g.s.flush() + + await invalidate_tag_cache("blog") + return redirect(url_for("blog.tag_groups_admin.index")) + + @bp.get("//") + @require_admin + async def edit(id: int): + tg = await g.s.get(TagGroup, id) + if not tg: + return redirect(url_for("blog.tag_groups_admin.index")) + + # Assigned tag IDs for this group + assigned_rows = list( + (await g.s.execute( + select(TagGroupTag.tag_id).where(TagGroupTag.tag_group_id == id) + )).scalars() + ) + assigned_tag_ids = set(assigned_rows) + + # All public, non-deleted tags + all_tags = list( + (await g.s.execute( + select(Tag).where( + Tag.deleted_at.is_(None), + (Tag.visibility == "public") | (Tag.visibility.is_(None)), + ).order_by(Tag.name) + )).scalars() + ) + + ctx = { + "group": tg, + "all_tags": all_tags, + "assigned_tag_ids": assigned_tag_ids, + } + + if not is_htmx_request(): + return await render_template("_types/blog/admin/tag_groups/edit.html", **ctx) + else: + return await render_template("_types/blog/admin/tag_groups/_edit_oob.html", **ctx) + + @bp.post("//") + @require_admin + async def save(id: int): + tg = await g.s.get(TagGroup, id) + if not tg: + return redirect(url_for("blog.tag_groups_admin.index")) + + form = await request.form + name = (form.get("name") or "").strip() + if name: + tg.name = name + tg.slug = _slugify(name) + tg.feature_image = (form.get("feature_image") or "").strip() or None + tg.colour = (form.get("colour") or "").strip() or None + tg.sort_order = int(form.get("sort_order") or 0) + + # Update tag assignments + selected_tag_ids = set() + for val in form.getlist("tag_ids"): + try: + selected_tag_ids.add(int(val)) + except (ValueError, TypeError): + pass + + # Remove old assignments + await g.s.execute( + delete(TagGroupTag).where(TagGroupTag.tag_group_id == id) + ) + await g.s.flush() + + # Add new assignments + for tid in selected_tag_ids: + g.s.add(TagGroupTag(tag_group_id=id, tag_id=tid)) + await g.s.flush() + + await invalidate_tag_cache("blog") + return redirect(url_for("blog.tag_groups_admin.edit", id=id)) + + @bp.post("//delete/") + @require_admin + async def delete_group(id: int): + tg = await g.s.get(TagGroup, id) + if tg: + await g.s.delete(tg) + await g.s.flush() + await invalidate_tag_cache("blog") + return redirect(url_for("blog.tag_groups_admin.index")) + + return bp diff --git a/blog/bp/blog/filters/qs.py b/blog/bp/blog/filters/qs.py new file mode 100644 index 0000000..073dd13 --- /dev/null +++ b/blog/bp/blog/filters/qs.py @@ -0,0 +1,120 @@ +from quart import request + +from typing import Iterable, Optional, Union + +from shared.browser.app.filters.qs_base import ( + KEEP, _norm, make_filter_set, build_qs, +) +from shared.browser.app.filters.query_types import BlogQuery + + +def decode() -> BlogQuery: + page = int(request.args.get("page", 1)) + search = request.args.get("search") + sort = request.args.get("sort") + liked = request.args.get("liked") + drafts = request.args.get("drafts") + + selected_tags = tuple(s.strip() for s in request.args.getlist("tag") if s.strip())[:1] + selected_authors = tuple(s.strip().lower() for s in request.args.getlist("author") if s.strip())[:1] + selected_groups = tuple(s.strip() for s in request.args.getlist("group") if s.strip())[:1] + view = request.args.get("view") or None + + return BlogQuery(page, search, sort, selected_tags, selected_authors, liked, view, drafts, selected_groups) + + +def makeqs_factory(): + """ + Build a makeqs(...) that starts from the current filters + page. + Auto-resets page to 1 when filters change unless you pass page explicitly. + """ + q = decode() + base_tags = [s for s in q.selected_tags if (s or "").strip()] + base_authors = [s for s in q.selected_authors if (s or "").strip()] + base_groups = [s for s in q.selected_groups if (s or "").strip()] + base_search = q.search or None + base_liked = q.liked or None + base_sort = q.sort or None + base_page = int(q.page or 1) + base_view = q.view or None + base_drafts = q.drafts or None + + def makeqs( + *, + clear_filters: bool = False, + add_tag: Union[str, Iterable[str], None] = None, + remove_tag: Union[str, Iterable[str], None] = None, + add_author: Union[str, Iterable[str], None] = None, + remove_author: Union[str, Iterable[str], None] = None, + add_group: Union[str, Iterable[str], None] = None, + remove_group: Union[str, Iterable[str], None] = None, + search: Union[str, None, object] = KEEP, + sort: Union[str, None, object] = KEEP, + page: Union[int, None, object] = None, + extra: Optional[Iterable[tuple]] = None, + leading_q: bool = True, + liked: Union[bool, None, object] = KEEP, + view: Union[str, None, object] = KEEP, + drafts: Union[str, None, object] = KEEP, + ) -> str: + groups = make_filter_set(base_groups, add_group, remove_group, clear_filters, single_select=True) + tags = make_filter_set(base_tags, add_tag, remove_tag, clear_filters, single_select=True) + authors = make_filter_set(base_authors, add_author, remove_author, clear_filters, single_select=True) + + # Mutual exclusion: selecting a group clears tags, selecting a tag clears groups + if add_group is not None: + tags = [] + if add_tag is not None: + groups = [] + + final_search = None if clear_filters else base_search if search is KEEP else ((search or "").strip() or None) + final_sort = base_sort if sort is KEEP else (sort or None) + final_liked = None if clear_filters else base_liked if liked is KEEP else liked + final_view = base_view if view is KEEP else (view or None) + final_drafts = None if clear_filters else base_drafts if drafts is KEEP else (drafts or None) + + # Did filters change? + filters_changed = ( + set(map(_norm, tags)) != set(map(_norm, base_tags)) + or set(map(_norm, authors)) != set(map(_norm, base_authors)) + or set(map(_norm, groups)) != set(map(_norm, base_groups)) + or final_search != base_search + or final_sort != base_sort + or final_liked != base_liked + or final_drafts != base_drafts + ) + + # Page logic + if page is KEEP: + final_page = 1 if filters_changed else base_page + else: + final_page = page + + # Build params + params = [] + for s in groups: + params.append(("group", s)) + for s in tags: + params.append(("tag", s)) + for s in authors: + params.append(("author", s)) + if final_search: + params.append(("search", final_search)) + if final_liked is not None: + params.append(("liked", final_liked)) + if final_sort: + params.append(("sort", final_sort)) + if final_view: + params.append(("view", final_view)) + if final_drafts: + params.append(("drafts", final_drafts)) + if final_page is not None: + params.append(("page", str(final_page))) + if extra: + for k, v in extra: + if v is not None: + params.append((k, str(v))) + + return build_qs(params, leading_q=leading_q) + + return makeqs diff --git a/blog/bp/blog/ghost/editor_api.py b/blog/bp/blog/ghost/editor_api.py new file mode 100644 index 0000000..c37fa96 --- /dev/null +++ b/blog/bp/blog/ghost/editor_api.py @@ -0,0 +1,256 @@ +""" +Editor API proxy – image/media/file uploads and oembed. + +Forwards requests to the Ghost Admin API with JWT auth so the browser +never needs direct Ghost access. +""" +from __future__ import annotations + +import logging +import os + +import httpx +from quart import Blueprint, request, jsonify, g +from sqlalchemy import select, or_ + +from shared.browser.app.authz import require_admin, require_login +from models import Snippet +from .ghost_admin_token import make_ghost_admin_jwt + +log = logging.getLogger(__name__) + +GHOST_ADMIN_API_URL = os.environ["GHOST_ADMIN_API_URL"] +MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB +MAX_MEDIA_SIZE = 100 * 1024 * 1024 # 100 MB +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50 MB + +ALLOWED_IMAGE_MIMETYPES = frozenset({ + "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml", +}) +ALLOWED_MEDIA_MIMETYPES = frozenset({ + "audio/mpeg", "audio/ogg", "audio/wav", "audio/mp4", "audio/aac", + "video/mp4", "video/webm", "video/ogg", +}) + +editor_api_bp = Blueprint("editor_api", __name__, url_prefix="/editor-api") + + +def _auth_header() -> dict[str, str]: + return {"Authorization": f"Ghost {make_ghost_admin_jwt()}"} + + +@editor_api_bp.post("/images/upload/") +@require_admin +async def upload_image(): + """Proxy image upload to Ghost Admin API.""" + files = await request.files + uploaded = files.get("file") + if not uploaded: + return jsonify({"errors": [{"message": "No file provided"}]}), 400 + + content = uploaded.read() + if len(content) > MAX_IMAGE_SIZE: + return jsonify({"errors": [{"message": "File too large (max 10 MB)"}]}), 413 + + if uploaded.content_type not in ALLOWED_IMAGE_MIMETYPES: + return jsonify({"errors": [{"message": f"Unsupported file type: {uploaded.content_type}"}]}), 415 + + url = f"{GHOST_ADMIN_API_URL}/images/upload/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post( + url, + headers=_auth_header(), + files={"file": (uploaded.filename, content, uploaded.content_type)}, + ) + + if not resp.is_success: + log.error("Ghost image upload failed %s: %s", resp.status_code, resp.text[:500]) + + return resp.json(), resp.status_code + + +@editor_api_bp.post("/media/upload/") +@require_admin +async def upload_media(): + """Proxy audio/video upload to Ghost Admin API.""" + files = await request.files + uploaded = files.get("file") + if not uploaded: + return jsonify({"errors": [{"message": "No file provided"}]}), 400 + + content = uploaded.read() + if len(content) > MAX_MEDIA_SIZE: + return jsonify({"errors": [{"message": "File too large (max 100 MB)"}]}), 413 + + if uploaded.content_type not in ALLOWED_MEDIA_MIMETYPES: + return jsonify({"errors": [{"message": f"Unsupported media type: {uploaded.content_type}"}]}), 415 + + ghost_files = {"file": (uploaded.filename, content, uploaded.content_type)} + + # Optional video thumbnail + thumbnail = files.get("thumbnail") + if thumbnail: + thumb_content = thumbnail.read() + ghost_files["thumbnail"] = (thumbnail.filename, thumb_content, thumbnail.content_type) + + url = f"{GHOST_ADMIN_API_URL}/media/upload/" + async with httpx.AsyncClient(timeout=60) as client: + resp = await client.post(url, headers=_auth_header(), files=ghost_files) + + if not resp.is_success: + log.error("Ghost media upload failed %s: %s", resp.status_code, resp.text[:500]) + + return resp.json(), resp.status_code + + +@editor_api_bp.post("/files/upload/") +@require_admin +async def upload_file(): + """Proxy file upload to Ghost Admin API.""" + files = await request.files + uploaded = files.get("file") + if not uploaded: + return jsonify({"errors": [{"message": "No file provided"}]}), 400 + + content = uploaded.read() + if len(content) > MAX_FILE_SIZE: + return jsonify({"errors": [{"message": "File too large (max 50 MB)"}]}), 413 + + url = f"{GHOST_ADMIN_API_URL}/files/upload/" + async with httpx.AsyncClient(timeout=60) as client: + resp = await client.post( + url, + headers=_auth_header(), + files={"file": (uploaded.filename, content, uploaded.content_type)}, + ) + + if not resp.is_success: + log.error("Ghost file upload failed %s: %s", resp.status_code, resp.text[:500]) + + return resp.json(), resp.status_code + + +@editor_api_bp.get("/oembed/") +@require_admin +async def oembed_proxy(): + """Proxy oembed lookups to Ghost Admin API.""" + params = dict(request.args) + if not params.get("url"): + return jsonify({"errors": [{"message": "url parameter required"}]}), 400 + + url = f"{GHOST_ADMIN_API_URL}/oembed/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, headers=_auth_header(), params=params) + + if not resp.is_success: + log.error("Ghost oembed failed %s: %s", resp.status_code, resp.text[:500]) + + return resp.json(), resp.status_code + + +# ── Snippets ──────────────────────────────────────────────────────── + +VALID_VISIBILITY = frozenset({"private", "shared", "admin"}) + + +@editor_api_bp.get("/snippets/") +@require_login +async def list_snippets(): + """Return snippets visible to the current user.""" + uid = g.user.id + is_admin = g.rights.get("admin") + + filters = [Snippet.user_id == uid, Snippet.visibility == "shared"] + if is_admin: + filters.append(Snippet.visibility == "admin") + + rows = (await g.s.execute( + select(Snippet).where(or_(*filters)).order_by(Snippet.name) + )).scalars().all() + + return jsonify([ + {"id": s.id, "name": s.name, "value": s.value, "visibility": s.visibility} + for s in rows + ]) + + +@editor_api_bp.post("/snippets/") +@require_login +async def create_snippet(): + """Create or upsert a snippet by (user_id, name).""" + data = await request.get_json(force=True) + name = (data.get("name") or "").strip() + value = data.get("value") + visibility = data.get("visibility", "private") + + if not name or value is None: + return jsonify({"error": "name and value are required"}), 400 + if visibility not in VALID_VISIBILITY: + return jsonify({"error": f"visibility must be one of {sorted(VALID_VISIBILITY)}"}), 400 + if visibility != "private" and not g.rights.get("admin"): + visibility = "private" + + uid = g.user.id + + existing = (await g.s.execute( + select(Snippet).where(Snippet.user_id == uid, Snippet.name == name) + )).scalar_one_or_none() + + if existing: + existing.value = value + existing.visibility = visibility + snippet = existing + else: + snippet = Snippet(user_id=uid, name=name, value=value, visibility=visibility) + g.s.add(snippet) + + await g.s.flush() + return jsonify({ + "id": snippet.id, "name": snippet.name, + "value": snippet.value, "visibility": snippet.visibility, + }), 200 if existing else 201 + + +@editor_api_bp.patch("/snippets//") +@require_login +async def patch_snippet(snippet_id: int): + """Update snippet visibility. Only admins may set shared/admin.""" + snippet = await g.s.get(Snippet, snippet_id) + if not snippet: + return jsonify({"error": "not found"}), 404 + + is_admin = g.rights.get("admin") + + if snippet.user_id != g.user.id and not is_admin: + return jsonify({"error": "forbidden"}), 403 + + data = await request.get_json(force=True) + visibility = data.get("visibility") + if visibility is not None: + if visibility not in VALID_VISIBILITY: + return jsonify({"error": f"visibility must be one of {sorted(VALID_VISIBILITY)}"}), 400 + if visibility != "private" and not is_admin: + return jsonify({"error": "only admins may set shared/admin visibility"}), 403 + snippet.visibility = visibility + + await g.s.flush() + return jsonify({ + "id": snippet.id, "name": snippet.name, + "value": snippet.value, "visibility": snippet.visibility, + }) + + +@editor_api_bp.delete("/snippets//") +@require_login +async def delete_snippet(snippet_id: int): + """Delete a snippet. Owners can delete their own; admins can delete any.""" + snippet = await g.s.get(Snippet, snippet_id) + if not snippet: + return jsonify({"error": "not found"}), 404 + + if snippet.user_id != g.user.id and not g.rights.get("admin"): + return jsonify({"error": "forbidden"}), 403 + + await g.s.delete(snippet) + await g.s.flush() + return jsonify({"ok": True}) diff --git a/blog/bp/blog/ghost/ghost_admin_token.py b/blog/bp/blog/ghost/ghost_admin_token.py new file mode 100644 index 0000000..1974075 --- /dev/null +++ b/blog/bp/blog/ghost/ghost_admin_token.py @@ -0,0 +1,46 @@ +import os +import time +import jwt # PyJWT +from typing import Tuple + + +def _split_key(raw_key: str) -> Tuple[str, bytes]: + """ + raw_key is the 'id:secret' from Ghost. + Returns (id, secret_bytes) + """ + key_id, key_secret_hex = raw_key.split(':', 1) + secret_bytes = bytes.fromhex(key_secret_hex) + return key_id, secret_bytes + + +def make_ghost_admin_jwt() -> str: + """ + Generate a short-lived JWT suitable for Authorization: Ghost + """ + raw_key = os.environ["GHOST_ADMIN_API_KEY"] + key_id, secret_bytes = _split_key(raw_key) + + now = int(time.time()) + + payload = { + "iat": now, + "exp": now + 5 * 60, # now + 5 minutes + "aud": "/admin/", + } + + headers = { + "alg": "HS256", + "kid": key_id, + "typ": "JWT", + } + + token = jwt.encode( + payload, + secret_bytes, + algorithm="HS256", + headers=headers, + ) + + # PyJWT returns str in recent versions; Ghost expects bare token string + return token diff --git a/blog/bp/blog/ghost/ghost_posts.py b/blog/bp/blog/ghost/ghost_posts.py new file mode 100644 index 0000000..7d16fbf --- /dev/null +++ b/blog/bp/blog/ghost/ghost_posts.py @@ -0,0 +1,204 @@ +""" +Ghost Admin API – post CRUD. + +Uses the same JWT auth and httpx patterns as ghost_sync.py. +""" +from __future__ import annotations + +import logging +import os + +import httpx + +from .ghost_admin_token import make_ghost_admin_jwt + +log = logging.getLogger(__name__) + +GHOST_ADMIN_API_URL = os.environ["GHOST_ADMIN_API_URL"] + + +def _auth_header() -> dict[str, str]: + return {"Authorization": f"Ghost {make_ghost_admin_jwt()}"} + + +def _check(resp: httpx.Response) -> None: + """Raise with the Ghost error body so callers see what went wrong.""" + if resp.is_success: + return + body = resp.text[:2000] + log.error("Ghost API %s %s → %s: %s", resp.request.method, resp.request.url, resp.status_code, body) + resp.raise_for_status() + + +async def get_post_for_edit(ghost_id: str, *, is_page: bool = False) -> dict | None: + """Fetch a single post/page by Ghost ID, including lexical source.""" + resource = "pages" if is_page else "posts" + url = ( + f"{GHOST_ADMIN_API_URL}/{resource}/{ghost_id}/" + "?formats=lexical,html,mobiledoc&include=newsletters" + ) + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, headers=_auth_header()) + if resp.status_code == 404: + return None + _check(resp) + return resp.json()[resource][0] + + +async def create_post( + title: str, + lexical_json: str, + status: str = "draft", + feature_image: str | None = None, + custom_excerpt: str | None = None, + feature_image_caption: str | None = None, +) -> dict: + """Create a new post in Ghost. Returns the created post dict.""" + post_body: dict = { + "title": title, + "lexical": lexical_json, + "mobiledoc": None, + "status": status, + } + if feature_image: + post_body["feature_image"] = feature_image + if custom_excerpt: + post_body["custom_excerpt"] = custom_excerpt + if feature_image_caption is not None: + post_body["feature_image_caption"] = feature_image_caption + payload = {"posts": [post_body]} + url = f"{GHOST_ADMIN_API_URL}/posts/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post(url, json=payload, headers=_auth_header()) + _check(resp) + return resp.json()["posts"][0] + + +async def create_page( + title: str, + lexical_json: str, + status: str = "draft", + feature_image: str | None = None, + custom_excerpt: str | None = None, + feature_image_caption: str | None = None, +) -> dict: + """Create a new page in Ghost (via /pages/ endpoint). Returns the created page dict.""" + page_body: dict = { + "title": title, + "lexical": lexical_json, + "mobiledoc": None, + "status": status, + } + if feature_image: + page_body["feature_image"] = feature_image + if custom_excerpt: + page_body["custom_excerpt"] = custom_excerpt + if feature_image_caption is not None: + page_body["feature_image_caption"] = feature_image_caption + payload = {"pages": [page_body]} + url = f"{GHOST_ADMIN_API_URL}/pages/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post(url, json=payload, headers=_auth_header()) + _check(resp) + return resp.json()["pages"][0] + + +async def update_post( + ghost_id: str, + lexical_json: str, + title: str | None, + updated_at: str, + feature_image: str | None = None, + custom_excerpt: str | None = None, + feature_image_caption: str | None = None, + status: str | None = None, + newsletter_slug: str | None = None, + email_segment: str | None = None, + email_only: bool | None = None, + is_page: bool = False, +) -> dict: + """Update an existing Ghost post. Returns the updated post dict. + + ``updated_at`` is Ghost's optimistic-locking token – pass the value + you received from ``get_post_for_edit``. + + When ``newsletter_slug`` is set the publish request also triggers an + email send via Ghost's query-parameter API: + ``?newsletter={slug}&email_segment={segment}``. + """ + post_body: dict = { + "lexical": lexical_json, + "mobiledoc": None, + "updated_at": updated_at, + } + if title is not None: + post_body["title"] = title + if feature_image is not None: + post_body["feature_image"] = feature_image or None + if custom_excerpt is not None: + post_body["custom_excerpt"] = custom_excerpt or None + if feature_image_caption is not None: + post_body["feature_image_caption"] = feature_image_caption + if status is not None: + post_body["status"] = status + if email_only: + post_body["email_only"] = True + resource = "pages" if is_page else "posts" + payload = {resource: [post_body]} + + url = f"{GHOST_ADMIN_API_URL}/{resource}/{ghost_id}/" + if newsletter_slug: + url += f"?newsletter={newsletter_slug}" + if email_segment: + url += f"&email_segment={email_segment}" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.put(url, json=payload, headers=_auth_header()) + _check(resp) + return resp.json()[resource][0] + + +_SETTINGS_FIELDS = ( + "slug", + "published_at", + "featured", + "visibility", + "email_only", + "custom_template", + "meta_title", + "meta_description", + "canonical_url", + "og_image", + "og_title", + "og_description", + "twitter_image", + "twitter_title", + "twitter_description", + "tags", + "feature_image_alt", +) + + +async def update_post_settings( + ghost_id: str, + updated_at: str, + is_page: bool = False, + **kwargs, +) -> dict: + """Update Ghost post/page settings (slug, tags, SEO, social, etc.). + + Only non-None keyword args are included in the PUT payload. + Accepts any key from ``_SETTINGS_FIELDS``. + """ + resource = "pages" if is_page else "posts" + post_body: dict = {"updated_at": updated_at} + for key in _SETTINGS_FIELDS: + val = kwargs.get(key) + if val is not None: + post_body[key] = val + + payload = {resource: [post_body]} + url = f"{GHOST_ADMIN_API_URL}/{resource}/{ghost_id}/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.put(url, json=payload, headers=_auth_header()) + _check(resp) + return resp.json()[resource][0] diff --git a/blog/bp/blog/ghost/ghost_sync.py b/blog/bp/blog/ghost/ghost_sync.py new file mode 100644 index 0000000..c3d92ee --- /dev/null +++ b/blog/bp/blog/ghost/ghost_sync.py @@ -0,0 +1,1240 @@ +from __future__ import annotations +import os +import re +import asyncio +from datetime import datetime +from html import escape as html_escape +from typing import Dict, Any, Optional + +import httpx +from sqlalchemy import select, delete, or_, and_ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm.attributes import flag_modified # for non-Mutable JSON columns + +# Content models +from models.ghost_content import ( + Post, Author, Tag, PostAuthor, PostTag +) +from shared.models.page_config import PageConfig + +# User-centric membership models +from shared.models import User +from shared.models.ghost_membership_entities import ( + GhostLabel, UserLabel, + GhostNewsletter, UserNewsletter, + GhostTier, GhostSubscription, +) + +from .ghost_admin_token import make_ghost_admin_jwt + +from urllib.parse import quote + +GHOST_ADMIN_API_URL = os.environ["GHOST_ADMIN_API_URL"] + +from shared.browser.app.utils import ( + utcnow +) + + + +def _auth_header() -> dict[str, str]: + return {"Authorization": f"Ghost {make_ghost_admin_jwt()}"} + + +def _iso(val: str | None) -> datetime | None: + if not val: + return None + return datetime.fromisoformat(val.replace("Z", "+00:00")) + +def _to_str_or_none(v) -> Optional[str]: + """Return a trimmed string if v is safely stringifiable; else None.""" + if v is None: + return None + # Disallow complex types that would stringify to JSON-like noise + if isinstance(v, (dict, list, set, tuple, bytes, bytearray)): + return None + s = str(v).strip() + return s or None + + +def _sanitize_member_payload(payload: dict) -> dict: + """Coerce types Ghost expects and drop empties to avoid 422/500 quirks.""" + out: dict = {} + + # email -> lowercase string + email = _to_str_or_none(payload.get("email")) + if email: + out["email"] = email.lower() + + # name / note must be strings if present + name = _to_str_or_none(payload.get("name")) + if name is not None: + out["name"] = name + + note = _to_str_or_none(payload.get("note")) + if note is not None: + out["note"] = note + + # subscribed -> bool + if "subscribed" in payload: + out["subscribed"] = bool(payload.get("subscribed")) + + # labels: keep only rows that have a non-empty id OR name + labels = [] + for item in payload.get("labels") or []: + gid = _to_str_or_none(item.get("id")) + gname = _to_str_or_none(item.get("name")) + if gid: + labels.append({"id": gid}) + elif gname: # only include if non-empty + labels.append({"name": gname}) + if labels: + out["labels"] = labels + + # newsletters: keep only rows with id OR name; coerce subscribed -> bool + newsletters = [] + for item in payload.get("newsletters") or []: + gid = _to_str_or_none(item.get("id")) + gname = _to_str_or_none(item.get("name")) + row = {"subscribed": bool(item.get("subscribed", True))} + if gid: + row["id"] = gid + newsletters.append(row) + elif gname: + row["name"] = gname + newsletters.append(row) + if newsletters: + out["newsletters"] = newsletters + + # id (if we carry a known ghost_id) + gid = _to_str_or_none(payload.get("id")) + if gid: + out["id"] = gid + + return out +# ===================== +# CONTENT UPSERT HELPERS +# ===================== + +async def _upsert_author(sess: AsyncSession, ga: Dict[str, Any]) -> Author: + res = await sess.execute(select(Author).where(Author.ghost_id == ga["id"])) + obj = res.scalar_one_or_none() + if obj is None: + obj = Author(ghost_id=ga["id"]) + sess.add(obj) + + # revive if soft-deleted + obj.deleted_at = None + + obj.slug = ga.get("slug") or obj.slug + obj.name = ga.get("name") or obj.name + obj.email = ga.get("email") or obj.email + obj.profile_image = ga.get("profile_image") + obj.cover_image = ga.get("cover_image") + obj.bio = ga.get("bio") + obj.website = ga.get("website") + obj.location = ga.get("location") + obj.facebook = ga.get("facebook") + obj.twitter = ga.get("twitter") + obj.created_at = _iso(ga.get("created_at")) or obj.created_at or utcnow() + obj.updated_at = _iso(ga.get("updated_at")) or utcnow() + + await sess.flush() + return obj + + +async def _upsert_tag(sess: AsyncSession, gt: Dict[str, Any]) -> Tag: + res = await sess.execute(select(Tag).where(Tag.ghost_id == gt["id"])) + obj = res.scalar_one_or_none() + if obj is None: + obj = Tag(ghost_id=gt["id"]) + sess.add(obj) + + obj.deleted_at = None # revive if soft-deleted + + obj.slug = gt.get("slug") or obj.slug + obj.name = gt.get("name") or obj.name + obj.description = gt.get("description") + obj.visibility = gt.get("visibility") or obj.visibility + obj.feature_image = gt.get("feature_image") + obj.meta_title = gt.get("meta_title") + obj.meta_description = gt.get("meta_description") + obj.created_at = _iso(gt.get("created_at")) or obj.created_at or utcnow() + obj.updated_at = _iso(gt.get("updated_at")) or utcnow() + + await sess.flush() + return obj + + +def _apply_ghost_fields(obj: Post, gp: Dict[str, Any], author_map: Dict[str, Author], tag_map: Dict[str, Tag]) -> None: + """Apply Ghost API fields to a Post ORM object.""" + obj.deleted_at = None # revive if soft-deleted + + obj.uuid = gp.get("uuid") or obj.uuid + obj.slug = gp.get("slug") or obj.slug + obj.title = gp.get("title") or obj.title + obj.html = gp.get("html") + obj.plaintext = gp.get("plaintext") + obj.mobiledoc = gp.get("mobiledoc") + obj.lexical = gp.get("lexical") + obj.feature_image = gp.get("feature_image") + obj.feature_image_alt = gp.get("feature_image_alt") + obj.feature_image_caption = gp.get("feature_image_caption") + obj.excerpt = gp.get("excerpt") + obj.custom_excerpt = gp.get("custom_excerpt") + obj.visibility = gp.get("visibility") or obj.visibility + obj.status = gp.get("status") or obj.status + obj.featured = bool(gp.get("featured") or False) + obj.is_page = bool(gp.get("page") or False) + obj.email_only = bool(gp.get("email_only") or False) + obj.canonical_url = gp.get("canonical_url") + obj.meta_title = gp.get("meta_title") + obj.meta_description = gp.get("meta_description") + obj.og_image = gp.get("og_image") + obj.og_title = gp.get("og_title") + obj.og_description = gp.get("og_description") + obj.twitter_image = gp.get("twitter_image") + obj.twitter_title = gp.get("twitter_title") + obj.twitter_description = gp.get("twitter_description") + obj.custom_template = gp.get("custom_template") + obj.reading_time = gp.get("reading_time") + obj.comment_id = gp.get("comment_id") + + obj.published_at = _iso(gp.get("published_at")) + obj.updated_at = _iso(gp.get("updated_at")) or obj.updated_at or utcnow() + obj.created_at = _iso(gp.get("created_at")) or obj.created_at or utcnow() + + pa = gp.get("primary_author") + obj.primary_author_id = author_map[pa["id"].strip()].id if pa else None # type: ignore[index] + + pt = gp.get("primary_tag") + obj.primary_tag_id = tag_map[pt["id"].strip()].id if (pt and pt["id"] in tag_map) else None # type: ignore[index] + + +async def _upsert_post(sess: AsyncSession, gp: Dict[str, Any], author_map: Dict[str, Author], tag_map: Dict[str, Tag]) -> tuple[Post, str | None]: + """Upsert a post. Returns (post, old_status) where old_status is None for new rows.""" + from sqlalchemy.exc import IntegrityError + + res = await sess.execute(select(Post).where(Post.ghost_id == gp["id"])) + obj = res.scalar_one_or_none() + + old_status = obj.status if obj is not None else None + + if obj is not None: + # Row exists — just update + _apply_ghost_fields(obj, gp, author_map, tag_map) + await sess.flush() + else: + # Row doesn't exist — try to insert within a savepoint + obj = Post(ghost_id=gp["id"]) # type: ignore[call-arg] + try: + async with sess.begin_nested(): + sess.add(obj) + _apply_ghost_fields(obj, gp, author_map, tag_map) + await sess.flush() + except IntegrityError: + # Race condition: another request inserted this ghost_id. + # Savepoint rolled back; re-select and update. + res = await sess.execute(select(Post).where(Post.ghost_id == gp["id"])) + obj = res.scalar_one() + _apply_ghost_fields(obj, gp, author_map, tag_map) + await sess.flush() + + # Backfill user_id from primary author email if not already set + if obj.user_id is None and obj.primary_author_id is not None: + pa_obj = author_map.get(gp.get("primary_author", {}).get("id", "")) + if pa_obj and pa_obj.email: + user_res = await sess.execute( + select(User).where(User.email.ilike(pa_obj.email)) + ) + matched_user = user_res.scalar_one_or_none() + if matched_user: + obj.user_id = matched_user.id + await sess.flush() + + # rebuild post_authors + await sess.execute(delete(PostAuthor).where(PostAuthor.post_id == obj.id)) + for idx, a in enumerate(gp.get("authors") or []): + aa = author_map[a["id"]] + sess.add(PostAuthor(post_id=obj.id, author_id=aa.id, sort_order=idx)) + + # rebuild post_tags + await sess.execute(delete(PostTag).where(PostTag.post_id == obj.id)) + for idx, t in enumerate(gp.get("tags") or []): + tt = tag_map[t["id"]] + sess.add(PostTag(post_id=obj.id, tag_id=tt.id, sort_order=idx)) + + # Auto-create PageConfig for pages + if obj.is_page: + existing_pc = (await sess.execute( + select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == obj.id) + )).scalar_one_or_none() + if existing_pc is None: + sess.add(PageConfig(container_type="page", container_id=obj.id, features={})) + await sess.flush() + + return obj, old_status + +async def _ghost_find_member_by_email(email: str) -> Optional[dict]: + """Return first Ghost member with this email, or None.""" + if not email: + return None + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get( + f"{GHOST_ADMIN_API_URL}/members/?filter=email:{quote(email)}&limit=1", + headers=_auth_header(), + ) + resp.raise_for_status() + members = resp.json().get("members") or [] + return members[0] if members else None + + +# --- add this helper next to fetch_all_posts_from_ghost() --- + +async def _fetch_all_from_ghost(endpoint: str) -> list[dict[str, Any]]: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get( + f"{GHOST_ADMIN_API_URL}/{endpoint}/?include=authors,tags&limit=all&formats=html,plaintext,mobiledoc,lexical", + headers=_auth_header(), + ) + resp.raise_for_status() + # admin posts endpoint returns {"posts": [...]}, pages returns {"pages": [...]} + key = "posts" if endpoint == "posts" else "pages" + return resp.json().get(key, []) + +async def fetch_all_posts_and_pages_from_ghost() -> list[dict[str, Any]]: + posts, pages = await asyncio.gather( + _fetch_all_from_ghost("posts"), + _fetch_all_from_ghost("pages"), + ) + # Be explicit: ensure page flag exists for pages (Ghost typically includes "page": true) + for p in pages: + p["page"] = True + return posts + pages + + +async def sync_all_content_from_ghost(sess: AsyncSession) -> None: + #data = await fetch_all_posts_from_ghost() + data = await fetch_all_posts_and_pages_from_ghost() + # Use a transaction so all upserts/soft-deletes commit together + # buckets of authors/tags we saw in Ghost + author_bucket: Dict[str, dict[str, Any]] = {} + tag_bucket: Dict[str, dict[str, Any]] = {} + + for p in data: + for a in p.get("authors") or []: + author_bucket[a["id"]] = a + if p.get("primary_author"): + author_bucket[p["primary_author"]["id"]] = p["primary_author"] + + for t in p.get("tags") or []: + tag_bucket[t["id"]] = t + if p.get("primary_tag"): + tag_bucket[p["primary_tag"]["id"]] = p["primary_tag"] + + # sets of ghost_ids we've seen in Ghost RIGHT NOW + seen_post_ids = {p["id"] for p in data} + seen_author_ids = set(author_bucket.keys()) + seen_tag_ids = set(tag_bucket.keys()) + + # upsert authors + author_map: Dict[str, Author] = {} + for ga in author_bucket.values(): + a = await _upsert_author(sess, ga) + author_map[ga["id"]] = a + + # upsert tags + tag_map: Dict[str, Tag] = {} + for gt in tag_bucket.values(): + t = await _upsert_tag(sess, gt) + tag_map[gt["id"]] = t + + # upsert posts (including M2M) + for gp in data: + await _upsert_post(sess, gp, author_map, tag_map) + + # soft-delete anything that no longer exists in Ghost + now = utcnow() + + # Authors not seen -> mark deleted_at if not already + db_authors = await sess.execute(select(Author)) + for local_author in db_authors.scalars(): + if local_author.ghost_id not in seen_author_ids: + if local_author.deleted_at is None: + local_author.deleted_at = now + + # Tags not seen -> mark deleted_at + db_tags = await sess.execute(select(Tag)) + for local_tag in db_tags.scalars(): + if local_tag.ghost_id not in seen_tag_ids: + if local_tag.deleted_at is None: + local_tag.deleted_at = now + + # Posts not seen -> mark deleted_at + db_posts = await sess.execute(select(Post)) + for local_post in db_posts.scalars(): + if local_post.ghost_id not in seen_post_ids: + if local_post.deleted_at is None: + local_post.deleted_at = now + + # transaction auto-commits here + + +#===================================================== +# MEMBERSHIP SYNC (USER-CENTRIC) Ghost -> DB +#===================================================== + +def _member_email(m: dict[str, Any]) -> Optional[str]: + email = (m.get("email") or "").strip().lower() or None + return email + + +# ---- small upsert helpers for related entities ---- + +async def _upsert_label(sess: AsyncSession, data: dict) -> GhostLabel: + res = await sess.execute(select(GhostLabel).where(GhostLabel.ghost_id == data["id"])) + obj = res.scalar_one_or_none() + if not obj: + obj = GhostLabel(ghost_id=data["id"]) + sess.add(obj) + obj.name = data.get("name") or obj.name + obj.slug = data.get("slug") or obj.slug + await sess.flush() + return obj + + +async def _upsert_newsletter(sess: AsyncSession, data: dict) -> GhostNewsletter: + res = await sess.execute(select(GhostNewsletter).where(GhostNewsletter.ghost_id == data["id"])) + obj = res.scalar_one_or_none() + if not obj: + obj = GhostNewsletter(ghost_id=data["id"]) + sess.add(obj) + obj.name = data.get("name") or obj.name + obj.slug = data.get("slug") or obj.slug + obj.description = data.get("description") or obj.description + await sess.flush() + return obj + + +async def _upsert_tier(sess: AsyncSession, data: dict) -> GhostTier: + res = await sess.execute(select(GhostTier).where(GhostTier.ghost_id == data["id"])) + obj = res.scalar_one_or_none() + if not obj: + obj = GhostTier(ghost_id=data["id"]) + sess.add(obj) + obj.name = data.get("name") or obj.name + obj.slug = data.get("slug") or obj.slug + obj.type = data.get("type") or obj.type + obj.visibility = data.get("visibility") or obj.visibility + await sess.flush() + return obj + + +def _price_cents(sd: dict) -> Optional[int]: + try: + return int((sd.get("price") or {}).get("amount")) + except Exception: + return None + + +# ---- application of member payload onto User + related tables ---- + +async def _find_or_create_user_by_ghost_or_email(sess: AsyncSession, data: dict) -> User: + ghost_id = data.get("id") + email = _member_email(data) + + if ghost_id: + res = await sess.execute(select(User).where(User.ghost_id == ghost_id)) + u = res.scalar_one_or_none() + if u: + return u + + if email: + res = await sess.execute(select(User).where(User.email.ilike(email))) + u = res.scalar_one_or_none() + if u: + if ghost_id and not u.ghost_id: + u.ghost_id = ghost_id + return u + + # create a new user (Ghost is source of truth for member list) + u = User(email=email or f"_ghost_{ghost_id}@invalid.local") + if ghost_id: + u.ghost_id = ghost_id + sess.add(u) + await sess.flush() + return u + + +async def _apply_user_membership(sess: AsyncSession, user: User, m: dict) -> User: + """Apply Ghost member payload to local User WITHOUT touching relationship collections directly. + We mutate join tables explicitly to avoid lazy-loads (which cause MissingGreenlet in async). + """ + sess.add(user) + + # scalar fields + user.name = m.get("name") or user.name + user.ghost_status = m.get("status") or user.ghost_status + user.ghost_subscribed = bool(m.get("subscribed", True)) + user.ghost_note = m.get("note") or user.ghost_note + user.avatar_image = m.get("avatar_image") or user.avatar_image + user.stripe_customer_id = ( + (m.get("stripe") or {}).get("customer_id") + or (m.get("customer") or {}).get("id") + or m.get("stripe_customer_id") + or user.stripe_customer_id + ) + user.ghost_raw = dict(m) + flag_modified(user, "ghost_raw") + + await sess.flush() # ensure user.id exists + + # Labels join + label_ids: list[int] = [] + for ld in m.get("labels") or []: + lbl = await _upsert_label(sess, ld) + label_ids.append(lbl.id) + await sess.execute(delete(UserLabel).where(UserLabel.user_id == user.id)) + for lid in label_ids: + sess.add(UserLabel(user_id=user.id, label_id=lid)) + await sess.flush() + + # Newsletters join with subscribed flag + nl_rows: list[tuple[int, bool]] = [] + for nd in m.get("newsletters") or []: + nl = await _upsert_newsletter(sess, nd) + nl_rows.append((nl.id, bool(nd.get("subscribed", True)))) + await sess.execute(delete(UserNewsletter).where(UserNewsletter.user_id == user.id)) + for nl_id, subbed in nl_rows: + sess.add(UserNewsletter(user_id=user.id, newsletter_id=nl_id, subscribed=subbed)) + await sess.flush() + + # Subscriptions + for sd in m.get("subscriptions") or []: + sid = sd.get("id") + if not sid: + continue + + tier_id: Optional[int] = None + if sd.get("tier"): + tier = await _upsert_tier(sess, sd["tier"]) + await sess.flush() + tier_id = tier.id + + res = await sess.execute(select(GhostSubscription).where(GhostSubscription.ghost_id == sid)) + sub = res.scalar_one_or_none() + if not sub: + sub = GhostSubscription(ghost_id=sid, user_id=user.id) + sess.add(sub) + + sub.user_id = user.id + sub.status = sd.get("status") or sub.status + sub.cadence = (sd.get("plan") or {}).get("interval") or sd.get("cadence") or sub.cadence + sub.price_amount = _price_cents(sd) + sub.price_currency = (sd.get("price") or {}).get("currency") or sub.price_currency + sub.stripe_customer_id = ( + (sd.get("customer") or {}).get("id") + or (sd.get("stripe") or {}).get("customer_id") + or sub.stripe_customer_id + ) + sub.stripe_subscription_id = ( + sd.get("stripe_subscription_id") + or (sd.get("stripe") or {}).get("subscription_id") + or sub.stripe_subscription_id + ) + if tier_id is not None: + sub.tier_id = tier_id + sub.raw = dict(sd) + flag_modified(sub, "raw") + + await sess.flush() + return user + + +# ===================================================== +# PUSH MEMBERS FROM LOCAL DB -> GHOST (DB -> Ghost) +# ===================================================== + +def _ghost_member_payload_base(u: User) -> dict: + """Compose writable Ghost member fields from local User, validating types.""" + email = _to_str_or_none(getattr(u, "email", None)) + payload: dict = {} + if email: + payload["email"] = email.lower() + + name = _to_str_or_none(getattr(u, "name", None)) + if name: + payload["name"] = name + + note = _to_str_or_none(getattr(u, "ghost_note", None)) + if note: + payload["note"] = note + + # If ghost_subscribed is None, default True (Ghost expects boolean) + subscribed = getattr(u, "ghost_subscribed", True) + payload["subscribed"] = bool(subscribed) + + return payload + +async def _newsletters_for_user(sess: AsyncSession, user_id: int) -> list[dict]: + """Return list of {'id': ghost_id, 'subscribed': bool} rows for Ghost API, excluding blanks.""" + q = await sess.execute( + select(GhostNewsletter.ghost_id, UserNewsletter.subscribed, GhostNewsletter.name) + .join(UserNewsletter, UserNewsletter.newsletter_id == GhostNewsletter.id) + .where(UserNewsletter.user_id == user_id) + ) + seen = set() + out: list[dict] = [] + for gid, subscribed, name in q.all(): + gid = (gid or "").strip() or None + name = (name or "").strip() or None + row: dict = {"subscribed": bool(subscribed)} + if gid: + key = ("id", gid) + if key in seen: + continue + row["id"] = gid + seen.add(key) + out.append(row) + elif name: + key = ("name", name.lower()) + if key in seen: + continue + row["name"] = name + seen.add(key) + out.append(row) + # else: skip + return out + +async def _labels_for_user(sess: AsyncSession, user_id: int) -> list[dict]: + """Return list of {'id': ghost_id} or {'name': name} for Ghost API, excluding blanks.""" + q = await sess.execute( + select(GhostLabel.ghost_id, GhostLabel.name) + .join(UserLabel, UserLabel.label_id == GhostLabel.id) + .where(UserLabel.user_id == user_id) + ) + seen = set() + out: list[dict] = [] + for gid, name in q.all(): + gid = (gid or "").strip() or None + name = (name or "").strip() or None + if gid: + key = ("id", gid) + if key not in seen: + out.append({"id": gid}) + seen.add(key) + elif name: + key = ("name", name.lower()) + if key not in seen: + out.append({"name": name}) + seen.add(key) + # else: skip empty label row + return out + + +async def _ghost_find_member_by_email(email: str) -> dict | None: + """Query Ghost for a member by email to resolve conflicts / missing IDs.""" + if not email: + return None + async with httpx.AsyncClient(timeout=20) as client: + resp = await client.get( + f"{GHOST_ADMIN_API_URL}/members/", + headers=_auth_header(), + params={"filter": f"email:{email}", "limit": 1}, + ) + resp.raise_for_status() + members = (resp.json() or {}).get("members") or [] + return members[0] if members else None + + +from urllib.parse import quote # make sure this import exists at top + +async def _ghost_find_member_by_email(email: str) -> Optional[dict]: + if not email: + return None + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get( + f"{GHOST_ADMIN_API_URL}/members/?filter=email:{quote(email)}&limit=1", + headers=_auth_header(), + ) + resp.raise_for_status() + members = resp.json().get("members") or [] + return members[0] if members else None + +async def _ghost_upsert_member(payload: dict, ghost_id: str | None = None) -> dict: + """Create/update a member, with sanitization + 5xx retry/backoff. + - Prefer PUT if ghost_id given. + - On 422: retry without name/note; if 'already exists', find-by-email then PUT. + - On 404: find-by-email and PUT; if still missing, POST create. + - On 5xx: small exponential backoff retry. + """ + safe_keys = ("email", "name", "note", "subscribed", "labels", "newsletters", "id") + pl_raw = {k: v for k, v in payload.items() if k in safe_keys} + pl = _sanitize_member_payload(pl_raw) + + async def _request_with_retry(client: httpx.AsyncClient, method: str, url: str, json: dict) -> httpx.Response: + delay = 0.5 + for attempt in range(3): + r = await client.request(method, url, headers=_auth_header(), json=json) + if r.status_code >= 500: + if attempt < 2: + await asyncio.sleep(delay) + delay *= 2 + continue + return r + return r # last response + + async with httpx.AsyncClient(timeout=30) as client: + + async def _put(mid: str, p: dict) -> dict: + r = await _request_with_retry( + client, "PUT", + f"{GHOST_ADMIN_API_URL}/members/{mid}/", + {"members": [p]}, + ) + if r.status_code == 404: + # Stale id: try by email, then create if absent + existing = await _ghost_find_member_by_email(p.get("email", "")) + if existing and existing.get("id"): + r2 = await _request_with_retry( + client, "PUT", + f"{GHOST_ADMIN_API_URL}/members/{existing['id']}/", + {"members": [p]}, + ) + r2.raise_for_status() + return (r2.json().get("members") or [None])[0] or {} + r3 = await _request_with_retry( + client, "POST", + f"{GHOST_ADMIN_API_URL}/members/", + {"members": [p]}, + ) + r3.raise_for_status() + return (r3.json().get("members") or [None])[0] or {} + + if r.status_code == 422: + body = (r.text or "").lower() + retry = dict(p) + dropped = False + if '"note"' in body or "for note" in body: + retry.pop("note", None); dropped = True + if '"name"' in body or "for name" in body: + retry.pop("name", None); dropped = True + if "labels.name" in body: + retry.pop("labels", None); dropped = True + if dropped: + r2 = await _request_with_retry( + client, "PUT", + f"{GHOST_ADMIN_API_URL}/members/{mid}/", + {"members": [retry]}, + ) + if r2.status_code == 404: + existing = await _ghost_find_member_by_email(retry.get("email", "")) + if existing and existing.get("id"): + r3 = await _request_with_retry( + client, "PUT", + f"{GHOST_ADMIN_API_URL}/members/{existing['id']}/", + {"members": [retry]}, + ) + r3.raise_for_status() + return (r3.json().get("members") or [None])[0] or {} + r3 = await _request_with_retry( + client, "POST", + f"{GHOST_ADMIN_API_URL}/members/", + {"members": [retry]}, + ) + r3.raise_for_status() + return (r3.json().get("members") or [None])[0] or {} + r2.raise_for_status() + return (r2.json().get("members") or [None])[0] or {} + r.raise_for_status() + return (r.json().get("members") or [None])[0] or {} + + async def _post_upsert(p: dict) -> dict: + r = await _request_with_retry( + client, "POST", + f"{GHOST_ADMIN_API_URL}/members/?upsert=true", + {"members": [p]}, + ) + if r.status_code == 422: + lower = (r.text or "").lower() + + # sanitize further name/note/labels on schema complaints + retry = dict(p) + changed = False + if '"note"' in lower or "for note" in lower: + retry.pop("note", None); changed = True + if '"name"' in lower or "for name" in lower: + retry.pop("name", None); changed = True + if "labels.name" in lower: + retry.pop("labels", None); changed = True + + if changed: + r2 = await _request_with_retry( + client, "POST", + f"{GHOST_ADMIN_API_URL}/members/?upsert=true", + {"members": [retry]}, + ) + if r2.status_code != 422: + r2.raise_for_status() + return (r2.json().get("members") or [None])[0] or {} + lower = (r2.text or "").lower() + + # existing email => find-by-email then PUT + if "already exists" in lower and "email address" in lower: + existing = await _ghost_find_member_by_email(p.get("email", "")) + if existing and existing.get("id"): + return await _put(existing["id"], p) + + # unrecoverable + raise httpx.HTTPStatusError( + "Validation error, cannot edit member.", + request=r.request, + response=r, + ) + r.raise_for_status() + return (r.json().get("members") or [None])[0] or {} + + if ghost_id: + return await _put(ghost_id, pl) + return await _post_upsert(pl) + +async def sync_member_to_ghost(sess: AsyncSession, user_id: int) -> Optional[str]: + res = await sess.execute(select(User).where(User.id == user_id)) + user = res.scalar_one_or_none() + if not user: + return None + + payload = _ghost_member_payload_base(user) + + labels = await _labels_for_user(sess, user.id) + if labels: + payload["labels"] = labels # Ghost accepts label ids on upsert + + ghost_member = await _ghost_upsert_member(payload, ghost_id=user.ghost_id) + + if ghost_member: + gm_id = ghost_member.get("id") + if gm_id and user.ghost_id != gm_id: + user.ghost_id = gm_id + user.ghost_raw = dict(ghost_member) + flag_modified(user, "ghost_raw") + await sess.flush() + return user.ghost_id or gm_id + return user.ghost_id + + +async def sync_members_to_ghost( + sess: AsyncSession, + changed_since: Optional[datetime] = None, + limit: Optional[int] = None, +) -> int: + """Upsert a batch of users to Ghost. Returns count processed.""" + stmt = select(User.id) + if changed_since: + stmt = stmt.where( + or_( + User.created_at >= changed_since, + and_(User.last_login_at != None, User.last_login_at >= changed_since), + ) + ) + if limit: + stmt = stmt.limit(limit) + + ids = [row[0] for row in (await sess.execute(stmt)).all()] + processed = 0 + for uid in ids: + try: + await sync_member_to_ghost(sess, uid) + processed += 1 + except httpx.HTTPStatusError as e: + # Log and continue; don't kill startup + print(f"[ghost sync] failed upsert for user {uid}: {e.response.status_code} {e.response.text}") + except Exception as e: + print(f"[ghost sync] failed upsert for user {uid}: {e}") + return processed + + +# ===================================================== +# Membership fetch/sync (Ghost -> DB) bulk + single +# ===================================================== + +async def fetch_all_members_from_ghost() -> list[dict[str, Any]]: + async with httpx.AsyncClient(timeout=60) as client: + resp = await client.get( + f"{GHOST_ADMIN_API_URL}/members/?include=labels,subscriptions,tiers,newsletters&limit=all", + headers=_auth_header(), + ) + resp.raise_for_status() + return resp.json().get("members", []) + + +async def sync_all_membership_from_ghost(sess: AsyncSession) -> None: + members = await fetch_all_members_from_ghost() + + # collect related lookups and ensure catalogs exist first (avoid FK races) + label_bucket: Dict[str, dict[str, Any]] = {} + tier_bucket: Dict[str, dict[str, Any]] = {} + newsletter_bucket: Dict[str, dict[str, Any]] = {} + + for m in members: + for l in m.get("labels") or []: + label_bucket[l["id"]] = l + for n in m.get("newsletters") or []: + newsletter_bucket[n["id"]] = n + for s in m.get("subscriptions") or []: + t = s.get("tier") + if isinstance(t, dict) and t.get("id"): + tier_bucket[t["id"]] = t + + for L in label_bucket.values(): + await _upsert_label(sess, L) + for T in tier_bucket.values(): + await _upsert_tier(sess, T) + for N in newsletter_bucket.values(): + await _upsert_newsletter(sess, N) + + # Users + for gm in members: + user = await _find_or_create_user_by_ghost_or_email(sess, gm) + await _apply_user_membership(sess, user, gm) + + # transaction auto-commits here + + +async def fetch_single_member_from_ghost(ghost_id: str) -> Optional[dict[str, Any]]: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get( + f"{GHOST_ADMIN_API_URL}/members/{ghost_id}/?include=labels,newsletters,subscriptions,tiers", + headers=_auth_header(), + ) + if resp.status_code == 404: + return None + resp.raise_for_status() + data = resp.json() + items = data.get("members") or data.get("member") or [] + if isinstance(items, dict): + return items + return (items[0] if items else None) + + +async def sync_single_member(sess: AsyncSession, ghost_id: str) -> None: + m = await fetch_single_member_from_ghost(ghost_id) + if m is None: + # If member deleted in Ghost, we won't delete local user here. + return + + # ensure catalogs for this payload + for l in m.get("labels") or []: + await _upsert_label(sess, l) + for n in m.get("newsletters") or []: + await _upsert_newsletter(sess, n) + for s in m.get("subscriptions") or []: + if isinstance(s.get("tier"), dict): + await _upsert_tier(sess, s["tier"]) + + user = await _find_or_create_user_by_ghost_or_email(sess, m) + await _apply_user_membership(sess, user, m) + # transaction auto-commits here + + +# ===================================================== +# Single-item content helpers (posts/authors/tags) +# ===================================================== + +async def fetch_single_post_from_ghost(ghost_id: str) -> Optional[dict[str, Any]]: + url = ( + f"{GHOST_ADMIN_API_URL}/posts/{ghost_id}/" + "?include=authors,tags&formats=html,plaintext,mobiledoc,lexical" + ) + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, headers=_auth_header()) + if resp.status_code == 404: + return None + resp.raise_for_status() + data = resp.json() + posts = data.get("posts") or [] + return posts[0] if posts else None + + +async def fetch_single_page_from_ghost(ghost_id: str) -> Optional[dict[str, Any]]: + url = ( + f"{GHOST_ADMIN_API_URL}/pages/{ghost_id}/" + "?include=authors,tags&formats=html,plaintext,mobiledoc,lexical" + ) + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, headers=_auth_header()) + if resp.status_code == 404: + return None + resp.raise_for_status() + data = resp.json() + pages = data.get("pages") or [] + return pages[0] if pages else None + + +async def fetch_single_author_from_ghost(ghost_id: str) -> Optional[dict[str, Any]]: + url = f"{GHOST_ADMIN_API_URL}/users/{ghost_id}/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, headers=_auth_header()) + if resp.status_code == 404: + return None + resp.raise_for_status() + data = resp.json() + users = data.get("users") or [] + return users[0] if users else None + + +async def fetch_single_tag_from_ghost(ghost_id: str) -> Optional[dict[str, Any]]: + url = f"{GHOST_ADMIN_API_URL}/tags/{ghost_id}/" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url, headers=_auth_header()) + if resp.status_code == 404: + return None + resp.raise_for_status() + data = resp.json() + tags = data.get("tags") or [] + return tags[0] if tags else None + + +def _build_ap_post_data(post, post_url: str, tag_objs: list) -> dict: + """Build rich AP object_data for a blog post/page. + + Produces a Note with HTML content (excerpt), feature image + inline + images as attachments, and tags as AP Hashtag objects. + """ + # Content HTML: title + excerpt + "Read more" link + parts: list[str] = [] + if post.title: + parts.append(f"

{html_escape(post.title)}

") + + body = post.plaintext or post.custom_excerpt or post.excerpt or "" + + if body: + for para in body.split("\n\n"): + para = para.strip() + if para: + parts.append(f"

{html_escape(para)}

") + + parts.append(f'

Read more \u2192

') + + # Hashtag links in content (Mastodon expects them inline too) + if tag_objs: + ht_links = [] + for t in tag_objs: + clean = t.slug.replace("-", "") + ht_links.append( + f'' + ) + parts.append(f'

{" ".join(ht_links)}

') + + obj: dict = { + "name": post.title or "", + "content": "\n".join(parts), + "url": post_url, + } + + # Attachments: feature image + inline images (max 4) + attachments: list[dict] = [] + seen: set[str] = set() + + if post.feature_image: + att: dict = {"type": "Image", "url": post.feature_image} + if post.feature_image_alt: + att["name"] = post.feature_image_alt + attachments.append(att) + seen.add(post.feature_image) + + if post.html: + for src in re.findall(r']+src="([^"]+)"', post.html): + if src not in seen and len(attachments) < 4: + attachments.append({"type": "Image", "url": src}) + seen.add(src) + + if attachments: + obj["attachment"] = attachments + + # AP Hashtag objects + if tag_objs: + obj["tag"] = [ + { + "type": "Hashtag", + "href": f"{post_url}tag/{t.slug}/", + "name": f"#{t.slug.replace('-', '')}", + } + for t in tag_objs + ] + + return obj + + +async def sync_single_post(sess: AsyncSession, ghost_id: str) -> None: + gp = await fetch_single_post_from_ghost(ghost_id) + if gp is None: + res = await sess.execute(select(Post).where(Post.ghost_id == ghost_id)) + obj = res.scalar_one_or_none() + if obj is not None and obj.deleted_at is None: + obj.deleted_at = utcnow() + return + + author_map: Dict[str, Author] = {} + tag_map: Dict[str, Tag] = {} + + for a in gp.get("authors") or []: + author_obj = await _upsert_author(sess, a) + author_map[a["id"]] = author_obj + if gp.get("primary_author"): + pa = gp["primary_author"] + author_obj = await _upsert_author(sess, pa) + author_map[pa["id"]] = author_obj + + for t in gp.get("tags") or []: + tag_obj = await _upsert_tag(sess, t) + tag_map[t["id"]] = tag_obj + if gp.get("primary_tag"): + pt = gp["primary_tag"] + tag_obj = await _upsert_tag(sess, pt) + tag_map[pt["id"]] = tag_obj + + post, old_status = await _upsert_post(sess, gp, author_map, tag_map) + + # Publish to federation inline (posts, not pages) + if not post.is_page and post.user_id: + from shared.services.federation_publish import try_publish + from shared.infrastructure.urls import app_url + post_url = app_url("blog", f"/{post.slug}/") + post_tags = [tag_map[t["id"]] for t in (gp.get("tags") or []) if t["id"] in tag_map] + + if post.status == "published": + activity_type = "Create" if old_status != "published" else "Update" + await try_publish( + sess, + user_id=post.user_id, + activity_type=activity_type, + object_type="Note", + object_data=_build_ap_post_data(post, post_url, post_tags), + source_type="Post", + source_id=post.id, + ) + elif old_status == "published" and post.status != "published": + await try_publish( + sess, + user_id=post.user_id, + activity_type="Delete", + object_type="Tombstone", + object_data={ + "id": post_url, + "formerType": "Note", + }, + source_type="Post", + source_id=post.id, + ) + + +async def sync_single_page(sess: AsyncSession, ghost_id: str) -> None: + gp = await fetch_single_page_from_ghost(ghost_id) + if gp is not None: + gp["page"] = True # Ghost /pages/ endpoint may omit this flag + if gp is None: + res = await sess.execute(select(Post).where(Post.ghost_id == ghost_id)) + obj = res.scalar_one_or_none() + if obj is not None and obj.deleted_at is None: + obj.deleted_at = utcnow() + return + + author_map: Dict[str, Author] = {} + tag_map: Dict[str, Tag] = {} + + for a in gp.get("authors") or []: + author_obj = await _upsert_author(sess, a) + author_map[a["id"]] = author_obj + if gp.get("primary_author"): + pa = gp["primary_author"] + author_obj = await _upsert_author(sess, pa) + author_map[pa["id"]] = author_obj + + for t in gp.get("tags") or []: + tag_obj = await _upsert_tag(sess, t) + tag_map[t["id"]] = tag_obj + if gp.get("primary_tag"): + pt = gp["primary_tag"] + tag_obj = await _upsert_tag(sess, pt) + tag_map[pt["id"]] = tag_obj + + post, old_status = await _upsert_post(sess, gp, author_map, tag_map) + + # Publish to federation inline (pages) + if post.user_id: + from shared.services.federation_publish import try_publish + from shared.infrastructure.urls import app_url + post_url = app_url("blog", f"/{post.slug}/") + post_tags = [tag_map[t["id"]] for t in (gp.get("tags") or []) if t["id"] in tag_map] + + if post.status == "published": + activity_type = "Create" if old_status != "published" else "Update" + await try_publish( + sess, + user_id=post.user_id, + activity_type=activity_type, + object_type="Note", + object_data=_build_ap_post_data(post, post_url, post_tags), + source_type="Post", + source_id=post.id, + ) + elif old_status == "published" and post.status != "published": + await try_publish( + sess, + user_id=post.user_id, + activity_type="Delete", + object_type="Tombstone", + object_data={ + "id": post_url, + "formerType": "Note", + }, + source_type="Post", + source_id=post.id, + ) + + +async def sync_single_author(sess: AsyncSession, ghost_id: str) -> None: + ga = await fetch_single_author_from_ghost(ghost_id) + if ga is None: + result = await sess.execute(select(Author).where(Author.ghost_id == ghost_id)) + author_obj = result.scalar_one_or_none() + if author_obj and author_obj.deleted_at is None: + author_obj.deleted_at = utcnow() + return + + await _upsert_author(sess, ga) + + +async def sync_single_tag(sess: AsyncSession, ghost_id: str) -> None: + gt = await fetch_single_tag_from_ghost(ghost_id) + if gt is None: + result = await sess.execute(select(Tag).where(Tag.ghost_id == ghost_id)) + tag_obj = result.scalar_one_or_none() + if tag_obj and tag_obj.deleted_at is None: + tag_obj.deleted_at = utcnow() + return + + await _upsert_tag(sess, gt) + + +# ---- explicit public exports (back-compat) ---- +__all__ = [ + # bulk content + "sync_all_content_from_ghost", + # bulk membership (user-centric) + "sync_all_membership_from_ghost", + # DB -> Ghost + "sync_member_to_ghost", + "sync_members_to_ghost", + # single fetch + "fetch_single_post_from_ghost", + "fetch_single_author_from_ghost", + "fetch_single_tag_from_ghost", + "fetch_single_member_from_ghost", + # single sync + "sync_single_post", + "sync_single_author", + "sync_single_tag", + "sync_single_member", +] diff --git a/blog/bp/blog/ghost/lexical_renderer.py b/blog/bp/blog/ghost/lexical_renderer.py new file mode 100644 index 0000000..fafe7b5 --- /dev/null +++ b/blog/bp/blog/ghost/lexical_renderer.py @@ -0,0 +1,668 @@ +""" +Lexical JSON → HTML renderer. + +Produces HTML matching Ghost's ``kg-*`` class conventions so the existing +``cards.css`` stylesheet works unchanged. + +Public API +---------- + render_lexical(doc) – Lexical JSON (dict or string) → HTML string +""" +from __future__ import annotations + +import html +import json +from typing import Callable + +import mistune + +# --------------------------------------------------------------------------- +# Registry +# --------------------------------------------------------------------------- + +_RENDERERS: dict[str, Callable[[dict], str]] = {} + + +def _renderer(node_type: str): + """Decorator — register a function as the renderer for *node_type*.""" + def decorator(fn: Callable[[dict], str]) -> Callable[[dict], str]: + _RENDERERS[node_type] = fn + return fn + return decorator + + +# --------------------------------------------------------------------------- +# Public entry point +# --------------------------------------------------------------------------- + +def render_lexical(doc: dict | str) -> str: + """Render a Lexical JSON document to an HTML string.""" + if isinstance(doc, str): + doc = json.loads(doc) + root = doc.get("root", doc) + return _render_children(root.get("children", [])) + + +# --------------------------------------------------------------------------- +# Core dispatch +# --------------------------------------------------------------------------- + +def _render_node(node: dict) -> str: + node_type = node.get("type", "") + renderer = _RENDERERS.get(node_type) + if renderer: + return renderer(node) + return "" + + +def _render_children(children: list[dict]) -> str: + return "".join(_render_node(c) for c in children) + + +# --------------------------------------------------------------------------- +# Text formatting +# --------------------------------------------------------------------------- + +# Lexical format bitmask +_FORMAT_BOLD = 1 +_FORMAT_ITALIC = 2 +_FORMAT_STRIKETHROUGH = 4 +_FORMAT_UNDERLINE = 8 +_FORMAT_CODE = 16 +_FORMAT_SUBSCRIPT = 32 +_FORMAT_SUPERSCRIPT = 64 +_FORMAT_HIGHLIGHT = 128 + +_FORMAT_TAGS: list[tuple[int, str, str]] = [ + (_FORMAT_BOLD, "", ""), + (_FORMAT_ITALIC, "", ""), + (_FORMAT_STRIKETHROUGH, "", ""), + (_FORMAT_UNDERLINE, "", ""), + (_FORMAT_CODE, "", ""), + (_FORMAT_SUBSCRIPT, "", ""), + (_FORMAT_SUPERSCRIPT, "", ""), + (_FORMAT_HIGHLIGHT, "", ""), +] + +# Element-level alignment from ``format`` field +_ALIGN_MAP = { + 1: "text-align: left", + 2: "text-align: center", + 3: "text-align: right", + 4: "text-align: justify", +} + + +def _align_style(node: dict) -> str: + fmt = node.get("format") + if isinstance(fmt, int) and fmt in _ALIGN_MAP: + return f' style="{_ALIGN_MAP[fmt]}"' + if isinstance(fmt, str) and fmt: + return f' style="text-align: {fmt}"' + return "" + + +def _wrap_format(text: str, fmt: int) -> str: + for mask, open_tag, close_tag in _FORMAT_TAGS: + if fmt & mask: + text = f"{open_tag}{text}{close_tag}" + return text + + +# --------------------------------------------------------------------------- +# Tier 1 — text nodes +# --------------------------------------------------------------------------- + +@_renderer("text") +def _text(node: dict) -> str: + text = html.escape(node.get("text", "")) + fmt = node.get("format", 0) + if isinstance(fmt, int) and fmt: + text = _wrap_format(text, fmt) + return text + + +@_renderer("linebreak") +def _linebreak(_node: dict) -> str: + return "
" + + +@_renderer("tab") +def _tab(_node: dict) -> str: + return "\t" + + +@_renderer("paragraph") +def _paragraph(node: dict) -> str: + inner = _render_children(node.get("children", [])) + if not inner: + inner = "
" + style = _align_style(node) + return f"{inner}

" + + +@_renderer("extended-text") +def _extended_text(node: dict) -> str: + return _paragraph(node) + + +@_renderer("heading") +def _heading(node: dict) -> str: + tag = node.get("tag", "h2") + inner = _render_children(node.get("children", [])) + style = _align_style(node) + return f"<{tag}{style}>{inner}" + + +@_renderer("extended-heading") +def _extended_heading(node: dict) -> str: + return _heading(node) + + +@_renderer("quote") +def _quote(node: dict) -> str: + inner = _render_children(node.get("children", [])) + return f"
{inner}
" + + +@_renderer("extended-quote") +def _extended_quote(node: dict) -> str: + return _quote(node) + + +@_renderer("aside") +def _aside(node: dict) -> str: + inner = _render_children(node.get("children", [])) + return f"" + + +@_renderer("link") +def _link(node: dict) -> str: + href = html.escape(node.get("url", ""), quote=True) + target = node.get("target", "") + rel = node.get("rel", "") + inner = _render_children(node.get("children", [])) + attrs = f' href="{href}"' + if target: + attrs += f' target="{html.escape(target, quote=True)}"' + if rel: + attrs += f' rel="{html.escape(rel, quote=True)}"' + return f"{inner}" + + +@_renderer("autolink") +def _autolink(node: dict) -> str: + return _link(node) + + +@_renderer("at-link") +def _at_link(node: dict) -> str: + return _link(node) + + +@_renderer("list") +def _list(node: dict) -> str: + tag = "ol" if node.get("listType") == "number" else "ul" + start = node.get("start") + inner = _render_children(node.get("children", [])) + attrs = "" + if tag == "ol" and start and start != 1: + attrs = f' start="{start}"' + return f"<{tag}{attrs}>{inner}" + + +@_renderer("listitem") +def _listitem(node: dict) -> str: + inner = _render_children(node.get("children", [])) + return f"
  • {inner}
  • " + + +@_renderer("horizontalrule") +def _horizontalrule(_node: dict) -> str: + return "
    " + + +@_renderer("code") +def _code(node: dict) -> str: + # Inline code nodes from Lexical — just render inner text + inner = _render_children(node.get("children", [])) + return f"{inner}" + + +@_renderer("codeblock") +def _codeblock(node: dict) -> str: + lang = node.get("language", "") + code = html.escape(node.get("code", "")) + cls = f' class="language-{html.escape(lang)}"' if lang else "" + return f'
    {code}
    ' + + +@_renderer("code-highlight") +def _code_highlight(node: dict) -> str: + text = html.escape(node.get("text", "")) + highlight_type = node.get("highlightType", "") + if highlight_type: + return f'{text}' + return text + + +# --------------------------------------------------------------------------- +# Tier 2 — common cards +# --------------------------------------------------------------------------- + +@_renderer("image") +def _image(node: dict) -> str: + src = node.get("src", "") + alt = node.get("alt", "") + caption = node.get("caption", "") + width = node.get("cardWidth", "") or node.get("width", "") + href = node.get("href", "") + + width_class = "" + if width == "wide": + width_class = " kg-width-wide" + elif width == "full": + width_class = " kg-width-full" + + img_tag = f'{html.escape(alt, quote=True)}' + if href: + img_tag = f'{img_tag}' + + parts = [f'
    '] + parts.append(img_tag) + if caption: + parts.append(f"
    {caption}
    ") + parts.append("
    ") + return "".join(parts) + + +@_renderer("gallery") +def _gallery(node: dict) -> str: + images = node.get("images", []) + if not images: + return "" + + rows = [] + for i in range(0, len(images), 3): + row_imgs = images[i:i + 3] + row_cls = f"kg-gallery-row" if len(row_imgs) <= 3 else "kg-gallery-row" + imgs_html = [] + for img in row_imgs: + src = img.get("src", "") + alt = img.get("alt", "") + caption = img.get("caption", "") + img_tag = f'{html.escape(alt, quote=True)}' + fig = f'" + imgs_html.append(fig) + rows.append(f'
    {"".join(imgs_html)}
    ') + + caption = node.get("caption", "") + caption_html = f"
    {caption}
    " if caption else "" + return ( + f'" + ) + + +@_renderer("html") +def _html_card(node: dict) -> str: + raw = node.get("html", "") + return f"{raw}" + + +@_renderer("markdown") +def _markdown(node: dict) -> str: + md_text = node.get("markdown", "") + rendered = mistune.html(md_text) + return f"{rendered}" + + +@_renderer("embed") +def _embed(node: dict) -> str: + embed_html = node.get("html", "") + caption = node.get("caption", "") + url = node.get("url", "") + caption_html = f"
    {caption}
    " if caption else "" + return ( + f'
    ' + f"{embed_html}{caption_html}
    " + ) + + +@_renderer("bookmark") +def _bookmark(node: dict) -> str: + url = node.get("url", "") + title = html.escape(node.get("metadata", {}).get("title", "") or node.get("title", "")) + description = html.escape(node.get("metadata", {}).get("description", "") or node.get("description", "")) + icon = node.get("metadata", {}).get("icon", "") or node.get("icon", "") + author = html.escape(node.get("metadata", {}).get("author", "") or node.get("author", "")) + publisher = html.escape(node.get("metadata", {}).get("publisher", "") or node.get("publisher", "")) + thumbnail = node.get("metadata", {}).get("thumbnail", "") or node.get("thumbnail", "") + caption = node.get("caption", "") + + icon_html = f'' if icon else "" + thumbnail_html = ( + f'
    ' + f'
    ' + ) if thumbnail else "" + + meta_parts = [] + if icon_html: + meta_parts.append(icon_html) + if author: + meta_parts.append(f'{author}') + if publisher: + meta_parts.append(f'{publisher}') + metadata_html = f'' if meta_parts else "" + + caption_html = f"
    {caption}
    " if caption else "" + + return ( + f'
    ' + f'' + f'
    ' + f'
    {title}
    ' + f'
    {description}
    ' + f'{metadata_html}' + f'
    ' + f'{thumbnail_html}' + f'
    ' + f'{caption_html}' + f'
    ' + ) + + +@_renderer("callout") +def _callout(node: dict) -> str: + color = node.get("backgroundColor", "grey") + emoji = node.get("calloutEmoji", "") + inner = _render_children(node.get("children", [])) + + emoji_html = f'
    {emoji}
    ' if emoji else "" + return ( + f'
    ' + f'{emoji_html}' + f'
    {inner}
    ' + f'
    ' + ) + + +@_renderer("button") +def _button(node: dict) -> str: + text = html.escape(node.get("buttonText", "")) + url = html.escape(node.get("buttonUrl", ""), quote=True) + alignment = node.get("alignment", "center") + return ( + f'
    ' + f'{text}' + f'
    ' + ) + + +@_renderer("toggle") +def _toggle(node: dict) -> str: + heading = node.get("heading", "") + # Toggle content is in children + inner = _render_children(node.get("children", [])) + return ( + f'
    ' + f'
    ' + f'

    {heading}

    ' + f'' + f'
    ' + f'
    {inner}
    ' + f'
    ' + ) + + +# --------------------------------------------------------------------------- +# Tier 3 — media & remaining cards +# --------------------------------------------------------------------------- + +@_renderer("audio") +def _audio(node: dict) -> str: + src = node.get("src", "") + title = html.escape(node.get("title", "")) + duration = node.get("duration", 0) + thumbnail = node.get("thumbnailSrc", "") + + duration_min = int(duration) // 60 + duration_sec = int(duration) % 60 + duration_str = f"{duration_min}:{duration_sec:02d}" + + if thumbnail: + thumb_html = ( + f'audio-thumbnail' + ) + else: + thumb_html = ( + '
    ' + '' + '
    ' + ) + + return ( + f'
    ' + f'{thumb_html}' + f'
    ' + f'
    {title}
    ' + f'
    ' + f'' + f'
    0:00
    ' + f'
    / {duration_str}
    ' + f'' + f'' + f'' + f'' + f'
    ' + f'
    ' + f'' + f'
    ' + ) + + +@_renderer("video") +def _video(node: dict) -> str: + src = node.get("src", "") + caption = node.get("caption", "") + width = node.get("cardWidth", "") + thumbnail = node.get("thumbnailSrc", "") or node.get("customThumbnailSrc", "") + loop = node.get("loop", False) + + width_class = "" + if width == "wide": + width_class = " kg-width-wide" + elif width == "full": + width_class = " kg-width-full" + + loop_attr = " loop" if loop else "" + poster_attr = f' poster="{html.escape(thumbnail, quote=True)}"' if thumbnail else "" + caption_html = f"
    {caption}
    " if caption else "" + + return ( + f'
    ' + f'
    ' + f'' + f'
    ' + f'{caption_html}' + f'
    ' + ) + + +@_renderer("file") +def _file(node: dict) -> str: + src = node.get("src", "") + title = html.escape(node.get("fileName", "") or node.get("title", "")) + caption = node.get("caption", "") + file_size = node.get("fileSize", 0) + file_name = html.escape(node.get("fileName", "")) + + # Format size + if file_size: + kb = file_size / 1024 + if kb < 1024: + size_str = f"{kb:.0f} KB" + else: + size_str = f"{kb / 1024:.1f} MB" + else: + size_str = "" + + caption_html = f'
    {caption}
    ' if caption else "" + size_html = f'
    {size_str}
    ' if size_str else "" + + return ( + f'' + ) + + +@_renderer("paywall") +def _paywall(_node: dict) -> str: + return "" + + +@_renderer("header") +def _header(node: dict) -> str: + heading = node.get("heading", "") + subheading = node.get("subheading", "") + size = node.get("size", "small") + style = node.get("style", "dark") + bg_image = node.get("backgroundImageSrc", "") + button_text = node.get("buttonText", "") + button_url = node.get("buttonUrl", "") + + bg_style = f' style="background-image: url({html.escape(bg_image, quote=True)})"' if bg_image else "" + heading_html = f"

    {heading}

    " if heading else "" + subheading_html = f"

    {subheading}

    " if subheading else "" + button_html = ( + f'{html.escape(button_text)}' + if button_text and button_url else "" + ) + + return ( + f'
    ' + f'{heading_html}{subheading_html}{button_html}' + f'
    ' + ) + + +@_renderer("signup") +def _signup(node: dict) -> str: + heading = node.get("heading", "") + subheading = node.get("subheading", "") + disclaimer = node.get("disclaimer", "") + button_text = html.escape(node.get("buttonText", "Subscribe")) + button_color = node.get("buttonColor", "") + bg_color = node.get("backgroundColor", "") + bg_image = node.get("backgroundImageSrc", "") + style = node.get("style", "dark") + + bg_style_parts = [] + if bg_color: + bg_style_parts.append(f"background-color: {bg_color}") + if bg_image: + bg_style_parts.append(f"background-image: url({html.escape(bg_image, quote=True)})") + style_attr = f' style="{"; ".join(bg_style_parts)}"' if bg_style_parts else "" + + heading_html = f"

    {heading}

    " if heading else "" + subheading_html = f"

    {subheading}

    " if subheading else "" + disclaimer_html = f'' if disclaimer else "" + btn_style = f' style="background-color: {button_color}"' if button_color else "" + + return ( + f'' + ) + + +@_renderer("product") +def _product(node: dict) -> str: + title = html.escape(node.get("productTitle", "") or node.get("title", "")) + description = node.get("productDescription", "") or node.get("description", "") + img_src = node.get("productImageSrc", "") + button_text = html.escape(node.get("buttonText", "")) + button_url = node.get("buttonUrl", "") + rating = node.get("rating", 0) + + img_html = ( + f'' + if img_src else "" + ) + button_html = ( + f'{button_text}' + if button_text and button_url else "" + ) + stars = "" + if rating: + active = int(rating) + stars_html = [] + for i in range(5): + cls = "kg-product-card-rating-active" if i < active else "" + stars_html.append( + f'' + f'' + f'' + ) + stars = f'
    {"".join(stars_html)}
    ' + + return ( + f'
    ' + f'{img_html}' + f'
    ' + f'

    {title}

    ' + f'{stars}' + f'
    {description}
    ' + f'{button_html}' + f'
    ' + f'
    ' + ) + + +@_renderer("email") +def _email(node: dict) -> str: + raw_html = node.get("html", "") + return f"{raw_html}" + + +@_renderer("email-cta") +def _email_cta(node: dict) -> str: + raw_html = node.get("html", "") + return f"{raw_html}" + + +@_renderer("call-to-action") +def _call_to_action(node: dict) -> str: + raw_html = node.get("html", "") + sponsor_label = node.get("sponsorLabel", "") + label_html = ( + f'{html.escape(sponsor_label)}' + if sponsor_label else "" + ) + return ( + f'
    ' + f'{label_html}{raw_html}' + f'
    ' + ) diff --git a/blog/bp/blog/ghost/lexical_validator.py b/blog/bp/blog/ghost/lexical_validator.py new file mode 100644 index 0000000..3cd39a2 --- /dev/null +++ b/blog/bp/blog/ghost/lexical_validator.py @@ -0,0 +1,86 @@ +""" +Server-side validation for Lexical editor JSON. + +Walk the document tree and reject any node whose ``type`` is not in +ALLOWED_NODE_TYPES. This is a belt-and-braces check: the Lexical +client already restricts which nodes can be created, but we validate +server-side too. +""" +from __future__ import annotations + +ALLOWED_NODE_TYPES: frozenset[str] = frozenset( + { + # Standard Lexical nodes + "root", + "paragraph", + "heading", + "quote", + "list", + "listitem", + "link", + "autolink", + "code", + "code-highlight", + "linebreak", + "text", + "horizontalrule", + "image", + "tab", + # Ghost "extended-*" variants + "extended-text", + "extended-heading", + "extended-quote", + # Ghost card types + "html", + "gallery", + "embed", + "bookmark", + "markdown", + "email", + "email-cta", + "button", + "callout", + "toggle", + "video", + "audio", + "file", + "product", + "header", + "signup", + "aside", + "codeblock", + "call-to-action", + "at-link", + "paywall", + } +) + + +def validate_lexical(doc: dict) -> tuple[bool, str | None]: + """Recursively validate a Lexical JSON document. + + Returns ``(True, None)`` when the document is valid, or + ``(False, reason)`` when an unknown node type is found. + """ + if not isinstance(doc, dict): + return False, "Document must be a JSON object" + + root = doc.get("root") + if not isinstance(root, dict): + return False, "Document must contain a 'root' object" + + return _walk(root) + + +def _walk(node: dict) -> tuple[bool, str | None]: + node_type = node.get("type") + if node_type is not None and node_type not in ALLOWED_NODE_TYPES: + return False, f"Disallowed node type: {node_type}" + + for child in node.get("children", []): + if isinstance(child, dict): + ok, reason = _walk(child) + if not ok: + return False, reason + + return True, None diff --git a/blog/bp/blog/ghost_db.py b/blog/bp/blog/ghost_db.py new file mode 100644 index 0000000..1e9eda6 --- /dev/null +++ b/blog/bp/blog/ghost_db.py @@ -0,0 +1,632 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Sequence, Tuple +from sqlalchemy import select, func, asc, desc, and_, or_ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload, joinedload + +from models.ghost_content import Post, Author, Tag, PostTag +from shared.models.page_config import PageConfig +from models.tag_group import TagGroup, TagGroupTag + + +class DBAPIError(Exception): + """Raised when our local DB returns something unexpected.""" + + +def _author_to_public(a: Optional[Author]) -> Optional[Dict[str, Any]]: + if a is None: + return None + if a.deleted_at is not None: + # treat deleted authors as missing + return None + return { + "id": a.ghost_id, + "slug": a.slug, + "name": a.name, + "profile_image": a.profile_image, + "cover_image": a.cover_image, + # expose more (bio, etc.) if needed + } + + +def _tag_to_public(t: Tag) -> Dict[str, Any]: + return { + "id": t.ghost_id, + "slug": t.slug, + "name": t.name, + "description": t.description, + "feature_image": t.feature_image, # fixed key + "visibility": t.visibility, + "deleted_at": t.deleted_at, + } + + +def _post_to_public(p: Post) -> Dict[str, Any]: + """ + Shape a Post to the public JSON used by the app, mirroring GhostClient._normalise_post. + """ + # Primary author: explicit or first available + primary_author = p.primary_author or (p.authors[0] if p.authors else None) + + # Primary tag: prefer explicit relationship, otherwise first public/non-deleted tag + primary_tag = getattr(p, "primary_tag", None) + if primary_tag is None: + public_tags = [ + t for t in (p.tags or []) + if t.deleted_at is None and (t.visibility or "public") == "public" + ] + primary_tag = public_tags[0] if public_tags else None + + return { + "id": p.id, + "ghost_id": p.ghost_id, + "slug": p.slug, + "title": p.title, + "html": p.html, + "is_page": p.is_page, + "excerpt": p.custom_excerpt or p.excerpt, + "custom_excerpt": p.custom_excerpt, + "published_at": p.published_at, + "updated_at": p.updated_at, + "visibility": p.visibility, + "status": p.status, + "deleted_at": p.deleted_at, + "feature_image": p.feature_image, + "user_id": p.user_id, + "publish_requested": p.publish_requested, + "primary_author": _author_to_public(primary_author), + "primary_tag": _tag_to_public(primary_tag) if primary_tag else None, + "tags": [ + _tag_to_public(t) + for t in (p.tags or []) + if t.deleted_at is None and (t.visibility or "public") == "public" + ], + "authors": [ + _author_to_public(a) + for a in (p.authors or []) + if a and a.deleted_at is None + ], + } + + +class DBClient: + """ + Drop-in replacement for GhostClient, but served from our mirrored tables. + Call methods with an AsyncSession. + """ + + def __init__(self, session: AsyncSession): + self.sess = session + + async def list_posts( + self, + limit: int = 10, + page: int = 1, + selected_tags: Optional[Sequence[str]] = None, + selected_authors: Optional[Sequence[str]] = None, + search: Optional[str] = None, + drafts: bool = False, + drafts_user_id: Optional[int] = None, + exclude_covered_tag_ids: Optional[Sequence[int]] = None, + ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + """ + List published posts, optionally filtered by tags/authors and a search term. + When drafts=True, lists draft posts instead (filtered by drafts_user_id if given). + Returns (posts, pagination). + """ + + # ---- base visibility filters + if drafts: + base_filters = [ + Post.deleted_at.is_(None), + Post.status == "draft", + Post.is_page.is_(False), + ] + if drafts_user_id is not None: + base_filters.append(Post.user_id == drafts_user_id) + else: + base_filters = [ + Post.deleted_at.is_(None), + Post.status == "published", + Post.is_page.is_(False), + ] + + q = select(Post).where(*base_filters) + + # ---- TAG FILTER (matches any tag on the post) + if selected_tags: + tag_slugs = list(selected_tags) + q = q.where( + Post.tags.any( + and_( + Tag.slug.in_(tag_slugs), + Tag.deleted_at.is_(None), + ) + ) + ) + + # ---- EXCLUDE-COVERED FILTER ("etc" mode: posts NOT covered by any group) + if exclude_covered_tag_ids: + covered_sq = ( + select(PostTag.post_id) + .join(Tag, Tag.id == PostTag.tag_id) + .where( + Tag.id.in_(list(exclude_covered_tag_ids)), + Tag.deleted_at.is_(None), + ) + ) + q = q.where(Post.id.notin_(covered_sq)) + + # ---- AUTHOR FILTER (matches primary or any author) + if selected_authors: + author_slugs = list(selected_authors) + q = q.where( + or_( + Post.primary_author.has( + and_( + Author.slug.in_(author_slugs), + Author.deleted_at.is_(None), + ) + ), + Post.authors.any( + and_( + Author.slug.in_(author_slugs), + Author.deleted_at.is_(None), + ) + ), + ) + ) + + # ---- SEARCH FILTER (title OR excerpt OR plaintext contains) + if search: + term = f"%{search.strip().lower()}%" + q = q.where( + or_( + func.lower(func.coalesce(Post.title, "")).like(term), + func.lower(func.coalesce(Post.excerpt, "")).like(term), + func.lower(func.coalesce(Post.plaintext,"")).like(term), + ) + ) + + # ---- ordering + if drafts: + q = q.order_by(desc(Post.updated_at)) + else: + q = q.order_by(desc(Post.published_at)) + + # ---- pagination math + if page < 1: + page = 1 + offset_val = (page - 1) * limit + + # ---- total count with SAME filters (including tag/author/search) + q_no_limit = q.with_only_columns(Post.id).order_by(None) + count_q = select(func.count()).select_from(q_no_limit.subquery()) + total = int((await self.sess.execute(count_q)).scalar() or 0) + + # ---- eager load relationships to avoid N+1 / greenlet issues + q = ( + q.options( + joinedload(Post.primary_author), + joinedload(Post.primary_tag), + selectinload(Post.authors), + selectinload(Post.tags), + ) + .limit(limit) + .offset(offset_val) + ) + + rows: List[Post] = list((await self.sess.execute(q)).scalars()) + posts = [_post_to_public(p) for p in rows] + + # ---- search_count: reflect same filters + search (i.e., equals total once filters applied) + search_count = total + + pages_total = (total + limit - 1) // limit if limit else 1 + pagination = { + "page": page, + "limit": limit, + "pages": pages_total, + "total": total, + "search_count": search_count, + "next": page + 1 if page < pages_total else None, + "prev": page - 1 if page > 1 else None, + } + + return posts, pagination + + async def list_pages( + self, + limit: int = 10, + page: int = 1, + search: Optional[str] = None, + ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + """ + List published pages (is_page=True) with their PageConfig eagerly loaded. + Returns (pages, pagination). + """ + base_filters = [ + Post.deleted_at.is_(None), + Post.status == "published", + Post.is_page.is_(True), + ] + + q = select(Post).where(*base_filters) + + if search: + term = f"%{search.strip().lower()}%" + q = q.where( + or_( + func.lower(func.coalesce(Post.title, "")).like(term), + func.lower(func.coalesce(Post.excerpt, "")).like(term), + func.lower(func.coalesce(Post.plaintext, "")).like(term), + ) + ) + + q = q.order_by(desc(Post.published_at)) + + if page < 1: + page = 1 + offset_val = (page - 1) * limit + + q_no_limit = q.with_only_columns(Post.id).order_by(None) + count_q = select(func.count()).select_from(q_no_limit.subquery()) + total = int((await self.sess.execute(count_q)).scalar() or 0) + + q = ( + q.options( + joinedload(Post.primary_author), + joinedload(Post.primary_tag), + selectinload(Post.authors), + selectinload(Post.tags), + joinedload(Post.page_config), + ) + .limit(limit) + .offset(offset_val) + ) + + rows: List[Post] = list((await self.sess.execute(q)).scalars()) + + def _page_to_public(p: Post) -> Dict[str, Any]: + d = _post_to_public(p) + pc = p.page_config + d["features"] = pc.features if pc else {} + return d + + pages_list = [_page_to_public(p) for p in rows] + + pages_total = (total + limit - 1) // limit if limit else 1 + pagination = { + "page": page, + "limit": limit, + "pages": pages_total, + "total": total, + "next": page + 1 if page < pages_total else None, + "prev": page - 1 if page > 1 else None, + } + + return pages_list, pagination + + async def posts_by_slug( + self, + slug: str, + include: Sequence[str] = ("tags", "authors"), + fields: Sequence[str] = ( + "id", + "slug", + "title", + "html", + "excerpt", + "custom_excerpt", + "published_at", + "feature_image", + ), + include_drafts: bool = False, + ) -> List[Dict[str, Any]]: + """ + Return posts (usually 1) matching this slug. + + Only returns published, non-deleted posts by default. + When include_drafts=True, also returns draft posts (for admin access). + + Eager-load related objects via selectinload/joinedload so we don't N+1 when + serializing in _post_to_public(). + """ + + # Build .options(...) dynamically based on `include` + load_options = [] + + # Tags + if "tags" in include: + load_options.append(selectinload(Post.tags)) + if hasattr(Post, "primary_tag"): + # joinedload is fine too; selectin keeps a single extra roundtrip + load_options.append(selectinload(Post.primary_tag)) + + # Authors + if "authors" in include: + if hasattr(Post, "primary_author"): + load_options.append(selectinload(Post.primary_author)) + if hasattr(Post, "authors"): + load_options.append(selectinload(Post.authors)) + + filters = [Post.deleted_at.is_(None), Post.slug == slug] + if not include_drafts: + filters.append(Post.status == "published") + + q = ( + select(Post) + .where(*filters) + .order_by(desc(Post.published_at)) + .options(*load_options) + ) + + result = await self.sess.execute(q) + rows: List[Post] = list(result.scalars()) + + return [(_post_to_public(p), p) for p in rows] + + async def list_tags( + self, + limit: int = 5000, + page: int = 1, + is_page=False, + ) -> List[Dict[str, Any]]: + """ + Return public, not-soft-deleted tags. + Include published_post_count = number of published (not deleted) posts using that tag. + """ + + if page < 1: + page = 1 + offset_val = (page - 1) * limit + + # Subquery: count published posts per tag + tag_post_counts_sq = ( + select( + PostTag.tag_id.label("tag_id"), + func.count().label("published_post_count"), + ) + .select_from(PostTag) + .join(Post, Post.id == PostTag.post_id) + .where( + Post.deleted_at.is_(None), + Post.published_at.is_not(None), + Post.is_page.is_(is_page), + ) + .group_by(PostTag.tag_id) + .subquery() + ) + + q = ( + select( + Tag, + func.coalesce(tag_post_counts_sq.c.published_post_count, 0).label( + "published_post_count" + ), + ) + .outerjoin( + tag_post_counts_sq, + tag_post_counts_sq.c.tag_id == Tag.id, + ) + .where( + Tag.deleted_at.is_(None), + (Tag.visibility == "public") | (Tag.visibility.is_(None)), + func.coalesce(tag_post_counts_sq.c.published_post_count, 0) > 0, + ) + .order_by(desc(func.coalesce(tag_post_counts_sq.c.published_post_count, 0)), asc(Tag.name)) + .limit(limit) + .offset(offset_val) + ) + + result = await self.sess.execute(q) + + # result will return rows like (Tag, published_post_count) + rows = list(result.all()) + + tags = [ + { + "id": tag.ghost_id, + "slug": tag.slug, + "name": tag.name, + "description": tag.description, + "feature_image": tag.feature_image, + "visibility": tag.visibility, + "published_post_count": count, + } + for (tag, count) in rows + ] + + return tags + + async def list_authors( + self, + limit: int = 5000, + page: int = 1, + is_page=False, + ) -> List[Dict[str, Any]]: + """ + Return non-deleted authors. + Include published_post_count = number of published (not deleted) posts by that author + (counted via Post.primary_author_id). + """ + + if page < 1: + page = 1 + offset_val = (page - 1) * limit + + # Subquery: count published posts per primary author + author_post_counts_sq = ( + select( + Post.primary_author_id.label("author_id"), + func.count().label("published_post_count"), + ) + .where( + Post.deleted_at.is_(None), + Post.published_at.is_not(None), + Post.is_page.is_(is_page), + ) + .group_by(Post.primary_author_id) + .subquery() + ) + + q = ( + select( + Author, + func.coalesce(author_post_counts_sq.c.published_post_count, 0).label( + "published_post_count" + ), + ) + .outerjoin( + author_post_counts_sq, + author_post_counts_sq.c.author_id == Author.id, + ) + .where( + Author.deleted_at.is_(None), + ) + .order_by(asc(Author.name)) + .limit(limit) + .offset(offset_val) + ) + + result = await self.sess.execute(q) + rows = list(result.all()) + + authors = [ + { + "id": a.ghost_id, + "slug": a.slug, + "name": a.name, + "bio": a.bio, + "profile_image": a.profile_image, + "cover_image": a.cover_image, + "website": a.website, + "location": a.location, + "facebook": a.facebook, + "twitter": a.twitter, + "published_post_count": count, + } + for (a, count) in rows + ] + + return authors + + async def count_drafts(self, user_id: Optional[int] = None) -> int: + """Count draft (non-page, non-deleted) posts, optionally for a single user.""" + q = select(func.count()).select_from(Post).where( + Post.deleted_at.is_(None), + Post.status == "draft", + Post.is_page.is_(False), + ) + if user_id is not None: + q = q.where(Post.user_id == user_id) + return int((await self.sess.execute(q)).scalar() or 0) + + async def list_tag_groups_with_counts(self) -> List[Dict[str, Any]]: + """ + Return all tag groups with aggregated published post counts. + Each group dict includes a `tag_slugs` list and `tag_ids` list. + Count = distinct published posts having ANY member tag. + Ordered by sort_order, name. + """ + # Subquery: distinct published post IDs per tag group + post_count_sq = ( + select( + TagGroupTag.tag_group_id.label("group_id"), + func.count(func.distinct(PostTag.post_id)).label("post_count"), + ) + .select_from(TagGroupTag) + .join(PostTag, PostTag.tag_id == TagGroupTag.tag_id) + .join(Post, Post.id == PostTag.post_id) + .where( + Post.deleted_at.is_(None), + Post.published_at.is_not(None), + Post.is_page.is_(False), + ) + .group_by(TagGroupTag.tag_group_id) + .subquery() + ) + + q = ( + select( + TagGroup, + func.coalesce(post_count_sq.c.post_count, 0).label("post_count"), + ) + .outerjoin(post_count_sq, post_count_sq.c.group_id == TagGroup.id) + .order_by(asc(TagGroup.sort_order), asc(TagGroup.name)) + ) + + rows = list((await self.sess.execute(q)).all()) + + groups = [] + for tg, count in rows: + # Fetch member tag slugs + ids for this group + tag_rows = list( + (await self.sess.execute( + select(Tag.slug, Tag.id) + .join(TagGroupTag, TagGroupTag.tag_id == Tag.id) + .where( + TagGroupTag.tag_group_id == tg.id, + Tag.deleted_at.is_(None), + (Tag.visibility == "public") | (Tag.visibility.is_(None)), + ) + )).all() + ) + groups.append({ + "id": tg.id, + "name": tg.name, + "slug": tg.slug, + "feature_image": tg.feature_image, + "colour": tg.colour, + "sort_order": tg.sort_order, + "post_count": count, + "tag_slugs": [r[0] for r in tag_rows], + "tag_ids": [r[1] for r in tag_rows], + }) + + return groups + + async def count_etc_posts(self, assigned_tag_ids: List[int]) -> int: + """ + Count published posts not covered by any tag group. + Includes posts with no tags and posts whose tags are all unassigned. + """ + base = [ + Post.deleted_at.is_(None), + Post.published_at.is_not(None), + Post.is_page.is_(False), + ] + if assigned_tag_ids: + covered_sq = ( + select(PostTag.post_id) + .join(Tag, Tag.id == PostTag.tag_id) + .where( + Tag.id.in_(assigned_tag_ids), + Tag.deleted_at.is_(None), + ) + ) + base.append(Post.id.notin_(covered_sq)) + + q = select(func.count()).select_from(Post).where(*base) + return int((await self.sess.execute(q)).scalar() or 0) + + async def list_drafts(self) -> List[Dict[str, Any]]: + """Return all draft (non-page, non-deleted) posts, newest-updated first.""" + q = ( + select(Post) + .where( + Post.deleted_at.is_(None), + Post.status == "draft", + Post.is_page.is_(False), + ) + .order_by(desc(Post.updated_at)) + .options( + joinedload(Post.primary_author), + joinedload(Post.primary_tag), + selectinload(Post.authors), + selectinload(Post.tags), + ) + ) + rows: List[Post] = list((await self.sess.execute(q)).scalars()) + return [_post_to_public(p) for p in rows] diff --git a/blog/bp/blog/routes.py b/blog/bp/blog/routes.py new file mode 100644 index 0000000..e6a6336 --- /dev/null +++ b/blog/bp/blog/routes.py @@ -0,0 +1,369 @@ +from __future__ import annotations + +#from quart import Blueprint, g + +import json +import os + +from quart import ( + request, + render_template, + make_response, + g, + Blueprint, + redirect, + url_for, +) +from .ghost_db import DBClient # adjust import path +from shared.db.session import get_session +from .filters.qs import makeqs_factory, decode +from .services.posts_data import posts_data +from .services.pages_data import pages_data + +from shared.browser.app.redis_cacher import cache_page, invalidate_tag_cache +from shared.browser.app.utils.htmx import is_htmx_request +from shared.browser.app.authz import require_admin +from shared.utils import host_url + +def register(url_prefix, title): + blogs_bp = Blueprint("blog", __name__, url_prefix) + + from .web_hooks.routes import ghost_webhooks + blogs_bp.register_blueprint(ghost_webhooks) + + from .ghost.editor_api import editor_api_bp + blogs_bp.register_blueprint(editor_api_bp) + + + + from ..post.routes import register as register_blog + blogs_bp.register_blueprint( + register_blog(), + ) + + from .admin.routes import register as register_tag_groups_admin + blogs_bp.register_blueprint(register_tag_groups_admin()) + + + @blogs_bp.before_app_serving + async def init(): + from .ghost.ghost_sync import ( + sync_all_content_from_ghost, + sync_all_membership_from_ghost, + ) + + async with get_session() as s: + await sync_all_content_from_ghost(s) + await sync_all_membership_from_ghost(s) + await s.commit() + + @blogs_bp.before_request + def route(): + g.makeqs_factory = makeqs_factory + + + @blogs_bp.context_processor + async def inject_root(): + return { + "blog_title": title, + "qs": makeqs_factory()(), + "unsplash_api_key": os.environ.get("UNSPLASH_ACCESS_KEY", ""), + } + + SORT_MAP = { + "newest": "published_at DESC", + "oldest": "published_at ASC", + "az": "title ASC", + "za": "title DESC", + "featured": "featured DESC, published_at DESC", + } + + @blogs_bp.get("/") + async def home(): + """Render the Ghost page with slug 'home' as the site homepage.""" + from ..post.services.post_data import post_data as _post_data + from shared.config import config as get_config + from shared.infrastructure.cart_identity import current_cart_identity + from shared.services.registry import services as svc + from shared.infrastructure.fragments import fetch_fragment, fetch_fragments + + p_data = await _post_data("home", g.s, include_drafts=False) + if not p_data: + # Fall back to blog index if "home" page doesn't exist yet + return redirect(host_url(url_for("blog.index"))) + + g.post_data = p_data + + # Build the same context the post blueprint's context_processor provides + db_post_id = p_data["post"]["id"] + post_slug = p_data["post"]["slug"] + + # Fetch container nav fragments from events + market + paginate_url = url_for( + 'blog.post.widget_paginate', + slug=post_slug, widget_domain='calendar', + ) + nav_params = { + "container_type": "page", + "container_id": str(db_post_id), + "post_slug": post_slug, + "paginate_url": paginate_url, + } + events_nav_html, market_nav_html = await fetch_fragments([ + ("events", "container-nav", nav_params), + ("market", "container-nav", nav_params), + ]) + container_nav_html = events_nav_html + market_nav_html + + ctx = { + **p_data, + "base_title": f"{get_config()['title']} {p_data['post']['title']}", + "container_nav_html": container_nav_html, + } + + # Page cart badge + if p_data["post"].get("is_page"): + ident = current_cart_identity() + page_summary = await svc.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + page_slug=post_slug, + ) + ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count + ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total) + + if not is_htmx_request(): + html = await render_template("_types/home/index.html", **ctx) + else: + html = await render_template("_types/home/_oob_elements.html", **ctx) + return await make_response(html) + + @blogs_bp.get("/index") + @blogs_bp.get("/index/") + async def index(): + """Blog listing — moved from / to /index.""" + + q = decode() + content_type = request.args.get("type", "posts") + + if content_type == "pages": + data = await pages_data(g.s, q.page, q.search) + context = { + **data, + "content_type": "pages", + "search": q.search, + "selected_tags": (), + "selected_authors": (), + "selected_groups": (), + "sort": None, + "view": None, + "drafts": None, + "draft_count": 0, + "tags": [], + "authors": [], + "tag_groups": [], + "posts": data.get("pages", []), + } + if not is_htmx_request(): + html = await render_template("_types/blog/index.html", **context) + elif q.page > 1: + html = await render_template("_types/blog/_page_cards.html", **context) + else: + html = await render_template("_types/blog/_oob_elements.html", **context) + return await make_response(html) + + # Default: posts listing + # Drafts filter requires login; ignore if not logged in + show_drafts = bool(q.drafts and g.user) + is_admin = bool((g.get("rights") or {}).get("admin")) + drafts_user_id = None if (not show_drafts or is_admin) else g.user.id + + # For the draft count badge: admin sees all drafts, non-admin sees own + count_drafts_uid = None if (g.user and is_admin) else (g.user.id if g.user else False) + + data = await posts_data( + g.s, q.page, q.search, q.sort, q.selected_tags, q.selected_authors, q.liked, + drafts=show_drafts, drafts_user_id=drafts_user_id, + count_drafts_for_user_id=count_drafts_uid, + selected_groups=q.selected_groups, + ) + + context = { + **data, + "content_type": "posts", + "selected_tags": q.selected_tags, + "selected_authors": q.selected_authors, + "selected_groups": q.selected_groups, + "sort": q.sort, + "search": q.search, + "view": q.view, + "drafts": q.drafts if show_drafts else None, + } + + # Determine which template to use based on request type and pagination + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/blog/index.html", **context) + elif q.page > 1: + # HTMX pagination: just blog cards + sentinel + html = await render_template("_types/blog/_cards.html", **context) + else: + # HTMX navigation (page 1): main panel + OOB elements + #main_panel = await render_template("_types/blog/_main_panel.html", **context) + html = await render_template("_types/blog/_oob_elements.html", **context) + #html = oob_elements + main_panel + + return await make_response(html) + + @blogs_bp.get("/new/") + @require_admin + async def new_post(): + if not is_htmx_request(): + html = await render_template("_types/blog_new/index.html") + else: + html = await render_template("_types/blog_new/_oob_elements.html") + return await make_response(html) + + @blogs_bp.post("/new/") + @require_admin + async def new_post_save(): + from .ghost.ghost_posts import create_post + from .ghost.lexical_validator import validate_lexical + from .ghost.ghost_sync import sync_single_post + + form = await request.form + title = form.get("title", "").strip() or "Untitled" + lexical_raw = form.get("lexical", "") + status = form.get("status", "draft") + feature_image = form.get("feature_image", "").strip() + custom_excerpt = form.get("custom_excerpt", "").strip() + feature_image_caption = form.get("feature_image_caption", "").strip() + + # Validate + try: + lexical_doc = json.loads(lexical_raw) + except (json.JSONDecodeError, TypeError): + html = await render_template( + "_types/blog_new/index.html", + save_error="Invalid JSON in editor content.", + ) + return await make_response(html, 400) + + ok, reason = validate_lexical(lexical_doc) + if not ok: + html = await render_template( + "_types/blog_new/index.html", + save_error=reason, + ) + return await make_response(html, 400) + + # Create in Ghost + ghost_post = await create_post( + title=title, + lexical_json=lexical_raw, + status=status, + feature_image=feature_image or None, + custom_excerpt=custom_excerpt or None, + feature_image_caption=feature_image_caption or None, + ) + + # Sync to local DB + await sync_single_post(g.s, ghost_post["id"]) + await g.s.flush() + + # Set user_id on the newly created post + from models.ghost_content import Post + from sqlalchemy import select + local_post = (await g.s.execute( + select(Post).where(Post.ghost_id == ghost_post["id"]) + )).scalar_one_or_none() + if local_post and local_post.user_id is None: + local_post.user_id = g.user.id + await g.s.flush() + + # Clear blog listing cache + await invalidate_tag_cache("blog") + + # Redirect to the edit page (post is likely a draft, so public detail would 404) + return redirect(host_url(url_for("blog.post.admin.edit", slug=ghost_post["slug"]))) + + + @blogs_bp.get("/new-page/") + @require_admin + async def new_page(): + if not is_htmx_request(): + html = await render_template("_types/blog_new/index.html", is_page=True) + else: + html = await render_template("_types/blog_new/_oob_elements.html", is_page=True) + return await make_response(html) + + @blogs_bp.post("/new-page/") + @require_admin + async def new_page_save(): + from .ghost.ghost_posts import create_page + from .ghost.lexical_validator import validate_lexical + from .ghost.ghost_sync import sync_single_page + + form = await request.form + title = form.get("title", "").strip() or "Untitled" + lexical_raw = form.get("lexical", "") + status = form.get("status", "draft") + feature_image = form.get("feature_image", "").strip() + custom_excerpt = form.get("custom_excerpt", "").strip() + feature_image_caption = form.get("feature_image_caption", "").strip() + + # Validate + try: + lexical_doc = json.loads(lexical_raw) + except (json.JSONDecodeError, TypeError): + html = await render_template( + "_types/blog_new/index.html", + save_error="Invalid JSON in editor content.", + is_page=True, + ) + return await make_response(html, 400) + + ok, reason = validate_lexical(lexical_doc) + if not ok: + html = await render_template( + "_types/blog_new/index.html", + save_error=reason, + is_page=True, + ) + return await make_response(html, 400) + + # Create in Ghost (as page) + ghost_page = await create_page( + title=title, + lexical_json=lexical_raw, + status=status, + feature_image=feature_image or None, + custom_excerpt=custom_excerpt or None, + feature_image_caption=feature_image_caption or None, + ) + + # Sync to local DB (uses pages endpoint) + await sync_single_page(g.s, ghost_page["id"]) + await g.s.flush() + + # Set user_id on the newly created page + from models.ghost_content import Post + from sqlalchemy import select + local_post = (await g.s.execute( + select(Post).where(Post.ghost_id == ghost_page["id"]) + )).scalar_one_or_none() + if local_post and local_post.user_id is None: + local_post.user_id = g.user.id + await g.s.flush() + + # Clear blog listing cache + await invalidate_tag_cache("blog") + + # Redirect to the page admin + return redirect(host_url(url_for("blog.post.admin.edit", slug=ghost_page["slug"]))) + + + @blogs_bp.get("/drafts/") + async def drafts(): + return redirect(host_url(url_for("blog.index")) + "?drafts=1") + + return blogs_bp \ No newline at end of file diff --git a/blog/bp/blog/services/pages_data.py b/blog/bp/blog/services/pages_data.py new file mode 100644 index 0000000..cc88fa1 --- /dev/null +++ b/blog/bp/blog/services/pages_data.py @@ -0,0 +1,18 @@ +from ..ghost_db import DBClient + + +async def pages_data(session, page, search): + client = DBClient(session) + + pages, pagination = await client.list_pages( + limit=10, + page=page, + search=search, + ) + + return { + "pages": pages, + "page": pagination.get("page", page), + "total_pages": pagination.get("pages", 1), + "search": search, + } diff --git a/blog/bp/blog/services/posts_data.py b/blog/bp/blog/services/posts_data.py new file mode 100644 index 0000000..3203aae --- /dev/null +++ b/blog/bp/blog/services/posts_data.py @@ -0,0 +1,142 @@ +import re + +from ..ghost_db import DBClient # adjust import path +from sqlalchemy import select +from models.ghost_content import PostLike +from shared.infrastructure.fragments import fetch_fragment +from quart import g + +async def posts_data( + session, + page, search, sort, selected_tags, selected_authors, liked, + drafts=False, drafts_user_id=None, count_drafts_for_user_id=None, + selected_groups=(), + ): + client = DBClient(session) + + # --- Tag-group resolution --- + tag_groups = await client.list_tag_groups_with_counts() + + # Collect all assigned tag IDs across groups + all_assigned_tag_ids = [] + for grp in tag_groups: + all_assigned_tag_ids.extend(grp["tag_ids"]) + + # Build slug-lookup for groups + group_by_slug = {grp["slug"]: grp for grp in tag_groups} + + # Resolve selected group → post filtering + # Groups and tags are mutually exclusive — groups override tags when set + effective_tags = selected_tags + etc_mode_tag_ids = None # set when "etc" is selected + if selected_groups: + group_slug = selected_groups[0] + if group_slug == "etc": + # etc = posts NOT covered by any group (includes untagged) + etc_mode_tag_ids = all_assigned_tag_ids + effective_tags = () + elif group_slug in group_by_slug: + effective_tags = tuple(group_by_slug[group_slug]["tag_slugs"]) + + # Compute "etc" virtual group + etc_count = await client.count_etc_posts(all_assigned_tag_ids) + if etc_count > 0 or (selected_groups and selected_groups[0] == "etc"): + tag_groups.append({ + "id": None, + "name": "etc", + "slug": "etc", + "feature_image": None, + "colour": None, + "sort_order": 999999, + "post_count": etc_count, + "tag_slugs": [], + "tag_ids": [], + }) + + posts, pagination = await client.list_posts( + limit=10, + page=page, + selected_tags=effective_tags, + selected_authors=selected_authors, + search=search, + drafts=drafts, + drafts_user_id=drafts_user_id, + exclude_covered_tag_ids=etc_mode_tag_ids, + ) + + # Get all post IDs in this batch + post_ids = [p["id"] for p in posts] + + # Add is_liked field to each post for current user + if g.user: + # Fetch all likes for this user and these posts in one query + liked_posts = await session.execute( + select(PostLike.post_id).where( + PostLike.user_id == g.user.id, + PostLike.post_id.in_(post_ids), + PostLike.deleted_at.is_(None), + ) + ) + liked_post_ids = {row[0] for row in liked_posts} + + # Add is_liked to each post + for post in posts: + post["is_liked"] = post["id"] in liked_post_ids + else: + # Not logged in - no posts are liked + for post in posts: + post["is_liked"] = False + + # Fetch card decoration fragments from events + card_widgets_html = {} + if post_ids: + post_slugs = [p.get("slug", "") for p in posts] + cards_html = await fetch_fragment("events", "container-cards", params={ + "post_ids": ",".join(str(pid) for pid in post_ids), + "post_slugs": ",".join(post_slugs), + }) + if cards_html: + card_widgets_html = _parse_card_fragments(cards_html) + + tags=await client.list_tags( + limit=50000 + ) + authors=await client.list_authors( + limit=50000 + ) + + # Draft count for the logged-in user (None → admin sees all) + draft_count = 0 + if count_drafts_for_user_id is not False: + draft_count = await client.count_drafts(user_id=count_drafts_for_user_id) + + return { + "posts": posts, + "page": pagination.get("page", page), + "total_pages": pagination.get("pages", 1), + "search_count": pagination.get("search_count"), + "tags": tags, + "authors": authors, + "draft_count": draft_count, + "tag_groups": tag_groups, + "selected_groups": selected_groups, + "card_widgets_html": card_widgets_html, + } + + +# Regex to extract per-post blocks delimited by comment markers +_CARD_MARKER_RE = re.compile( + r'(.*?)', + re.DOTALL, +) + + +def _parse_card_fragments(html: str) -> dict[str, str]: + """Parse the container-cards fragment into {post_id_str: html} dict.""" + result = {} + for m in _CARD_MARKER_RE.finditer(html): + post_id_str = m.group(1) + inner = m.group(2).strip() + if inner: + result[post_id_str] = inner + return result diff --git a/blog/bp/blog/web_hooks/routes.py b/blog/bp/blog/web_hooks/routes.py new file mode 100644 index 0000000..b02138b --- /dev/null +++ b/blog/bp/blog/web_hooks/routes.py @@ -0,0 +1,120 @@ +# suma_browser/webhooks.py +from __future__ import annotations +import os +from quart import Blueprint, request, abort, Response, g + +from ..ghost.ghost_sync import ( + sync_single_member, + sync_single_page, + sync_single_post, + sync_single_author, + sync_single_tag, +) +from shared.browser.app.redis_cacher import clear_cache +from shared.browser.app.csrf import csrf_exempt + +ghost_webhooks = Blueprint("ghost_webhooks", __name__, url_prefix="/__ghost-webhook") + +def _check_secret(req) -> None: + expected = os.getenv("GHOST_WEBHOOK_SECRET") + if not expected: + # if you don't set a secret, we allow anything (dev mode) + return + got = req.args.get("secret") or req.headers.get("X-Webhook-Secret") + if got != expected: + abort(401) + +def _extract_id(data: dict, key: str) -> str | None: + """ + key is "post", "tag", or "user"/"author". + Ghost usually sends { key: { current: { id: ... }, previous: { id: ... } } } + We'll try current first, then previous. + """ + block = data.get(key) or {} + cur = block.get("current") or {} + prev = block.get("previous") or {} + return cur.get("id") or prev.get("id") + + +@csrf_exempt +@ghost_webhooks.route("/member/", methods=["POST"]) +#@ghost_webhooks.post("/member/") +async def webhook_member() -> Response: + _check_secret(request) + + data = await request.get_json(force=True, silent=True) or {} + ghost_id = _extract_id(data, "member") + if not ghost_id: + abort(400, "no member id") + + # sync one post + #async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] # we'll set this in app.py + await sync_single_member(g.s, ghost_id) + return Response(status=204) + +@csrf_exempt +@ghost_webhooks.post("/post/") +@clear_cache(tag='blog') +async def webhook_post() -> Response: + _check_secret(request) + + data = await request.get_json(force=True, silent=True) or {} + ghost_id = _extract_id(data, "post") + if not ghost_id: + abort(400, "no post id") + + # sync one post + #async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] # we'll set this in app.py + await sync_single_post(g.s, ghost_id) + + return Response(status=204) + +@csrf_exempt +@ghost_webhooks.post("/page/") +@clear_cache(tag='blog') +async def webhook_page() -> Response: + _check_secret(request) + + data = await request.get_json(force=True, silent=True) or {} + ghost_id = _extract_id(data, "page") + if not ghost_id: + abort(400, "no page id") + + # sync one post + #async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] # we'll set this in app.py + await sync_single_page(g.s, ghost_id) + + return Response(status=204) + +@csrf_exempt +@ghost_webhooks.post("/author/") +@clear_cache(tag='blog') +async def webhook_author() -> Response: + _check_secret(request) + + data = await request.get_json(force=True, silent=True) or {} + # Ghost calls them "user" in webhook payload in many versions, + # and you want authors in your mirror. We'll try both keys. + ghost_id = _extract_id(data, "user") or _extract_id(data, "author") + if not ghost_id: + abort(400, "no author id") + + #async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] + await sync_single_author(g.s, ghost_id) + + return Response(status=204) + +@csrf_exempt +@ghost_webhooks.post("/tag/") +@clear_cache(tag='blog') +async def webhook_tag() -> Response: + _check_secret(request) + + data = await request.get_json(force=True, silent=True) or {} + ghost_id = _extract_id(data, "tag") + if not ghost_id: + abort(400, "no tag id") + + #async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] + await sync_single_tag(g.s, ghost_id) + return Response(status=204) diff --git a/blog/bp/fragments/__init__.py b/blog/bp/fragments/__init__.py new file mode 100644 index 0000000..a4af44b --- /dev/null +++ b/blog/bp/fragments/__init__.py @@ -0,0 +1 @@ +from .routes import register as register_fragments diff --git a/blog/bp/fragments/routes.py b/blog/bp/fragments/routes.py new file mode 100644 index 0000000..07d6e67 --- /dev/null +++ b/blog/bp/fragments/routes.py @@ -0,0 +1,52 @@ +"""Blog app fragment endpoints. + +Exposes HTML fragments at ``/internal/fragments/`` for consumption +by other coop apps via the fragment client. +""" + +from __future__ import annotations + +from quart import Blueprint, Response, g, render_template, request + +from shared.infrastructure.fragments import FRAGMENT_HEADER +from shared.services.navigation import get_navigation_tree + + +def register(): + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + # Registry of fragment handlers: type -> async callable returning HTML str + _handlers: dict[str, object] = {} + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.get("/") + async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/html") + html = await handler() + return Response(html, status=200, content_type="text/html") + + # --- nav-tree fragment --- + async def _nav_tree_handler(): + app_name = request.args.get("app_name", "") + path = request.args.get("path", "/") + first_seg = path.strip("/").split("/")[0] + menu_items = await get_navigation_tree(g.s) + return await render_template( + "fragments/nav_tree.html", + menu_items=menu_items, + frag_app_name=app_name, + frag_first_seg=first_seg, + ) + + _handlers["nav-tree"] = _nav_tree_handler + + # Store handlers dict on blueprint so app code can register handlers + bp._fragment_handlers = _handlers + + return bp diff --git a/blog/bp/menu_items/__init__.py b/blog/bp/menu_items/__init__.py new file mode 100644 index 0000000..be248ab --- /dev/null +++ b/blog/bp/menu_items/__init__.py @@ -0,0 +1,3 @@ +from .routes import register + +__all__ = ["register"] diff --git a/blog/bp/menu_items/routes.py b/blog/bp/menu_items/routes.py new file mode 100644 index 0000000..26ac745 --- /dev/null +++ b/blog/bp/menu_items/routes.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +from quart import Blueprint, render_template, make_response, request, jsonify, g + +from shared.browser.app.authz import require_admin +from .services.menu_items import ( + get_all_menu_items, + get_menu_item_by_id, + create_menu_item, + update_menu_item, + delete_menu_item, + search_pages, + MenuItemError, +) +from shared.browser.app.utils.htmx import is_htmx_request + +def register(): + bp = Blueprint("menu_items", __name__, url_prefix='/settings/menu_items') + + async def get_menu_items_nav_oob(): + """Helper to generate OOB update for root nav menu items""" + menu_items = await get_all_menu_items(g.s) + + nav_oob = await render_template( + "_types/menu_items/_nav_oob.html", + menu_items=menu_items, + ) + return nav_oob + + @bp.get("/") + @require_admin + async def list_menu_items(): + """List all menu items""" + menu_items = await get_all_menu_items(g.s) + + + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/menu_items/index.html", + menu_items=menu_items, + ) + else: + + html = await render_template( + "_types/menu_items/_oob_elements.html", + menu_items=menu_items, + ) + #html = await render_template("_types/root/settings/_oob_elements.html") + + + return await make_response(html) + + @bp.get("/new/") + @require_admin + async def new_menu_item(): + """Show form to create new menu item""" + html = await render_template( + "_types/menu_items/_form.html", + menu_item=None, + ) + return await make_response(html) + + @bp.post("/") + @require_admin + async def create_menu_item_route(): + """Create a new menu item""" + form = await request.form + post_id = form.get("post_id") + + if not post_id: + return jsonify({"message": "Page is required", "errors": {"post_id": ["Please select a page"]}}), 422 + + try: + post_id = int(post_id) + except ValueError: + return jsonify({"message": "Invalid page ID", "errors": {"post_id": ["Invalid page"]}}), 422 + + try: + menu_item = await create_menu_item(g.s, post_id) + await g.s.flush() + + # Get updated list and nav OOB + menu_items = await get_all_menu_items(g.s) + nav_oob = await get_menu_items_nav_oob() + + html = await render_template( + "_types/menu_items/_list.html", + menu_items=menu_items, + ) + return await make_response(html + nav_oob, 200) + + except MenuItemError as e: + return jsonify({"message": str(e), "errors": {}}), 400 + + @bp.get("//edit/") + @require_admin + async def edit_menu_item(item_id: int): + """Show form to edit menu item""" + menu_item = await get_menu_item_by_id(g.s, item_id) + if not menu_item: + return await make_response("Menu item not found", 404) + + html = await render_template( + "_types/menu_items/_form.html", + menu_item=menu_item, + ) + return await make_response(html) + + @bp.put("//") + @require_admin + async def update_menu_item_route(item_id: int): + """Update a menu item""" + form = await request.form + post_id = form.get("post_id") + + if not post_id: + return jsonify({"message": "Page is required", "errors": {"post_id": ["Please select a page"]}}), 422 + + try: + post_id = int(post_id) + except ValueError: + return jsonify({"message": "Invalid page ID", "errors": {"post_id": ["Invalid page"]}}), 422 + + try: + menu_item = await update_menu_item(g.s, item_id, post_id=post_id) + await g.s.flush() + + # Get updated list and nav OOB + menu_items = await get_all_menu_items(g.s) + nav_oob = await get_menu_items_nav_oob() + + html = await render_template( + "_types/menu_items/_list.html", + menu_items=menu_items, + ) + return await make_response(html + nav_oob, 200) + + except MenuItemError as e: + return jsonify({"message": str(e), "errors": {}}), 400 + + @bp.delete("//") + @require_admin + async def delete_menu_item_route(item_id: int): + """Delete a menu item""" + success = await delete_menu_item(g.s, item_id) + + if not success: + return await make_response("Menu item not found", 404) + + await g.s.flush() + + # Get updated list and nav OOB + menu_items = await get_all_menu_items(g.s) + nav_oob = await get_menu_items_nav_oob() + + html = await render_template( + "_types/menu_items/_list.html", + menu_items=menu_items, + ) + return await make_response(html + nav_oob, 200) + + @bp.get("/pages/search/") + @require_admin + async def search_pages_route(): + """Search for pages to add as menu items""" + query = request.args.get("q", "").strip() + page = int(request.args.get("page", 1)) + per_page = 10 + + pages, total = await search_pages(g.s, query, page, per_page) + has_more = (page * per_page) < total + + html = await render_template( + "_types/menu_items/_page_search_results.html", + pages=pages, + query=query, + page=page, + has_more=has_more, + ) + return await make_response(html) + + @bp.post("/reorder/") + @require_admin + async def reorder_menu_items_route(): + """Reorder menu items""" + from .services.menu_items import reorder_menu_items + + form = await request.form + item_ids_str = form.get("item_ids", "") + + if not item_ids_str: + return jsonify({"message": "No items to reorder", "errors": {}}), 400 + + try: + item_ids = [int(id.strip()) for id in item_ids_str.split(",") if id.strip()] + except ValueError: + return jsonify({"message": "Invalid item IDs", "errors": {}}), 400 + + await reorder_menu_items(g.s, item_ids) + await g.s.flush() + + # Get updated list and nav OOB + menu_items = await get_all_menu_items(g.s) + nav_oob = await get_menu_items_nav_oob() + + html = await render_template( + "_types/menu_items/_list.html", + menu_items=menu_items, + ) + return await make_response(html + nav_oob, 200) + + return bp diff --git a/blog/bp/menu_items/services/menu_items.py b/blog/bp/menu_items/services/menu_items.py new file mode 100644 index 0000000..d79c89f --- /dev/null +++ b/blog/bp/menu_items/services/menu_items.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func +from shared.models.menu_node import MenuNode +from models.ghost_content import Post +from shared.services.relationships import attach_child, detach_child + + +class MenuItemError(ValueError): + """Base error for menu item service operations.""" + + +async def get_all_menu_items(session: AsyncSession) -> list[MenuNode]: + """ + Get all menu nodes (excluding deleted), ordered by sort_order. + """ + result = await session.execute( + select(MenuNode) + .where(MenuNode.deleted_at.is_(None), MenuNode.depth == 0) + .order_by(MenuNode.sort_order.asc(), MenuNode.id.asc()) + ) + return list(result.scalars().all()) + + +async def get_menu_item_by_id(session: AsyncSession, item_id: int) -> MenuNode | None: + """Get a menu node by ID (excluding deleted).""" + result = await session.execute( + select(MenuNode) + .where(MenuNode.id == item_id, MenuNode.deleted_at.is_(None)) + ) + return result.scalar_one_or_none() + + +async def create_menu_item( + session: AsyncSession, + post_id: int, + sort_order: int | None = None +) -> MenuNode: + """ + Create a MenuNode + ContainerRelation for a page. + If sort_order is not provided, adds to end of list. + """ + # Verify post exists and is a page + post = await session.scalar( + select(Post).where(Post.id == post_id) + ) + if not post: + raise MenuItemError(f"Post {post_id} does not exist.") + + if not post.is_page: + raise MenuItemError("Only pages can be added as menu items, not posts.") + + # If no sort_order provided, add to end + if sort_order is None: + max_order = await session.scalar( + select(func.max(MenuNode.sort_order)) + .where(MenuNode.deleted_at.is_(None), MenuNode.depth == 0) + ) + sort_order = (max_order or 0) + 1 + + # Check for duplicate (same page, not deleted) + existing = await session.scalar( + select(MenuNode).where( + MenuNode.container_type == "page", + MenuNode.container_id == post_id, + MenuNode.deleted_at.is_(None), + ) + ) + if existing: + raise MenuItemError("Menu item for this page already exists.") + + menu_node = MenuNode( + container_type="page", + container_id=post_id, + label=post.title, + slug=post.slug, + feature_image=post.feature_image, + sort_order=sort_order, + ) + session.add(menu_node) + await session.flush() + await attach_child(session, "page", post_id, "menu_node", menu_node.id) + + return menu_node + + +async def update_menu_item( + session: AsyncSession, + item_id: int, + post_id: int | None = None, + sort_order: int | None = None +) -> MenuNode: + """Update an existing menu node.""" + menu_node = await get_menu_item_by_id(session, item_id) + if not menu_node: + raise MenuItemError(f"Menu item {item_id} not found.") + + if post_id is not None: + # Verify post exists and is a page + post = await session.scalar( + select(Post).where(Post.id == post_id) + ) + if not post: + raise MenuItemError(f"Post {post_id} does not exist.") + + if not post.is_page: + raise MenuItemError("Only pages can be added as menu items, not posts.") + + # Check for duplicate (same page, different menu node) + existing = await session.scalar( + select(MenuNode).where( + MenuNode.container_type == "page", + MenuNode.container_id == post_id, + MenuNode.id != item_id, + MenuNode.deleted_at.is_(None), + ) + ) + if existing: + raise MenuItemError("Menu item for this page already exists.") + + old_post_id = menu_node.container_id + menu_node.container_id = post_id + menu_node.label = post.title + menu_node.slug = post.slug + menu_node.feature_image = post.feature_image + + if sort_order is not None: + menu_node.sort_order = sort_order + + await session.flush() + + if post_id is not None and post_id != old_post_id: + await detach_child(session, "page", old_post_id, "menu_node", menu_node.id) + await attach_child(session, "page", post_id, "menu_node", menu_node.id) + + return menu_node + + +async def delete_menu_item(session: AsyncSession, item_id: int) -> bool: + """Soft delete a menu node.""" + menu_node = await get_menu_item_by_id(session, item_id) + if not menu_node: + return False + + menu_node.deleted_at = func.now() + await session.flush() + await detach_child(session, "page", menu_node.container_id, "menu_node", menu_node.id) + + return True + + +async def reorder_menu_items( + session: AsyncSession, + item_ids: list[int] +) -> list[MenuNode]: + """ + Reorder menu nodes by providing a list of IDs in desired order. + Updates sort_order for each node. + """ + items = [] + for index, item_id in enumerate(item_ids): + menu_node = await get_menu_item_by_id(session, item_id) + if menu_node: + menu_node.sort_order = index + items.append(menu_node) + + await session.flush() + + return items + + +async def search_pages( + session: AsyncSession, + query: str, + page: int = 1, + per_page: int = 10 +) -> tuple[list[Post], int]: + """ + Search for pages (not posts) by title. + Returns (pages, total_count). + """ + filters = [ + Post.is_page == True, # noqa: E712 + Post.status == "published", + Post.deleted_at.is_(None) + ] + + if query: + filters.append(Post.title.ilike(f"%{query}%")) + + # Get total count + count_result = await session.execute( + select(func.count(Post.id)).where(*filters) + ) + total = count_result.scalar() or 0 + + # Get paginated results + offset = (page - 1) * per_page + result = await session.execute( + select(Post) + .where(*filters) + .order_by(Post.title.asc()) + .limit(per_page) + .offset(offset) + ) + pages = list(result.scalars().all()) + + return pages, total diff --git a/blog/bp/post/admin/routes.py b/blog/bp/post/admin/routes.py new file mode 100644 index 0000000..c468a43 --- /dev/null +++ b/blog/bp/post/admin/routes.py @@ -0,0 +1,688 @@ +from __future__ import annotations + + +from quart import ( + render_template, + make_response, + Blueprint, + g, + request, + redirect, + url_for, +) +from shared.browser.app.authz import require_admin, require_post_author +from shared.browser.app.utils.htmx import is_htmx_request +from shared.utils import host_url + +def register(): + bp = Blueprint("admin", __name__, url_prefix='/admin') + + + @bp.get("/") + @require_admin + async def admin(slug: str): + from shared.browser.app.utils.htmx import is_htmx_request + from shared.models.page_config import PageConfig + from sqlalchemy import select as sa_select + + # Load features for page admin + post = (g.post_data or {}).get("post", {}) + features = {} + sumup_configured = False + sumup_merchant_code = "" + sumup_checkout_prefix = "" + if post.get("is_page"): + pc = (await g.s.execute( + sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post["id"]) + )).scalar_one_or_none() + if pc: + features = pc.features or {} + sumup_configured = bool(pc.sumup_api_key) + sumup_merchant_code = pc.sumup_merchant_code or "" + sumup_checkout_prefix = pc.sumup_checkout_prefix or "" + + ctx = { + "features": features, + "sumup_configured": sumup_configured, + "sumup_merchant_code": sumup_merchant_code, + "sumup_checkout_prefix": sumup_checkout_prefix, + } + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/post/admin/index.html", **ctx) + else: + # HTMX request: main panel + OOB elements + html = await render_template("_types/post/admin/_oob_elements.html", **ctx) + + return await make_response(html) + + @bp.put("/features/") + @require_admin + async def update_features(slug: str): + """Update PageConfig.features for a page.""" + from shared.models.page_config import PageConfig + from models.ghost_content import Post + from sqlalchemy import select as sa_select + from quart import jsonify + import json + + post = g.post_data.get("post") + if not post or not post.get("is_page"): + return jsonify({"error": "This is not a page."}), 400 + + post_id = post["id"] + + # Load or create PageConfig + pc = (await g.s.execute( + sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id) + )).scalar_one_or_none() + if pc is None: + pc = PageConfig(container_type="page", container_id=post_id, features={}) + g.s.add(pc) + await g.s.flush() + from shared.services.relationships import attach_child + await attach_child(g.s, "page", post_id, "page_config", pc.id) + + # Parse request body + body = await request.get_json() + if body is None: + # Fall back to form data + form = await request.form + body = {} + for key in ("calendar", "market"): + val = form.get(key) + if val is not None: + body[key] = val in ("true", "1", "on") + + if not isinstance(body, dict): + return jsonify({"error": "Expected JSON object with feature flags."}), 400 + + # Merge features + features = dict(pc.features or {}) + for key, val in body.items(): + if isinstance(val, bool): + features[key] = val + elif val in ("true", "1", "on"): + features[key] = True + elif val in ("false", "0", "off", None): + features[key] = False + + pc.features = features + from sqlalchemy.orm.attributes import flag_modified + flag_modified(pc, "features") + await g.s.flush() + + # Return updated features panel + html = await render_template( + "_types/post/admin/_features_panel.html", + features=features, + post=post, + sumup_configured=bool(pc.sumup_api_key), + sumup_merchant_code=pc.sumup_merchant_code or "", + sumup_checkout_prefix=pc.sumup_checkout_prefix or "", + ) + return await make_response(html) + + @bp.put("/admin/sumup/") + @require_admin + async def update_sumup(slug: str): + """Update PageConfig SumUp credentials for a page.""" + from shared.models.page_config import PageConfig + from sqlalchemy import select as sa_select + from quart import jsonify + + post = g.post_data.get("post") + if not post or not post.get("is_page"): + return jsonify({"error": "This is not a page."}), 400 + + post_id = post["id"] + + pc = (await g.s.execute( + sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id) + )).scalar_one_or_none() + if pc is None: + pc = PageConfig(container_type="page", container_id=post_id, features={}) + g.s.add(pc) + await g.s.flush() + from shared.services.relationships import attach_child + await attach_child(g.s, "page", post_id, "page_config", pc.id) + + form = await request.form + merchant_code = (form.get("merchant_code") or "").strip() + api_key = (form.get("api_key") or "").strip() + checkout_prefix = (form.get("checkout_prefix") or "").strip() + + pc.sumup_merchant_code = merchant_code or None + pc.sumup_checkout_prefix = checkout_prefix or None + # Only update API key if non-empty (allows updating other fields without re-entering key) + if api_key: + pc.sumup_api_key = api_key + + await g.s.flush() + + features = pc.features or {} + html = await render_template( + "_types/post/admin/_features_panel.html", + features=features, + post=post, + sumup_configured=bool(pc.sumup_api_key), + sumup_merchant_code=pc.sumup_merchant_code or "", + sumup_checkout_prefix=pc.sumup_checkout_prefix or "", + ) + return await make_response(html) + + @bp.get("/data/") + @require_admin + async def data(slug: str): + if not is_htmx_request(): + html = await render_template( + "_types/post_data/index.html", + ) + else: + html = await render_template( + "_types/post_data/_oob_elements.html", + ) + + return await make_response(html) + + @bp.get("/entries/calendar//") + @require_admin + async def calendar_view(slug: str, calendar_id: int): + """Show calendar month view for browsing entries""" + from shared.models.calendars import Calendar + from shared.utils.calendar_helpers import parse_int_arg, add_months, build_calendar_weeks + from shared.services.registry import services + from sqlalchemy import select + from datetime import datetime, timezone + import calendar as pycalendar + from quart import session as qsession + from ..services.entry_associations import get_post_entry_ids + + # Get month/year from query params + today = datetime.now(timezone.utc).date() + month = parse_int_arg("month") + year = parse_int_arg("year") + + if year is None: + year = today.year + if month is None or not (1 <= month <= 12): + month = today.month + + # Load calendar + result = await g.s.execute( + select(Calendar).where(Calendar.id == calendar_id, Calendar.deleted_at.is_(None)) + ) + calendar_obj = result.scalar_one_or_none() + if not calendar_obj: + return await make_response("Calendar not found", 404) + + # Build calendar data + prev_month_year, prev_month = add_months(year, month, -1) + next_month_year, next_month = add_months(year, month, +1) + prev_year = year - 1 + next_year = year + 1 + + weeks = build_calendar_weeks(year, month) + month_name = pycalendar.month_name[month] + weekday_names = [pycalendar.day_abbr[i] for i in range(7)] + + # Get entries for this month + period_start = datetime(year, month, 1, tzinfo=timezone.utc) + next_y, next_m = add_months(year, month, +1) + period_end = datetime(next_y, next_m, 1, tzinfo=timezone.utc) + + user = getattr(g, "user", None) + user_id = user.id if user else None + is_admin = bool(user and getattr(user, "is_admin", False)) + session_id = qsession.get("calendar_sid") + + month_entries = await services.calendar.visible_entries_for_period( + g.s, calendar_obj.id, period_start, period_end, + user_id=user_id, is_admin=is_admin, session_id=session_id, + ) + + # Get associated entry IDs for this post + post_id = g.post_data["post"]["id"] + associated_entry_ids = await get_post_entry_ids(g.s, post_id) + + html = await render_template( + "_types/post/admin/_calendar_view.html", + calendar=calendar_obj, + year=year, + month=month, + month_name=month_name, + weekday_names=weekday_names, + weeks=weeks, + prev_month=prev_month, + prev_month_year=prev_month_year, + next_month=next_month, + next_month_year=next_month_year, + prev_year=prev_year, + next_year=next_year, + month_entries=month_entries, + associated_entry_ids=associated_entry_ids, + ) + return await make_response(html) + + @bp.get("/entries/") + @require_admin + async def entries(slug: str): + from ..services.entry_associations import get_post_entry_ids + from shared.models.calendars import Calendar + from sqlalchemy import select + + post_id = g.post_data["post"]["id"] + associated_entry_ids = await get_post_entry_ids(g.s, post_id) + + # Load ALL calendars (not just this post's calendars) + result = await g.s.execute( + select(Calendar) + .where(Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + all_calendars = result.scalars().all() + + # Load entries and post for each calendar + for calendar in all_calendars: + await g.s.refresh(calendar, ["entries", "post"]) + if not is_htmx_request(): + html = await render_template( + "_types/post_entries/index.html", + all_calendars=all_calendars, + associated_entry_ids=associated_entry_ids, + ) + else: + html = await render_template( + "_types/post_entries/_oob_elements.html", + all_calendars=all_calendars, + associated_entry_ids=associated_entry_ids, + ) + + return await make_response(html) + + @bp.post("/entries//toggle/") + @require_admin + async def toggle_entry(slug: str, entry_id: int): + from ..services.entry_associations import toggle_entry_association, get_post_entry_ids, get_associated_entries + from shared.models.calendars import Calendar + from sqlalchemy import select + from quart import jsonify + + post_id = g.post_data["post"]["id"] + is_associated, error = await toggle_entry_association(g.s, post_id, entry_id) + + if error: + return jsonify({"message": error, "errors": {}}), 400 + + await g.s.flush() + + # Return updated association status + associated_entry_ids = await get_post_entry_ids(g.s, post_id) + + # Load ALL calendars + result = await g.s.execute( + select(Calendar) + .where(Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + all_calendars = result.scalars().all() + + # Load entries and post for each calendar + for calendar in all_calendars: + await g.s.refresh(calendar, ["entries", "post"]) + + # Fetch associated entries for nav display + associated_entries = await get_associated_entries(g.s, post_id) + + # Load calendars for this post (for nav display) + calendars = ( + await g.s.execute( + select(Calendar) + .where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + ).scalars().all() + + # Return the associated entries admin list + OOB update for nav entries + admin_list = await render_template( + "_types/post/admin/_associated_entries.html", + all_calendars=all_calendars, + associated_entry_ids=associated_entry_ids, + ) + + nav_entries_oob = await render_template( + "_types/post/admin/_nav_entries_oob.html", + associated_entries=associated_entries, + calendars=calendars, + post=g.post_data["post"], + ) + + return await make_response(admin_list + nav_entries_oob) + + @bp.get("/settings/") + @require_post_author + async def settings(slug: str): + from ...blog.ghost.ghost_posts import get_post_for_edit + + ghost_id = g.post_data["post"]["ghost_id"] + is_page = bool(g.post_data["post"].get("is_page")) + ghost_post = await get_post_for_edit(ghost_id, is_page=is_page) + save_success = request.args.get("saved") == "1" + + if not is_htmx_request(): + html = await render_template( + "_types/post_settings/index.html", + ghost_post=ghost_post, + save_success=save_success, + ) + else: + html = await render_template( + "_types/post_settings/_oob_elements.html", + ghost_post=ghost_post, + save_success=save_success, + ) + + return await make_response(html) + + @bp.post("/settings/") + @require_post_author + async def settings_save(slug: str): + from ...blog.ghost.ghost_posts import update_post_settings + from ...blog.ghost.ghost_sync import sync_single_post, sync_single_page + from shared.browser.app.redis_cacher import invalidate_tag_cache + + ghost_id = g.post_data["post"]["ghost_id"] + is_page = bool(g.post_data["post"].get("is_page")) + form = await request.form + + updated_at = form.get("updated_at", "") + + # Build kwargs — only include fields that were submitted + kwargs: dict = {} + + # Text fields + for field in ( + "slug", "custom_template", "meta_title", "meta_description", + "canonical_url", "og_image", "og_title", "og_description", + "twitter_image", "twitter_title", "twitter_description", + "feature_image_alt", + ): + val = form.get(field) + if val is not None: + kwargs[field] = val.strip() + + # Select fields + visibility = form.get("visibility") + if visibility is not None: + kwargs["visibility"] = visibility + + # Datetime + published_at = form.get("published_at", "").strip() + if published_at: + kwargs["published_at"] = published_at + + # Checkbox fields: present = True, absent = False + kwargs["featured"] = form.get("featured") == "on" + kwargs["email_only"] = form.get("email_only") == "on" + + # Tags — comma-separated string → list of {"name": "..."} dicts + tags_str = form.get("tags", "").strip() + if tags_str: + kwargs["tags"] = [{"name": t.strip()} for t in tags_str.split(",") if t.strip()] + else: + kwargs["tags"] = [] + + # Update in Ghost + await update_post_settings( + ghost_id=ghost_id, + updated_at=updated_at, + is_page=is_page, + **kwargs, + ) + + # Sync to local DB + if is_page: + await sync_single_page(g.s, ghost_id) + else: + await sync_single_post(g.s, ghost_id) + await g.s.flush() + + # Clear caches + await invalidate_tag_cache("blog") + await invalidate_tag_cache("post.post_detail") + + return redirect(host_url(url_for("blog.post.admin.settings", slug=slug)) + "?saved=1") + + @bp.get("/edit/") + @require_post_author + async def edit(slug: str): + from ...blog.ghost.ghost_posts import get_post_for_edit + from shared.models.ghost_membership_entities import GhostNewsletter + from sqlalchemy import select as sa_select + + ghost_id = g.post_data["post"]["ghost_id"] + is_page = bool(g.post_data["post"].get("is_page")) + ghost_post = await get_post_for_edit(ghost_id, is_page=is_page) + save_success = request.args.get("saved") == "1" + + newsletters = (await g.s.execute( + sa_select(GhostNewsletter).order_by(GhostNewsletter.name) + )).scalars().all() + + if not is_htmx_request(): + html = await render_template( + "_types/post_edit/index.html", + ghost_post=ghost_post, + save_success=save_success, + newsletters=newsletters, + ) + else: + html = await render_template( + "_types/post_edit/_oob_elements.html", + ghost_post=ghost_post, + save_success=save_success, + newsletters=newsletters, + ) + + return await make_response(html) + + @bp.post("/edit/") + @require_post_author + async def edit_save(slug: str): + import json + from ...blog.ghost.ghost_posts import update_post + from ...blog.ghost.lexical_validator import validate_lexical + from ...blog.ghost.ghost_sync import sync_single_post, sync_single_page + from shared.browser.app.redis_cacher import invalidate_tag_cache + + ghost_id = g.post_data["post"]["ghost_id"] + is_page = bool(g.post_data["post"].get("is_page")) + form = await request.form + title = form.get("title", "").strip() + lexical_raw = form.get("lexical", "") + updated_at = form.get("updated_at", "") + status = form.get("status", "draft") + publish_mode = form.get("publish_mode", "web") + newsletter_slug = form.get("newsletter_slug", "").strip() or None + feature_image = form.get("feature_image", "").strip() + custom_excerpt = form.get("custom_excerpt", "").strip() + feature_image_caption = form.get("feature_image_caption", "").strip() + + # Validate the lexical JSON + try: + lexical_doc = json.loads(lexical_raw) + except (json.JSONDecodeError, TypeError): + from ...blog.ghost.ghost_posts import get_post_for_edit + ghost_post = await get_post_for_edit(ghost_id, is_page=is_page) + html = await render_template( + "_types/post_edit/index.html", + ghost_post=ghost_post, + save_error="Invalid JSON in editor content.", + ) + return await make_response(html, 400) + + ok, reason = validate_lexical(lexical_doc) + if not ok: + from ...blog.ghost.ghost_posts import get_post_for_edit + ghost_post = await get_post_for_edit(ghost_id, is_page=is_page) + html = await render_template( + "_types/post_edit/index.html", + ghost_post=ghost_post, + save_error=reason, + ) + return await make_response(html, 400) + + # Update in Ghost (content save — no status change yet) + ghost_post = await update_post( + ghost_id=ghost_id, + lexical_json=lexical_raw, + title=title or None, + updated_at=updated_at, + feature_image=feature_image, + custom_excerpt=custom_excerpt, + feature_image_caption=feature_image_caption, + is_page=is_page, + ) + + # Publish workflow + is_admin = bool((g.get("rights") or {}).get("admin")) + publish_requested_msg = None + + # Guard: if already emailed, force publish_mode to "web" to prevent re-send + already_emailed = bool(ghost_post.get("email") and ghost_post["email"].get("status")) + if already_emailed and publish_mode in ("email", "both"): + publish_mode = "web" + + if status == "published" and ghost_post.get("status") != "published" and not is_admin: + # Non-admin requesting publish: don't send status to Ghost, set local flag + publish_requested_msg = "Publish requested — an admin will review." + elif status and status != ghost_post.get("status"): + # Status is changing — determine email params based on publish_mode + email_kwargs: dict = {} + if status == "published" and publish_mode in ("email", "both") and newsletter_slug: + email_kwargs["newsletter_slug"] = newsletter_slug + email_kwargs["email_segment"] = "all" + if publish_mode == "email": + email_kwargs["email_only"] = True + + from ...blog.ghost.ghost_posts import update_post as _up + ghost_post = await _up( + ghost_id=ghost_id, + lexical_json=lexical_raw, + title=None, + updated_at=ghost_post["updated_at"], + status=status, + is_page=is_page, + **email_kwargs, + ) + + # Sync to local DB + if is_page: + await sync_single_page(g.s, ghost_id) + else: + await sync_single_post(g.s, ghost_id) + await g.s.flush() + + # Handle publish_requested flag on the local post + from models.ghost_content import Post + from sqlalchemy import select as sa_select + local_post = (await g.s.execute( + sa_select(Post).where(Post.ghost_id == ghost_id) + )).scalar_one_or_none() + if local_post: + if publish_requested_msg: + local_post.publish_requested = True + elif status == "published" and is_admin: + local_post.publish_requested = False + await g.s.flush() + + # Clear caches + await invalidate_tag_cache("blog") + await invalidate_tag_cache("post.post_detail") + + # Redirect to GET to avoid resubmit warning on refresh (PRG pattern) + redirect_url = host_url(url_for("blog.post.admin.edit", slug=slug)) + "?saved=1" + if publish_requested_msg: + redirect_url += "&publish_requested=1" + return redirect(redirect_url) + + + @bp.get("/markets/") + @require_admin + async def markets(slug: str): + """List markets for this page.""" + from shared.services.registry import services + + post = (g.post_data or {}).get("post", {}) + post_id = post.get("id") + if not post_id: + return await make_response("Post not found", 404) + + page_markets = await services.market.marketplaces_for_container(g.s, "page", post_id) + + html = await render_template( + "_types/post/admin/_markets_panel.html", + markets=page_markets, + post=post, + ) + return await make_response(html) + + @bp.post("/markets/new/") + @require_admin + async def create_market(slug: str): + """Create a new market for this page.""" + from ..services.markets import create_market as _create_market, MarketError + from shared.services.registry import services + from quart import jsonify + + post = (g.post_data or {}).get("post", {}) + post_id = post.get("id") + if not post_id: + return jsonify({"error": "Post not found"}), 404 + + form = await request.form + name = (form.get("name") or "").strip() + + try: + await _create_market(g.s, post_id, name) + except MarketError as e: + return jsonify({"error": str(e)}), 400 + + # Return updated markets list + page_markets = await services.market.marketplaces_for_container(g.s, "page", post_id) + + html = await render_template( + "_types/post/admin/_markets_panel.html", + markets=page_markets, + post=post, + ) + return await make_response(html) + + @bp.delete("/markets//") + @require_admin + async def delete_market(slug: str, market_slug: str): + """Soft-delete a market.""" + from ..services.markets import soft_delete_market + from shared.services.registry import services + from quart import jsonify + + post = (g.post_data or {}).get("post", {}) + post_id = post.get("id") + + deleted = await soft_delete_market(g.s, slug, market_slug) + if not deleted: + return jsonify({"error": "Market not found"}), 404 + + # Return updated markets list + page_markets = await services.market.marketplaces_for_container(g.s, "page", post_id) + + html = await render_template( + "_types/post/admin/_markets_panel.html", + markets=page_markets, + post=post, + ) + return await make_response(html) + + return bp diff --git a/blog/bp/post/routes.py b/blog/bp/post/routes.py new file mode 100644 index 0000000..7aa3fb4 --- /dev/null +++ b/blog/bp/post/routes.py @@ -0,0 +1,180 @@ +from __future__ import annotations + + +from quart import ( + render_template, + make_response, + g, + Blueprint, + abort, + url_for, + request, +) +from .services.post_data import post_data +from .services.post_operations import toggle_post_like +from shared.services.registry import services +from shared.infrastructure.fragments import fetch_fragment, fetch_fragments + +from shared.browser.app.redis_cacher import cache_page, clear_cache + + +from .admin.routes import register as register_admin +from shared.config import config +from shared.browser.app.utils.htmx import is_htmx_request + +def register(): + bp = Blueprint("post", __name__, url_prefix='/') + bp.register_blueprint( + register_admin() + ) + + # Calendar blueprints now live in the events service. + # Post pages link to events_url() instead of embedding calendars. + + @bp.url_value_preprocessor + def pull_blog(endpoint, values): + g.post_slug = values.get("slug") + + @bp.before_request + async def hydrate_post_data(): + slug = getattr(g, "post_slug", None) + if not slug: + return # not a blog route or no slug in this URL + + is_admin = bool((g.get("rights") or {}).get("admin")) + # Always include drafts so we can check ownership below + p_data = await post_data(slug, g.s, include_drafts=True) + if not p_data: + abort(404) + return + + # Access control for draft posts + if p_data["post"].get("status") != "published": + if is_admin: + pass # admin can see all drafts + elif g.user and p_data["post"].get("user_id") == g.user.id: + pass # author can see their own drafts + else: + abort(404) + return + + g.post_data = p_data + + @bp.context_processor + async def context(): + p_data = getattr(g, "post_data", None) + if p_data: + from shared.infrastructure.cart_identity import current_cart_identity + + db_post_id = (g.post_data.get("post") or {}).get("id") + post_slug = (g.post_data.get("post") or {}).get("slug", "") + + # Fetch container nav fragments from events + market + paginate_url = url_for( + 'blog.post.widget_paginate', + slug=post_slug, widget_domain='calendar', + ) + nav_params = { + "container_type": "page", + "container_id": str(db_post_id), + "post_slug": post_slug, + "paginate_url": paginate_url, + } + events_nav_html, market_nav_html = await fetch_fragments([ + ("events", "container-nav", nav_params), + ("market", "container-nav", nav_params), + ]) + container_nav_html = events_nav_html + market_nav_html + + ctx = { + **p_data, + "base_title": f"{config()['title']} {p_data['post']['title']}", + "container_nav_html": container_nav_html, + } + + # Page cart badge via service + post_dict = p_data.get("post") or {} + if post_dict.get("is_page"): + ident = current_cart_identity() + page_summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + page_slug=post_dict["slug"], + ) + ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count + ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total) + + return ctx + else: + return {} + + @bp.get("/") + @cache_page(tag="post.post_detail") + async def post_detail(slug: str): + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/post/index.html") + else: + # HTMX request: main panel + OOB elements + html = await render_template("_types/post/_oob_elements.html") + + return await make_response(html) + + @bp.post("/like/toggle/") + @clear_cache(tag="post.post_detail", tag_scope="user") + async def like_toggle(slug: str): + from shared.utils import host_url + + # Get post_id from g.post_data + if not g.user: + html = await render_template( + "_types/browse/like/button.html", + slug=slug, + liked=False, + like_url=host_url(url_for('blog.post.like_toggle', slug=slug)), + item_type='post', + ) + resp = make_response(html, 403) + return resp + + post_id = g.post_data["post"]["id"] + user_id = g.user.id + + liked, error = await toggle_post_like(g.s, user_id, post_id) + + if error: + resp = make_response(error, 404) + return resp + + html = await render_template( + "_types/browse/like/button.html", + slug=slug, + liked=liked, + like_url=host_url(url_for('blog.post.like_toggle', slug=slug)), + item_type='post', + ) + return html + + @bp.get("/w//") + async def widget_paginate(slug: str, widget_domain: str): + """Proxies paginated widget requests to the appropriate fragment provider.""" + page = int(request.args.get("page", 1)) + post_id = g.post_data["post"]["id"] + + if widget_domain == "calendar": + html = await fetch_fragment("events", "container-nav", params={ + "container_type": "page", + "container_id": str(post_id), + "post_slug": slug, + "page": str(page), + "paginate_url": url_for( + 'blog.post.widget_paginate', + slug=slug, widget_domain='calendar', + ), + }) + return await make_response(html or "") + abort(404) + + return bp + + diff --git a/blog/bp/post/services/entry_associations.py b/blog/bp/post/services/entry_associations.py new file mode 100644 index 0000000..5afe195 --- /dev/null +++ b/blog/bp/post/services/entry_associations.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.services.registry import services + + +async def toggle_entry_association( + session: AsyncSession, + post_id: int, + entry_id: int +) -> tuple[bool, str | None]: + """ + Toggle association between a post and calendar entry. + Returns (is_now_associated, error_message). + """ + post = await services.blog.get_post_by_id(session, post_id) + if not post: + return False, "Post not found" + + is_associated = await services.calendar.toggle_entry_post( + session, entry_id, "post", post_id, + ) + return is_associated, None + + +async def get_post_entry_ids( + session: AsyncSession, + post_id: int +) -> set[int]: + """ + Get all entry IDs associated with this post. + Returns a set of entry IDs. + """ + return await services.calendar.entry_ids_for_content(session, "post", post_id) + + +async def get_associated_entries( + session: AsyncSession, + post_id: int, + page: int = 1, + per_page: int = 10 +) -> dict: + """ + Get paginated associated entries for this post. + Returns dict with entries (CalendarEntryDTOs), total_count, and has_more. + """ + entries, has_more = await services.calendar.associated_entries( + session, "post", post_id, page, + ) + total_count = len(entries) + (page - 1) * per_page + if has_more: + total_count += 1 # at least one more + + return { + "entries": entries, + "total_count": total_count, + "has_more": has_more, + "page": page, + } diff --git a/blog/bp/post/services/markets.py b/blog/bp/post/services/markets.py new file mode 100644 index 0000000..c825bb8 --- /dev/null +++ b/blog/bp/post/services/markets.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import re +import unicodedata + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.page_config import PageConfig +from shared.contracts.dtos import MarketPlaceDTO +from shared.services.registry import services + + +class MarketError(ValueError): + """Base error for market service operations.""" + + +def slugify(value: str, max_len: int = 255) -> str: + if value is None: + value = "" + value = unicodedata.normalize("NFKD", value) + value = value.encode("ascii", "ignore").decode("ascii") + value = value.lower() + value = value.replace("/", "-") + value = re.sub(r"[^a-z0-9]+", "-", value) + value = re.sub(r"-{2,}", "-", value) + value = value.strip("-")[:max_len].strip("-") + return value or "market" + + +async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPlaceDTO: + name = (name or "").strip() + if not name: + raise MarketError("Market name must not be empty.") + slug = slugify(name) + + post = await services.blog.get_post_by_id(sess, post_id) + if not post: + raise MarketError(f"Post {post_id} does not exist.") + + if not post.is_page: + raise MarketError("Markets can only be created on pages, not posts.") + + pc = (await sess.execute( + select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id) + )).scalar_one_or_none() + if pc is None or not (pc.features or {}).get("market"): + raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.") + + try: + return await services.market.create_marketplace(sess, "page", post_id, name, slug) + except ValueError as e: + raise MarketError(str(e)) from e + + +async def soft_delete_market(sess: AsyncSession, post_slug: str, market_slug: str) -> bool: + post = await services.blog.get_post_by_slug(sess, post_slug) + if not post: + return False + + return await services.market.soft_delete_marketplace(sess, "page", post.id, market_slug) diff --git a/blog/bp/post/services/post_data.py b/blog/bp/post/services/post_data.py new file mode 100644 index 0000000..0d0d225 --- /dev/null +++ b/blog/bp/post/services/post_data.py @@ -0,0 +1,42 @@ +from ...blog.ghost_db import DBClient # adjust import path +from sqlalchemy import select +from models.ghost_content import PostLike +from quart import g + +async def post_data(slug, session, include_drafts=False): + client = DBClient(session) + posts = (await client.posts_by_slug(slug, include_drafts=include_drafts)) + + if not posts: + # 404 page (you can make a template for this if you want) + return None + post, original_post = posts[0] + + # Check if current user has liked this post + is_liked = False + if g.user: + liked_record = await session.scalar( + select(PostLike).where( + PostLike.user_id == g.user.id, + PostLike.post_id == post["id"], + PostLike.deleted_at.is_(None), + ) + ) + is_liked = liked_record is not None + + # Add is_liked to post dict + post["is_liked"] = is_liked + + tags=await client.list_tags( + limit=50000 + ) # <-- new + authors=await client.list_authors( + limit=50000 + ) + + return { + "post": post, + "original_post": original_post, + "tags": tags, + "authors": authors, + } diff --git a/blog/bp/post/services/post_operations.py b/blog/bp/post/services/post_operations.py new file mode 100644 index 0000000..e4bb102 --- /dev/null +++ b/blog/bp/post/services/post_operations.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Optional + +from sqlalchemy import select, func, update +from sqlalchemy.ext.asyncio import AsyncSession + +from models.ghost_content import Post, PostLike + + +async def toggle_post_like( + session: AsyncSession, + user_id: int, + post_id: int, +) -> tuple[bool, Optional[str]]: + """ + Toggle a post like for a given user using soft deletes. + Returns (liked_state, error_message). + - If error_message is not None, an error occurred. + - liked_state indicates whether post is now liked (True) or unliked (False). + """ + + # Verify post exists + post_exists = await session.scalar( + select(Post.id).where(Post.id == post_id, Post.deleted_at.is_(None)) + ) + if not post_exists: + return False, "Post not found" + + # Check if like exists (not deleted) + existing = await session.scalar( + select(PostLike).where( + PostLike.user_id == user_id, + PostLike.post_id == post_id, + PostLike.deleted_at.is_(None), + ) + ) + + if existing: + # Unlike: soft delete the like + await session.execute( + update(PostLike) + .where( + PostLike.user_id == user_id, + PostLike.post_id == post_id, + PostLike.deleted_at.is_(None), + ) + .values(deleted_at=func.now()) + ) + return False, None + else: + # Like: add a new like + new_like = PostLike( + user_id=user_id, + post_id=post_id, + ) + session.add(new_like) + return True, None diff --git a/blog/bp/snippets/__init__.py b/blog/bp/snippets/__init__.py new file mode 100644 index 0000000..be248ab --- /dev/null +++ b/blog/bp/snippets/__init__.py @@ -0,0 +1,3 @@ +from .routes import register + +__all__ = ["register"] diff --git a/blog/bp/snippets/routes.py b/blog/bp/snippets/routes.py new file mode 100644 index 0000000..8f4778a --- /dev/null +++ b/blog/bp/snippets/routes.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from quart import Blueprint, render_template, make_response, request, g, abort +from sqlalchemy import select, or_ +from sqlalchemy.orm import selectinload + +from shared.browser.app.authz import require_login +from shared.browser.app.utils.htmx import is_htmx_request +from models import Snippet + + +VALID_VISIBILITY = frozenset({"private", "shared", "admin"}) + + +async def _visible_snippets(session): + """Return snippets visible to the current user (own + shared + admin-if-admin).""" + uid = g.user.id + is_admin = g.rights.get("admin") + + filters = [Snippet.user_id == uid, Snippet.visibility == "shared"] + if is_admin: + filters.append(Snippet.visibility == "admin") + + rows = (await session.execute( + select(Snippet).where(or_(*filters)).order_by(Snippet.name) + )).scalars().all() + + return rows + + +def register(): + bp = Blueprint("snippets", __name__, url_prefix="/settings/snippets") + + @bp.get("/") + @require_login + async def list_snippets(): + """List snippets visible to the current user.""" + snippets = await _visible_snippets(g.s) + is_admin = g.rights.get("admin") + + if not is_htmx_request(): + html = await render_template( + "_types/snippets/index.html", + snippets=snippets, + is_admin=is_admin, + ) + else: + html = await render_template( + "_types/snippets/_oob_elements.html", + snippets=snippets, + is_admin=is_admin, + ) + + return await make_response(html) + + @bp.delete("//") + @require_login + async def delete_snippet(snippet_id: int): + """Delete a snippet. Owners delete their own; admins can delete any.""" + snippet = await g.s.get(Snippet, snippet_id) + if not snippet: + abort(404) + + is_admin = g.rights.get("admin") + if snippet.user_id != g.user.id and not is_admin: + abort(403) + + await g.s.delete(snippet) + await g.s.flush() + + snippets = await _visible_snippets(g.s) + html = await render_template( + "_types/snippets/_list.html", + snippets=snippets, + is_admin=is_admin, + ) + return await make_response(html) + + @bp.patch("//visibility/") + @require_login + async def patch_visibility(snippet_id: int): + """Change snippet visibility. Admin only.""" + if not g.rights.get("admin"): + abort(403) + + snippet = await g.s.get(Snippet, snippet_id) + if not snippet: + abort(404) + + form = await request.form + visibility = form.get("visibility", "").strip() + + if visibility not in VALID_VISIBILITY: + abort(400) + + snippet.visibility = visibility + await g.s.flush() + + snippets = await _visible_snippets(g.s) + html = await render_template( + "_types/snippets/_list.html", + snippets=snippets, + is_admin=True, + ) + return await make_response(html) + + return bp diff --git a/blog/config/app-config.yaml b/blog/config/app-config.yaml new file mode 100644 index 0000000..3aa6a76 --- /dev/null +++ b/blog/config/app-config.yaml @@ -0,0 +1,84 @@ +# App-wide settings +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: Rose Ash +market_root: /market +market_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + blog: "http://localhost:8000" + market: "http://localhost:8001" + cart: "http://localhost:8002" + events: "http://localhost:8003" + federation: "http://localhost:8004" +cache: + fs_root: _snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/wines + - branded-goods/ciders + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + - ---just-lem-just-wholefoods-jelly-crystals-lemon-12-x-85g-vf067-2-html + product-details: + - General Information + - A Note About Prices + +# SumUp payment settings (fill these in for live usage) +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + checkout_reference_prefix: 'dev-' + diff --git a/blog/entrypoint.sh b/blog/entrypoint.sh new file mode 100644 index 0000000..685a882 --- /dev/null +++ b/blog/entrypoint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional: wait for Postgres to be reachable +if [[ -n "${DATABASE_HOST:-}" && -n "${DATABASE_PORT:-}" ]]; then + echo "Waiting for Postgres at ${DATABASE_HOST}:${DATABASE_PORT}..." + for i in {1..60}; do + (echo > /dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}) >/dev/null 2>&1 && break || true + sleep 1 + done +fi + +# Run DB migrations only if RUN_MIGRATIONS=true (blog service only) +if [[ "${RUN_MIGRATIONS:-}" == "true" ]]; then + echo "Running Alembic migrations..." + (cd shared && alembic upgrade head) +fi + +# Clear Redis page cache on deploy +if [[ -n "${REDIS_URL:-}" && "${REDIS_URL}" != "no" ]]; then + echo "Flushing Redis cache..." + python3 -c " +import redis, os +r = redis.from_url(os.environ['REDIS_URL']) +r.flushall() +print('Redis cache cleared.') +" || echo "Redis flush failed (non-fatal), continuing..." +fi + +# Start the app +echo "Starting Hypercorn (${APP_MODULE:-app:app})..." +PYTHONUNBUFFERED=1 exec hypercorn "${APP_MODULE:-app:app}" --bind 0.0.0.0:${PORT:-8000} diff --git a/blog/models/__init__.py b/blog/models/__init__.py new file mode 100644 index 0000000..e434f4a --- /dev/null +++ b/blog/models/__init__.py @@ -0,0 +1,14 @@ +from .ghost_content import Post, Author, Tag, PostAuthor, PostTag, PostLike +from .snippet import Snippet +from .tag_group import TagGroup, TagGroupTag + +# Shared models — canonical definitions live in shared/models/ +from shared.models.ghost_membership_entities import ( + GhostLabel, UserLabel, + GhostNewsletter, UserNewsletter, + GhostTier, GhostSubscription, +) +from shared.models.menu_item import MenuItem +from shared.models.kv import KV +from shared.models.magic_link import MagicLink +from shared.models.user import User diff --git a/blog/models/ghost_content.py b/blog/models/ghost_content.py new file mode 100644 index 0000000..cd18161 --- /dev/null +++ b/blog/models/ghost_content.py @@ -0,0 +1,3 @@ +from shared.models.ghost_content import ( # noqa: F401 + Tag, Post, Author, PostAuthor, PostTag, PostLike, +) diff --git a/blog/models/ghost_membership_entities.py b/blog/models/ghost_membership_entities.py new file mode 100644 index 0000000..d07520f --- /dev/null +++ b/blog/models/ghost_membership_entities.py @@ -0,0 +1,12 @@ +# Re-export from canonical shared location +from shared.models.ghost_membership_entities import ( + GhostLabel, UserLabel, + GhostNewsletter, UserNewsletter, + GhostTier, GhostSubscription, +) + +__all__ = [ + "GhostLabel", "UserLabel", + "GhostNewsletter", "UserNewsletter", + "GhostTier", "GhostSubscription", +] diff --git a/blog/models/kv.py b/blog/models/kv.py new file mode 100644 index 0000000..d54f0a3 --- /dev/null +++ b/blog/models/kv.py @@ -0,0 +1,4 @@ +# Re-export from canonical shared location +from shared.models.kv import KV + +__all__ = ["KV"] diff --git a/blog/models/magic_link.py b/blog/models/magic_link.py new file mode 100644 index 0000000..9031ca4 --- /dev/null +++ b/blog/models/magic_link.py @@ -0,0 +1,4 @@ +# Re-export from canonical shared location +from shared.models.magic_link import MagicLink + +__all__ = ["MagicLink"] diff --git a/blog/models/menu_item.py b/blog/models/menu_item.py new file mode 100644 index 0000000..f36a146 --- /dev/null +++ b/blog/models/menu_item.py @@ -0,0 +1,4 @@ +# Re-export from canonical shared location +from shared.models.menu_item import MenuItem + +__all__ = ["MenuItem"] diff --git a/blog/models/snippet.py b/blog/models/snippet.py new file mode 100644 index 0000000..47cad35 --- /dev/null +++ b/blog/models/snippet.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, UniqueConstraint, Index, func +from sqlalchemy.orm import Mapped, mapped_column + +from shared.db.base import Base + + +class Snippet(Base): + __tablename__ = "snippets" + __table_args__ = ( + UniqueConstraint("user_id", "name", name="uq_snippets_user_name"), + Index("ix_snippets_visibility", "visibility"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False, + ) + name: Mapped[str] = mapped_column(String(255), nullable=False) + value: Mapped[str] = mapped_column(Text, nullable=False) + visibility: Mapped[str] = mapped_column( + String(20), nullable=False, default="private", server_default="private", + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), + ) diff --git a/blog/models/tag_group.py b/blog/models/tag_group.py new file mode 100644 index 0000000..77ddc41 --- /dev/null +++ b/blog/models/tag_group.py @@ -0,0 +1,52 @@ +from datetime import datetime +from typing import List, Optional +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import ( + Integer, + String, + Text, + DateTime, + ForeignKey, + UniqueConstraint, + func, +) +from shared.db.base import Base + + +class TagGroup(Base): + __tablename__ = "tag_groups" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[str] = mapped_column(String(191), unique=True, nullable=False) + feature_image: Mapped[Optional[str]] = mapped_column(Text()) + colour: Mapped[Optional[str]] = mapped_column(String(32)) + sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() + ) + + tag_links: Mapped[List["TagGroupTag"]] = relationship( + "TagGroupTag", back_populates="group", cascade="all, delete-orphan", passive_deletes=True + ) + + +class TagGroupTag(Base): + __tablename__ = "tag_group_tags" + __table_args__ = ( + UniqueConstraint("tag_group_id", "tag_id", name="uq_tag_group_tag"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + tag_group_id: Mapped[int] = mapped_column( + ForeignKey("tag_groups.id", ondelete="CASCADE"), nullable=False + ) + tag_id: Mapped[int] = mapped_column( + ForeignKey("tags.id", ondelete="CASCADE"), nullable=False + ) + + group: Mapped["TagGroup"] = relationship("TagGroup", back_populates="tag_links") diff --git a/blog/models/user.py b/blog/models/user.py new file mode 100644 index 0000000..3feae81 --- /dev/null +++ b/blog/models/user.py @@ -0,0 +1,4 @@ +# Re-export from canonical shared location +from shared.models.user import User + +__all__ = ["User"] diff --git a/blog/path_setup.py b/blog/path_setup.py new file mode 100644 index 0000000..c7166f7 --- /dev/null +++ b/blog/path_setup.py @@ -0,0 +1,9 @@ +import sys +import os + +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p) diff --git a/blog/services/__init__.py b/blog/services/__init__.py new file mode 100644 index 0000000..11d9769 --- /dev/null +++ b/blog/services/__init__.py @@ -0,0 +1,28 @@ +"""Blog app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the blog app. + + Blog owns: Post, Tag, Author, PostAuthor, PostTag, PostLike. + Standard deployment registers all 4 services as real DB impls + (shared DB). For composable deployments, swap non-owned services + with stubs from shared.services.stubs. + """ + from shared.services.registry import services + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + services.blog = SqlBlogService() + if not services.has("calendar"): + services.calendar = SqlCalendarService() + if not services.has("market"): + services.market = SqlMarketService() + if not services.has("cart"): + services.cart = SqlCartService() + if not services.has("federation"): + from shared.services.federation_impl import SqlFederationService + services.federation = SqlFederationService() diff --git a/blog/templates/_email/magic_link.html b/blog/templates/_email/magic_link.html new file mode 100644 index 0000000..3c1eac6 --- /dev/null +++ b/blog/templates/_email/magic_link.html @@ -0,0 +1,33 @@ + + + + + + +
    + + +
    +

    {{ site_name }}

    +

    Sign in to your account

    +

    + Click the button below to sign in. This link will expire in 15 minutes. +

    +
    + + Sign in + +
    +

    Or copy and paste this link into your browser:

    +

    + {{ link_url }} +

    +
    +

    + If you did not request this email, you can safely ignore it. +

    +
    +
    + + diff --git a/blog/templates/_email/magic_link.txt b/blog/templates/_email/magic_link.txt new file mode 100644 index 0000000..28a2efb --- /dev/null +++ b/blog/templates/_email/magic_link.txt @@ -0,0 +1,8 @@ +Hello, + +Click this link to sign in: +{{ link_url }} + +This link will expire in 15 minutes. + +If you did not request this, you can ignore this email. diff --git a/blog/templates/_types/blog/_action_buttons.html b/blog/templates/_types/blog/_action_buttons.html new file mode 100644 index 0000000..7184ab0 --- /dev/null +++ b/blog/templates/_types/blog/_action_buttons.html @@ -0,0 +1,64 @@ +{# New Post/Page + Drafts toggle — shown in aside (desktop + mobile) #} +
    + {% if has_access('blog.new_post') %} + {% set new_href = url_for('blog.new_post')|host %} + + New Post + + {% set new_page_href = url_for('blog.new_page')|host %} + + New Page + + {% endif %} + {% if g.user and (draft_count or drafts) %} + {% if drafts %} + {% set drafts_off_href = (current_local_href ~ {'drafts': None}|qs)|host %} + + Drafts + {{ draft_count }} + + {% else %} + {% set drafts_on_href = (current_local_href ~ {'drafts': '1'}|qs)|host %} + + Drafts + {{ draft_count }} + + {% endif %} + {% endif %} +
    diff --git a/blog/templates/_types/blog/_card.html b/blog/templates/_types/blog/_card.html new file mode 100644 index 0000000..89ce8e7 --- /dev/null +++ b/blog/templates/_types/blog/_card.html @@ -0,0 +1,80 @@ +{% import 'macros/stickers.html' as stick %} +
    + {# ❤️ like button - OUTSIDE the link, aligned with image top #} + {% if g.user %} +
    + {% set slug = post.slug %} + {% set liked = post.is_liked or False %} + {% set like_url = url_for('blog.post.like_toggle', slug=slug)|host %} + {% set item_type = 'post' %} + {% include "_types/browse/like/button.html" %} +
    + {% endif %} + + {% set _href=url_for('blog.post.post_detail', slug=post.slug)|host %} + +
    +

    + {{ post.title }} +

    + + {% if post.status == "draft" %} +
    + Draft + {% if post.publish_requested %} + Publish requested + {% endif %} +
    + {% if post.updated_at %} +

    + Updated: {{ post.updated_at.strftime("%-d %b %Y at %H:%M") }} +

    + {% endif %} + {% elif post.published_at %} +

    + Published: {{ post.published_at.strftime("%-d %b %Y at %H:%M") }} +

    + {% endif %} + +
    + + {% if post.feature_image %} +
    + +
    + {% endif %} + {% if post.custom_excerpt %} +

    + {{ post.custom_excerpt }} +

    + {% else %} + {% if post.excerpt %} +

    + {{ post.excerpt }} +

    + {% endif %} + {% endif %} +
    + + {# Card decorations — via fragments #} + {% if card_widgets_html %} + {% set _card_html = card_widgets_html.get(post.id|string, "") %} + {% if _card_html %}{{ _card_html | safe }}{% endif %} + {% endif %} + + {% include '_types/blog/_card/at_bar.html' %} + +
    diff --git a/blog/templates/_types/blog/_card/at_bar.html b/blog/templates/_types/blog/_card/at_bar.html new file mode 100644 index 0000000..f226d92 --- /dev/null +++ b/blog/templates/_types/blog/_card/at_bar.html @@ -0,0 +1,19 @@ +
    + {% if post.tags %} +
    +
    in
    +
      + {% include '_types/blog/_card/tags.html' %} +
    +
    + {% endif %} +
    + {% if post.authors %} +
    +
    by
    +
      + {% include '_types/blog/_card/authors.html' %} +
    +
    + {% endif %} +
    diff --git a/blog/templates/_types/blog/_card/author.html b/blog/templates/_types/blog/_card/author.html new file mode 100644 index 0000000..7ddddf7 --- /dev/null +++ b/blog/templates/_types/blog/_card/author.html @@ -0,0 +1,21 @@ +{% macro author(author) %} + {% if author %} + {% if author.profile_image %} + {{ author.name }} + {% else %} +
    + {# optional fallback circle with first letter +
    + {{ author.name[:1] }} +
    #} + {% endif %} + + + {{ author.name }} + + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/blog/templates/_types/blog/_card/authors.html b/blog/templates/_types/blog/_card/authors.html new file mode 100644 index 0000000..5b8911d --- /dev/null +++ b/blog/templates/_types/blog/_card/authors.html @@ -0,0 +1,32 @@ +{# --- AUTHORS LIST STARTS HERE --- #} + {% if post.authors and post.authors|length %} + {% for a in post.authors %} + {% for author in authors if author.slug==a.slug %} +
  • + + {% if author.profile_image %} + {{ author.name }} + {% else %} + {# optional fallback circle with first letter #} +
    + {{ author.name[:1] }} +
    + {% endif %} + + + {{ author.name }} + +
    +
  • + {% endfor %} + {% endfor %} + {% endif %} + + {# --- AUTHOR LIST ENDS HERE --- #} \ No newline at end of file diff --git a/blog/templates/_types/blog/_card/tag.html b/blog/templates/_types/blog/_card/tag.html new file mode 100644 index 0000000..137cb0c --- /dev/null +++ b/blog/templates/_types/blog/_card/tag.html @@ -0,0 +1,19 @@ +{% macro tag(tag) %} + {% if tag %} + {% if tag.feature_image %} + {{ tag.name }} + {% else %} +
    + {{ tag.name[:1] }} +
    + {% endif %} + + + {{ tag.name }} + + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/blog/templates/_types/blog/_card/tag_group.html b/blog/templates/_types/blog/_card/tag_group.html new file mode 100644 index 0000000..21c9974 --- /dev/null +++ b/blog/templates/_types/blog/_card/tag_group.html @@ -0,0 +1,22 @@ +{% macro tag_group(group) %} + {% if group %} + {% if group.feature_image %} + {{ group.name }} + {% else %} +
    + {{ group.name[:1] }} +
    + {% endif %} + + + {{ group.name }} + + {% endif %} +{% endmacro %} diff --git a/blog/templates/_types/blog/_card/tags.html b/blog/templates/_types/blog/_card/tags.html new file mode 100644 index 0000000..2ea7ad1 --- /dev/null +++ b/blog/templates/_types/blog/_card/tags.html @@ -0,0 +1,17 @@ +{% import '_types/blog/_card/tag.html' as dotag %} +{# --- TAG LIST STARTS HERE --- #} + {% if post.tags and post.tags|length %} + {% for t in post.tags %} + {% for tag in tags if tag.slug==t.slug %} +
  • + + {{dotag.tag(tag)}} + +
  • + {% endfor %} + {% endfor %} + {% endif %} + {# --- TAG LIST ENDS HERE --- #} \ No newline at end of file diff --git a/blog/templates/_types/blog/_card_tile.html b/blog/templates/_types/blog/_card_tile.html new file mode 100644 index 0000000..f03ca16 --- /dev/null +++ b/blog/templates/_types/blog/_card_tile.html @@ -0,0 +1,59 @@ +
    + {% set _href=url_for('blog.post.post_detail', slug=post.slug)|host %} + + {% if post.feature_image %} +
    + +
    + {% endif %} + +
    +

    + {{ post.title }} +

    + + {% if post.status == "draft" %} +
    + Draft + {% if post.publish_requested %} + Publish requested + {% endif %} +
    + {% if post.updated_at %} +

    + Updated: {{ post.updated_at.strftime("%-d %b %Y at %H:%M") }} +

    + {% endif %} + {% elif post.published_at %} +

    + Published: {{ post.published_at.strftime("%-d %b %Y at %H:%M") }} +

    + {% endif %} + + {% if post.custom_excerpt %} +

    + {{ post.custom_excerpt }} +

    + {% elif post.excerpt %} +

    + {{ post.excerpt }} +

    + {% endif %} +
    +
    + + {% include '_types/blog/_card/at_bar.html' %} +
    diff --git a/blog/templates/_types/blog/_cards.html b/blog/templates/_types/blog/_cards.html new file mode 100644 index 0000000..82eee98 --- /dev/null +++ b/blog/templates/_types/blog/_cards.html @@ -0,0 +1,111 @@ +{% for post in posts %} + {% if view == 'tile' %} + {% include "_types/blog/_card_tile.html" %} + {% else %} + {% include "_types/blog/_card.html" %} + {% endif %} +{% endfor %} +{% if page < total_pages|int %} + + + + + +{% else %} +
    End of results
    +{% endif %} + diff --git a/blog/templates/_types/blog/_main_panel.html b/blog/templates/_types/blog/_main_panel.html new file mode 100644 index 0000000..055e164 --- /dev/null +++ b/blog/templates/_types/blog/_main_panel.html @@ -0,0 +1,84 @@ + + {# Content type tabs: Posts | Pages #} +
    + {% set posts_href = (url_for('blog.index'))|host %} + {% set pages_href = (url_for('blog.index') ~ '?type=pages')|host %} + Posts + Pages +
    + + {% if content_type == 'pages' %} + {# Pages listing #} +
    + {% set page_num = page %} + {% include "_types/blog/_page_cards.html" %} +
    +
    + {% else %} + + {# View toggle bar - desktop only #} + + + {# Cards container - list or grid based on view #} + {% if view == 'tile' %} +
    + {% include "_types/blog/_cards.html" %} +
    + {% else %} +
    + {% include "_types/blog/_cards.html" %} +
    + {% endif %} +
    + {% endif %}{# end content_type check #} diff --git a/blog/templates/_types/blog/_oob_elements.html b/blog/templates/_types/blog/_oob_elements.html new file mode 100644 index 0000000..2aa02cb --- /dev/null +++ b/blog/templates/_types/blog/_oob_elements.html @@ -0,0 +1,40 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob_.html' import root_header with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-header-child', 'blog-header-child', '_types/blog/header/_header.html')}} + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + + +{# Filter container - blog doesn't have child_summary but still needs this element #} +{% block filter %} + {% include "_types/blog/mobile/_filter/summary.html" %} +{% endblock %} + +{# Aside with filters #} +{% block aside %} + {% include "_types/blog/desktop/menu.html" %} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/root/_nav.html' %} + {% include '_types/root/_nav_panel.html' %} +{% endblock %} + + +{% block content %} + {% include '_types/blog/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog/_page_card.html b/blog/templates/_types/blog/_page_card.html new file mode 100644 index 0000000..b4a75b9 --- /dev/null +++ b/blog/templates/_types/blog/_page_card.html @@ -0,0 +1,56 @@ +{# Single page card for pages listing #} +
    + {% set _href = url_for('blog.post.post_detail', slug=page.slug)|host %} + +
    +

    + {{ page.title }} +

    + + {# Feature badges #} + {% if page.features %} +
    + {% if page.features.get('calendar') %} + + Calendar + + {% endif %} + {% if page.features.get('market') %} + + Market + + {% endif %} +
    + {% endif %} + + {% if page.published_at %} +

    + Published: {{ page.published_at.strftime("%-d %b %Y at %H:%M") }} +

    + {% endif %} +
    + + {% if page.feature_image %} +
    + +
    + {% endif %} + {% if page.custom_excerpt or page.excerpt %} +

    + {{ page.custom_excerpt or page.excerpt }} +

    + {% endif %} +
    +
    diff --git a/blog/templates/_types/blog/_page_cards.html b/blog/templates/_types/blog/_page_cards.html new file mode 100644 index 0000000..6d1f008 --- /dev/null +++ b/blog/templates/_types/blog/_page_cards.html @@ -0,0 +1,19 @@ +{# Page cards loop with pagination sentinel #} +{% for page in pages %} + {% include "_types/blog/_page_card.html" %} +{% endfor %} +{% if page_num < total_pages|int %} +
    +{% else %} + {% if pages %} +
    End of results
    + {% else %} +
    No pages found.
    + {% endif %} +{% endif %} diff --git a/blog/templates/_types/blog/admin/tag_groups/_edit_header.html b/blog/templates/_types/blog/admin/tag_groups/_edit_header.html new file mode 100644 index 0000000..ade4ee9 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/_edit_header.html @@ -0,0 +1,9 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='tag-groups-edit-row', oob=oob) %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{ admin_nav_item(url_for('blog.tag_groups_admin.edit', id=group.id), 'pencil', group.name, select_colours, aclass='') }} + {% call links.desktop_nav() %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/blog/admin/tag_groups/_edit_main_panel.html b/blog/templates/_types/blog/admin/tag_groups/_edit_main_panel.html new file mode 100644 index 0000000..7d1fa96 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/_edit_main_panel.html @@ -0,0 +1,79 @@ +
    + + {# --- Edit group form --- #} +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + {# --- Tag checkboxes --- #} +
    + +
    + {% for tag in all_tags %} + + {% endfor %} +
    +
    + +
    + +
    +
    + + {# --- Delete form --- #} +
    + + +
    + +
    diff --git a/blog/templates/_types/blog/admin/tag_groups/_edit_oob.html b/blog/templates/_types/blog/admin/tag_groups/_edit_oob.html new file mode 100644 index 0000000..116bc7b --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/_edit_oob.html @@ -0,0 +1,17 @@ +{% extends 'oob_elements.html' %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('tag-groups-header-child', 'tag-groups-edit-child', '_types/blog/admin/tag_groups/_edit_header.html')}} + {{oob_header('root-settings-header-child', 'tag-groups-header-child', '_types/blog/admin/tag_groups/_header.html')}} + + {% from '_types/root/settings/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + +{% block mobile_menu %} +{% endblock %} + +{% block content %} + {% include '_types/blog/admin/tag_groups/_edit_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog/admin/tag_groups/_header.html b/blog/templates/_types/blog/admin/tag_groups/_header.html new file mode 100644 index 0000000..d9c3095 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/_header.html @@ -0,0 +1,9 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='tag-groups-row', oob=oob) %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{ admin_nav_item(url_for('blog.tag_groups_admin.index'), 'tags', 'Tag Groups', select_colours, aclass='') }} + {% call links.desktop_nav() %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/blog/admin/tag_groups/_main_panel.html b/blog/templates/_types/blog/admin/tag_groups/_main_panel.html new file mode 100644 index 0000000..1c8b8f4 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/_main_panel.html @@ -0,0 +1,73 @@ +
    + + {# --- Create new group form --- #} +
    + +

    New Group

    +
    + + + +
    + + +
    + + {# --- Existing groups list --- #} + {% if groups %} +
      + {% for group in groups %} +
    • + {% if group.feature_image %} + {{ group.name }} + {% else %} +
      + {{ group.name[:1] }} +
      + {% endif %} +
      + + {{ group.name }} + + {{ group.slug }} +
      + order: {{ group.sort_order }} +
    • + {% endfor %} +
    + {% else %} +

    No tag groups yet.

    + {% endif %} + + {# --- Unassigned tags --- #} + {% if unassigned_tags %} +
    +

    Unassigned Tags ({{ unassigned_tags|length }})

    +
    + {% for tag in unassigned_tags %} + + {{ tag.name }} + + {% endfor %} +
    +
    + {% endif %} + +
    diff --git a/blog/templates/_types/blog/admin/tag_groups/_oob_elements.html b/blog/templates/_types/blog/admin/tag_groups/_oob_elements.html new file mode 100644 index 0000000..cb00363 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/_oob_elements.html @@ -0,0 +1,16 @@ +{% extends 'oob_elements.html' %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-settings-header-child', 'tag-groups-header-child', '_types/blog/admin/tag_groups/_header.html')}} + + {% from '_types/root/settings/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + +{% block mobile_menu %} +{% endblock %} + +{% block content %} + {% include '_types/blog/admin/tag_groups/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog/admin/tag_groups/edit.html b/blog/templates/_types/blog/admin/tag_groups/edit.html new file mode 100644 index 0000000..5fefbc6 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/edit.html @@ -0,0 +1,13 @@ +{% extends '_types/blog/admin/tag_groups/index.html' %} + +{% block tag_groups_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/blog/admin/tag_groups/_edit_header.html' import header_row with context %} + {{ header_row() }} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/blog/admin/tag_groups/_edit_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog/admin/tag_groups/index.html b/blog/templates/_types/blog/admin/tag_groups/index.html new file mode 100644 index 0000000..680b051 --- /dev/null +++ b/blog/templates/_types/blog/admin/tag_groups/index.html @@ -0,0 +1,20 @@ +{% extends '_types/root/settings/index.html' %} + +{% block root_settings_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/blog/admin/tag_groups/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block tag_groups_header_child %} + {% endblock %} +
    + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/blog/admin/tag_groups/_main_panel.html' %} +{% endblock %} + +{% block _main_mobile_menu %} +{% endblock %} diff --git a/blog/templates/_types/blog/desktop/menu.html b/blog/templates/_types/blog/desktop/menu.html new file mode 100644 index 0000000..2c1afc4 --- /dev/null +++ b/blog/templates/_types/blog/desktop/menu.html @@ -0,0 +1,19 @@ +{% from 'macros/search.html' import search_desktop %} +{{ search_desktop(current_local_href, search, search_count, hx_select) }} +{% include '_types/blog/_action_buttons.html' %} +
    + {% include '_types/blog/desktop/menu/tag_groups.html' %} + {% include '_types/blog/desktop/menu/authors.html' %} +
    + +
    + +
    + + \ No newline at end of file diff --git a/blog/templates/_types/blog/desktop/menu/authors.html b/blog/templates/_types/blog/desktop/menu/authors.html new file mode 100644 index 0000000..de939e0 --- /dev/null +++ b/blog/templates/_types/blog/desktop/menu/authors.html @@ -0,0 +1,62 @@ + {% import '_types/blog/_card/author.html' as doauthor %} + + {# Author filter bar #} + + diff --git a/blog/templates/_types/blog/desktop/menu/tag_groups.html b/blog/templates/_types/blog/desktop/menu/tag_groups.html new file mode 100644 index 0000000..e23a879 --- /dev/null +++ b/blog/templates/_types/blog/desktop/menu/tag_groups.html @@ -0,0 +1,70 @@ + {# Tag group filter bar #} + diff --git a/blog/templates/_types/blog/desktop/menu/tags.html b/blog/templates/_types/blog/desktop/menu/tags.html new file mode 100644 index 0000000..c20b5bc --- /dev/null +++ b/blog/templates/_types/blog/desktop/menu/tags.html @@ -0,0 +1,59 @@ + {% import '_types/blog/_card/tag.html' as dotag %} + + {# Tag filter bar #} + + diff --git a/blog/templates/_types/blog/header/_header.html b/blog/templates/_types/blog/header/_header.html new file mode 100644 index 0000000..67325b9 --- /dev/null +++ b/blog/templates/_types/blog/header/_header.html @@ -0,0 +1,7 @@ + +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='blog-row', oob=oob) %} +
    + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/blog/templates/_types/blog/index.html b/blog/templates/_types/blog/index.html new file mode 100644 index 0000000..5978020 --- /dev/null +++ b/blog/templates/_types/blog/index.html @@ -0,0 +1,37 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %} + {{ super() }} + +{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('root-blog-header', '_types/blog/header/_header.html') %} + {% block root_blog_header %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block aside %} + {% include "_types/blog/desktop/menu.html" %} +{% endblock %} + +{% block filter %} + {% include "_types/blog/mobile/_filter/summary.html" %} +{% endblock %} + +{% block content %} + {% include '_types/blog/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog/mobile/_filter/_hamburger.html b/blog/templates/_types/blog/mobile/_filter/_hamburger.html new file mode 100644 index 0000000..10e0b9c --- /dev/null +++ b/blog/templates/_types/blog/mobile/_filter/_hamburger.html @@ -0,0 +1,13 @@ +
    + + + + + + + + +
    diff --git a/blog/templates/_types/blog/mobile/_filter/summary.html b/blog/templates/_types/blog/mobile/_filter/summary.html new file mode 100644 index 0000000..4ed013b --- /dev/null +++ b/blog/templates/_types/blog/mobile/_filter/summary.html @@ -0,0 +1,14 @@ +{% import 'macros/layout.html' as layout %} + +{% call layout.details('/filter', 'md:hidden') %} + {% call layout.filter_summary("filter-summary-mobile", current_local_href, search, search_count, hx_select) %} + {% include '_types/blog/mobile/_filter/summary/tag_groups.html' %} + {% include '_types/blog/mobile/_filter/summary/authors.html' %} + {% endcall %} + {% include '_types/blog/_action_buttons.html' %} +
    + {% include '_types/blog/desktop/menu/tag_groups.html' %} + {% include '_types/blog/desktop/menu/authors.html' %} +
    +{% endcall %} + \ No newline at end of file diff --git a/blog/templates/_types/blog/mobile/_filter/summary/authors.html b/blog/templates/_types/blog/mobile/_filter/summary/authors.html new file mode 100644 index 0000000..32796d9 --- /dev/null +++ b/blog/templates/_types/blog/mobile/_filter/summary/authors.html @@ -0,0 +1,31 @@ +{% if selected_authors and selected_authors|length %} +
      + {% for st in selected_authors %} + {% for author in authors %} + {% if st == author.slug %} +
    • + {% if author.profile_image %} + {{ author.name }} + {% else %} + {# optional fallback circle with first letter #} +
      + {{ author.name[:1] }} +
      + {% endif %} + + + {{ author.name }} + + + {{author.published_post_count}} + +
    • + {% endif %} + {% endfor %} + {% endfor %} +
    +{% endif %} \ No newline at end of file diff --git a/blog/templates/_types/blog/mobile/_filter/summary/tag_groups.html b/blog/templates/_types/blog/mobile/_filter/summary/tag_groups.html new file mode 100644 index 0000000..7bf142e --- /dev/null +++ b/blog/templates/_types/blog/mobile/_filter/summary/tag_groups.html @@ -0,0 +1,33 @@ +{% if selected_groups and selected_groups|length %} +
      + {% for sg in selected_groups %} + {% for group in tag_groups %} + {% if sg == group.slug %} +
    • + {% if group.feature_image %} + {{ group.name }} + {% else %} +
      + {{ group.name[:1] }} +
      + {% endif %} + + + {{ group.name }} + + + {{group.post_count}} + +
    • + {% endif %} + {% endfor %} + {% endfor %} +
    +{% endif %} diff --git a/blog/templates/_types/blog/mobile/_filter/summary/tags.html b/blog/templates/_types/blog/mobile/_filter/summary/tags.html new file mode 100644 index 0000000..df6169d --- /dev/null +++ b/blog/templates/_types/blog/mobile/_filter/summary/tags.html @@ -0,0 +1,31 @@ +{% if selected_tags and selected_tags|length %} +
      + {% for st in selected_tags %} + {% for tag in tags %} + {% if st == tag.slug %} +
    • + {% if tag.feature_image %} + {{ tag.name }} + {% else %} + {# optional fallback circle with first letter #} +
      + {{ tag.name[:1] }} +
      + {% endif %} + + + {{ tag.name }} + + + {{tag.published_post_count}} + +
    • + {% endif %} + {% endfor %} + {% endfor %} +
    +{% endif %} \ No newline at end of file diff --git a/blog/templates/_types/blog/not_found.html b/blog/templates/_types/blog/not_found.html new file mode 100644 index 0000000..f539822 --- /dev/null +++ b/blog/templates/_types/blog/not_found.html @@ -0,0 +1,22 @@ +{% extends '_types/root/_index.html' %} + +{% block content %} +
    +
    📝
    +

    Post Not Found

    +

    + The post "{{ slug }}" could not be found. +

    + + ← Back to Blog + +
    +{% endblock %} diff --git a/blog/templates/_types/blog_drafts/_main_panel.html b/blog/templates/_types/blog_drafts/_main_panel.html new file mode 100644 index 0000000..8cb0b7a --- /dev/null +++ b/blog/templates/_types/blog_drafts/_main_panel.html @@ -0,0 +1,55 @@ +
    + +
    +

    Drafts

    + {% set new_href = url_for('blog.new_post')|host %} + + New Post + +
    + + {% if drafts %} + + {% else %} +

    No drafts yet.

    + {% endif %} + +
    diff --git a/blog/templates/_types/blog_drafts/_oob_elements.html b/blog/templates/_types/blog_drafts/_oob_elements.html new file mode 100644 index 0000000..8d9790b --- /dev/null +++ b/blog/templates/_types/blog_drafts/_oob_elements.html @@ -0,0 +1,12 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/blog/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block content %} + {% include '_types/blog_drafts/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog_drafts/index.html b/blog/templates/_types/blog_drafts/index.html new file mode 100644 index 0000000..6ce38f1 --- /dev/null +++ b/blog/templates/_types/blog_drafts/index.html @@ -0,0 +1,11 @@ +{% extends '_types/root/_index.html' %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('root-blog-header', '_types/blog/header/_header.html') %} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/blog_drafts/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog_new/_main_panel.html b/blog/templates/_types/blog_new/_main_panel.html new file mode 100644 index 0000000..6c3d264 --- /dev/null +++ b/blog/templates/_types/blog_new/_main_panel.html @@ -0,0 +1,259 @@ +{# ── Error banner ── #} +{% if save_error %} +
    + Save failed: {{ save_error }} +
    +{% endif %} + +
    + + + + + + {# ── Feature image ── #} +
    + {# Empty state: add link #} +
    + +
    + + {# Filled state: image preview + controls #} + + + {# Upload spinner overlay #} + + + {# Hidden file input #} + +
    + + {# ── Title ── #} + + + {# ── Excerpt ── #} + + + {# ── Editor mount point ── #} +
    + + {# ── Status + Save footer ── #} +
    + + + +
    +
    + +{# ── Koenig editor assets ── #} + + + + diff --git a/blog/templates/_types/blog_new/_oob_elements.html b/blog/templates/_types/blog_new/_oob_elements.html new file mode 100644 index 0000000..61e78f5 --- /dev/null +++ b/blog/templates/_types/blog_new/_oob_elements.html @@ -0,0 +1,12 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/blog/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block content %} + {% include '_types/blog_new/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/blog_new/index.html b/blog/templates/_types/blog_new/index.html new file mode 100644 index 0000000..3c802d4 --- /dev/null +++ b/blog/templates/_types/blog_new/index.html @@ -0,0 +1,11 @@ +{% extends '_types/root/_index.html' %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('root-blog-header', '_types/blog/header/_header.html') %} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/blog_new/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/home/_oob_elements.html b/blog/templates/_types/home/_oob_elements.html new file mode 100644 index 0000000..03a4f17 --- /dev/null +++ b/blog/templates/_types/home/_oob_elements.html @@ -0,0 +1,19 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block content %} +
    +
    + {% if post.html %} + {{post.html|safe}} + {% endif %} +
    +
    +{% endblock %} diff --git a/blog/templates/_types/home/index.html b/blog/templates/_types/home/index.html new file mode 100644 index 0000000..e5df191 --- /dev/null +++ b/blog/templates/_types/home/index.html @@ -0,0 +1,14 @@ +{% extends '_types/root/_index.html' %} +{% block meta %} + {% include '_types/post/_meta.html' %} +{% endblock %} + +{% block content %} +
    +
    + {% if post.html %} + {{post.html|safe}} + {% endif %} +
    +
    +{% endblock %} diff --git a/blog/templates/_types/menu_items/_form.html b/blog/templates/_types/menu_items/_form.html new file mode 100644 index 0000000..8eed1c0 --- /dev/null +++ b/blog/templates/_types/menu_items/_form.html @@ -0,0 +1,125 @@ + + + diff --git a/blog/templates/_types/menu_items/_list.html b/blog/templates/_types/menu_items/_list.html new file mode 100644 index 0000000..3892f07 --- /dev/null +++ b/blog/templates/_types/menu_items/_list.html @@ -0,0 +1,68 @@ +
    + {% if menu_items %} +
    + {% for item in menu_items %} +
    + {# Drag handle #} +
    + +
    + + {# Page image #} + {% if item.feature_image %} + {{ item.label }} + {% else %} +
    + {% endif %} + + {# Page title #} +
    +
    {{ item.label }}
    +
    {{ item.slug }}
    +
    + + {# Sort order #} +
    + Order: {{ item.sort_order }} +
    + + {# Actions #} +
    + + +
    +
    + {% endfor %} +
    + {% else %} +
    + +

    No menu items yet. Add one to get started!

    +
    + {% endif %} +
    diff --git a/blog/templates/_types/menu_items/_main_panel.html b/blog/templates/_types/menu_items/_main_panel.html new file mode 100644 index 0000000..bc502dd --- /dev/null +++ b/blog/templates/_types/menu_items/_main_panel.html @@ -0,0 +1,20 @@ +
    +
    + +
    + + {# Form container #} + + + {# Menu items list #} + +
    diff --git a/blog/templates/_types/menu_items/_nav_oob.html b/blog/templates/_types/menu_items/_nav_oob.html new file mode 100644 index 0000000..e25189a --- /dev/null +++ b/blog/templates/_types/menu_items/_nav_oob.html @@ -0,0 +1,31 @@ +{% set _app_slugs = {'cart': cart_url('/')} %} +{% set _first_seg = request.path.strip('/').split('/')[0] %} + diff --git a/blog/templates/_types/menu_items/_oob_elements.html b/blog/templates/_types/menu_items/_oob_elements.html new file mode 100644 index 0000000..c242593 --- /dev/null +++ b/blog/templates/_types/menu_items/_oob_elements.html @@ -0,0 +1,23 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-settings-header-child', 'menu_items-header-child', '_types/menu_items/header/_header.html')}} + + {% from '_types/root/settings/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} + +{% endblock %} + +{% block mobile_menu %} +{#% include '_types/root/settings/_nav.html' %#} +{% endblock %} + +{% block content %} + {% include '_types/menu_items/_main_panel.html' %} +{% endblock %} + diff --git a/blog/templates/_types/menu_items/_page_search_results.html b/blog/templates/_types/menu_items/_page_search_results.html new file mode 100644 index 0000000..df36d0d --- /dev/null +++ b/blog/templates/_types/menu_items/_page_search_results.html @@ -0,0 +1,44 @@ +{% if pages %} +
    + {% for post in pages %} +
    + + {# Page image #} + {% if post.feature_image %} + {{ post.title }} + {% else %} +
    + {% endif %} + + {# Page info #} +
    +
    {{ post.title }}
    +
    {{ post.slug }}
    +
    +
    + {% endfor %} + + {# Infinite scroll sentinel #} + {% if has_more %} +
    + Loading more... +
    + {% endif %} +
    +{% elif query %} +
    + No pages found matching "{{ query }}" +
    +{% endif %} diff --git a/blog/templates/_types/menu_items/header/_header.html b/blog/templates/_types/menu_items/header/_header.html new file mode 100644 index 0000000..55a18d6 --- /dev/null +++ b/blog/templates/_types/menu_items/header/_header.html @@ -0,0 +1,9 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='menu_items-row', oob=oob) %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{ admin_nav_item(url_for('menu_items.list_menu_items'), 'bars', 'Menu Items', select_colours, aclass='') }} + {% call links.desktop_nav() %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/menu_items/index.html b/blog/templates/_types/menu_items/index.html new file mode 100644 index 0000000..5bcf7da --- /dev/null +++ b/blog/templates/_types/menu_items/index.html @@ -0,0 +1,20 @@ +{% extends '_types/root/settings/index.html' %} + +{% block root_settings_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/menu_items/header/_header.html' import header_row with context %} + {{ header_row() }} + + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/menu_items/_main_panel.html' %} +{% endblock %} + +{% block _main_mobile_menu %} +{% endblock %} diff --git a/blog/templates/_types/post/_entry_container.html b/blog/templates/_types/post/_entry_container.html new file mode 100644 index 0000000..3c3965a --- /dev/null +++ b/blog/templates/_types/post/_entry_container.html @@ -0,0 +1,24 @@ +
    +
    + {% include '_types/post/_entry_items.html' with context %} +
    +
    + + diff --git a/blog/templates/_types/post/_entry_items.html b/blog/templates/_types/post/_entry_items.html new file mode 100644 index 0000000..d221e85 --- /dev/null +++ b/blog/templates/_types/post/_entry_items.html @@ -0,0 +1,38 @@ +{# Get entries from either direct variable or associated_entries dict #} +{% set entry_list = entries if entries is defined else (associated_entries.entries if associated_entries is defined else []) %} +{% set current_page = page if page is defined else (associated_entries.page if associated_entries is defined else 1) %} +{% set has_more_entries = has_more if has_more is defined else (associated_entries.has_more if associated_entries is defined else False) %} + +{% for entry in entry_list %} + {% set _entry_path = '/' + post.slug + '/calendars/' + entry.calendar_slug + '/' + entry.start_at.year|string + '/' + entry.start_at.month|string + '/' + entry.start_at.day|string + '/entries/' + entry.id|string + '/' %} + + {% if post.feature_image %} + {{ post.title }} + {% else %} +
    + {% endif %} +
    +
    {{ entry.name }}
    +
    + {{ entry.start_at.strftime('%b %d, %Y at %H:%M') }} + {% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    +
    +
    +{% endfor %} + +{# Load more entries one at a time until container is full #} +{% if has_more_entries %} +
    +
    +{% endif %} diff --git a/blog/templates/_types/post/_main_panel.html b/blog/templates/_types/post/_main_panel.html new file mode 100644 index 0000000..52a2c3a --- /dev/null +++ b/blog/templates/_types/post/_main_panel.html @@ -0,0 +1,65 @@ +{# Main panel fragment for HTMX navigation - post/page article content #} +
    + {# Draft indicator + edit link (shown for both posts and pages) #} + {% if post.status == "draft" %} +
    + Draft + {% if post.publish_requested %} + Publish requested + {% endif %} + {% set is_admin = (g.get("rights") or {}).get("admin") %} + {% if is_admin or (g.user and post.user_id == g.user.id) %} + {% set edit_href = url_for('blog.post.admin.edit', slug=post.slug)|host %} + + Edit + + {% endif %} +
    + {% endif %} + + {% if not post.is_page %} + {# ── Blog post chrome: like button, excerpt, tags/authors ── #} + {% if g.user %} +
    + {% set slug = post.slug %} + {% set liked = post.is_liked or False %} + {% set like_url = url_for('blog.post.like_toggle', slug=slug)|host %} + {% set item_type = 'post' %} + {% include "_types/browse/like/button.html" %} +
    + {% endif %} + + {% if post.custom_excerpt %} +
    + {{post.custom_excerpt|safe}} +
    + {% endif %} + + {% endif %} + + {% if post.feature_image %} +
    + +
    + {% endif %} +
    + {% if post.html %} + {{post.html|safe}} + {% endif %} +
    +
    +
    diff --git a/blog/templates/_types/post/_meta.html b/blog/templates/_types/post/_meta.html new file mode 100644 index 0000000..c4ef2ad --- /dev/null +++ b/blog/templates/_types/post/_meta.html @@ -0,0 +1,124 @@ +{# --- social/meta_post.html --- #} +{# Context expected: + site, post, request +#} + +{# Visibility → robots #} +{% set is_public = (post.visibility == 'public') %} +{% set is_published = (post.status == 'published') %} +{% set robots_here = 'index,follow' if (is_public and is_published and not post.email_only) else 'noindex,nofollow' %} + +{# Compute canonical early so both this file and base can use it #} +{% set _site_url = site().url.rstrip('/') if site and site().url else '' %} +{% set _post_path = request.path if request else ('/posts/' ~ (post.slug or post.uuid)) %} +{% set canonical = post.canonical_url or (_site_url ~ _post_path if _site_url else (request.url if request else None)) %} + +{# Include common base (charset, viewport, robots default, RSS, Org/WebSite JSON-LD) #} +{% set robots_override = robots_here %} +{% include 'social/meta_base.html' %} + +{# ---- Titles / descriptions ---- #} +{% set og_title = post.og_title or base_title %} +{% set tw_title = post.twitter_title or base_title %} + +{# Description best-effort, trimmed #} +{% set desc_source = post.meta_description + or post.og_description + or post.twitter_description + or post.custom_excerpt + or post.excerpt + or (post.plaintext if post.plaintext else (post.html|striptags if post.html else '')) %} +{% set description = (desc_source|trim|replace('\n',' ')|replace('\r',' ')|striptags)|truncate(160, True, '…') %} + +{# Image priority #} +{% set image_url = post.og_image + or post.twitter_image + or post.feature_image + or (site().default_image if site and site().default_image else None) %} + +{# Dates #} +{% set published_iso = post.published_at.isoformat() if post.published_at else None %} +{% set updated_iso = post.updated_at.isoformat() if post.updated_at + else (post.created_at.isoformat() if post.created_at else None) %} + +{# Authors / tags #} +{% set primary_author = post.primary_author %} +{% set authors = post.authors or ([primary_author] if primary_author else []) %} +{% set tag_names = (post.tags or []) | map(attribute='name') | list %} +{% set is_article = not post.is_page %} + +{{ base_title }} + +{% if canonical %}{% endif %} + +{# ---- Open Graph ---- #} + + + + +{% if canonical %}{% endif %} +{% if image_url %}{% endif %} +{% if is_article and published_iso %}{% endif %} +{% if is_article and updated_iso %} + + +{% endif %} +{% if is_article and post.primary_tag and post.primary_tag.name %} + +{% endif %} +{% if is_article %} + {% for t in tag_names %} + + {% endfor %} +{% endif %} + +{# ---- Twitter ---- #} + +{% if site and site().twitter_site %}{% endif %} +{% if primary_author and primary_author.twitter %} + +{% endif %} + + +{% if image_url %}{% endif %} + +{# ---- JSON-LD author value (no list comprehensions) ---- #} +{% if authors and authors|length == 1 %} + {% set author_value = {"@type": "Person", "name": authors[0].name} %} +{% elif authors %} + {% set ns = namespace(arr=[]) %} + {% for a in authors %} + {% set _ = ns.arr.append({"@type": "Person", "name": a.name}) %} + {% endfor %} + {% set author_value = ns.arr %} +{% else %} + {% set author_value = none %} +{% endif %} + +{# ---- JSON-LD using combine for optionals ---- #} +{% set jsonld = { + "@context": "https://schema.org", + "@type": "BlogPosting" if is_article else "WebPage", + "mainEntityOfPage": canonical, + "headline": base_title, + "description": description, + "image": image_url, + "datePublished": published_iso, + "author": author_value, + "publisher": { + "@type": "Organization", + "name": site().title if site and site().title else "", + "logo": {"@type": "ImageObject", "url": site().logo if site and site().logo else image_url} + } +} %} + +{% if updated_iso %} + {% set jsonld = jsonld | combine({"dateModified": updated_iso}) %} +{% endif %} +{% if tag_names %} + {% set jsonld = jsonld | combine({"keywords": tag_names | join(", ")}) %} +{% endif %} + + diff --git a/blog/templates/_types/post/_nav.html b/blog/templates/_types/post/_nav.html new file mode 100644 index 0000000..037bdcd --- /dev/null +++ b/blog/templates/_types/post/_nav.html @@ -0,0 +1,15 @@ +{% import 'macros/links.html' as links %} + {# Widget-driven container nav — entries, calendars, markets #} + {% if container_nav_widgets %} +
    + {% include '_types/post/admin/_nav_entries.html' %} +
    + {% endif %} + + {# Admin link #} + {% if post and has_access('blog.post.admin.admin') %} + {% call links.link(url_for('blog.post.admin.admin', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + + {% endcall %} + {% endif %} diff --git a/blog/templates/_types/post/_oob_elements.html b/blog/templates/_types/post/_oob_elements.html new file mode 100644 index 0000000..d8bda2c --- /dev/null +++ b/blog/templates/_types/post/_oob_elements.html @@ -0,0 +1,36 @@ +{% extends 'oob_elements.html' %} + + +{# OOB elements for HTMX navigation - all elements that need updating #} +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + + +{% block oobs %} + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% from '_types/root/_n/macros.html' import header with context %} +{% call header(id='root-header-child', oob=True) %} + {% call header() %} + {% from '_types/post/header/_header.html' import header_row with context %} + {{header_row()}} +
    + +
    + {% endcall %} +{% endcall %} + + +{# Mobile menu #} + +{% block mobile_menu %} + {% include '_types/post/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post/admin/_associated_entries.html b/blog/templates/_types/post/admin/_associated_entries.html new file mode 100644 index 0000000..d9fe853 --- /dev/null +++ b/blog/templates/_types/post/admin/_associated_entries.html @@ -0,0 +1,50 @@ +
    +

    Associated Entries

    + {% if associated_entry_ids %} +
    + {% for calendar in all_calendars %} + {% for entry in calendar.entries %} + {% if entry.id in associated_entry_ids and entry.deleted_at is none %} + + {% endif %} + {% endfor %} + {% endfor %} +
    + {% else %} +
    No entries associated yet. Browse calendars below to add entries.
    + {% endif %} +
    diff --git a/blog/templates/_types/post/admin/_calendar_view.html b/blog/templates/_types/post/admin/_calendar_view.html new file mode 100644 index 0000000..80ae33f --- /dev/null +++ b/blog/templates/_types/post/admin/_calendar_view.html @@ -0,0 +1,88 @@ +
    + {# Month/year navigation #} +
    + +
    + + {# Calendar grid #} +
    + + +
    + {% for week in weeks %} + {% for day in week %} +
    +
    {{ day.date.day }}
    + + {# Entries for this day #} +
    + {% for e in month_entries %} + {% if e.start_at.date() == day.date %} + {% if e.id in associated_entry_ids %} + {# Associated entry - show with delete button #} +
    + {{ e.name }} + +
    + {% else %} + {# Non-associated entry - clickable to add #} + + {% endif %} + {% endif %} + {% endfor %} +
    +
    + {% endfor %} + {% endfor %} +
    +
    +
    diff --git a/blog/templates/_types/post/admin/_features_panel.html b/blog/templates/_types/post/admin/_features_panel.html new file mode 100644 index 0000000..19f9296 --- /dev/null +++ b/blog/templates/_types/post/admin/_features_panel.html @@ -0,0 +1,112 @@ +{# Feature toggles for PageConfig #} +
    +

    Page Features

    + +
    + + + +
    + + {# SumUp credentials — shown when calendar or market is enabled #} + {% if features.get('calendar') or features.get('market') %} +
    +

    + + SumUp Payment +

    +

    + Configure per-page SumUp credentials. Leave blank to use the global merchant account. +

    + +
    +
    + + +
    + +
    + + + {% if sumup_configured %} +

    Key is set. Leave blank to keep current key.

    + {% endif %} +
    + +
    + + +
    + + + + {% if sumup_configured %} + + Connected + + {% endif %} +
    +
    + {% endif %} +
    diff --git a/blog/templates/_types/post/admin/_main_panel.html b/blog/templates/_types/post/admin/_main_panel.html new file mode 100644 index 0000000..58d5238 --- /dev/null +++ b/blog/templates/_types/post/admin/_main_panel.html @@ -0,0 +1,7 @@ +{# Main panel fragment for HTMX navigation - post admin #} +
    +
    +
    diff --git a/blog/templates/_types/post/admin/_markets_panel.html b/blog/templates/_types/post/admin/_markets_panel.html new file mode 100644 index 0000000..d40076a --- /dev/null +++ b/blog/templates/_types/post/admin/_markets_panel.html @@ -0,0 +1,44 @@ +
    +

    Markets

    + + {% if markets %} +
      + {% for m in markets %} +
    • +
      + {{ m.name }} + /{{ m.slug }}/ +
      + +
    • + {% endfor %} +
    + {% else %} +

    No markets yet.

    + {% endif %} + +
    + + +
    +
    diff --git a/blog/templates/_types/post/admin/_nav.html b/blog/templates/_types/post/admin/_nav.html new file mode 100644 index 0000000..c0bfab6 --- /dev/null +++ b/blog/templates/_types/post/admin/_nav.html @@ -0,0 +1,28 @@ +{% import 'macros/links.html' as links %} + + + +{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + entries +{% endcall %} +{% call links.link(url_for('blog.post.admin.data', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + data +{% endcall %} +{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + edit +{% endcall %} +{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + settings +{% endcall %} \ No newline at end of file diff --git a/blog/templates/_types/post/admin/_nav_entries.html b/blog/templates/_types/post/admin/_nav_entries.html new file mode 100644 index 0000000..47290d4 --- /dev/null +++ b/blog/templates/_types/post/admin/_nav_entries.html @@ -0,0 +1,50 @@ + + {# Left scroll arrow - desktop only #} + + + {# Widget-driven nav items container #} +
    +
    + {% for wdata in container_nav_widgets %} + {% with ctx=wdata.ctx %} + {% include wdata.widget.template with context %} + {% endwith %} + {% endfor %} +
    +
    + + + + {# Right scroll arrow - desktop only #} + diff --git a/blog/templates/_types/post/admin/_nav_entries_oob.html b/blog/templates/_types/post/admin/_nav_entries_oob.html new file mode 100644 index 0000000..eecc3d5 --- /dev/null +++ b/blog/templates/_types/post/admin/_nav_entries_oob.html @@ -0,0 +1,80 @@ +{# OOB swap for nav entries and calendars when toggling associations or editing calendars #} +{% import 'macros/links.html' as links %} + +{# Associated Entries and Calendars - vertical on mobile, horizontal with arrows on desktop #} +{% if (associated_entries and associated_entries.entries) or calendars %} +
    + {# Left scroll arrow - desktop only #} + + +
    +
    + {# Calendar entries #} + {% if associated_entries and associated_entries.entries %} + {% for entry in associated_entries.entries %} + {% set _entry_path = '/' + post.slug + '/calendars/' + entry.calendar_slug + '/' + entry.start_at.year|string + '/' + entry.start_at.month|string + '/' + entry.start_at.day|string + '/entries/' + entry.id|string + '/' %} + +
    +
    +
    {{ entry.name }}
    +
    + {{ entry.start_at.strftime('%b %d, %Y at %H:%M') }} + {% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    +
    +
    + {% endfor %} + {% endif %} + {# Calendar links #} + {% if calendars %} + {% for calendar in calendars %} + {% set local_href=events_url('/' + post.slug + '/calendars/' + calendar.slug + '/') %} + + +
    {{calendar.name}}
    +
    + {% endfor %} + {% endif %} +
    +
    + + + + {# Right scroll arrow - desktop only #} + +
    +{% else %} + {# Empty placeholder to remove nav items when all are disassociated/deleted #} +
    +{% endif %} diff --git a/blog/templates/_types/post/admin/_oob_elements.html b/blog/templates/_types/post/admin/_oob_elements.html new file mode 100644 index 0000000..d397c68 --- /dev/null +++ b/blog/templates/_types/post/admin/_oob_elements.html @@ -0,0 +1,22 @@ +{% extends "oob_elements.html" %} +{# OOB elements for post admin page #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-header-child', 'post-admin-header-child', '_types/post/admin/header/_header.html')}} + + {% from '_types/post/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block mobile_menu %} + {% include '_types/post/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post/admin/_main_panel.html' %} +{% endblock %} \ No newline at end of file diff --git a/blog/templates/_types/post/admin/header/_header.html b/blog/templates/_types/post/admin/header/_header.html new file mode 100644 index 0000000..2708e4f --- /dev/null +++ b/blog/templates/_types/post/admin/header/_header.html @@ -0,0 +1,13 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post-admin-row', oob=oob) %} + {% call links.link( + url_for('blog.post.admin.admin', slug=post.slug), + hx_select_search) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/post/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/blog/templates/_types/post/admin/index.html b/blog/templates/_types/post/admin/index.html new file mode 100644 index 0000000..1a7cc45 --- /dev/null +++ b/blog/templates/_types/post/admin/index.html @@ -0,0 +1,18 @@ +{% extends '_types/post/index.html' %} +{% import 'macros/layout.html' as layout %} + +{% block post_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %} + {% block post_admin_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/post/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post/admin/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post/header/_header.html b/blog/templates/_types/post/header/_header.html new file mode 100644 index 0000000..143e79d --- /dev/null +++ b/blog/templates/_types/post/header/_header.html @@ -0,0 +1,28 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post-row', oob=oob) %} + {% call links.link(url_for('blog.post.post_detail', slug=post.slug), hx_select_search ) %} + {% if post.feature_image %} + + {% endif %} + + {{ post.title | truncate(160, True, '…') }} + + {% endcall %} + {% call links.desktop_nav() %} + {% if page_cart_count is defined and page_cart_count > 0 %} + + + {{ page_cart_count }} + + {% endif %} + {% include '_types/post/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/blog/templates/_types/post/index.html b/blog/templates/_types/post/index.html new file mode 100644 index 0000000..56ed99c --- /dev/null +++ b/blog/templates/_types/post/index.html @@ -0,0 +1,25 @@ +{% extends '_types/root/_index.html' %} +{% import 'macros/layout.html' as layout %} +{% block meta %} + {% include '_types/post/_meta.html' %} +{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% block post_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/post/_nav.html' %} +{% endblock %} + + +{% block aside %} +{% endblock %} + +{% block content %} + {% include '_types/post/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post_data/_main_panel.html b/blog/templates/_types/post_data/_main_panel.html new file mode 100644 index 0000000..83dcc32 --- /dev/null +++ b/blog/templates/_types/post_data/_main_panel.html @@ -0,0 +1,137 @@ +{% macro render_scalar_table(obj) -%} +
    + + + + + + + + + {% for col in obj.__mapper__.columns %} + {% set key = col.key %} + {% set val = obj|attr(key) %} + {% if key != "_sa_instance_state" %} + + + + + {% endif %} + {% endfor %} + +
    FieldValue
    {{ key }} + {% if val is none %} + + {% elif val.__class__.__name__ in ["datetime", "date"] and val.isoformat is defined %} +
    {{ val.isoformat() }}
    + {% elif val is string %} +
    {{ val }}
    + {% else %} +
    {{ val }}
    + {% endif %} +
    +
    +{%- endmacro %} + +{% macro render_model(obj, depth=0, max_depth=2) -%} + {% if obj is none %} + + {% else %} +
    + {{ render_scalar_table(obj) }} + +
    + {% for rel in obj.__mapper__.relationships %} + {% set rel_name = rel.key %} + {% set loaded = rel.key in obj.__dict__ %} + {% if loaded %} + {% set value = obj|attr(rel_name) %} + {% else %} + {% set value = none %} + {% endif %} + +
    +
    + Relationship: {{ rel_name }} + + {{ 'many' if rel.uselist else 'one' }} → {{ rel.mapper.class_.__name__ }} + {% if not loaded %} • not loaded{% endif %} + +
    + +
    + {% if value is none %} + + + {% elif rel.uselist %} + {% set items = value or [] %} +
    {{ items|length }} item{{ '' if items|length == 1 else 's' }}
    + + {% if items %} +
    + + + + + + + + + {% for it in items %} + + + + + {% endfor %} + +
    #Summary
    {{ loop.index }} + {% set ident = [] %} + {% for k in ['id','ghost_id','uuid','slug','name','title'] if k in it.__mapper__.c %} + {% set v = (it|attr(k))|default('', true) %} + {% do ident.append(k ~ '=' ~ v) %} + {% endfor %} +
    {{ (ident|join(' • ')) or it|string }}
    + + {% if depth < max_depth %} +
    + {{ render_model(it, depth+1, max_depth) }} +
    + {% else %} +
    …max depth reached…
    + {% endif %} +
    +
    + {% endif %} + + {% else %} + {% set child = value %} + {% set ident = [] %} + {% for k in ['id','ghost_id','uuid','slug','name','title'] if k in child.__mapper__.c %} + {% set v = (child|attr(k))|default('', true) %} + {% do ident.append(k ~ '=' ~ v) %} + {% endfor %} +
    {{ (ident|join(' • ')) or child|string }}
    + + {% if depth < max_depth %} +
    + {{ render_model(child, depth+1, max_depth) }} +
    + {% else %} +
    …max depth reached…
    + {% endif %} + {% endif %} +
    +
    + {% endfor %} +
    +
    + {% endif %} +{%- endmacro %} + +
    +
    + Model: Post • Table: {{ original_post.__tablename__ }} +
    + {{ render_model(original_post, 0, 2) }} +
    + diff --git a/blog/templates/_types/post_data/_nav.html b/blog/templates/_types/post_data/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/blog/templates/_types/post_data/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/blog/templates/_types/post_data/_oob_elements.html b/blog/templates/_types/post_data/_oob_elements.html new file mode 100644 index 0000000..32fd0c7 --- /dev/null +++ b/blog/templates/_types/post_data/_oob_elements.html @@ -0,0 +1,28 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'post_data-header-child', '_types/post_data/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/post_data/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/post_data/_main_panel.html" %} +{% endblock %} + + diff --git a/blog/templates/_types/post_data/header/_header.html b/blog/templates/_types/post_data/header/_header.html new file mode 100644 index 0000000..27eaf6f --- /dev/null +++ b/blog/templates/_types/post_data/header/_header.html @@ -0,0 +1,15 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post_data-row', oob=oob) %} + + +
    data
    +
    + {% call links.desktop_nav() %} + {#% include '_types/post_data/_nav.html' %#} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/blog/templates/_types/post_data/index.html b/blog/templates/_types/post_data/index.html new file mode 100644 index 0000000..1df67b8 --- /dev/null +++ b/blog/templates/_types/post_data/index.html @@ -0,0 +1,24 @@ +{% extends '_types/post/admin/index.html' %} + +{% block ___app_title %} + {% import 'macros/links.html' as links %} + {% call links.menu_row() %} + {% call links.link(url_for('blog.post.admin.data', slug=post.slug), hx_select_search) %} + +
    + data +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/post_data/_nav.html' %} + {% endcall %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/post_data/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post_data/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post_edit/_main_panel.html b/blog/templates/_types/post_edit/_main_panel.html new file mode 100644 index 0000000..05d9251 --- /dev/null +++ b/blog/templates/_types/post_edit/_main_panel.html @@ -0,0 +1,352 @@ +{# ── Error banner ── #} +{% if save_error %} +
    + Save failed: {{ save_error }} +
    +{% endif %} + +
    + + + + + + + {# ── Feature image ── #} +
    + {# Empty state: add link #} +
    + +
    + + {# Filled state: image preview + controls #} +
    + + {# Delete button (top-right, visible on hover) #} + + + {# Caption input #} + +
    + + {# Upload spinner overlay #} + + + {# Hidden file input #} + +
    + + {# ── Title ── #} + + + {# ── Excerpt ── #} + + + {# ── Editor mount point ── #} +
    + + {# ── Initial Lexical JSON from Ghost ── #} + + + {# ── Status + Publish mode + Save footer ── #} + {% set already_emailed = ghost_post and ghost_post.email and ghost_post.email.status %} +
    + + + {# Publish mode — only relevant when publishing #} + + + {# Newsletter picker — only when email is involved #} + + + + + {% if save_success %} + Saved. + {% endif %} + {% if request.args.get('publish_requested') %} + Publish requested — an admin will review. + {% endif %} + {% if post and post.publish_requested %} + Publish requested + {% endif %} + {% if already_emailed %} + + Emailed{% if ghost_post.newsletter %} to {{ ghost_post.newsletter.name }}{% endif %} + + {% endif %} +
    + + {# ── Publish-mode show/hide logic ── #} + +
    + +{# ── Koenig editor assets ── #} + + + + diff --git a/blog/templates/_types/post_edit/_nav.html b/blog/templates/_types/post_edit/_nav.html new file mode 100644 index 0000000..0b1d08a --- /dev/null +++ b/blog/templates/_types/post_edit/_nav.html @@ -0,0 +1,5 @@ +{% import 'macros/links.html' as links %} +{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + + settings +{% endcall %} diff --git a/blog/templates/_types/post_edit/_oob_elements.html b/blog/templates/_types/post_edit/_oob_elements.html new file mode 100644 index 0000000..694096c --- /dev/null +++ b/blog/templates/_types/post_edit/_oob_elements.html @@ -0,0 +1,19 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'post_edit-header-child', '_types/post_edit/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block mobile_menu %} + {% include '_types/post_edit/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post_edit/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post_edit/header/_header.html b/blog/templates/_types/post_edit/header/_header.html new file mode 100644 index 0000000..60e07e7 --- /dev/null +++ b/blog/templates/_types/post_edit/header/_header.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post_edit-row', oob=oob) %} + {% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search) %} + +
    + edit +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/post_edit/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/post_edit/index.html b/blog/templates/_types/post_edit/index.html new file mode 100644 index 0000000..b5c7212 --- /dev/null +++ b/blog/templates/_types/post_edit/index.html @@ -0,0 +1,17 @@ +{% extends '_types/post/admin/index.html' %} + +{% block post_admin_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-admin-header-child', '_types/post_edit/header/_header.html') %} + {% block post_edit_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/post_edit/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post_edit/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post_entries/_main_panel.html b/blog/templates/_types/post_entries/_main_panel.html new file mode 100644 index 0000000..342041e --- /dev/null +++ b/blog/templates/_types/post_entries/_main_panel.html @@ -0,0 +1,48 @@ +
    + + {# Associated Entries List #} + {% include '_types/post/admin/_associated_entries.html' %} + + {# Calendars Browser #} +
    +

    Browse Calendars

    + {% for calendar in all_calendars %} +
    + + {% if calendar.post.feature_image %} + {{ calendar.post.title }} + {% else %} +
    + {% endif %} +
    +
    + + {{ calendar.name }} +
    +
    + {{ calendar.post.title }} +
    +
    +
    +
    +
    Loading calendar...
    +
    +
    + {% else %} +
    No calendars found.
    + {% endfor %} +
    +
    \ No newline at end of file diff --git a/blog/templates/_types/post_entries/_nav.html b/blog/templates/_types/post_entries/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/blog/templates/_types/post_entries/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/blog/templates/_types/post_entries/_oob_elements.html b/blog/templates/_types/post_entries/_oob_elements.html new file mode 100644 index 0000000..3ef5559 --- /dev/null +++ b/blog/templates/_types/post_entries/_oob_elements.html @@ -0,0 +1,28 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'post_entries-header-child', '_types/post_entries/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/post_entries/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/post_entries/_main_panel.html" %} +{% endblock %} + + diff --git a/blog/templates/_types/post_entries/header/_header.html b/blog/templates/_types/post_entries/header/_header.html new file mode 100644 index 0000000..019c000 --- /dev/null +++ b/blog/templates/_types/post_entries/header/_header.html @@ -0,0 +1,17 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post_entries-row', oob=oob) %} + {% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search) %} + +
    + entries +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/post_entries/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/blog/templates/_types/post_entries/index.html b/blog/templates/_types/post_entries/index.html new file mode 100644 index 0000000..382d297 --- /dev/null +++ b/blog/templates/_types/post_entries/index.html @@ -0,0 +1,19 @@ +{% extends '_types/post/admin/index.html' %} + + + +{% block post_admin_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-admin-header-child', '_types/post_entries/header/_header.html') %} + {% block post_entries_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/post_entries/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post_entries/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post_settings/_main_panel.html b/blog/templates/_types/post_settings/_main_panel.html new file mode 100644 index 0000000..038fab1 --- /dev/null +++ b/blog/templates/_types/post_settings/_main_panel.html @@ -0,0 +1,198 @@ +{# ── Post/Page Settings Form ── #} +{% set gp = ghost_post or {} %} +{% set _is_page = post.is_page if post else False %} + +{% macro field_label(text, field_for=None) %} + +{% endmacro %} + +{% macro text_input(name, value='', placeholder='', type='text', maxlength=None) %} + +{% endmacro %} + +{% macro textarea_input(name, value='', placeholder='', rows=3, maxlength=None) %} + +{% endmacro %} + +{% macro checkbox_input(name, checked=False, label='') %} + +{% endmacro %} + +{% macro section(title, open=False) %} +
    + + {{ title }} + +
    + {{ caller() }} +
    +
    +{% endmacro %} + +
    + + + +
    + + {# ── General ── #} + {% call section('General', open=True) %} +
    + {{ field_label('Slug', 'settings-slug') }} + {{ text_input('slug', gp.slug or '', 'page-slug' if _is_page else 'post-slug') }} +
    +
    + {{ field_label('Published at', 'settings-published_at') }} + +
    +
    + {{ checkbox_input('featured', gp.featured, 'Featured page' if _is_page else 'Featured post') }} +
    +
    + {{ field_label('Visibility', 'settings-visibility') }} + +
    +
    + {{ checkbox_input('email_only', gp.email_only, 'Email only') }} +
    + {% endcall %} + + {# ── Tags ── #} + {% call section('Tags') %} +
    + {{ field_label('Tags (comma-separated)', 'settings-tags') }} + {% set tag_names = gp.tags|map(attribute='name')|list|join(', ') if gp.tags else '' %} + {{ text_input('tags', tag_names, 'news, updates, featured') }} +

    Unknown tags will be created automatically.

    +
    + {% endcall %} + + {# ── Feature Image ── #} + {% call section('Feature Image') %} +
    + {{ field_label('Alt text', 'settings-feature_image_alt') }} + {{ text_input('feature_image_alt', gp.feature_image_alt or '', 'Describe the feature image') }} +
    + {% endcall %} + + {# ── SEO / Meta ── #} + {% call section('SEO / Meta') %} +
    + {{ field_label('Meta title', 'settings-meta_title') }} + {{ text_input('meta_title', gp.meta_title or '', 'SEO title', maxlength=300) }} +

    Recommended: 70 characters. Max: 300.

    +
    +
    + {{ field_label('Meta description', 'settings-meta_description') }} + {{ textarea_input('meta_description', gp.meta_description or '', 'SEO description', rows=2, maxlength=500) }} +

    Recommended: 156 characters.

    +
    +
    + {{ field_label('Canonical URL', 'settings-canonical_url') }} + {{ text_input('canonical_url', gp.canonical_url or '', 'https://example.com/original-post', type='url') }} +
    + {% endcall %} + + {# ── Facebook / OpenGraph ── #} + {% call section('Facebook / OpenGraph') %} +
    + {{ field_label('OG title', 'settings-og_title') }} + {{ text_input('og_title', gp.og_title or '') }} +
    +
    + {{ field_label('OG description', 'settings-og_description') }} + {{ textarea_input('og_description', gp.og_description or '', rows=2) }} +
    +
    + {{ field_label('OG image URL', 'settings-og_image') }} + {{ text_input('og_image', gp.og_image or '', 'https://...', type='url') }} +
    + {% endcall %} + + {# ── X / Twitter ── #} + {% call section('X / Twitter') %} +
    + {{ field_label('Twitter title', 'settings-twitter_title') }} + {{ text_input('twitter_title', gp.twitter_title or '') }} +
    +
    + {{ field_label('Twitter description', 'settings-twitter_description') }} + {{ textarea_input('twitter_description', gp.twitter_description or '', rows=2) }} +
    +
    + {{ field_label('Twitter image URL', 'settings-twitter_image') }} + {{ text_input('twitter_image', gp.twitter_image or '', 'https://...', type='url') }} +
    + {% endcall %} + + {# ── Advanced ── #} + {% call section('Advanced') %} +
    + {{ field_label('Custom template', 'settings-custom_template') }} + {{ text_input('custom_template', gp.custom_template or '', 'custom-page.hbs' if _is_page else 'custom-post.hbs') }} +
    + {% endcall %} + +
    + + {# ── Save footer ── #} +
    + + + {% if save_success %} + Saved. + {% endif %} +
    +
    diff --git a/blog/templates/_types/post_settings/_nav.html b/blog/templates/_types/post_settings/_nav.html new file mode 100644 index 0000000..a08d80a --- /dev/null +++ b/blog/templates/_types/post_settings/_nav.html @@ -0,0 +1,5 @@ +{% import 'macros/links.html' as links %} +{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + + edit +{% endcall %} diff --git a/blog/templates/_types/post_settings/_oob_elements.html b/blog/templates/_types/post_settings/_oob_elements.html new file mode 100644 index 0000000..d2d6beb --- /dev/null +++ b/blog/templates/_types/post_settings/_oob_elements.html @@ -0,0 +1,19 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'post_settings-header-child', '_types/post_settings/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block mobile_menu %} + {% include '_types/post_settings/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post_settings/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/post_settings/header/_header.html b/blog/templates/_types/post_settings/header/_header.html new file mode 100644 index 0000000..ba187fe --- /dev/null +++ b/blog/templates/_types/post_settings/header/_header.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post_settings-row', oob=oob) %} + {% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search) %} + +
    + settings +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/post_settings/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/post_settings/index.html b/blog/templates/_types/post_settings/index.html new file mode 100644 index 0000000..59835f4 --- /dev/null +++ b/blog/templates/_types/post_settings/index.html @@ -0,0 +1,17 @@ +{% extends '_types/post/admin/index.html' %} + +{% block post_admin_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-admin-header-child', '_types/post_settings/header/_header.html') %} + {% block post_settings_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/post_settings/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/post_settings/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/root/header/_header.html b/blog/templates/_types/root/header/_header.html new file mode 100644 index 0000000..7792cd5 --- /dev/null +++ b/blog/templates/_types/root/header/_header.html @@ -0,0 +1,42 @@ +{% set select_colours = " + [.hover-capable_&]:hover:bg-yellow-300 + aria-selected:bg-stone-500 aria-selected:text-white + [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 +"%} +{% import 'macros/links.html' as links %} + +{% macro header_row(oob=False) %} + {% call links.menu_row(id='root-row', oob=oob) %} +
    + {# Cart mini — fetched from cart app as fragment #} + {% if cart_mini_html %} + {{ cart_mini_html | safe }} + {% endif %} + + {# Site title #} +
    + {% from 'macros/title.html' import title with context %} + {{ title('flex justify-center md:justify-start')}} +
    + + {# Desktop nav #} + + {% include '_types/root/_hamburger.html' %} +
    + {% endcall %} + {# Mobile user info #} +
    + {% if auth_menu_html %} + {{ auth_menu_html | safe }} + {% endif %} +
    +{% endmacro %} diff --git a/blog/templates/_types/root/settings/_main_panel.html b/blog/templates/_types/root/settings/_main_panel.html new file mode 100644 index 0000000..9f4c9a8 --- /dev/null +++ b/blog/templates/_types/root/settings/_main_panel.html @@ -0,0 +1,2 @@ +
    +
    diff --git a/blog/templates/_types/root/settings/_nav.html b/blog/templates/_types/root/settings/_nav.html new file mode 100644 index 0000000..f9d4420 --- /dev/null +++ b/blog/templates/_types/root/settings/_nav.html @@ -0,0 +1,5 @@ +{% from 'macros/admin_nav.html' import admin_nav_item %} +{{ admin_nav_item(url_for('menu_items.list_menu_items'), 'bars', 'Menu Items', select_colours) }} +{{ admin_nav_item(url_for('snippets.list_snippets'), 'puzzle-piece', 'Snippets', select_colours) }} +{{ admin_nav_item(url_for('blog.tag_groups_admin.index'), 'tags', 'Tag Groups', select_colours) }} +{{ admin_nav_item(url_for('settings.cache'), 'refresh', 'Cache', select_colours) }} diff --git a/blog/templates/_types/root/settings/_oob_elements.html b/blog/templates/_types/root/settings/_oob_elements.html new file mode 100644 index 0000000..fbe1bf3 --- /dev/null +++ b/blog/templates/_types/root/settings/_oob_elements.html @@ -0,0 +1,26 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob_.html' import root_header with context %} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-header-child', 'root-settings-header-child', '_types/root/settings/header/_header.html')}} + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} +{% include '_types/root/settings/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include '_types/root/settings/_main_panel.html' %} +{% endblock %} + diff --git a/blog/templates/_types/root/settings/cache/_header.html b/blog/templates/_types/root/settings/cache/_header.html new file mode 100644 index 0000000..64f8535 --- /dev/null +++ b/blog/templates/_types/root/settings/cache/_header.html @@ -0,0 +1,9 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='cache-row', oob=oob) %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{ admin_nav_item(url_for('settings.cache'), 'refresh', 'Cache', select_colours, aclass='') }} + {% call links.desktop_nav() %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/root/settings/cache/_main_panel.html b/blog/templates/_types/root/settings/cache/_main_panel.html new file mode 100644 index 0000000..854012d --- /dev/null +++ b/blog/templates/_types/root/settings/cache/_main_panel.html @@ -0,0 +1,14 @@ +
    +
    +
    + + +
    +
    +
    +
    diff --git a/blog/templates/_types/root/settings/cache/_oob_elements.html b/blog/templates/_types/root/settings/cache/_oob_elements.html new file mode 100644 index 0000000..5989bf7 --- /dev/null +++ b/blog/templates/_types/root/settings/cache/_oob_elements.html @@ -0,0 +1,16 @@ +{% extends 'oob_elements.html' %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-settings-header-child', 'cache-header-child', '_types/root/settings/cache/_header.html')}} + + {% from '_types/root/settings/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + +{% block mobile_menu %} +{% endblock %} + +{% block content %} + {% include '_types/root/settings/cache/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/root/settings/cache/index.html b/blog/templates/_types/root/settings/cache/index.html new file mode 100644 index 0000000..05706f8 --- /dev/null +++ b/blog/templates/_types/root/settings/cache/index.html @@ -0,0 +1,20 @@ +{% extends '_types/root/settings/index.html' %} + +{% block root_settings_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/root/settings/cache/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block cache_header_child %} + {% endblock %} +
    + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/root/settings/cache/_main_panel.html' %} +{% endblock %} + +{% block _main_mobile_menu %} +{% endblock %} diff --git a/blog/templates/_types/root/settings/header/_header.html b/blog/templates/_types/root/settings/header/_header.html new file mode 100644 index 0000000..69e7c72 --- /dev/null +++ b/blog/templates/_types/root/settings/header/_header.html @@ -0,0 +1,11 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='root-settings-row', oob=oob) %} + {% call links.link(url_for('settings.home'), hx_select_search) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/root/settings/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/blog/templates/_types/root/settings/index.html b/blog/templates/_types/root/settings/index.html new file mode 100644 index 0000000..1773f3d --- /dev/null +++ b/blog/templates/_types/root/settings/index.html @@ -0,0 +1,18 @@ +{% extends '_types/root/_index.html' %} +{% import 'macros/layout.html' as layout %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('root-settings-header-child', '_types/root/settings/header/_header.html') %} + {% block root_settings_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/root/settings/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/root/settings/_main_panel.html' %} +{% endblock %} \ No newline at end of file diff --git a/blog/templates/_types/snippets/_list.html b/blog/templates/_types/snippets/_list.html new file mode 100644 index 0000000..2b982ca --- /dev/null +++ b/blog/templates/_types/snippets/_list.html @@ -0,0 +1,73 @@ +
    + {% if snippets %} +
    + {% for s in snippets %} +
    + {# Name #} +
    +
    {{ s.name }}
    +
    + {% if s.user_id == g.user.id %} + You + {% else %} + User #{{ s.user_id }} + {% endif %} +
    +
    + + {# Visibility badge #} + {% set badge_colours = { + 'private': 'bg-stone-200 text-stone-700', + 'shared': 'bg-blue-100 text-blue-700', + 'admin': 'bg-amber-100 text-amber-700', + } %} + + {{ s.visibility }} + + + {# Admin: inline visibility select #} + {% if is_admin %} + + {% endif %} + + {# Delete button #} + {% if s.user_id == g.user.id or is_admin %} + + {% endif %} +
    + {% endfor %} +
    + {% else %} +
    + +

    No snippets yet. Create one from the blog editor.

    +
    + {% endif %} +
    diff --git a/blog/templates/_types/snippets/_main_panel.html b/blog/templates/_types/snippets/_main_panel.html new file mode 100644 index 0000000..73b50b7 --- /dev/null +++ b/blog/templates/_types/snippets/_main_panel.html @@ -0,0 +1,9 @@ +
    +
    +

    Snippets

    +
    + +
    + {% include '_types/snippets/_list.html' %} +
    +
    diff --git a/blog/templates/_types/snippets/_oob_elements.html b/blog/templates/_types/snippets/_oob_elements.html new file mode 100644 index 0000000..a1377cf --- /dev/null +++ b/blog/templates/_types/snippets/_oob_elements.html @@ -0,0 +1,18 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-settings-header-child', 'snippets-header-child', '_types/snippets/header/_header.html')}} + + {% from '_types/root/settings/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + +{% block mobile_menu %} +{% endblock %} + +{% block content %} + {% include '_types/snippets/_main_panel.html' %} +{% endblock %} diff --git a/blog/templates/_types/snippets/header/_header.html b/blog/templates/_types/snippets/header/_header.html new file mode 100644 index 0000000..0882518 --- /dev/null +++ b/blog/templates/_types/snippets/header/_header.html @@ -0,0 +1,9 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='snippets-row', oob=oob) %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{ admin_nav_item(url_for('snippets.list_snippets'), 'puzzle-piece', 'Snippets', select_colours, aclass='') }} + {% call links.desktop_nav() %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/blog/templates/_types/snippets/index.html b/blog/templates/_types/snippets/index.html new file mode 100644 index 0000000..90f0106 --- /dev/null +++ b/blog/templates/_types/snippets/index.html @@ -0,0 +1,20 @@ +{% extends '_types/root/settings/index.html' %} + +{% block root_settings_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/snippets/header/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block snippets_header_child %} + {% endblock %} +
    + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/snippets/_main_panel.html' %} +{% endblock %} + +{% block _main_mobile_menu %} +{% endblock %} diff --git a/blog/templates/fragments/nav_tree.html b/blog/templates/fragments/nav_tree.html new file mode 100644 index 0000000..df41dc8 --- /dev/null +++ b/blog/templates/fragments/nav_tree.html @@ -0,0 +1,32 @@ +{# Nav-tree fragment — rendered by blog, consumed by all apps. + Uses frag_app_name / frag_first_seg instead of request.path / app_name + so the consuming app's context is reflected correctly. + No hx-boost — cross-app nav links are full page navigations. #} +{% set _app_slugs = { + 'cart': cart_url('/'), + 'market': market_url('/'), + 'events': events_url('/'), + 'federation': federation_url('/'), + 'account': account_url('/'), +} %} + diff --git a/blog/templates/macros/admin_nav.html b/blog/templates/macros/admin_nav.html new file mode 100644 index 0000000..738a319 --- /dev/null +++ b/blog/templates/macros/admin_nav.html @@ -0,0 +1,21 @@ +{# + Shared admin navigation macro + Use this instead of duplicate _nav.html files +#} + +{% macro admin_nav_item(href, icon='cog', label='', select_colours='', aclass=styles.nav_button) %} + {% import 'macros/links.html' as links %} + {% call links.link(href, hx_select_search, select_colours, True, aclass=aclass) %} + + {{ label }} + {% endcall %} +{% endmacro %} + +{% macro placeholder_nav() %} +{# Placeholder for admin sections without specific nav items #} + +{% endmacro %} diff --git a/blog/templates/macros/scrolling_menu.html b/blog/templates/macros/scrolling_menu.html new file mode 100644 index 0000000..d1a823a --- /dev/null +++ b/blog/templates/macros/scrolling_menu.html @@ -0,0 +1,68 @@ +{# + Scrolling menu macro with arrow navigation + + Creates a horizontally scrollable menu (desktop) or vertically scrollable (mobile) + with arrow buttons that appear/hide based on content overflow. + + Parameters: + - container_id: Unique ID for the scroll container + - items: List of items to iterate over + - item_content: Caller block that renders each item (receives 'item' variable) + - wrapper_class: Optional additional classes for outer wrapper + - container_class: Optional additional classes for scroll container + - item_class: Optional additional classes for each item wrapper +#} + +{% macro scrolling_menu(container_id, items, wrapper_class='', container_class='', item_class='') %} + {% if items %} + {# Left scroll arrow - desktop only #} + + + {# Scrollable container #} +
    +
    + {% for item in items %} +
    + {{ caller(item) }} +
    + {% endfor %} +
    +
    + + + + {# Right scroll arrow - desktop only #} + + {% endif %} +{% endmacro %} diff --git a/blog/templates/macros/stickers.html b/blog/templates/macros/stickers.html new file mode 100644 index 0000000..2be5b9f --- /dev/null +++ b/blog/templates/macros/stickers.html @@ -0,0 +1,24 @@ +{% macro sticker(src, title, enabled, size=40, found=false) -%} + + + + {{ title|capitalize }} + + + + + +{%- endmacro -%} + diff --git a/cart/.gitignore b/cart/.gitignore new file mode 100644 index 0000000..be20105 --- /dev/null +++ b/cart/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +.env +node_modules/ +*.egg-info/ +dist/ +build/ +.venv/ diff --git a/cart/Dockerfile b/cart/Dockerfile new file mode 100644 index 0000000..7fb990e --- /dev/null +++ b/cart/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# ---------- Python application ---------- +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + PIP_NO_CACHE_DIR=1 \ + APP_PORT=8000 \ + APP_MODULE=app:app + +WORKDIR /app + +# Install system deps + psql client +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +COPY shared/requirements.txt ./requirements.txt +RUN pip install -r requirements.txt + +# Shared code (replaces submodule) +COPY shared/ ./shared/ + +# App code +COPY cart/ ./ + +# Sibling models for cross-domain SQLAlchemy imports +COPY blog/__init__.py ./blog/__init__.py +COPY blog/models/ ./blog/models/ +COPY market/__init__.py ./market/__init__.py +COPY market/models/ ./market/models/ +COPY events/__init__.py ./events/__init__.py +COPY events/models/ ./events/models/ +COPY federation/__init__.py ./federation/__init__.py +COPY federation/models/ ./federation/models/ +COPY account/__init__.py ./account/__init__.py +COPY account/models/ ./account/models/ + +# ---------- Runtime setup ---------- +COPY cart/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +RUN useradd -m -u 10001 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE ${APP_PORT} +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/cart/README.md b/cart/README.md new file mode 100644 index 0000000..22374d1 --- /dev/null +++ b/cart/README.md @@ -0,0 +1,76 @@ +# Cart App + +Shopping cart, checkout, and order management service for the Rose Ash cooperative. + +## Architecture + +One of five Quart microservices sharing a single PostgreSQL database: + +| App | Port | Domain | +|-----|------|--------| +| blog (coop) | 8000 | Auth, blog, admin, menus, snippets | +| market | 8001 | Product browsing, Suma scraping | +| **cart** | 8002 | Shopping cart, checkout, orders | +| events | 8003 | Calendars, bookings, tickets | +| federation | 8004 | ActivityPub, fediverse social | + +## Structure + +``` +app.py # Application factory (create_base_app + blueprints) +path_setup.py # Adds project root + app dir to sys.path +config/app-config.yaml # App URLs, SumUp config +models/ # Cart-domain models (Order, OrderItem, PageConfig) +bp/ + cart/ # Cart blueprint + global_routes.py # Add to cart, checkout, webhooks, return page + page_routes.py # Page-scoped cart and checkout + overview_routes.py # Cart overview / summary page + services/ # Business logic + checkout.py # Order creation, SumUp integration + check_sumup_status.py # Payment status polling + calendar_cart.py # Calendar entry cart queries + page_cart.py # Page-scoped cart queries + get_cart.py # Cart item queries + identity.py # Cart identity (user_id / session_id) + total.py # Price calculations + clear_cart_for_order.py # Soft-delete cart after checkout + order/ # Single order detail view + orders/ # Order listing view +services/ # register_domain_services() — wires cart + calendar + market +shared/ # Submodule -> git.rose-ash.com/coop/shared.git +``` + +## Cross-Domain Communication + +- `services.calendar.*` — claim/confirm entries for orders, adopt on login +- `services.market.*` — marketplace queries for page-scoped carts +- `services.blog.*` — post lookup for page context +- `shared.services.navigation` — site navigation tree + +## Domain Events + +- `checkout.py` emits `order.created` via `shared.events.emit_event` +- `check_sumup_status.py` emits `order.paid` via `shared.events.emit_event` + +## Checkout Flow + +``` +1. User clicks "Checkout" +2. create_order_from_cart() creates Order + OrderItems +3. services.calendar.claim_entries_for_order() marks entries as "ordered" +4. emit: order.created event +5. SumUp hosted checkout created, user redirected +6. SumUp webhook / return page triggers check_sumup_status() +7. If PAID: services.calendar.confirm_entries_for_order(), emit: order.paid +``` + +## Running + +```bash +export DATABASE_URL_ASYNC=postgresql+asyncpg://user:pass@localhost/coop +export REDIS_URL=redis://localhost:6379/0 +export SECRET_KEY=your-secret-key + +hypercorn app:app --bind 0.0.0.0:8002 +``` diff --git a/cart/__init__.py b/cart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cart/app.py b/cart/app.py new file mode 100644 index 0000000..dad13cd --- /dev/null +++ b/cart/app.py @@ -0,0 +1,235 @@ +from __future__ import annotations +import path_setup # noqa: F401 # adds shared/ to sys.path + +from decimal import Decimal +from pathlib import Path + +from quart import g, abort, request +from jinja2 import FileSystemLoader, ChoiceLoader +from sqlalchemy import select + +from shared.infrastructure.factory import create_base_app + +from bp import ( + register_cart_overview, + register_page_cart, + register_cart_global, + register_orders, + register_fragments, +) +from bp.cart.services import ( + get_cart, + total, + get_calendar_cart_entries, + calendar_total, + get_ticket_cart_entries, + ticket_total, +) +from bp.cart.services.page_cart import ( + get_cart_for_page, + get_calendar_entries_for_page, + get_tickets_for_page, +) +from bp.cart.services.ticket_groups import group_tickets + + +async def _load_cart(): + """Load the full cart for the cart app (before each request).""" + g.cart = await get_cart(g.s) + + +async def cart_context() -> dict: + """ + Cart app context processor. + + - cart / calendar_cart_entries / total / calendar_total: direct DB + (cart app owns this data) + - cart_count: derived from cart + calendar entries (for _mini.html) + - nav_tree_html: fetched from blog as fragment + + When g.page_post exists, cart and calendar_cart_entries are page-scoped. + Global cart_count / cart_total stay global for cart-mini. + """ + from shared.infrastructure.context import base_context + from shared.services.navigation import get_navigation_tree + from shared.infrastructure.fragments import fetch_fragment + + ctx = await base_context() + + ctx["nav_tree_html"] = await fetch_fragment( + "blog", "nav-tree", + params={"app_name": "cart", "path": request.path}, + ) + # Fallback for _nav.html when nav-tree fragment fetch fails + ctx["menu_items"] = await get_navigation_tree(g.s) + + # Cart app owns cart data — use g.cart from _load_cart + all_cart = getattr(g, "cart", None) or [] + all_cal = await get_calendar_cart_entries(g.s) + all_tickets = await get_ticket_cart_entries(g.s) + + # Global counts for cart-mini (always global) + cart_qty = sum(ci.quantity for ci in all_cart) if all_cart else 0 + ctx["cart_count"] = cart_qty + len(all_cal) + len(all_tickets) + ctx["cart_total"] = (total(all_cart) or Decimal(0)) + (calendar_total(all_cal) or Decimal(0)) + (ticket_total(all_tickets) or Decimal(0)) + + # Page-scoped data when viewing a page cart + page_post = getattr(g, "page_post", None) + if page_post: + page_cart = await get_cart_for_page(g.s, page_post.id) + page_cal = await get_calendar_entries_for_page(g.s, page_post.id) + page_tickets = await get_tickets_for_page(g.s, page_post.id) + ctx["cart"] = page_cart + ctx["calendar_cart_entries"] = page_cal + ctx["ticket_cart_entries"] = page_tickets + ctx["page_post"] = page_post + ctx["page_config"] = getattr(g, "page_config", None) + else: + ctx["cart"] = all_cart + ctx["calendar_cart_entries"] = all_cal + ctx["ticket_cart_entries"] = all_tickets + + ctx["ticket_groups"] = group_tickets(ctx.get("ticket_cart_entries", [])) + ctx["total"] = total + ctx["calendar_total"] = calendar_total + ctx["ticket_total"] = ticket_total + + return ctx + + +def create_app() -> "Quart": + from shared.models.page_config import PageConfig + from shared.services.registry import services + from services import register_domain_services + + app = create_base_app( + "cart", + context_fn=cart_context, + before_request_fns=[_load_cart], + domain_services_fn=register_domain_services, + ) + + # App-specific templates override shared templates + app_templates = str(Path(__file__).resolve().parent / "templates") + app.jinja_loader = ChoiceLoader([ + FileSystemLoader(app_templates), + app.jinja_loader, + ]) + + app.jinja_env.globals["cart_quantity_url"] = lambda product_id: f"/quantity/{product_id}/" + app.jinja_env.globals["cart_delete_url"] = lambda product_id: f"/delete/{product_id}/" + + app.register_blueprint(register_fragments()) + + # --- Page slug hydration (follows events/market app pattern) --- + + @app.url_value_preprocessor + def pull_page_slug(endpoint, values): + if values and "page_slug" in values: + g.page_slug = values.pop("page_slug") + + @app.url_defaults + def inject_page_slug(endpoint, values): + slug = g.get("page_slug") + if slug and "page_slug" not in values: + if app.url_map.is_endpoint_expecting(endpoint, "page_slug"): + values["page_slug"] = slug + + @app.before_request + async def hydrate_page(): + slug = getattr(g, "page_slug", None) + if not slug: + return + post = await services.blog.get_post_by_slug(g.s, slug) + if not post or not post.is_page: + abort(404) + g.page_post = post + g.page_config = ( + await g.s.execute( + select(PageConfig).where( + PageConfig.container_type == "page", + PageConfig.container_id == post.id, + ) + ) + ).scalar_one_or_none() + + # --- Blueprint registration --- + # Static prefixes first, dynamic (page_slug) last + + # Orders blueprint + app.register_blueprint(register_orders(url_prefix="/orders")) + + # Global routes (webhook, return, add — specific paths under /) + app.register_blueprint( + register_cart_global(url_prefix="/"), + url_prefix="/", + ) + + # Cart overview at GET / + app.register_blueprint( + register_cart_overview(url_prefix="/"), + url_prefix="/", + ) + + # Page cart at // (dynamic, matched last) + app.register_blueprint( + register_page_cart(url_prefix="/"), + url_prefix="/", + ) + + # --- Reconcile stale pending orders on startup --- + @app.before_serving + async def _reconcile_pending_orders(): + """Check SumUp status for orders stuck in 'pending' with a checkout ID. + + Handles the case where SumUp webhooks fired while the service was down + or were rejected (e.g. CSRF). Runs once on boot. + """ + import logging + from datetime import datetime, timezone, timedelta + from sqlalchemy import select + from sqlalchemy.orm import selectinload + from shared.db.session import get_session + from shared.models.order import Order + from bp.cart.services.check_sumup_status import check_sumup_status + + log = logging.getLogger("cart.reconcile") + + try: + async with get_session() as sess: + async with sess.begin(): + # Orders that are pending, have a SumUp checkout, and are + # older than 2 minutes (avoid racing with in-flight checkouts) + cutoff = datetime.now(timezone.utc) - timedelta(minutes=2) + result = await sess.execute( + select(Order) + .where( + Order.status == "pending", + Order.sumup_checkout_id.isnot(None), + Order.created_at < cutoff, + ) + .options(selectinload(Order.page_config)) + .limit(50) + ) + stale_orders = result.scalars().all() + + if not stale_orders: + return + + log.info("Reconciling %d stale pending orders", len(stale_orders)) + for order in stale_orders: + try: + await check_sumup_status(sess, order) + log.info( + "Order %d reconciled: %s", + order.id, order.status, + ) + except Exception: + log.exception("Failed to reconcile order %d", order.id) + except Exception: + log.exception("Order reconciliation failed") + + return app + + +app = create_app() diff --git a/cart/bp/__init__.py b/cart/bp/__init__.py new file mode 100644 index 0000000..e75b584 --- /dev/null +++ b/cart/bp/__init__.py @@ -0,0 +1,6 @@ +from .cart.overview_routes import register as register_cart_overview +from .cart.page_routes import register as register_page_cart +from .cart.global_routes import register as register_cart_global +from .order.routes import register as register_order +from .orders.routes import register as register_orders +from .fragments import register_fragments diff --git a/cart/bp/cart/global_routes.py b/cart/bp/cart/global_routes.py new file mode 100644 index 0000000..ba2459f --- /dev/null +++ b/cart/bp/cart/global_routes.py @@ -0,0 +1,294 @@ +# bp/cart/global_routes.py — Global cart routes (webhook, return, add) + +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, redirect, url_for, make_response +from sqlalchemy import select + +from shared.models.market import CartItem +from shared.models.order import Order +from shared.models.market_place import MarketPlace +from shared.services.registry import services +from .services import ( + current_cart_identity, + get_cart, + total, + get_calendar_cart_entries, + calendar_total, + get_ticket_cart_entries, + ticket_total, + check_sumup_status, +) +from .services.checkout import ( + find_or_create_cart_item, + create_order_from_cart, + resolve_page_config, + build_sumup_description, + build_sumup_reference, + build_webhook_url, + validate_webhook_secret, + get_order_with_details, +) +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.browser.app.csrf import csrf_exempt + + +def register(url_prefix: str) -> Blueprint: + bp = Blueprint("cart_global", __name__, url_prefix=url_prefix) + + @bp.post("/add//") + async def add_to_cart(product_id: int): + ident = current_cart_identity() + + cart_item = await find_or_create_cart_item( + g.s, + product_id, + ident["user_id"], + ident["session_id"], + ) + + if not cart_item: + return await make_response("Product not found", 404) + + if request.headers.get("HX-Request") == "true": + # Redirect to overview for HTMX + return redirect(url_for("cart_overview.overview")) + + return redirect(url_for("cart_overview.overview")) + + @bp.post("/quantity//") + async def update_quantity(product_id: int): + ident = current_cart_identity() + form = await request.form + count = int(form.get("count", 0)) + + filters = [ + CartItem.deleted_at.is_(None), + CartItem.product_id == product_id, + ] + if ident["user_id"] is not None: + filters.append(CartItem.user_id == ident["user_id"]) + else: + filters.append(CartItem.session_id == ident["session_id"]) + + existing = await g.s.scalar(select(CartItem).where(*filters)) + + if existing: + existing.quantity = max(count, 0) + await g.s.flush() + + resp = await make_response("", 200) + resp.headers["HX-Refresh"] = "true" + return resp + + @bp.post("/ticket-quantity/") + async def update_ticket_quantity(): + """Adjust reserved ticket count (+/- pattern, like products).""" + ident = current_cart_identity() + form = await request.form + entry_id = int(form.get("entry_id", 0)) + count = max(int(form.get("count", 0)), 0) + tt_raw = (form.get("ticket_type_id") or "").strip() + ticket_type_id = int(tt_raw) if tt_raw else None + + await services.calendar.adjust_ticket_quantity( + g.s, entry_id, count, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=ticket_type_id, + ) + await g.s.flush() + + resp = await make_response("", 200) + resp.headers["HX-Refresh"] = "true" + return resp + + @bp.post("/delete//") + async def delete_item(product_id: int): + ident = current_cart_identity() + + filters = [ + CartItem.deleted_at.is_(None), + CartItem.product_id == product_id, + ] + if ident["user_id"] is not None: + filters.append(CartItem.user_id == ident["user_id"]) + else: + filters.append(CartItem.session_id == ident["session_id"]) + + existing = await g.s.scalar(select(CartItem).where(*filters)) + + if existing: + await g.s.delete(existing) + await g.s.flush() + + resp = await make_response("", 200) + resp.headers["HX-Refresh"] = "true" + return resp + + @bp.post("/checkout/") + async def checkout(): + """Legacy global checkout (for orphan items without page scope).""" + cart = await get_cart(g.s) + calendar_entries = await get_calendar_cart_entries(g.s) + tickets = await get_ticket_cart_entries(g.s) + + if not cart and not calendar_entries and not tickets: + return redirect(url_for("cart_overview.overview")) + + product_total = total(cart) or 0 + calendar_amount = calendar_total(calendar_entries) or 0 + ticket_amount = ticket_total(tickets) or 0 + cart_total = product_total + calendar_amount + ticket_amount + + if cart_total <= 0: + return redirect(url_for("cart_overview.overview")) + + try: + page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets) + except ValueError as e: + html = await render_template( + "_types/cart/checkout_error.html", + order=None, + error=str(e), + ) + return await make_response(html, 400) + + ident = current_cart_identity() + order = await create_order_from_cart( + g.s, + cart, + calendar_entries, + ident.get("user_id"), + ident.get("session_id"), + product_total, + calendar_amount, + ticket_total=ticket_amount, + ) + + if page_config: + order.page_config_id = page_config.id + + redirect_url = url_for("cart_global.checkout_return", order_id=order.id, _external=True) + order.sumup_reference = build_sumup_reference(order.id, page_config=page_config) + description = build_sumup_description(cart, order.id, ticket_count=len(tickets)) + + webhook_base_url = url_for("cart_global.checkout_webhook", order_id=order.id, _external=True) + webhook_url = build_webhook_url(webhook_base_url) + + checkout_data = await sumup_create_checkout( + order, + redirect_url=redirect_url, + webhook_url=webhook_url, + description=description, + page_config=page_config, + ) + order.sumup_checkout_id = checkout_data.get("id") + order.sumup_status = checkout_data.get("status") + order.description = checkout_data.get("description") + + hosted_cfg = checkout_data.get("hosted_checkout") or {} + hosted_url = hosted_cfg.get("hosted_checkout_url") or checkout_data.get("hosted_checkout_url") + order.sumup_hosted_url = hosted_url + + await g.s.flush() + + if not hosted_url: + html = await render_template( + "_types/cart/checkout_error.html", + order=order, + error="No hosted checkout URL returned from SumUp.", + ) + return await make_response(html, 500) + + return redirect(hosted_url) + + @csrf_exempt + @bp.post("/checkout/webhook//") + async def checkout_webhook(order_id: int): + """Webhook endpoint for SumUp CHECKOUT_STATUS_CHANGED events.""" + if not validate_webhook_secret(request.args.get("token")): + return "", 204 + + try: + payload = await request.get_json() + except Exception: + payload = None + + if not isinstance(payload, dict): + return "", 204 + + if payload.get("event_type") != "CHECKOUT_STATUS_CHANGED": + return "", 204 + + checkout_id = payload.get("id") + if not checkout_id: + return "", 204 + + result = await g.s.execute(select(Order).where(Order.id == order_id)) + order = result.scalar_one_or_none() + if not order: + return "", 204 + + if order.sumup_checkout_id and order.sumup_checkout_id != checkout_id: + return "", 204 + + try: + await check_sumup_status(g.s, order) + except Exception: + pass + + return "", 204 + + @bp.get("/checkout/return//") + async def checkout_return(order_id: int): + """Handle the browser returning from SumUp after payment.""" + order = await get_order_with_details(g.s, order_id) + + if not order: + html = await render_template( + "_types/cart/checkout_return.html", + order=None, + status="missing", + calendar_entries=[], + ) + return await make_response(html) + + # Resolve page/market slugs so product links render correctly + if order.page_config: + post = await services.blog.get_post_by_id(g.s, order.page_config.container_id) + if post: + g.page_slug = post.slug + result = await g.s.execute( + select(MarketPlace).where( + MarketPlace.container_type == "page", + MarketPlace.container_id == post.id, + MarketPlace.deleted_at.is_(None), + ).limit(1) + ) + mp = result.scalar_one_or_none() + if mp: + g.market_slug = mp.slug + + if order.sumup_checkout_id: + try: + await check_sumup_status(g.s, order) + except Exception: + pass + + status = (order.status or "pending").lower() + + calendar_entries = await services.calendar.get_entries_for_order(g.s, order.id) + order_tickets = await services.calendar.get_tickets_for_order(g.s, order.id) + await g.s.flush() + + html = await render_template( + "_types/cart/checkout_return.html", + order=order, + status=status, + calendar_entries=calendar_entries, + order_tickets=order_tickets, + ) + return await make_response(html) + + return bp diff --git a/cart/bp/cart/overview_routes.py b/cart/bp/cart/overview_routes.py new file mode 100644 index 0000000..15f9eb6 --- /dev/null +++ b/cart/bp/cart/overview_routes.py @@ -0,0 +1,31 @@ +# bp/cart/overview_routes.py — Cart overview (list of page carts) + +from __future__ import annotations + +from quart import Blueprint, render_template, make_response + +from shared.browser.app.utils.htmx import is_htmx_request +from .services import get_cart_grouped_by_page + + +def register(url_prefix: str) -> Blueprint: + bp = Blueprint("cart_overview", __name__, url_prefix=url_prefix) + + @bp.get("/") + async def overview(): + from quart import g + page_groups = await get_cart_grouped_by_page(g.s) + + if not is_htmx_request(): + html = await render_template( + "_types/cart/overview/index.html", + page_groups=page_groups, + ) + else: + html = await render_template( + "_types/cart/overview/_oob_elements.html", + page_groups=page_groups, + ) + return await make_response(html) + + return bp diff --git a/cart/bp/cart/page_routes.py b/cart/bp/cart/page_routes.py new file mode 100644 index 0000000..6526093 --- /dev/null +++ b/cart/bp/cart/page_routes.py @@ -0,0 +1,129 @@ +# bp/cart/page_routes.py — Per-page cart (view + checkout) + +from __future__ import annotations + +from quart import Blueprint, g, render_template, redirect, make_response, url_for + +from shared.browser.app.utils.htmx import is_htmx_request +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config +from .services import ( + total, + calendar_total, + ticket_total, +) +from .services.page_cart import get_cart_for_page, get_calendar_entries_for_page, get_tickets_for_page +from .services.ticket_groups import group_tickets +from .services.checkout import ( + create_order_from_cart, + build_sumup_description, + build_sumup_reference, + build_webhook_url, +) +from .services import current_cart_identity + + +def register(url_prefix: str) -> Blueprint: + bp = Blueprint("page_cart", __name__, url_prefix=url_prefix) + + @bp.get("/") + async def page_view(): + post = g.page_post + cart = await get_cart_for_page(g.s, post.id) + cal_entries = await get_calendar_entries_for_page(g.s, post.id) + page_tickets = await get_tickets_for_page(g.s, post.id) + + ticket_groups = group_tickets(page_tickets) + + tpl_ctx = dict( + page_post=post, + page_config=getattr(g, "page_config", None), + cart=cart, + calendar_cart_entries=cal_entries, + ticket_cart_entries=page_tickets, + ticket_groups=ticket_groups, + total=total, + calendar_total=calendar_total, + ticket_total=ticket_total, + ) + + if not is_htmx_request(): + html = await render_template("_types/cart/page/index.html", **tpl_ctx) + else: + html = await render_template("_types/cart/page/_oob_elements.html", **tpl_ctx) + return await make_response(html) + + @bp.post("/checkout/") + async def page_checkout(): + post = g.page_post + page_config = getattr(g, "page_config", None) + + cart = await get_cart_for_page(g.s, post.id) + cal_entries = await get_calendar_entries_for_page(g.s, post.id) + page_tickets = await get_tickets_for_page(g.s, post.id) + + if not cart and not cal_entries and not page_tickets: + return redirect(url_for("page_cart.page_view")) + + product_total = total(cart) or 0 + calendar_amount = calendar_total(cal_entries) or 0 + ticket_amount = ticket_total(page_tickets) or 0 + cart_total = product_total + calendar_amount + ticket_amount + + if cart_total <= 0: + return redirect(url_for("page_cart.page_view")) + + # Create order scoped to this page + ident = current_cart_identity() + order = await create_order_from_cart( + g.s, + cart, + cal_entries, + ident.get("user_id"), + ident.get("session_id"), + product_total, + calendar_amount, + ticket_total=ticket_amount, + page_post_id=post.id, + ) + + # Set page_config on order + if page_config: + order.page_config_id = page_config.id + + # Build SumUp checkout details — webhook/return use global routes + redirect_url = url_for("cart_global.checkout_return", order_id=order.id, _external=True) + order.sumup_reference = build_sumup_reference(order.id, page_config=page_config) + description = build_sumup_description(cart, order.id, ticket_count=len(page_tickets)) + + webhook_base_url = url_for("cart_global.checkout_webhook", order_id=order.id, _external=True) + webhook_url = build_webhook_url(webhook_base_url) + + checkout_data = await sumup_create_checkout( + order, + redirect_url=redirect_url, + webhook_url=webhook_url, + description=description, + page_config=page_config, + ) + order.sumup_checkout_id = checkout_data.get("id") + order.sumup_status = checkout_data.get("status") + order.description = checkout_data.get("description") + + hosted_cfg = checkout_data.get("hosted_checkout") or {} + hosted_url = hosted_cfg.get("hosted_checkout_url") or checkout_data.get("hosted_checkout_url") + order.sumup_hosted_url = hosted_url + + await g.s.flush() + + if not hosted_url: + html = await render_template( + "_types/cart/checkout_error.html", + order=order, + error="No hosted checkout URL returned from SumUp.", + ) + return await make_response(html, 500) + + return redirect(hosted_url) + + return bp diff --git a/cart/bp/cart/services/__init__.py b/cart/bp/cart/services/__init__.py new file mode 100644 index 0000000..8ba68b4 --- /dev/null +++ b/cart/bp/cart/services/__init__.py @@ -0,0 +1,13 @@ +from .get_cart import get_cart +from .identity import current_cart_identity +from .total import total +from .clear_cart_for_order import clear_cart_for_order +from .calendar_cart import get_calendar_cart_entries, calendar_total, get_ticket_cart_entries, ticket_total +from .check_sumup_status import check_sumup_status +from .page_cart import ( + get_cart_for_page, + get_calendar_entries_for_page, + get_tickets_for_page, + get_cart_grouped_by_page, +) + diff --git a/cart/bp/cart/services/calendar_cart.py b/cart/bp/cart/services/calendar_cart.py new file mode 100644 index 0000000..febd778 --- /dev/null +++ b/cart/bp/cart/services/calendar_cart.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from decimal import Decimal + +from shared.services.registry import services +from .identity import current_cart_identity + + +async def get_calendar_cart_entries(session): + """ + Return all *pending* calendar entries (as CalendarEntryDTOs) for the + current cart identity (user or anonymous session). + """ + ident = current_cart_identity() + return await services.calendar.pending_entries( + session, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + +def calendar_total(entries) -> Decimal: + """ + Total cost of pending calendar entries. + """ + return sum( + (Decimal(str(e.cost)) if e.cost else Decimal(0)) + for e in entries + if e.cost is not None + ) + + +async def get_ticket_cart_entries(session): + """Return all reserved tickets (as TicketDTOs) for the current identity.""" + ident = current_cart_identity() + return await services.calendar.pending_tickets( + session, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + +def ticket_total(tickets) -> Decimal: + """Total cost of reserved tickets.""" + return sum((Decimal(str(t.price)) if t.price else Decimal(0) for t in tickets), Decimal(0)) diff --git a/cart/bp/cart/services/check_sumup_status.py b/cart/bp/cart/services/check_sumup_status.py new file mode 100644 index 0000000..269a03d --- /dev/null +++ b/cart/bp/cart/services/check_sumup_status.py @@ -0,0 +1,43 @@ +from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout +from shared.events import emit_activity +from shared.services.registry import services +from .clear_cart_for_order import clear_cart_for_order + + +async def check_sumup_status(session, order): + # Use order's page_config for per-page SumUp credentials + page_config = getattr(order, "page_config", None) + checkout_data = await sumup_get_checkout(order.sumup_checkout_id, page_config=page_config) + order.sumup_status = checkout_data.get("status") or order.sumup_status + sumup_status = (order.sumup_status or "").upper() + + if sumup_status == "PAID": + if order.status != "paid": + order.status = "paid" + await services.calendar.confirm_entries_for_order( + session, order.id, order.user_id, order.session_id + ) + await services.calendar.confirm_tickets_for_order(session, order.id) + + # Clear cart only after payment is confirmed + page_post_id = page_config.container_id if page_config else None + await clear_cart_for_order(session, order, page_post_id=page_post_id) + + await emit_activity( + session, + activity_type="rose:OrderPaid", + actor_uri="internal:cart", + object_type="rose:Order", + object_data={ + "order_id": order.id, + "user_id": order.user_id, + }, + source_type="order", + source_id=order.id, + ) + elif sumup_status == "FAILED": + order.status = "failed" + else: + order.status = sumup_status.lower() or order.status + + await session.flush() diff --git a/cart/bp/cart/services/checkout.py b/cart/bp/cart/services/checkout.py new file mode 100644 index 0000000..0db306b --- /dev/null +++ b/cart/bp/cart/services/checkout.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from typing import Optional +from urllib.parse import urlencode + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from shared.models.market import Product, CartItem +from shared.models.order import Order, OrderItem +from shared.models.page_config import PageConfig +from shared.models.market_place import MarketPlace +from shared.config import config +from shared.contracts.dtos import CalendarEntryDTO +from shared.events import emit_activity +from shared.services.registry import services + + +async def find_or_create_cart_item( + session: AsyncSession, + product_id: int, + user_id: Optional[int], + session_id: Optional[str], +) -> Optional[CartItem]: + """ + Find an existing cart item for this product/identity, or create a new one. + Returns None if the product doesn't exist. + Increments quantity if item already exists. + """ + # Make sure product exists + product = await session.scalar( + select(Product).where(Product.id == product_id) + ) + if not product: + return None + + # Look for existing cart item + filters = [ + CartItem.deleted_at.is_(None), + CartItem.product_id == product_id, + ] + if user_id is not None: + filters.append(CartItem.user_id == user_id) + else: + filters.append(CartItem.session_id == session_id) + + existing = await session.scalar(select(CartItem).where(*filters)) + + if existing: + existing.quantity += 1 + return existing + else: + cart_item = CartItem( + user_id=user_id, + session_id=session_id, + product_id=product.id, + quantity=1, + ) + session.add(cart_item) + return cart_item + + +async def resolve_page_config( + session: AsyncSession, + cart: list[CartItem], + calendar_entries: list[CalendarEntryDTO], + tickets=None, +) -> Optional["PageConfig"]: + """Determine the PageConfig for this order. + + Returns PageConfig or None (use global credentials). + Raises ValueError if items span multiple pages. + """ + post_ids: set[int] = set() + + # From cart items via market_place + for ci in cart: + if ci.market_place_id: + mp = await session.get(MarketPlace, ci.market_place_id) + if mp: + post_ids.add(mp.container_id) + + # From calendar entries via calendar + for entry in calendar_entries: + if entry.calendar_container_id: + post_ids.add(entry.calendar_container_id) + + # From tickets via calendar_container_id + for tk in (tickets or []): + if tk.calendar_container_id: + post_ids.add(tk.calendar_container_id) + + if len(post_ids) > 1: + raise ValueError("Cannot checkout items from multiple pages") + + if not post_ids: + return None # global credentials + + post_id = post_ids.pop() + pc = (await session.execute( + select(PageConfig).where( + PageConfig.container_type == "page", + PageConfig.container_id == post_id, + ) + )).scalar_one_or_none() + return pc + + +async def create_order_from_cart( + session: AsyncSession, + cart: list[CartItem], + calendar_entries: list[CalendarEntryDTO], + user_id: Optional[int], + session_id: Optional[str], + product_total: float, + calendar_total: float, + *, + ticket_total: float = 0, + page_post_id: int | None = None, +) -> Order: + """ + Create an Order and OrderItems from the current cart + calendar entries + tickets. + + When *page_post_id* is given, only calendar entries/tickets whose calendar + belongs to that page are marked as "ordered". Otherwise all pending + entries are updated (legacy behaviour). + """ + cart_total = product_total + calendar_total + ticket_total + + # Determine currency from first product + first_product = cart[0].product if cart else None + currency = (first_product.regular_price_currency if first_product else None) or "GBP" + + # Create order + order = Order( + user_id=user_id, + session_id=session_id, + status="pending", + currency=currency, + total_amount=cart_total, + ) + session.add(order) + await session.flush() + + # Create order items from cart + for ci in cart: + price = ci.product.special_price or ci.product.regular_price or 0 + oi = OrderItem( + order=order, + product_id=ci.product.id, + product_title=ci.product.title, + quantity=ci.quantity, + unit_price=price, + currency=currency, + ) + session.add(oi) + + # Mark pending calendar entries as "ordered" via calendar service + await services.calendar.claim_entries_for_order( + session, order.id, user_id, session_id, page_post_id + ) + + # Claim reserved tickets for this order + await services.calendar.claim_tickets_for_order( + session, order.id, user_id, session_id, page_post_id + ) + + await emit_activity( + session, + activity_type="Create", + actor_uri="internal:cart", + object_type="rose:Order", + object_data={ + "order_id": order.id, + "user_id": user_id, + "session_id": session_id, + }, + source_type="order", + source_id=order.id, + ) + + return order + + +def build_sumup_description(cart: list[CartItem], order_id: int, *, ticket_count: int = 0) -> str: + """Build a human-readable description for SumUp checkout.""" + titles = [ci.product.title for ci in cart if ci.product and ci.product.title] + item_count = sum(ci.quantity for ci in cart) + + parts = [] + if titles: + if len(titles) <= 3: + parts.append(", ".join(titles)) + else: + parts.append(", ".join(titles[:3]) + f" + {len(titles) - 3} more") + if ticket_count: + parts.append(f"{ticket_count} ticket{'s' if ticket_count != 1 else ''}") + + summary = ", ".join(parts) if parts else "order items" + total_count = item_count + ticket_count + + return f"Order {order_id} ({total_count} item{'s' if total_count != 1 else ''}): {summary}" + + +def build_sumup_reference(order_id: int, page_config=None) -> str: + """Build a SumUp reference with configured prefix.""" + if page_config and page_config.sumup_checkout_prefix: + prefix = page_config.sumup_checkout_prefix + else: + sumup_cfg = config().get("sumup", {}) or {} + prefix = sumup_cfg.get("checkout_reference_prefix", "") + return f"{prefix}{order_id}" + + +def build_webhook_url(base_url: str) -> str: + """Add webhook secret token to URL if configured.""" + sumup_cfg = config().get("sumup", {}) or {} + webhook_secret = sumup_cfg.get("webhook_secret") + + if webhook_secret: + sep = "&" if "?" in base_url else "?" + return f"{base_url}{sep}{urlencode({'token': webhook_secret})}" + + return base_url + + +def validate_webhook_secret(token: Optional[str]) -> bool: + """Validate webhook token against configured secret.""" + sumup_cfg = config().get("sumup", {}) or {} + webhook_secret = sumup_cfg.get("webhook_secret") + + if not webhook_secret: + return True # No secret configured, allow all + + return token is not None and token == webhook_secret + + +async def get_order_with_details(session: AsyncSession, order_id: int) -> Optional[Order]: + """Fetch an order with items and calendar entries eagerly loaded.""" + result = await session.execute( + select(Order) + .options( + selectinload(Order.items).selectinload(OrderItem.product), + ) + .where(Order.id == order_id) + ) + return result.scalar_one_or_none() diff --git a/cart/bp/cart/services/clear_cart_for_order.py b/cart/bp/cart/services/clear_cart_for_order.py new file mode 100644 index 0000000..3643839 --- /dev/null +++ b/cart/bp/cart/services/clear_cart_for_order.py @@ -0,0 +1,37 @@ +from sqlalchemy import update, func, select + +from shared.models.market import CartItem +from shared.models.market_place import MarketPlace +from shared.models.order import Order + + +async def clear_cart_for_order(session, order: Order, *, page_post_id: int | None = None) -> None: + """ + Soft-delete CartItem rows belonging to this order's user_id/session_id. + + When *page_post_id* is given, only items whose market_place belongs to + that page are cleared. Otherwise all items are cleared (legacy behaviour). + """ + filters = [CartItem.deleted_at.is_(None)] + if order.user_id is not None: + filters.append(CartItem.user_id == order.user_id) + if order.session_id is not None: + filters.append(CartItem.session_id == order.session_id) + + if len(filters) == 1: + # no user_id/session_id on order – nothing to clear + return + + if page_post_id is not None: + mp_ids = select(MarketPlace.id).where( + MarketPlace.container_type == "page", + MarketPlace.container_id == page_post_id, + MarketPlace.deleted_at.is_(None), + ).scalar_subquery() + filters.append(CartItem.market_place_id.in_(mp_ids)) + + await session.execute( + update(CartItem) + .where(*filters) + .values(deleted_at=func.now()) + ) diff --git a/cart/bp/cart/services/get_cart.py b/cart/bp/cart/services/get_cart.py new file mode 100644 index 0000000..ad1c0ce --- /dev/null +++ b/cart/bp/cart/services/get_cart.py @@ -0,0 +1,25 @@ +from sqlalchemy import select +from sqlalchemy.orm import selectinload + +from shared.models.market import CartItem +from .identity import current_cart_identity + +async def get_cart(session): + ident = current_cart_identity() + + filters = [CartItem.deleted_at.is_(None)] + if ident["user_id"] is not None: + filters.append(CartItem.user_id == ident["user_id"]) + else: + filters.append(CartItem.session_id == ident["session_id"]) + + result = await session.execute( + select(CartItem) + .where(*filters) + .order_by(CartItem.created_at.desc()) + .options( + selectinload(CartItem.product), + selectinload(CartItem.market_place), + ) + ) + return result.scalars().all() diff --git a/cart/bp/cart/services/identity.py b/cart/bp/cart/services/identity.py new file mode 100644 index 0000000..50ecb70 --- /dev/null +++ b/cart/bp/cart/services/identity.py @@ -0,0 +1,4 @@ +# Re-export from canonical shared location +from shared.infrastructure.cart_identity import CartIdentity, current_cart_identity + +__all__ = ["CartIdentity", "current_cart_identity"] diff --git a/cart/bp/cart/services/page_cart.py b/cart/bp/cart/services/page_cart.py new file mode 100644 index 0000000..ce59113 --- /dev/null +++ b/cart/bp/cart/services/page_cart.py @@ -0,0 +1,212 @@ +""" +Page-scoped cart queries. + +Groups cart items and calendar entries by their owning page (Post), +determined via CartItem.market_place.container_id and CalendarEntry.calendar.container_id +(where container_type == "page"). +""" +from __future__ import annotations + +from collections import defaultdict + +from sqlalchemy import select +from sqlalchemy.orm import selectinload + +from shared.models.market import CartItem +from shared.models.market_place import MarketPlace +from shared.models.page_config import PageConfig +from shared.services.registry import services +from .identity import current_cart_identity + + +async def get_cart_for_page(session, post_id: int) -> list[CartItem]: + """Return cart items scoped to a specific page (via MarketPlace.container_id).""" + ident = current_cart_identity() + + filters = [ + CartItem.deleted_at.is_(None), + MarketPlace.container_type == "page", + MarketPlace.container_id == post_id, + MarketPlace.deleted_at.is_(None), + ] + if ident["user_id"] is not None: + filters.append(CartItem.user_id == ident["user_id"]) + else: + filters.append(CartItem.session_id == ident["session_id"]) + + result = await session.execute( + select(CartItem) + .join(MarketPlace, CartItem.market_place_id == MarketPlace.id) + .where(*filters) + .order_by(CartItem.created_at.desc()) + .options( + selectinload(CartItem.product), + selectinload(CartItem.market_place), + ) + ) + return result.scalars().all() + + +async def get_calendar_entries_for_page(session, post_id: int): + """Return pending calendar entries (DTOs) scoped to a specific page.""" + ident = current_cart_identity() + return await services.calendar.entries_for_page( + session, post_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + +async def get_tickets_for_page(session, post_id: int): + """Return reserved tickets (DTOs) scoped to a specific page.""" + ident = current_cart_identity() + return await services.calendar.tickets_for_page( + session, post_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + +async def get_cart_grouped_by_page(session) -> list[dict]: + """ + Load all cart items + calendar entries for the current identity, + grouped by market_place (one card per market). + + Returns a list of dicts: + { + "post": Post | None, + "page_config": PageConfig | None, + "market_place": MarketPlace | None, + "cart_items": [...], + "calendar_entries": [...], + "product_count": int, + "product_total": float, + "calendar_count": int, + "calendar_total": float, + "total": float, + } + + Calendar entries (no market concept) attach to a page-level group. + Items without a market_place go in an orphan bucket (post=None). + """ + from .get_cart import get_cart + from .calendar_cart import get_calendar_cart_entries, get_ticket_cart_entries + from .total import total as calc_product_total + from .calendar_cart import calendar_total as calc_calendar_total, ticket_total as calc_ticket_total + + cart_items = await get_cart(session) + cal_entries = await get_calendar_cart_entries(session) + all_tickets = await get_ticket_cart_entries(session) + + # Group cart items by market_place_id + market_groups: dict[int | None, dict] = {} + for ci in cart_items: + mp_id = ci.market_place_id if ci.market_place else None + if mp_id not in market_groups: + market_groups[mp_id] = { + "market_place": ci.market_place, + "post_id": ci.market_place.container_id if ci.market_place else None, + "cart_items": [], + "calendar_entries": [], + "tickets": [], + } + market_groups[mp_id]["cart_items"].append(ci) + + # Attach calendar entries to an existing market group for the same page, + # or create a page-level group if no market group exists for that page. + page_to_market: dict[int | None, int | None] = {} + for mp_id, grp in market_groups.items(): + pid = grp["post_id"] + if pid is not None and pid not in page_to_market: + page_to_market[pid] = mp_id + + for ce in cal_entries: + pid = ce.calendar_container_id or None + if pid in page_to_market: + market_groups[page_to_market[pid]]["calendar_entries"].append(ce) + else: + # Create a page-level group for calendar-only entries + key = ("cal", pid) + if key not in market_groups: + market_groups[key] = { + "market_place": None, + "post_id": pid, + "cart_items": [], + "calendar_entries": [], + "tickets": [], + } + if pid is not None: + page_to_market[pid] = key + market_groups[key]["calendar_entries"].append(ce) + + # Attach tickets to page groups (via calendar_container_id) + for tk in all_tickets: + pid = tk.calendar_container_id or None + if pid in page_to_market: + market_groups[page_to_market[pid]]["tickets"].append(tk) + else: + key = ("tk", pid) + if key not in market_groups: + market_groups[key] = { + "market_place": None, + "post_id": pid, + "cart_items": [], + "calendar_entries": [], + "tickets": [], + } + if pid is not None: + page_to_market[pid] = key + market_groups[key]["tickets"].append(tk) + + # Batch-load Post DTOs and PageConfig objects + post_ids = list({ + grp["post_id"] for grp in market_groups.values() + if grp["post_id"] is not None + }) + posts_by_id: dict[int, object] = {} + configs_by_post: dict[int, PageConfig] = {} + + if post_ids: + for p in await services.blog.get_posts_by_ids(session, post_ids): + posts_by_id[p.id] = p + + pc_result = await session.execute( + select(PageConfig).where( + PageConfig.container_type == "page", + PageConfig.container_id.in_(post_ids), + ) + ) + for pc in pc_result.scalars().all(): + configs_by_post[pc.container_id] = pc + + # Build result list (markets with pages first, orphan last) + result = [] + for _key, grp in sorted( + market_groups.items(), + key=lambda kv: (kv[1]["post_id"] is None, kv[1]["post_id"] or 0), + ): + items = grp["cart_items"] + entries = grp["calendar_entries"] + tks = grp["tickets"] + prod_total = calc_product_total(items) or 0 + cal_total = calc_calendar_total(entries) or 0 + tk_total = calc_ticket_total(tks) or 0 + pid = grp["post_id"] + + result.append({ + "post": posts_by_id.get(pid) if pid else None, + "page_config": configs_by_post.get(pid) if pid else None, + "market_place": grp["market_place"], + "cart_items": items, + "calendar_entries": entries, + "tickets": tks, + "product_count": sum(ci.quantity for ci in items), + "product_total": prod_total, + "calendar_count": len(entries), + "calendar_total": cal_total, + "ticket_count": len(tks), + "ticket_total": tk_total, + "total": prod_total + cal_total + tk_total, + }) + + return result diff --git a/cart/bp/cart/services/ticket_groups.py b/cart/bp/cart/services/ticket_groups.py new file mode 100644 index 0000000..cd5d910 --- /dev/null +++ b/cart/bp/cart/services/ticket_groups.py @@ -0,0 +1,43 @@ +"""Group individual TicketDTOs by (entry_id, ticket_type_id) for cart display.""" +from __future__ import annotations + +from collections import OrderedDict + + +def group_tickets(tickets) -> list[dict]: + """ + Group a flat list of TicketDTOs into aggregate rows. + + Returns list of dicts: + { + "entry_id": int, + "entry_name": str, + "entry_start_at": datetime, + "entry_end_at": datetime | None, + "ticket_type_id": int | None, + "ticket_type_name": str | None, + "price": Decimal | None, + "quantity": int, + "line_total": float, + } + """ + groups: OrderedDict[tuple, dict] = OrderedDict() + + for tk in tickets: + key = (tk.entry_id, getattr(tk, "ticket_type_id", None)) + if key not in groups: + groups[key] = { + "entry_id": tk.entry_id, + "entry_name": tk.entry_name, + "entry_start_at": tk.entry_start_at, + "entry_end_at": tk.entry_end_at, + "ticket_type_id": getattr(tk, "ticket_type_id", None), + "ticket_type_name": tk.ticket_type_name, + "price": tk.price, + "quantity": 0, + "line_total": 0, + } + groups[key]["quantity"] += 1 + groups[key]["line_total"] += float(tk.price or 0) + + return list(groups.values()) diff --git a/cart/bp/cart/services/total.py b/cart/bp/cart/services/total.py new file mode 100644 index 0000000..8dcdaf9 --- /dev/null +++ b/cart/bp/cart/services/total.py @@ -0,0 +1,13 @@ +from decimal import Decimal + + +def total(cart): + return sum( + ( + Decimal(str(item.product.special_price or item.product.regular_price)) + * item.quantity + ) + for item in cart + if (item.product.special_price or item.product.regular_price) is not None + ) + \ No newline at end of file diff --git a/cart/bp/fragments/__init__.py b/cart/bp/fragments/__init__.py new file mode 100644 index 0000000..a4af44b --- /dev/null +++ b/cart/bp/fragments/__init__.py @@ -0,0 +1 @@ +from .routes import register as register_fragments diff --git a/cart/bp/fragments/routes.py b/cart/bp/fragments/routes.py new file mode 100644 index 0000000..6724837 --- /dev/null +++ b/cart/bp/fragments/routes.py @@ -0,0 +1,70 @@ +"""Cart app fragment endpoints. + +Exposes HTML fragments at ``/internal/fragments/`` for consumption +by other coop apps via the fragment client. + +Fragments: + cart-mini Cart icon with badge (or logo when empty) + account-nav-item "orders" link for account dashboard +""" + +from __future__ import annotations + +from quart import Blueprint, Response, request, render_template, g + +from shared.infrastructure.fragments import FRAGMENT_HEADER + + +def register(): + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + # --------------------------------------------------------------- + # Fragment handlers + # --------------------------------------------------------------- + + async def _cart_mini(): + from shared.services.registry import services + + user_id = request.args.get("user_id", type=int) + session_id = request.args.get("session_id") + + summary = await services.cart.cart_summary( + g.s, user_id=user_id, session_id=session_id, + ) + count = summary.count + summary.calendar_count + summary.ticket_count + return await render_template("fragments/cart_mini.html", cart_count=count) + + async def _account_nav_item(): + from shared.infrastructure.urls import cart_url + + href = cart_url("/orders/") + return ( + '' + ) + + _handlers = { + "cart-mini": _cart_mini, + "account-nav-item": _account_nav_item, + } + + # --------------------------------------------------------------- + # Routing + # --------------------------------------------------------------- + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.get("/") + async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/html") + html = await handler() + return Response(html, status=200, content_type="text/html") + + return bp diff --git a/cart/bp/order/filters/qs.py b/cart/bp/order/filters/qs.py new file mode 100644 index 0000000..03707e8 --- /dev/null +++ b/cart/bp/order/filters/qs.py @@ -0,0 +1,74 @@ +# suma_browser/app/bp/order/filters/qs.py +from quart import request + +from typing import Iterable, Optional, Union + +from shared.browser.app.filters.qs_base import KEEP, build_qs +from shared.browser.app.filters.query_types import OrderQuery + + +def decode() -> OrderQuery: + """ + Decode current query string into an OrderQuery(page, search). + """ + try: + page = int(request.args.get("page", 1) or 1) + except ValueError: + page = 1 + + search = request.args.get("search") or None + return OrderQuery(page, search) + + +def makeqs_factory(): + """ + Build a makeqs(...) that starts from the current filters + page. + + Behaviour: + - If filters change and you don't explicitly pass page, + the page is reset to 1 (same pattern as browse/blog). + - You can clear search with search=None. + """ + q = decode() + base_search = q.search or None + base_page = int(q.page or 1) + + def makeqs( + *, + clear_filters: bool = False, + search: Union[str, None, object] = KEEP, + page: Union[int, None, object] = None, + extra: Optional[Iterable[tuple]] = None, + leading_q: bool = True, + ) -> str: + filters_changed = False + + # --- search logic --- + if search is KEEP and not clear_filters: + final_search = base_search + else: + filters_changed = True + final_search = (search or None) + + # --- page logic --- + if page is None: + final_page = 1 if filters_changed else base_page + else: + final_page = page + + # --- build params --- + params: list[tuple[str, str]] = [] + + if final_search: + params.append(("search", final_search)) + if final_page is not None: + params.append(("page", str(final_page))) + + if extra: + for k, v in extra: + if v is not None: + params.append((k, str(v))) + + return build_qs(params, leading_q=leading_q) + + return makeqs diff --git a/cart/bp/order/routes.py b/cart/bp/order/routes.py new file mode 100644 index 0000000..b85087f --- /dev/null +++ b/cart/bp/order/routes.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from quart import Blueprint, g, render_template, redirect, url_for, make_response +from sqlalchemy import select, func, or_, cast, String, exists +from sqlalchemy.orm import selectinload + + +from shared.models.market import Product +from shared.models.order import Order, OrderItem +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config + +from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page +from bp.cart.services import check_sumup_status +from shared.browser.app.utils.htmx import is_htmx_request + +from .filters.qs import makeqs_factory, decode + + +def register() -> Blueprint: + bp = Blueprint("order", __name__, url_prefix='/') + + ORDERS_PER_PAGE = 10 # keep in sync with browse page size / your preference + + @bp.before_request + def route(): + # this is the crucial bit for the |qs filter + g.makeqs_factory = makeqs_factory + + @bp.get("/") + async def order_detail(order_id: int): + """ + Show a single order + items. + """ + result = await g.s.execute( + select(Order) + .options( + selectinload(Order.items).selectinload(OrderItem.product) + ) + .where(Order.id == order_id) + ) + order = result.scalar_one_or_none() + if not order: + return await make_response("Order not found", 404) + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/order/index.html", order=order,) + else: + # HTMX navigation (page 1): main panel + OOB elements + html = await render_template("_types/order/_oob_elements.html", order=order,) + + return await make_response(html) + + @bp.get("/pay/") + async def order_pay(order_id: int): + """ + Re-open the SumUp payment page for this order. + If already paid, just go back to the order detail. + If not, (re)create a SumUp checkout and redirect. + """ + result = await g.s.execute(select(Order).where(Order.id == order_id)) + order = result.scalar_one_or_none() + if not order: + return await make_response("Order not found", 404) + + if order.status == "paid": + # Already paid; nothing to pay + return redirect(url_for("orders.order.order_detail", order_id=order.id)) + + # Prefer to reuse existing hosted URL if we have one + if order.sumup_hosted_url: + return redirect(order.sumup_hosted_url) + + # Otherwise, create a fresh checkout for this order + redirect_url = url_for("cart_global.checkout_return", order_id=order.id, _external=True) + + sumup_cfg = config().get("sumup", {}) or {} + webhook_secret = sumup_cfg.get("webhook_secret") + + webhook_url = url_for("cart_global.checkout_webhook", order_id=order.id, _external=True) + if webhook_secret: + from urllib.parse import urlencode + + sep = "&" if "?" in webhook_url else "?" + webhook_url = f"{webhook_url}{sep}{urlencode({'token': webhook_secret})}" + + checkout_data = await sumup_create_checkout( + order, + redirect_url=redirect_url, + webhook_url=webhook_url, + ) + + order.sumup_checkout_id = checkout_data.get("id") + order.sumup_status = checkout_data.get("status") + + hosted_cfg = checkout_data.get("hosted_checkout") or {} + hosted_url = hosted_cfg.get("hosted_checkout_url") or checkout_data.get("hosted_checkout_url") + order.sumup_hosted_url = hosted_url + + await g.s.flush() + + if not hosted_url: + html = await render_template( + "_types/cart/checkout_error.html", + order=order, + error="No hosted checkout URL returned from SumUp when trying to reopen payment.", + ) + return await make_response(html, 500) + + return redirect(hosted_url) + + @bp.post("/recheck/") + async def order_recheck(order_id: int): + """ + Manually re-check this order's status with SumUp. + Useful if the webhook hasn't fired or the user didn't return correctly. + """ + result = await g.s.execute(select(Order).where(Order.id == order_id)) + order = result.scalar_one_or_none() + if not order: + return await make_response("Order not found", 404) + + # If we don't have a checkout ID yet, nothing to query + if not order.sumup_checkout_id: + return redirect(url_for("orders.order.order_detail", order_id=order.id)) + + try: + await check_sumup_status(g.s, order) + except Exception: + # In a real app, log the error; here we just fall back to previous status + pass + + return redirect(url_for("orders.order.order_detail", order_id=order.id)) + + + return bp + diff --git a/cart/bp/orders/filters/qs.py b/cart/bp/orders/filters/qs.py new file mode 100644 index 0000000..984e2c3 --- /dev/null +++ b/cart/bp/orders/filters/qs.py @@ -0,0 +1,77 @@ +# suma_browser/app/bp/orders/filters/qs.py +from quart import request + +from typing import Iterable, Optional, Union + +from shared.browser.app.filters.qs_base import KEEP, build_qs +from shared.browser.app.filters.query_types import OrderQuery + + +def decode() -> OrderQuery: + """ + Decode current query string into an OrderQuery(page, search). + """ + try: + page = int(request.args.get("page", 1) or 1) + except ValueError: + page = 1 + + search = request.args.get("search") or None + return OrderQuery(page, search) + + +def makeqs_factory(): + """ + Build a makeqs(...) that starts from the current filters + page. + + Behaviour: + - If filters change and you don't explicitly pass page, + the page is reset to 1 (same pattern as browse/blog). + - You can clear search with search=None. + """ + q = decode() + base_search = q.search or None + base_page = int(q.page or 1) + + def makeqs( + *, + clear_filters: bool = False, + search: Union[str, None, object] = KEEP, + page: Union[int, None, object] = None, + extra: Optional[Iterable[tuple]] = None, + leading_q: bool = True, + ) -> str: + filters_changed = False + + # --- search logic --- + if search is KEEP and not clear_filters: + final_search = base_search + else: + filters_changed = True + if search is KEEP: + final_search = None + else: + final_search = (search or None) + + # --- page logic --- + if page is None: + final_page = 1 if filters_changed else base_page + else: + final_page = page + + # --- build params --- + params: list[tuple[str, str]] = [] + + if final_search: + params.append(("search", final_search)) + if final_page is not None: + params.append(("page", str(final_page))) + + if extra: + for k, v in extra: + if v is not None: + params.append((k, str(v))) + + return build_qs(params, leading_q=leading_q) + + return makeqs diff --git a/cart/bp/orders/routes.py b/cart/bp/orders/routes.py new file mode 100644 index 0000000..e7363c2 --- /dev/null +++ b/cart/bp/orders/routes.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +from quart import Blueprint, g, render_template, redirect, url_for, make_response +from sqlalchemy import select, func, or_, cast, String, exists +from sqlalchemy.orm import selectinload + + +from shared.models.market import Product +from shared.models.order import Order, OrderItem +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config + +from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page +from bp.cart.services import check_sumup_status +from shared.browser.app.utils.htmx import is_htmx_request +from bp import register_order + +from .filters.qs import makeqs_factory, decode + + +def register(url_prefix: str) -> Blueprint: + bp = Blueprint("orders", __name__, url_prefix=url_prefix) + bp.register_blueprint( + register_order(), + ) + ORDERS_PER_PAGE = 10 # keep in sync with browse page size / your preference + + oob = { + "extends": "_types/root/_index.html", + "child_id": "auth-header-child", + "header": "_types/auth/header/_header.html", + "nav": "_types/auth/_nav.html", + "main": "_types/auth/_main_panel.html", + } + + @bp.context_processor + def inject_oob(): + return {"oob": oob} + + @bp.before_request + def route(): + # this is the crucial bit for the |qs filter + g.makeqs_factory = makeqs_factory + + @bp.get("/") + async def list_orders(): + + # --- decode filters from query string (page + search) --- + q = decode() + page, search = q.page, q.search + + # sanity clamp page + if page < 1: + page = 1 + + # --- build where clause for search --- + where_clause = None + if search: + term = f"%{search.strip()}%" + conditions = [ + Order.status.ilike(term), + Order.currency.ilike(term), + Order.sumup_checkout_id.ilike(term), + Order.sumup_status.ilike(term), + Order.description.ilike(term), + ] + + conditions.append( + exists( + select(1) + .select_from(OrderItem) + .join(Product, Product.id == OrderItem.product_id) + .where( + OrderItem.order_id == Order.id, + or_( + OrderItem.product_title.ilike(term), + Product.title.ilike(term), + Product.description_short.ilike(term), + Product.description_html.ilike(term), + Product.slug.ilike(term), + Product.brand.ilike(term), + ), + ) + ) + ) + + # allow exact ID match or partial (string) match + try: + search_id = int(search) + except (TypeError, ValueError): + search_id = None + + if search_id is not None: + conditions.append(Order.id == search_id) + else: + conditions.append(cast(Order.id, String).ilike(term)) + + where_clause = or_(*conditions) + + # --- total count & total pages (respecting search) --- + count_stmt = select(func.count()).select_from(Order) + if where_clause is not None: + count_stmt = count_stmt.where(where_clause) + + total_count_result = await g.s.execute(count_stmt) + total_count = total_count_result.scalar_one() or 0 + total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE) + + # clamp page if beyond range (just in case) + if page > total_pages: + page = total_pages + + # --- paginated orders (respecting search) --- + offset = (page - 1) * ORDERS_PER_PAGE + stmt = ( + select(Order) + .order_by(Order.created_at.desc()) + .offset(offset) + .limit(ORDERS_PER_PAGE) + ) + if where_clause is not None: + stmt = stmt.where(where_clause) + + result = await g.s.execute(stmt) + orders = result.scalars().all() + + context = { + "orders": orders, + "page": page, + "total_pages": total_pages, + "search": search, + "search_count": total_count, # For search display + } + + # Determine which template to use based on request type and pagination + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/orders/index.html", **context) + elif page > 1: + # HTMX pagination: just table rows + sentinel + html = await render_template("_types/orders/_rows.html", **context) + else: + # HTMX navigation (page 1): main panel + OOB elements + html = await render_template("_types/orders/_oob_elements.html", **context) + + resp = await make_response(html) + resp.headers["Hx-Push-Url"] = _current_url_without_page() + return _vary(resp) + + return bp + diff --git a/cart/config/app-config.yaml b/cart/config/app-config.yaml new file mode 100644 index 0000000..3aa6a76 --- /dev/null +++ b/cart/config/app-config.yaml @@ -0,0 +1,84 @@ +# App-wide settings +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: Rose Ash +market_root: /market +market_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + blog: "http://localhost:8000" + market: "http://localhost:8001" + cart: "http://localhost:8002" + events: "http://localhost:8003" + federation: "http://localhost:8004" +cache: + fs_root: _snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/wines + - branded-goods/ciders + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + - ---just-lem-just-wholefoods-jelly-crystals-lemon-12-x-85g-vf067-2-html + product-details: + - General Information + - A Note About Prices + +# SumUp payment settings (fill these in for live usage) +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + checkout_reference_prefix: 'dev-' + diff --git a/cart/entrypoint.sh b/cart/entrypoint.sh new file mode 100644 index 0000000..dc7838b --- /dev/null +++ b/cart/entrypoint.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional: wait for Postgres to be reachable +if [[ -n "${DATABASE_HOST:-}" && -n "${DATABASE_PORT:-}" ]]; then + echo "Waiting for Postgres at ${DATABASE_HOST}:${DATABASE_PORT}..." + for i in {1..60}; do + (echo > /dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}) >/dev/null 2>&1 && break || true + sleep 1 + done +fi + +# NOTE: Cart app does NOT run Alembic migrations. +# Migrations are managed by the blog app which owns the shared database schema. + +# Clear Redis page cache on deploy +if [[ -n "${REDIS_URL:-}" && "${REDIS_URL}" != "no" ]]; then + echo "Flushing Redis cache..." + python3 -c " +import redis, os +r = redis.from_url(os.environ['REDIS_URL']) +r.flushall() +print('Redis cache cleared.') +" || echo "Redis flush failed (non-fatal), continuing..." +fi + +# Start the app +echo "Starting Hypercorn (${APP_MODULE:-app:app})..." +PYTHONUNBUFFERED=1 exec hypercorn "${APP_MODULE:-app:app}" --bind 0.0.0.0:${PORT:-8000} diff --git a/cart/models/__init__.py b/cart/models/__init__.py new file mode 100644 index 0000000..508c4b0 --- /dev/null +++ b/cart/models/__init__.py @@ -0,0 +1,2 @@ +from .order import Order, OrderItem +from .page_config import PageConfig diff --git a/cart/models/order.py b/cart/models/order.py new file mode 100644 index 0000000..93953fe --- /dev/null +++ b/cart/models/order.py @@ -0,0 +1 @@ +from shared.models.order import Order, OrderItem # noqa: F401 diff --git a/cart/models/page_config.py b/cart/models/page_config.py new file mode 100644 index 0000000..ec23c6d --- /dev/null +++ b/cart/models/page_config.py @@ -0,0 +1 @@ +from shared.models.page_config import PageConfig # noqa: F401 diff --git a/cart/path_setup.py b/cart/path_setup.py new file mode 100644 index 0000000..c7166f7 --- /dev/null +++ b/cart/path_setup.py @@ -0,0 +1,9 @@ +import sys +import os + +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p) diff --git a/cart/services/__init__.py b/cart/services/__init__.py new file mode 100644 index 0000000..390cd88 --- /dev/null +++ b/cart/services/__init__.py @@ -0,0 +1,28 @@ +"""Cart app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the cart app. + + Cart owns: Order, OrderItem. + Standard deployment registers all 4 services as real DB impls + (shared DB). For composable deployments, swap non-owned services + with stubs from shared.services.stubs. + """ + from shared.services.registry import services + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + services.cart = SqlCartService() + if not services.has("blog"): + services.blog = SqlBlogService() + if not services.has("calendar"): + services.calendar = SqlCalendarService() + if not services.has("market"): + services.market = SqlMarketService() + if not services.has("federation"): + from shared.services.federation_impl import SqlFederationService + services.federation = SqlFederationService() diff --git a/cart/templates/_types/auth/header/_header.html b/cart/templates/_types/auth/header/_header.html new file mode 100644 index 0000000..c59a712 --- /dev/null +++ b/cart/templates/_types/auth/header/_header.html @@ -0,0 +1,12 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='auth-row', oob=oob) %} + {% call links.link(account_url('/'), hx_select_search ) %} + +
    account
    + {% endcall %} + {% call links.desktop_nav() %} + {% include "_types/auth/_nav.html" %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/cart/templates/_types/auth/index.html b/cart/templates/_types/auth/index.html new file mode 100644 index 0000000..3c66bf1 --- /dev/null +++ b/cart/templates/_types/auth/index.html @@ -0,0 +1,18 @@ +{% extends oob.extends %} + + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row(oob.child_id, oob.header) %} + {% block auth_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include oob.nav %} +{% endblock %} + +{% block content %} + {% include oob.main %} +{% endblock %} diff --git a/cart/templates/_types/cart/_cart.html b/cart/templates/_types/cart/_cart.html new file mode 100644 index 0000000..30e3d22 --- /dev/null +++ b/cart/templates/_types/cart/_cart.html @@ -0,0 +1,260 @@ +{% macro show_cart(oob=False) %} +
    + {# Empty cart #} + {% if not cart and not calendar_cart_entries and not ticket_cart_entries %} +
    +
    + +
    +

    + Your cart is empty +

    + {# +

    + Add some items from the shop to see them here. +

    + #} +
    + + {% else %} + +
    + {# Items list #} +
    + {% for item in cart %} + {% from '_types/product/_cart.html' import cart_item with context %} + {{ cart_item()}} + {% endfor %} + {% if calendar_cart_entries %} +
    +

    + Calendar bookings +

    + +
      + {% for entry in calendar_cart_entries %} +
    • +
      +
      + {{ entry.name or entry.calendar_name }} +
      +
      + {{ entry.start_at }} + {% if entry.end_at %} + – {{ entry.end_at }} + {% endif %} +
      +
      +
      + £{{ "%.2f"|format(entry.cost or 0) }} +
      +
    • + {% endfor %} +
    +
    + {% endif %} + {% if ticket_groups is defined and ticket_groups %} +
    +

    + + Event tickets +

    + +
    + {% for tg in ticket_groups %} +
    +
    +
    +
    +

    + {{ tg.entry_name }} +

    + {% if tg.ticket_type_name %} +

    + {{ tg.ticket_type_name }} +

    + {% endif %} +

    + {{ tg.entry_start_at.strftime('%-d %b %Y, %H:%M') }} + {% if tg.entry_end_at %} + – {{ tg.entry_end_at.strftime('%-d %b %Y, %H:%M') }} + {% endif %} +

    +
    +
    +

    + £{{ "%.2f"|format(tg.price or 0) }} +

    +
    +
    + +
    +
    + Quantity + {% set qty_url = url_for('cart_global.update_ticket_quantity') %} + +
    + + + {% if tg.ticket_type_id %} + + {% endif %} + + +
    + + + {{ tg.quantity }} + + +
    + + + {% if tg.ticket_type_id %} + + {% endif %} + + +
    +
    + +
    +

    + Line total: + £{{ "%.2f"|format(tg.line_total) }} +

    +
    +
    +
    +
    + {% endfor %} +
    +
    + {% endif %} +
    + {{summary(cart, total, calendar_total, calendar_cart_entries, ticket_total, ticket_cart_entries)}} + +
    + + {% endif %} +
    +{% endmacro %} + + +{% macro summary(cart, total, calendar_total, calendar_cart_entries, ticket_total, ticket_cart_entries, oob=False) %} + +{% endmacro %} + +{% macro cart_total(cart, total) %} + {% set cart_total = total(cart) %} + {% if cart_total %} + {% set symbol = "£" if cart[0].product.regular_price_currency == "GBP" else cart[0].product.regular_price_currency %} + {{ symbol }}{{ "%.2f"|format(cart_total) }} + {% else %} + – + {% endif %} +{% endmacro %} + + +{% macro cart_grand_total(cart, total, calendar_total, calendar_cart_entries, ticket_total, ticket_cart_entries) %} + {% set product_total = total(cart) or 0 %} + {% set cal_total = calendar_total(calendar_cart_entries) or 0 %} + {% set tk_total = ticket_total(ticket_cart_entries) or 0 %} + {% set grand = product_total + cal_total + tk_total %} + + {% if cart and cart[0].product.regular_price_currency %} + {% set symbol = "£" if cart[0].product.regular_price_currency == "GBP" else cart[0].product.regular_price_currency %} + {% else %} + {% set symbol = "£" %} + {% endif %} + + {{ symbol }}{{ "%.2f"|format(grand) }} +{% endmacro %} \ No newline at end of file diff --git a/cart/templates/_types/cart/_main_panel.html b/cart/templates/_types/cart/_main_panel.html new file mode 100644 index 0000000..3872387 --- /dev/null +++ b/cart/templates/_types/cart/_main_panel.html @@ -0,0 +1,4 @@ +
    + {% from '_types/cart/_cart.html' import show_cart with context %} + {{ show_cart() }} +
    \ No newline at end of file diff --git a/cart/templates/_types/cart/_mini.html b/cart/templates/_types/cart/_mini.html new file mode 100644 index 0000000..a8255e4 --- /dev/null +++ b/cart/templates/_types/cart/_mini.html @@ -0,0 +1,45 @@ +{% macro mini(oob=False, count=None) %} +
    + {# cart_count is set by the context processor in all apps. + Cart app computes it from g.cart + calendar_cart_entries; + other apps get it from the cart internal API. + count param allows explicit override when macro is imported without context. #} + {% if count is not none %} + {% set _count = count %} + {% elif cart_count is defined and cart_count is not none %} + {% set _count = cart_count %} + {% elif cart is defined and cart is not none %} + {% set _count = (cart | sum(attribute="quantity")) + ((calendar_cart_entries | length) if calendar_cart_entries else 0) %} + {% else %} + {% set _count = 0 %} + {% endif %} + + {% if _count == 0 %} +
    + + + +
    + {% else %} + + + + + + {{ _count }} + + + {% endif %} +
    +{% endmacro %} diff --git a/cart/templates/_types/cart/_nav.html b/cart/templates/_types/cart/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/cart/templates/_types/cart/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/cart/templates/_types/cart/_oob_elements.html b/cart/templates/_types/cart/_oob_elements.html new file mode 100644 index 0000000..6e54a8b --- /dev/null +++ b/cart/templates/_types/cart/_oob_elements.html @@ -0,0 +1,28 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-header-child', 'cart-header-child', '_types/cart/header/_header.html')}} + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/cart/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/cart/_main_panel.html" %} +{% endblock %} + + diff --git a/cart/templates/_types/cart/checkout_error.html b/cart/templates/_types/cart/checkout_error.html new file mode 100644 index 0000000..a15b1e9 --- /dev/null +++ b/cart/templates/_types/cart/checkout_error.html @@ -0,0 +1,38 @@ +{% extends '_types/root/index.html' %} + +{% block filter %} +
    +

    + Checkout error +

    +

    + We tried to start your payment with SumUp but hit a problem. +

    +
    +{% endblock %} + +{% block content %} +
    +
    +

    Something went wrong.

    +

    + {{ error or "Unexpected error while creating the hosted checkout session." }} +

    + {% if order %} +

    + Order ID: #{{ order.id }} +

    + {% endif %} +
    + + +
    +{% endblock %} diff --git a/cart/templates/_types/cart/checkout_return.html b/cart/templates/_types/cart/checkout_return.html new file mode 100644 index 0000000..b08a09d --- /dev/null +++ b/cart/templates/_types/cart/checkout_return.html @@ -0,0 +1,68 @@ +{% extends '_types/root/index.html' %} + +{% block filter %} +
    +
    +

    + {% if order.status == 'paid' %} + Payment received + {% elif order.status == 'failed' %} + Payment failed + {% elif order.status == 'missing' %} + Order not found + {% else %} + Payment status: {{ order.status|default('pending')|capitalize }} + {% endif %} +

    +

    + {% if order.status == 'paid' %} + Thanks for your order. + {% elif order.status == 'failed' %} + Something went wrong while processing your payment. You can try again below. + {% elif order.status == 'missing' %} + We couldn't find that order – it may have expired or never been created. + {% else %} + We’re still waiting for a final confirmation from SumUp. + {% endif %} +

    +
    + +
    +{% endblock %} + +{% block aside %} + {# no aside content for now #} +{% endblock %} + +{% block content %} +
    + {% if order %} +
    + {% include '_types/order/_summary.html' %} +
    + {% else %} +
    + We couldn’t find that order. If you reached this page from an old link, please start a new order. +
    + {% endif %} + {% include '_types/order/_items.html' %} + {% include '_types/order/_calendar_items.html' %} + {% include '_types/order/_ticket_items.html' %} + + {% if order.status == 'failed' and order %} +
    +

    Your payment was not completed.

    +

    + You can go back to your cart and try checkout again. If the problem persists, + please contact us and mention order #{{ order.id }}. +

    +
    + {% elif order.status == 'paid' %} +
    +

    All done!

    +

    We’ll start processing your order shortly.

    +
    + {% endif %} + +
    +{% endblock %} diff --git a/cart/templates/_types/cart/header/_header.html b/cart/templates/_types/cart/header/_header.html new file mode 100644 index 0000000..b5d913d --- /dev/null +++ b/cart/templates/_types/cart/header/_header.html @@ -0,0 +1,12 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='cart-row', oob=oob) %} + {% call links.link(cart_url('/'), hx_select_search ) %} + +

    cart

    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/cart/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/cart/templates/_types/cart/index.html b/cart/templates/_types/cart/index.html new file mode 100644 index 0000000..78570d9 --- /dev/null +++ b/cart/templates/_types/cart/index.html @@ -0,0 +1,22 @@ +{% extends '_types/root/_index.html' %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('cart-header-child', '_types/cart/header/_header.html') %} + {% block cart_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} +{% include '_types/cart/_nav.html' %} +{% endblock %} + + +{% block aside %} +{% endblock %} + +{% block content %} + {% include '_types/cart/_main_panel.html' %} +{% endblock %} diff --git a/cart/templates/_types/cart/overview/_main_panel.html b/cart/templates/_types/cart/overview/_main_panel.html new file mode 100644 index 0000000..0ac484e --- /dev/null +++ b/cart/templates/_types/cart/overview/_main_panel.html @@ -0,0 +1,147 @@ +
    + {% if not page_groups or (page_groups | length == 0) %} +
    +
    + +
    +

    + Your cart is empty +

    +
    + + {% else %} + {# Check if there are any items at all across all groups #} + {% set ns = namespace(has_items=false) %} + {% for grp in page_groups %} + {% if grp.cart_items or grp.calendar_entries or grp.get('tickets') %} + {% set ns.has_items = true %} + {% endif %} + {% endfor %} + + {% if not ns.has_items %} +
    +
    + +
    +

    + Your cart is empty +

    +
    + {% else %} +
    + {% for grp in page_groups %} + {% if grp.cart_items or grp.calendar_entries or grp.get('tickets') %} + + {% if grp.post %} + {# Market / page cart card #} + +
    + {% if grp.post.feature_image %} + {{ grp.post.title }} + {% else %} +
    + +
    + {% endif %} + +
    +

    + {% if grp.market_place %} + {{ grp.market_place.name }} + {% else %} + {{ grp.post.title }} + {% endif %} +

    + {% if grp.market_place %} +

    {{ grp.post.title }}

    + {% endif %} + +
    + {% if grp.product_count > 0 %} + + + {{ grp.product_count }} item{{ 's' if grp.product_count != 1 }} + + {% endif %} + {% if grp.calendar_count > 0 %} + + + {{ grp.calendar_count }} booking{{ 's' if grp.calendar_count != 1 }} + + {% endif %} + {% if grp.ticket_count is defined and grp.ticket_count > 0 %} + + + {{ grp.ticket_count }} ticket{{ 's' if grp.ticket_count != 1 }} + + {% endif %} +
    +
    + +
    +
    + £{{ "%.2f"|format(grp.total) }} +
    +
    + View cart → +
    +
    +
    +
    + + {% else %} + {# Orphan bucket (items without a page) #} +
    +
    +
    + +
    + +
    +

    + Other items +

    +
    + {% if grp.product_count > 0 %} + + + {{ grp.product_count }} item{{ 's' if grp.product_count != 1 }} + + {% endif %} + {% if grp.calendar_count > 0 %} + + + {{ grp.calendar_count }} booking{{ 's' if grp.calendar_count != 1 }} + + {% endif %} + {% if grp.ticket_count is defined and grp.ticket_count > 0 %} + + + {{ grp.ticket_count }} ticket{{ 's' if grp.ticket_count != 1 }} + + {% endif %} +
    +
    + +
    +
    + £{{ "%.2f"|format(grp.total) }} +
    +
    +
    +
    + {% endif %} + + {% endif %} + {% endfor %} +
    + {% endif %} + {% endif %} +
    diff --git a/cart/templates/_types/cart/overview/_oob_elements.html b/cart/templates/_types/cart/overview/_oob_elements.html new file mode 100644 index 0000000..af27fdc --- /dev/null +++ b/cart/templates/_types/cart/overview/_oob_elements.html @@ -0,0 +1,24 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for cart overview HTMX navigation #} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-header-child', 'cart-header-child', '_types/cart/header/_header.html')}} + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/cart/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/cart/overview/_main_panel.html" %} +{% endblock %} diff --git a/cart/templates/_types/cart/overview/index.html b/cart/templates/_types/cart/overview/index.html new file mode 100644 index 0000000..bf1faf0 --- /dev/null +++ b/cart/templates/_types/cart/overview/index.html @@ -0,0 +1,22 @@ +{% extends '_types/root/_index.html' %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('cart-header-child', '_types/cart/header/_header.html') %} + {% block cart_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} +{% include '_types/cart/_nav.html' %} +{% endblock %} + + +{% block aside %} +{% endblock %} + +{% block content %} + {% include '_types/cart/overview/_main_panel.html' %} +{% endblock %} diff --git a/cart/templates/_types/cart/page/_main_panel.html b/cart/templates/_types/cart/page/_main_panel.html new file mode 100644 index 0000000..7b62eb9 --- /dev/null +++ b/cart/templates/_types/cart/page/_main_panel.html @@ -0,0 +1,4 @@ +
    + {% from '_types/cart/_cart.html' import show_cart with context %} + {{ show_cart() }} +
    diff --git a/cart/templates/_types/cart/page/_oob_elements.html b/cart/templates/_types/cart/page/_oob_elements.html new file mode 100644 index 0000000..b5416fc --- /dev/null +++ b/cart/templates/_types/cart/page/_oob_elements.html @@ -0,0 +1,27 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for page cart HTMX navigation #} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('root-header-child', 'cart-header-child', '_types/cart/header/_header.html')}} + + {% from '_types/cart/page/header/_header.html' import page_header_row with context %} + {{ page_header_row(oob=True) }} + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/cart/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/cart/page/_main_panel.html" %} +{% endblock %} diff --git a/cart/templates/_types/cart/page/header/_header.html b/cart/templates/_types/cart/page/header/_header.html new file mode 100644 index 0000000..6afb1fb --- /dev/null +++ b/cart/templates/_types/cart/page/header/_header.html @@ -0,0 +1,25 @@ +{% import 'macros/links.html' as links %} +{% macro page_header_row(oob=False) %} + {% call links.menu_row(id='page-cart-row', oob=oob) %} + {% call links.link(cart_url('/' + page_post.slug + '/'), hx_select_search) %} + {% if page_post.feature_image %} + + {% endif %} + + {{ page_post.title | truncate(160, True, '...') }} + + {% endcall %} + {% call links.desktop_nav() %} + + + All carts + + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/cart/templates/_types/cart/page/index.html b/cart/templates/_types/cart/page/index.html new file mode 100644 index 0000000..4fa9814 --- /dev/null +++ b/cart/templates/_types/cart/page/index.html @@ -0,0 +1,24 @@ +{% extends '_types/root/_index.html' %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('cart-header-child', '_types/cart/header/_header.html') %} + {% block cart_header_child %} + {% from '_types/cart/page/header/_header.html' import page_header_row with context %} + {{ page_header_row() }} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} +{% include '_types/cart/_nav.html' %} +{% endblock %} + + +{% block aside %} +{% endblock %} + +{% block content %} + {% include '_types/cart/page/_main_panel.html' %} +{% endblock %} diff --git a/cart/templates/_types/order/_calendar_items.html b/cart/templates/_types/order/_calendar_items.html new file mode 100644 index 0000000..019f048 --- /dev/null +++ b/cart/templates/_types/order/_calendar_items.html @@ -0,0 +1,43 @@ +{# --- NEW: calendar bookings in this order --- #} + {% if order and calendar_entries %} +
    +

    + Calendar bookings in this order +

    + +
      + {% for entry in calendar_entries %} +
    • +
      +
      + {{ entry.name }} + {# Small status pill #} + + {{ entry.state|capitalize }} + +
      +
      + {{ entry.start_at.strftime('%-d %b %Y, %H:%M') }} + {% if entry.end_at %} + – {{ entry.end_at.strftime('%-d %b %Y, %H:%M') }} + {% endif %} +
      +
      +
      + £{{ "%.2f"|format(entry.cost or 0) }} +
      +
    • + {% endfor %} +
    +
    + {% endif %} \ No newline at end of file diff --git a/cart/templates/_types/order/_items.html b/cart/templates/_types/order/_items.html new file mode 100644 index 0000000..27b2a9f --- /dev/null +++ b/cart/templates/_types/order/_items.html @@ -0,0 +1,51 @@ +{# Items list #} +{% if order and order.items %} + +{% endif %} \ No newline at end of file diff --git a/cart/templates/_types/order/_main_panel.html b/cart/templates/_types/order/_main_panel.html new file mode 100644 index 0000000..679b846 --- /dev/null +++ b/cart/templates/_types/order/_main_panel.html @@ -0,0 +1,7 @@ +
    + {# Order summary card #} + {% include '_types/order/_summary.html' %} + {% include '_types/order/_items.html' %} + {% include '_types/order/_calendar_items.html' %} + +
    \ No newline at end of file diff --git a/cart/templates/_types/order/_nav.html b/cart/templates/_types/order/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/cart/templates/_types/order/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/cart/templates/_types/order/_oob_elements.html b/cart/templates/_types/order/_oob_elements.html new file mode 100644 index 0000000..31d1e17 --- /dev/null +++ b/cart/templates/_types/order/_oob_elements.html @@ -0,0 +1,30 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('orders-header-child', 'order-header-child', '_types/order/header/_header.html')}} + + {% from '_types/order/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/order/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/order/_main_panel.html" %} +{% endblock %} + + diff --git a/cart/templates/_types/order/_summary.html b/cart/templates/_types/order/_summary.html new file mode 100644 index 0000000..ffe560b --- /dev/null +++ b/cart/templates/_types/order/_summary.html @@ -0,0 +1,52 @@ +
    +

    + Order ID: + #{{ order.id }} +

    + +

    + Created: + {% if order.created_at %} + {{ order.created_at.strftime('%-d %b %Y, %H:%M') }} + {% else %} + — + {% endif %} +

    + +

    + Description: + {{ order.description or '–' }} +

    + +

    + Status: + + {{ order.status or 'pending' }} + +

    + +

    + Currency: + {{ order.currency or 'GBP' }} +

    + +

    + Total: + {% if order.total_amount %} + {{ order.currency or 'GBP' }} {{ '%.2f'|format(order.total_amount) }} + {% else %} + – + {% endif %} +

    + +
    + + \ No newline at end of file diff --git a/cart/templates/_types/order/_ticket_items.html b/cart/templates/_types/order/_ticket_items.html new file mode 100644 index 0000000..ef06c0b --- /dev/null +++ b/cart/templates/_types/order/_ticket_items.html @@ -0,0 +1,49 @@ +{# --- Tickets in this order --- #} + {% if order and order_tickets %} +
    +

    + Event tickets in this order +

    + +
      + {% for tk in order_tickets %} +
    • +
      +
      + {{ tk.entry_name }} + {# Small status pill #} + + {{ tk.state|replace('_', ' ')|capitalize }} + +
      + {% if tk.ticket_type_name %} +
      {{ tk.ticket_type_name }}
      + {% endif %} +
      + {{ tk.entry_start_at.strftime('%-d %b %Y, %H:%M') }} + {% if tk.entry_end_at %} + – {{ tk.entry_end_at.strftime('%-d %b %Y, %H:%M') }} + {% endif %} +
      +
      + {{ tk.code }} +
      +
      +
      + £{{ "%.2f"|format(tk.price or 0) }} +
      +
    • + {% endfor %} +
    +
    + {% endif %} \ No newline at end of file diff --git a/cart/templates/_types/order/header/_header.html b/cart/templates/_types/order/header/_header.html new file mode 100644 index 0000000..4d7f74b --- /dev/null +++ b/cart/templates/_types/order/header/_header.html @@ -0,0 +1,17 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='order-row', oob=oob) %} + {% call links.link(url_for('orders.order.order_detail', order_id=order.id), hx_select_search ) %} + +
    + Order +
    +
    + {{ order.id }} +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/order/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/cart/templates/_types/order/index.html b/cart/templates/_types/order/index.html new file mode 100644 index 0000000..c3d301e --- /dev/null +++ b/cart/templates/_types/order/index.html @@ -0,0 +1,68 @@ +{% extends '_types/orders/index.html' %} + + +{% block orders_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('order-header-child', '_types/order/header/_header.html') %} + {% block order_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/order/_nav.html' %} +{% endblock %} + + + +{% block filter %} +
    +
    +

    + Placed {% if order.created_at %}{{ order.created_at.strftime('%-d %b %Y, %H:%M') }}{% else %}—{% endif %} · Status: {{ order.status or 'pending' }} +

    +
    +
    + + + All orders + + + {# Re-check status button #} +
    + + +
    + + {% if order.status != 'paid' %} + + + Open payment page + + {% endif %} +
    +
    +{% endblock %} + +{% block content %} + {% include '_types/order/_main_panel.html' %} +{% endblock %} + +{% block aside %} +{% endblock %} diff --git a/cart/templates/_types/orders/_main_panel.html b/cart/templates/_types/orders/_main_panel.html new file mode 100644 index 0000000..01ad410 --- /dev/null +++ b/cart/templates/_types/orders/_main_panel.html @@ -0,0 +1,26 @@ +
    + {% if not orders %} +
    + No orders yet. +
    + {% else %} +
    + + + + + + + + + + + + + {# rows + infinite-scroll sentinel #} + {% include "_types/orders/_rows.html" %} + +
    OrderCreatedDescriptionTotalStatus
    +
    + {% endif %} +
    diff --git a/cart/templates/_types/orders/_nav.html b/cart/templates/_types/orders/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/cart/templates/_types/orders/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/cart/templates/_types/orders/_oob_elements.html b/cart/templates/_types/orders/_oob_elements.html new file mode 100644 index 0000000..741e8fa --- /dev/null +++ b/cart/templates/_types/orders/_oob_elements.html @@ -0,0 +1,38 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('auth-header-child', 'orders-header-child', '_types/orders/header/_header.html')}} + + {% from '_types/auth/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block aside %} + {% from 'macros/search.html' import search_desktop %} + {{ search_desktop(current_local_href, search, search_count, hx_select) }} +{% endblock %} + +{% block filter %} +{% include '_types/orders/_summary.html' %} +{% endblock %} + +{% block mobile_menu %} + {% include '_types/orders/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/orders/_main_panel.html" %} +{% endblock %} + + diff --git a/cart/templates/_types/orders/_rows.html b/cart/templates/_types/orders/_rows.html new file mode 100644 index 0000000..33a459c --- /dev/null +++ b/cart/templates/_types/orders/_rows.html @@ -0,0 +1,164 @@ +{# suma_browser/templates/_types/order/_orders_rows.html #} + +{# --- existing rows, but split into desktop/tablet vs mobile --- #} +{% for order in orders %} + {# Desktop / tablet table row #} + + + #{{ order.id }} + + + {% if order.created_at %} + {{ order.created_at.strftime('%-d %b %Y, %H:%M') }} + {% else %} + — + {% endif %} + + + {{ order.description or '' }} + + + + {{ order.currency or 'GBP' }} + {{ '%.2f'|format(order.total_amount or 0) }} + + + {# status pill, roughly matching existing styling #} + + {{ order.status or 'pending' }} + + + + + View + + + + + {# Mobile card row #} + + +
    +
    + + #{{ order.id }} + + + + {{ order.status or 'pending' }} + +
    + +
    + {{ order.created_at or '' }} +
    + +
    +
    + {{ order.currency or 'GBP' }} + {{ '%.2f'|format(order.total_amount or 0) }} +
    + + + View + +
    +
    + + +{% endfor %} + +{# --- sentinel / end-of-results --- #} +{% if page < total_pages|int %} + + + {# Mobile sentinel content #} +
    + {% include "sentinel/mobile_content.html" %} +
    + + {# Desktop sentinel content #} + + + +{% else %} + + + End of results + + +{% endif %} diff --git a/cart/templates/_types/orders/_summary.html b/cart/templates/_types/orders/_summary.html new file mode 100644 index 0000000..f812413 --- /dev/null +++ b/cart/templates/_types/orders/_summary.html @@ -0,0 +1,11 @@ +
    +
    +

    + Recent orders placed via the checkout. +

    +
    +
    + {% from 'macros/search.html' import search_mobile %} + {{ search_mobile(current_local_href, search, search_count, hx_select) }} +
    +
    \ No newline at end of file diff --git a/cart/templates/_types/orders/header/_header.html b/cart/templates/_types/orders/header/_header.html new file mode 100644 index 0000000..32c1659 --- /dev/null +++ b/cart/templates/_types/orders/header/_header.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='orders-row', oob=oob) %} + {% call links.link(url_for('orders.list_orders'), hx_select_search, ) %} + +
    + Orders +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/orders/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/cart/templates/_types/orders/index.html b/cart/templates/_types/orders/index.html new file mode 100644 index 0000000..7ee80a0 --- /dev/null +++ b/cart/templates/_types/orders/index.html @@ -0,0 +1,29 @@ +{% extends '_types/auth/index.html' %} + + +{% block auth_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('orders-header-child', '_types/orders/header/_header.html') %} + {% block orders_header_child %} + {% endblock %} + {% endcall %} + +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/orders/_nav.html' %} +{% endblock %} + +{% block aside %} + {% from 'macros/search.html' import search_desktop %} + {{ search_desktop(current_local_href, search, search_count, hx_select) }} +{% endblock %} + + +{% block filter %} + {% include '_types/orders/_summary.html' %} +{% endblock %} + +{% block content %} +{% include '_types/orders/_main_panel.html' %} +{% endblock %} diff --git a/cart/templates/_types/product/_cart.html b/cart/templates/_types/product/_cart.html new file mode 100644 index 0000000..2c68284 --- /dev/null +++ b/cart/templates/_types/product/_cart.html @@ -0,0 +1,250 @@ +{% macro add(slug, cart, oob='false') %} +{% set quantity = cart + | selectattr('product.slug', 'equalto', slug) + | sum(attribute='quantity') %} + +
    + + {% if not quantity %} +
    + + + + +
    + + {% else %} +
    + +
    + + + +
    + + + + + + + + + {{ quantity }} + + + + + + +
    + + + +
    +
    + {% endif %} +
    +{% endmacro %} + + + +{% macro cart_item(oob=False) %} + +{% set p = item.product %} +{% set unit_price = p.special_price or p.regular_price %} +
    +
    + {% if p.image %} + {{ p.title }} + {% else %} +
    + No image +
    'market', 'product', p.slug + {% endif %} +
    + + {# Details #} +
    +
    +
    +

    + {% set href=url_for('market.browse.product.product_detail', product_slug=p.slug) %} + + {{ p.title }} + +

    + + {% if p.brand %} +

    + {{ p.brand }} +

    + {% endif %} + + {% if item.is_deleted %} +

    + + This item is no longer available or price has changed +

    + {% endif %} +
    + + {# Unit price #} +
    + {% if unit_price %} + {% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %} +

    + {{ symbol }}{{ "%.2f"|format(unit_price) }} +

    + {% if p.special_price and p.special_price != p.regular_price %} +

    + {{ symbol }}{{ "%.2f"|format(p.regular_price) }} +

    + {% endif %} + {% else %} +

    No price

    + {% endif %} +
    +
    + +
    +
    + Quantity +
    + + + +
    + + {{ item.quantity }} + +
    + + + +
    +
    + +
    + {% if unit_price %} + {% set line_total = unit_price * item.quantity %} + {% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %} +

    + Line total: + {{ symbol }}{{ "%.2f"|format(line_total) }} +

    + {% endif %} +
    +
    +
    +
    + +{% endmacro %} diff --git a/cart/templates/fragments/cart_mini.html b/cart/templates/fragments/cart_mini.html new file mode 100644 index 0000000..4725a02 --- /dev/null +++ b/cart/templates/fragments/cart_mini.html @@ -0,0 +1,27 @@ +
    + {% if cart_count == 0 %} +
    + + + +
    + {% else %} + + + + {{ cart_count }} + + + {% endif %} +
    diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cb4b741 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,175 @@ +x-app-common: &app-common + networks: + appnet: + externalnet: + deploy: + placement: + constraints: + - node.labels.gpu != true + volumes: + - /root/rose-ash/_config/app-config.yaml:/app/config/app-config.yaml:ro + +x-app-env: &app-env + DATABASE_URL: postgresql+asyncpg://postgres:change-me@db:5432/appdb + ALEMBIC_DATABASE_URL: postgresql+psycopg://postgres:change-me@db:5432/appdb + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + MAIL_FROM: ${MAIL_FROM} + SMTP_USER: ${SMTP_USER} + SMTP_PASS: ${SMTP_PASS} + GHOST_API_URL: ${GHOST_API_URL} + GHOST_ADMIN_API_URL: ${GHOST_ADMIN_API_URL} + GHOST_PUBLIC_URL: ${GHOST_PUBLIC_URL} + GHOST_CONTENT_API_KEY: ${GHOST_CONTENT_API_KEY} + GHOST_WEBHOOK_SECRET: ${GHOST_WEBHOOK_SECRET} + GHOST_ADMIN_API_KEY: ${GHOST_ADMIN_API_KEY} + REDIS_URL: redis://redis:6379 + SECRET_KEY: ${SECRET_KEY} + SUMUP_API_KEY: ${SUMUP_API_KEY} + APP_URL_BLOG: https://blog.rose-ash.com + APP_URL_MARKET: https://market.rose-ash.com + APP_URL_CART: https://cart.rose-ash.com + APP_URL_EVENTS: https://events.rose-ash.com + APP_URL_FEDERATION: https://federation.rose-ash.com + APP_URL_ACCOUNT: https://account.rose-ash.com + APP_URL_ARTDAG: https://celery-artdag.rose-ash.com + INTERNAL_URL_BLOG: http://blog:8000 + INTERNAL_URL_MARKET: http://market:8000 + INTERNAL_URL_CART: http://cart:8000 + INTERNAL_URL_EVENTS: http://events:8000 + INTERNAL_URL_FEDERATION: http://federation:8000 + INTERNAL_URL_ACCOUNT: http://account:8000 + AP_DOMAIN: federation.rose-ash.com + AP_DOMAIN_BLOG: blog.rose-ash.com + AP_DOMAIN_MARKET: market.rose-ash.com + AP_DOMAIN_EVENTS: events.rose-ash.com + EXTERNAL_INBOXES: "artdag|https://celery-artdag.rose-ash.com/inbox" + +services: + blog: + <<: *app-common + image: registry.rose-ash.com:5000/blog:latest + build: + context: . + dockerfile: blog/Dockerfile + environment: + <<: *app-env + DATABASE_HOST: db + DATABASE_PORT: "5432" + RUN_MIGRATIONS: "true" + + market: + <<: *app-common + image: registry.rose-ash.com:5000/market:latest + build: + context: . + dockerfile: market/Dockerfile + volumes: + - /root/rose-ash/_config/app-config.yaml:/app/config/app-config.yaml:ro + - /root/rose-ash/_snapshot:/app/_snapshot + environment: + <<: *app-env + DATABASE_HOST: db + DATABASE_PORT: "5432" + + cart: + <<: *app-common + image: registry.rose-ash.com:5000/cart:latest + build: + context: . + dockerfile: cart/Dockerfile + environment: + <<: *app-env + DATABASE_HOST: db + DATABASE_PORT: "5432" + + events: + <<: *app-common + image: registry.rose-ash.com:5000/events:latest + build: + context: . + dockerfile: events/Dockerfile + environment: + <<: *app-env + DATABASE_HOST: db + DATABASE_PORT: "5432" + + federation: + <<: *app-common + image: registry.rose-ash.com:5000/federation:latest + build: + context: . + dockerfile: federation/Dockerfile + environment: + <<: *app-env + DATABASE_HOST: db + DATABASE_PORT: "5432" + + account: + <<: *app-common + image: registry.rose-ash.com:5000/account:latest + build: + context: . + dockerfile: account/Dockerfile + environment: + <<: *app-env + DATABASE_HOST: db + DATABASE_PORT: "5432" + + db: + image: postgres:16 + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me} + POSTGRES_DB: ${POSTGRES_DB:-appdb} + volumes: + - db_data_1:/var/lib/postgresql/data + networks: + appnet: + configs: + - source: schema_sql + target: /run/configs/schema_sql + mode: 0444 + deploy: + placement: + constraints: + - node.labels.gpu != true + + adminer: + image: adminer + networks: + appnet: + externalnet: + deploy: + placement: + constraints: + - node.labels.gpu != true + + redis: + image: redis:7-alpine + container_name: redis + volumes: + - redis_data:/data + networks: + appnet: + command: + redis-server + --maxmemory 256mb + --maxmemory-policy allkeys-lru + deploy: + placement: + constraints: + - node.labels.gpu != true + +volumes: + db_data_1: + redis_data: +networks: + appnet: + driver: overlay + externalnet: + driver: overlay + external: true +configs: + schema_sql: + file: ./schema.sql diff --git a/events/.gitignore b/events/.gitignore new file mode 100644 index 0000000..27275ba --- /dev/null +++ b/events/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.env +node_modules/ diff --git a/events/Dockerfile b/events/Dockerfile new file mode 100644 index 0000000..90c5ad9 --- /dev/null +++ b/events/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + PIP_NO_CACHE_DIR=1 \ + APP_PORT=8000 \ + APP_MODULE=app:app + +WORKDIR /app + +# Install system deps + psql client +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +COPY shared/requirements.txt ./requirements.txt +RUN pip install -r requirements.txt + +# Shared code (replaces submodule) +COPY shared/ ./shared/ + +# App code +COPY events/ ./ + +# Sibling models for cross-domain SQLAlchemy imports +COPY blog/__init__.py ./blog/__init__.py +COPY blog/models/ ./blog/models/ +COPY market/__init__.py ./market/__init__.py +COPY market/models/ ./market/models/ +COPY cart/__init__.py ./cart/__init__.py +COPY cart/models/ ./cart/models/ +COPY federation/__init__.py ./federation/__init__.py +COPY federation/models/ ./federation/models/ +COPY account/__init__.py ./account/__init__.py +COPY account/models/ ./account/models/ + +# ---------- Runtime setup ---------- +COPY events/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +RUN useradd -m -u 10001 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE ${APP_PORT} +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/events/README.md b/events/README.md new file mode 100644 index 0000000..5327685 --- /dev/null +++ b/events/README.md @@ -0,0 +1,78 @@ +# Events App + +Calendar and event booking service for the Rose Ash cooperative platform. Manages calendars, time slots, calendar entries (bookings), tickets, and ticket types. + +## Architecture + +One of five Quart microservices sharing a single PostgreSQL database: + +| App | Port | Domain | +|-----|------|--------| +| blog (coop) | 8000 | Auth, blog, admin, menus, snippets | +| market | 8001 | Product browsing, Suma scraping | +| cart | 8002 | Shopping cart, checkout, orders | +| **events** | 8003 | Calendars, bookings, tickets | +| federation | 8004 | ActivityPub, fediverse social | + +## Structure + +``` +app.py # Application factory (create_base_app + blueprints) +path_setup.py # Adds project root + app dir to sys.path +config/app-config.yaml # App URLs, feature flags +models/ # Events-domain models + calendars.py # Calendar, CalendarEntry, CalendarSlot, + # TicketType, Ticket, CalendarEntryPost +bp/ # Blueprints + calendars/ # Calendar listing + calendar/ # Single calendar view and admin + calendar_entries/ # Calendar entries listing + calendar_entry/ # Single entry view and admin + day/ # Day view and admin + slots/ # Slot listing + slot/ # Single slot management + ticket_types/ # Ticket type listing + ticket_type/ # Single ticket type management + tickets/ # Ticket listing + ticket_admin/ # Ticket administration + markets/ # Page-scoped marketplace views + payments/ # Payment-related views +services/ # register_domain_services() — wires calendar + market + cart +shared/ # Submodule -> git.rose-ash.com/coop/shared.git +``` + +## Models + +All events-domain models live in `models/calendars.py`: + +| Model | Description | +|-------|-------------| +| **Calendar** | Container for entries, scoped to a page via `container_type + container_id` | +| **CalendarEntry** | A bookable event/time slot. Has `state` (pending/ordered/provisional), `cost`, ownership (`user_id`/`session_id`), and `order_id` (plain integer, no FK) | +| **CalendarSlot** | Recurring time bands (day-of-week + time range) within a calendar | +| **TicketType** | Named ticket categories with price and count | +| **Ticket** | Individual ticket with unique code, state, and `order_id` (plain integer, no FK) | +| **CalendarEntryPost** | Junction linking entries to content via `content_type + content_id` | + +`order_id` on CalendarEntry and Ticket is a plain integer column — no FK constraint to the orders table. The cart app writes these values via service calls, not directly. + +## Cross-Domain Communication + +- `services.market.*` — marketplace queries for page views +- `services.cart.*` — cart summary for context processor +- `services.federation.*` — AP publishing for new entries +- `shared.services.navigation` — site navigation tree + +## Migrations + +This app does **not** run Alembic migrations on startup. Migrations are managed in the `shared/` submodule and run from the blog app's entrypoint. + +## Running + +```bash +export DATABASE_URL_ASYNC=postgresql+asyncpg://user:pass@localhost/coop +export REDIS_URL=redis://localhost:6379/0 +export SECRET_KEY=your-secret-key + +hypercorn app:app --bind 0.0.0.0:8003 +``` diff --git a/events/__init__.py b/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/app.py b/events/app.py new file mode 100644 index 0000000..48bb697 --- /dev/null +++ b/events/app.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import path_setup # noqa: F401 # adds shared/ to sys.path +from pathlib import Path + +from quart import g, abort, request +from jinja2 import FileSystemLoader, ChoiceLoader + +from shared.infrastructure.factory import create_base_app + +from bp import register_all_events, register_calendars, register_markets, register_payments, register_page, register_fragments + + +async def events_context() -> dict: + """ + Events app context processor. + + - nav_tree_html: fetched from blog as fragment + - cart_count/cart_total: via cart service (shared DB) + """ + from shared.infrastructure.context import base_context + from shared.services.navigation import get_navigation_tree + from shared.services.registry import services + from shared.infrastructure.cart_identity import current_cart_identity + from shared.infrastructure.fragments import fetch_fragment + + ctx = await base_context() + + ctx["nav_tree_html"] = await fetch_fragment( + "blog", "nav-tree", + params={"app_name": "events", "path": request.path}, + ) + # Fallback for _nav.html when nav-tree fragment fetch fails + ctx["menu_items"] = await get_navigation_tree(g.s) + + # Cart data via service (replaces cross-app HTTP API) + ident = current_cart_identity() + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count + ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total) + + return ctx + + +def create_app() -> "Quart": + from shared.services.registry import services + from services import register_domain_services + + app = create_base_app( + "events", + context_fn=events_context, + domain_services_fn=register_domain_services, + ) + + # App-specific templates override shared templates + app_templates = str(Path(__file__).resolve().parent / "templates") + app.jinja_loader = ChoiceLoader([ + FileSystemLoader(app_templates), + app.jinja_loader, + ]) + + # All events: / — global view across all pages + app.register_blueprint( + register_all_events(), + url_prefix="/", + ) + + # Page summary: // — upcoming events across all calendars + app.register_blueprint( + register_page(), + url_prefix="/", + ) + + # Calendars nested under post slug: //calendars/... + app.register_blueprint( + register_calendars(), + url_prefix="//calendars", + ) + + # Markets nested under post slug: //markets/... + app.register_blueprint( + register_markets(), + url_prefix="//markets", + ) + + # Payments nested under post slug: //payments/... + app.register_blueprint( + register_payments(), + url_prefix="//payments", + ) + + app.register_blueprint(register_fragments()) + + # --- Auto-inject slug into url_for() calls --- + @app.url_value_preprocessor + def pull_slug(endpoint, values): + if values and "slug" in values: + g.post_slug = values.pop("slug") + + @app.url_defaults + def inject_slug(endpoint, values): + slug = g.get("post_slug") + if slug and "slug" not in values: + if app.url_map.is_endpoint_expecting(endpoint, "slug"): + values["slug"] = slug + + # --- Load post data for slug --- + @app.before_request + async def hydrate_post(): + slug = getattr(g, "post_slug", None) + if not slug: + return + post = await services.blog.get_post_by_slug(g.s, slug) + if not post: + abort(404) + g.post_data = { + "post": { + "id": post.id, + "title": post.title, + "slug": post.slug, + "feature_image": post.feature_image, + "status": post.status, + "visibility": post.visibility, + }, + } + + @app.context_processor + async def inject_post(): + post_data = getattr(g, "post_data", None) + if not post_data: + return {} + post_id = post_data["post"]["id"] + calendars = await services.calendar.calendars_for_container(g.s, "page", post_id) + markets = await services.market.marketplaces_for_container(g.s, "page", post_id) + return { + **post_data, + "calendars": calendars, + "markets": markets, + } + + # Tickets blueprint — user-facing ticket views and QR codes + from bp.tickets.routes import register as register_tickets + app.register_blueprint(register_tickets()) + + # Ticket admin — check-in interface (admin only) + from bp.ticket_admin.routes import register as register_ticket_admin + app.register_blueprint(register_ticket_admin()) + + return app + + +app = create_app() diff --git a/events/bp/__init__.py b/events/bp/__init__.py new file mode 100644 index 0000000..68e3b31 --- /dev/null +++ b/events/bp/__init__.py @@ -0,0 +1,6 @@ +from .all_events.routes import register as register_all_events +from .calendars.routes import register as register_calendars +from .markets.routes import register as register_markets +from .payments.routes import register as register_payments +from .page.routes import register as register_page +from .fragments import register_fragments diff --git a/events/bp/all_events/__init__.py b/events/bp/all_events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/all_events/routes.py b/events/bp/all_events/routes.py new file mode 100644 index 0000000..58732b8 --- /dev/null +++ b/events/bp/all_events/routes.py @@ -0,0 +1,143 @@ +""" +All-events blueprint — shows upcoming events across ALL pages' calendars. + +Mounted at / (root of events app). No slug context — works independently +of the post/slug machinery. + +Routes: + GET / — full page with first page of entries + GET /all-entries — HTMX fragment for infinite scroll + POST /all-tickets/adjust — adjust ticket quantity inline +""" +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, render_template_string, make_response + +from shared.browser.app.utils.htmx import is_htmx_request +from shared.infrastructure.cart_identity import current_cart_identity +from shared.services.registry import services + + +def register() -> Blueprint: + bp = Blueprint("all_events", __name__) + + async def _load_entries(page, per_page=20): + """Load all upcoming entries + pending ticket counts + page info.""" + entries, has_more = await services.calendar.upcoming_entries_for_container( + g.s, page=page, per_page=per_page, + ) + + # Pending ticket counts keyed by entry_id + ident = current_cart_identity() + pending_tickets = {} + if entries: + tickets = await services.calendar.pending_tickets( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + for t in tickets: + if t.entry_id is not None: + pending_tickets[t.entry_id] = pending_tickets.get(t.entry_id, 0) + 1 + + # Batch-load page info for container_ids + page_info = {} # {post_id: {title, slug}} + if entries: + post_ids = list({ + e.calendar_container_id + for e in entries + if e.calendar_container_type == "page" and e.calendar_container_id + }) + if post_ids: + posts = await services.blog.get_posts_by_ids(g.s, post_ids) + for p in posts: + page_info[p.id] = {"title": p.title, "slug": p.slug} + + return entries, has_more, pending_tickets, page_info + + @bp.get("/") + async def index(): + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets, page_info = await _load_entries(page) + + ctx = dict( + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page_info=page_info, + page=page, + view=view, + ) + + if is_htmx_request(): + html = await render_template("_types/all_events/_main_panel.html", **ctx) + else: + html = await render_template("_types/all_events/index.html", **ctx) + + return await make_response(html, 200) + + @bp.get("/all-entries") + async def entries_fragment(): + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets, page_info = await _load_entries(page) + + html = await render_template( + "_types/all_events/_cards.html", + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page_info=page_info, + page=page, + view=view, + ) + return await make_response(html, 200) + + @bp.post("/all-tickets/adjust") + async def adjust_ticket(): + """Adjust ticket quantity, return updated widget + OOB cart-mini.""" + ident = current_cart_identity() + form = await request.form + entry_id = int(form.get("entry_id", 0)) + count = max(int(form.get("count", 0)), 0) + tt_raw = (form.get("ticket_type_id") or "").strip() + ticket_type_id = int(tt_raw) if tt_raw else None + + await services.calendar.adjust_ticket_quantity( + g.s, entry_id, count, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=ticket_type_id, + ) + + # Get updated ticket count for this entry + tickets = await services.calendar.pending_tickets( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + qty = sum(1 for t in tickets if t.entry_id == entry_id) + + # Load entry DTO for the widget template + entry = await services.calendar.entry_by_id(g.s, entry_id) + + # Updated cart count for OOB mini-cart + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + cart_count = summary.count + summary.calendar_count + summary.ticket_count + + # Render widget + OOB cart-mini + widget_html = await render_template( + "_types/page_summary/_ticket_widget.html", + entry=entry, + qty=qty, + ticket_url="/all-tickets/adjust", + ) + mini_html = await render_template_string( + '{% from "_types/cart/_mini.html" import mini with context %}' + '{{ mini(oob="true") }}', + cart_count=cart_count, + ) + return await make_response(widget_html + mini_html, 200) + + return bp diff --git a/events/bp/calendar/admin/routes.py b/events/bp/calendar/admin/routes.py new file mode 100644 index 0000000..3d042ff --- /dev/null +++ b/events/bp/calendar/admin/routes.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g +) + + +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + + + + +def register(): + bp = Blueprint("admin", __name__, url_prefix='/admin') + # ---------- Pages ---------- + @bp.get("/") + @require_admin + async def admin(calendar_slug: str, **kwargs): + from shared.browser.app.utils.htmx import is_htmx_request + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/calendar/admin/index.html") + else: + # HTMX request: main panel + OOB elements + html = await render_template("_types/calendar/admin/_oob_elements.html") + + return await make_response(html) + + + @bp.get("/description/") + @require_admin + async def calendar_description_edit(calendar_slug: str, **kwargs): + # g.post and g.calendar should already be set by the parent calendar bp + html = await render_template( + "_types/calendar/admin/_description_edit.html", + post=g.post_data['post'], + calendar=g.calendar, + ) + return await make_response(html) + + + @bp.post("/description/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def calendar_description_save(calendar_slug: str, **kwargs): + form = await request.form + description = (form.get("description") or "").strip() or None + + # simple inline update, or call a service if you prefer + g.calendar.description = description + await g.s.flush() + + html = await render_template( + "_types/calendar/admin/_description.html", + post=g.post_data['post'], + calendar=g.calendar, + oob=True + ) + return await make_response(html) + + + @bp.get("/description/view/") + @require_admin + async def calendar_description_view(calendar_slug: str, **kwargs): + # just render the display version without touching the DB (used by Cancel) + html = await render_template( + "_types/calendar/admin/_description.html", + post=g.post_data['post'], + calendar=g.calendar, + ) + return await make_response(html) + + return bp diff --git a/events/bp/calendar/routes.py b/events/bp/calendar/routes.py new file mode 100644 index 0000000..4bd544f --- /dev/null +++ b/events/bp/calendar/routes.py @@ -0,0 +1,251 @@ +from __future__ import annotations +from datetime import datetime, timezone + +from quart import ( + request, render_template, make_response, Blueprint, g, abort, session as qsession +) + + +from sqlalchemy import select + +from models.calendars import Calendar + +from sqlalchemy.orm import selectinload, with_loader_criteria +from shared.browser.app.authz import require_admin + +from .admin.routes import register as register_admin +from .services import get_visible_entries_for_period +from .services.calendar_view import ( + parse_int_arg, + add_months, + build_calendar_weeks, + get_calendar_by_post_and_slug, + get_calendar_by_slug, + update_calendar_description, +) +from shared.browser.app.utils.htmx import is_htmx_request + +from ..slots.routes import register as register_slots + +from models.calendars import CalendarSlot + +from bp.calendars.services.calendars import soft_delete + +from bp.day.routes import register as register_day + +from shared.browser.app.redis_cacher import cache_page, clear_cache + +from sqlalchemy import select + +import calendar as pycalendar + + +def register(): + bp = Blueprint("calendar", __name__, url_prefix='/') + + bp.register_blueprint( + register_admin(), + ) + bp.register_blueprint( + register_slots(), + ) + bp.register_blueprint( + register_day() + ) + + @bp.url_value_preprocessor + def pull(endpoint, values): + g.calendar_slug = values.get("calendar_slug") + + @bp.before_request + async def hydrate_calendar_data(): + calendar_slug = getattr(g, "calendar_slug", None) + + # Standalone mode (events app): no post context + post_data = getattr(g, "post_data", None) + if post_data: + post_id = (post_data.get("post") or {}).get("id") + cal = await get_calendar_by_post_and_slug(g.s, post_id, calendar_slug) + else: + cal = await get_calendar_by_slug(g.s, calendar_slug) + + if not cal: + abort(404) + return + + g.calendar = cal + + @bp.context_processor + async def inject_root(): + + return { + "calendar": getattr(g, "calendar", None), + } + + # ---------- Pages ---------- + + + # ---------- Pages ---------- + + @bp.get("/") + @cache_page(tag="calendars") + async def get(calendar_slug: str, **kwargs): + """ + Show a month-view calendar for this calendar. + + - One month at a time + - Outer arrows: +/- 1 year + - Inner arrows: +/- 1 month + """ + + # --- Determine year & month from query params --- + today = datetime.now(timezone.utc).date() + + month = parse_int_arg("month") + year = parse_int_arg("year") + + if year is None: + year = today.year + if month is None or not (1 <= month <= 12): + month = today.month + + # --- Helpers to move between months --- + prev_month_year, prev_month = add_months(year, month, -1) + next_month_year, next_month = add_months(year, month, +1) + prev_year = year - 1 + next_year = year + 1 + + # --- Build weeks grid (list of weeks, each week = 7 days) --- + weeks = build_calendar_weeks(year, month) + month_name = pycalendar.month_name[month] + weekday_names = [pycalendar.day_abbr[i] for i in range(7)] + + # --- Period boundaries for this calendar view --- + period_start = datetime(year, month, 1, tzinfo=timezone.utc) + next_y, next_m = add_months(year, month, +1) + period_end = datetime(next_y, next_m, 1, tzinfo=timezone.utc) + + # --- Identity & admin flag --- + user = getattr(g, "user", None) + session_id = qsession.get("calendar_sid") + + visible = await get_visible_entries_for_period( + sess=g.s, + calendar_id=g.calendar.id, + period_start=period_start, + period_end=period_end, + user=user, + session_id=session_id, + ) + + month_entries = visible.merged_entries + user_entries = visible.user_entries + confirmed_entries = visible.confirmed_entries + + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/calendar/index.html", + qsession=qsession, + year=year, + month=month, + month_name=month_name, + weekday_names=weekday_names, + weeks=weeks, + prev_month=prev_month, + prev_month_year=prev_month_year, + next_month=next_month, + next_month_year=next_month_year, + prev_year=prev_year, + next_year=next_year, + user_entries=user_entries, + confirmed_entries=confirmed_entries, + month_entries=month_entries, + ) + else: + + html = await render_template( + "_types/calendar/_oob_elements.html", + qsession=qsession, + year=year, + month=month, + month_name=month_name, + weekday_names=weekday_names, + weeks=weeks, + prev_month=prev_month, + prev_month_year=prev_month_year, + next_month=next_month, + next_month_year=next_month_year, + prev_year=prev_year, + next_year=next_year, + user_entries=user_entries, + confirmed_entries=confirmed_entries, + month_entries=month_entries, + ) + + return await make_response(html) + + + @bp.put("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def put(calendar_slug: str, **kwargs): + """ + Idempotent update for calendar configuration. + Accepts HTMX form (POST/PUT) and optional JSON. + """ + # Try JSON first + data = await request.get_json(silent=True) + description = None + + if data and isinstance(data, dict): + description = (data.get("description") or "").strip() + else: + form = await request.form + description = (form.get("description") or "").strip() + + await update_calendar_description(g.calendar, description) + html = await render_template("_types/calendar/admin/index.html") + return await make_response(html, 200) + + + @bp.delete("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def delete(calendar_slug: str, **kwargs): + from shared.browser.app.utils.htmx import is_htmx_request + + cal = g.calendar + cal.deleted_at = datetime.now(timezone.utc) + await g.s.flush() + + # If we have post context (blog-embedded mode), update nav + post_data = getattr(g, "post_data", None) + html = await render_template("_types/calendars/index.html") + + if post_data: + from ..post.services.entry_associations import get_associated_entries + + post_id = (post_data.get("post") or {}).get("id") + cals = ( + await g.s.execute( + select(Calendar) + .where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + ).scalars().all() + + associated_entries = await get_associated_entries(g.s, post_id) + + nav_oob = await render_template( + "_types/post/admin/_nav_entries_oob.html", + associated_entries=associated_entries, + calendars=cals, + post=post_data["post"], + ) + html = html + nav_oob + + return await make_response(html, 200) + + + return bp diff --git a/events/bp/calendar/services/__init__.py b/events/bp/calendar/services/__init__.py new file mode 100644 index 0000000..a8110ed --- /dev/null +++ b/events/bp/calendar/services/__init__.py @@ -0,0 +1 @@ +from .visiblity import get_visible_entries_for_period diff --git a/events/bp/calendar/services/adopt_session_entries_for_user.py b/events/bp/calendar/services/adopt_session_entries_for_user.py new file mode 100644 index 0000000..8e0fa2f --- /dev/null +++ b/events/bp/calendar/services/adopt_session_entries_for_user.py @@ -0,0 +1,25 @@ +from sqlalchemy import select, update +from models.calendars import CalendarEntry + +from sqlalchemy import func + +async def adopt_session_entries_for_user(session, user_id: int, session_id: str | None) -> None: + if not session_id: + return + # (Optional) Mark any existing entries for this user as deleted to avoid duplicates + await session.execute( + update(CalendarEntry) + .where(CalendarEntry.deleted_at.is_(None), CalendarEntry.user_id == user_id) + .values(deleted_at=func.now()) + ) + # Reassign anonymous entries to the user + result = await session.execute( + select(CalendarEntry).where( + CalendarEntry.deleted_at.is_(None), + CalendarEntry.session_id == session_id + ) + ) + anon_entries = result.scalars().all() + for entry in anon_entries: + entry.user_id = user_id + # No commit here; caller will commit diff --git a/events/bp/calendar/services/calendar.py b/events/bp/calendar/services/calendar.py new file mode 100644 index 0000000..e1bda42 --- /dev/null +++ b/events/bp/calendar/services/calendar.py @@ -0,0 +1,28 @@ +from __future__ import annotations + + +from models.calendars import Calendar +from ...calendars.services.calendars import CalendarError + +async def update_calendar_config(sess, calendar_id: int, *, description: str | None, slots: list | None): + """Update description and slots for a calendar.""" + cal = await sess.get(Calendar, calendar_id) + if not cal: + raise CalendarError(f"Calendar {calendar_id} not found.") + cal.description = (description or '').strip() or None + # Validate slots shape a bit + norm_slots: list[dict] = [] + if slots: + for s in slots: + if not isinstance(s, dict): + continue + norm_slots.append({ + "days": str(s.get("days", ""))[:7].lower(), + "time_from": str(s.get("time_from", ""))[:5], + "time_to": str(s.get("time_to", ""))[:5], + "cost_name": (s.get("cost_name") or "")[:64], + "description": (s.get("description") or "")[:255], + }) + cal.slots = norm_slots or None + await sess.flush() + return cal diff --git a/events/bp/calendar/services/calendar_view.py b/events/bp/calendar/services/calendar_view.py new file mode 100644 index 0000000..71fe331 --- /dev/null +++ b/events/bp/calendar/services/calendar_view.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Optional +import calendar as pycalendar + +from quart import request +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload, with_loader_criteria + +from models.calendars import Calendar, CalendarSlot + +def parse_int_arg(name: str, default: Optional[int] = None) -> Optional[int]: + """Parse an integer query parameter from the request.""" + val = request.args.get(name, "").strip() + if not val: + return default + try: + return int(val) + except ValueError: + return default + + +def add_months(year: int, month: int, delta: int) -> tuple[int, int]: + """Add (or subtract) months to a given year/month, handling year overflow.""" + new_month = month + delta + new_year = year + (new_month - 1) // 12 + new_month = ((new_month - 1) % 12) + 1 + return new_year, new_month + + +def build_calendar_weeks(year: int, month: int) -> list[list[dict]]: + """ + Build a calendar grid for the given year and month. + Returns a list of weeks, where each week is a list of 7 day dictionaries. + """ + today = datetime.now(timezone.utc).date() + cal = pycalendar.Calendar(firstweekday=0) # 0 = Monday + weeks: list[list[dict]] = [] + + for week in cal.monthdatescalendar(year, month): + week_days = [] + for d in week: + week_days.append( + { + "date": d, + "in_month": (d.month == month), + "is_today": (d == today), + } + ) + weeks.append(week_days) + + return weeks + + +async def get_calendar_by_post_and_slug( + session: AsyncSession, + post_id: int, + calendar_slug: str, +) -> Optional[Calendar]: + """ + Fetch a calendar by post_id and slug, with slots eagerly loaded. + Returns None if not found. + """ + result = await session.execute( + select(Calendar) + .options( + selectinload(Calendar.slots), + with_loader_criteria(CalendarSlot, CalendarSlot.deleted_at.is_(None)), + ) + .where( + Calendar.container_type == "page", + Calendar.container_id == post_id, + Calendar.slug == calendar_slug, + Calendar.deleted_at.is_(None), + ) + ) + return result.scalar_one_or_none() + + +async def get_calendar_by_slug( + session: AsyncSession, + calendar_slug: str, +) -> Optional[Calendar]: + """ + Fetch a calendar by slug only (for standalone events service). + With slots eagerly loaded. Returns None if not found. + """ + result = await session.execute( + select(Calendar) + .options( + selectinload(Calendar.slots), + with_loader_criteria(CalendarSlot, CalendarSlot.deleted_at.is_(None)), + ) + .where( + Calendar.slug == calendar_slug, + Calendar.deleted_at.is_(None), + ) + ) + return result.scalar_one_or_none() + + +async def update_calendar_description( + calendar: Calendar, + description: Optional[str], +) -> None: + """Update calendar description (in-place on the calendar object).""" + calendar.description = description or None diff --git a/events/bp/calendar/services/slots.py b/events/bp/calendar/services/slots.py new file mode 100644 index 0000000..4c40445 --- /dev/null +++ b/events/bp/calendar/services/slots.py @@ -0,0 +1,118 @@ + +from __future__ import annotations +from datetime import time +from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import CalendarSlot + + +class SlotError(ValueError): + pass + +def _b(v): + if isinstance(v, bool): + return v + s = str(v).lower() + return s in {"1","true","t","yes","y","on"} + +async def list_slots(sess: AsyncSession, calendar_id: int) -> Sequence[CalendarSlot]: + res = await sess.execute( + select(CalendarSlot) + .where(CalendarSlot.calendar_id == calendar_id, CalendarSlot.deleted_at.is_(None)) + .order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc()) + ) + return res.scalars().all() + +async def create_slot(sess: AsyncSession, calendar_id: int, *, name: str, description: str | None, + days: dict, time_start: time, time_end: time, cost: float | None): + if not name: + raise SlotError("name is required") + if not time_start or not time_end or time_end <= time_start: + raise SlotError("time range invalid") + slot = CalendarSlot( + calendar_id=calendar_id, + name=name, + description=(description or None), + mon=_b(days.get("mon")), tue=_b(days.get("tue")), wed=_b(days.get("wed")), + thu=_b(days.get("thu")), fri=_b(days.get("fri")), sat=_b(days.get("sat")), sun=_b(days.get("sun")), + time_start=time_start, time_end=time_end, cost=cost, + ) + sess.add(slot) + await sess.flush() + return slot + +async def update_slot( + sess: AsyncSession, + slot_id: int, + *, + name: str | None = None, + description: str | None = None, + days: dict | None = None, + time_start: time | None = None, + time_end: time | None = None, + cost: float | None = None, + flexible: bool | None = None, # NEW +): + slot = await sess.get(CalendarSlot, slot_id) + if not slot or slot.deleted_at is not None: + raise SlotError("slot not found") + + if name is not None: + slot.name = name + + if description is not None: + slot.description = description or None + + if days is not None: + slot.mon = _b(days.get("mon", slot.mon)) + slot.tue = _b(days.get("tue", slot.tue)) + slot.wed = _b(days.get("wed", slot.wed)) + slot.thu = _b(days.get("thu", slot.thu)) + slot.fri = _b(days.get("fri", slot.fri)) + slot.sat = _b(days.get("sat", slot.sat)) + slot.sun = _b(days.get("sun", slot.sun)) + + if time_start is not None: + slot.time_start = time_start + if time_end is not None: + slot.time_end = time_end + + if (time_start or time_end) and slot.time_end <= slot.time_start: + raise SlotError("time range invalid") + + if cost is not None: + slot.cost = cost + + # NEW: update flexible flag only if explicitly provided + if flexible is not None: + slot.flexible = flexible + + await sess.flush() + return slot + +async def soft_delete_slot(sess: AsyncSession, slot_id: int): + slot = await sess.get(CalendarSlot, slot_id) + if not slot or slot.deleted_at is not None: + return + from datetime import datetime, timezone + slot.deleted_at = datetime.now(timezone.utc) + await sess.flush() + + +async def get_slot(sess: AsyncSession, slot_id: int) -> CalendarSlot | None: + return await sess.get(CalendarSlot, slot_id) + +async def update_slot_description( + sess: AsyncSession, + slot_id: int, + description: str | None, +) -> CalendarSlot: + slot = await sess.get(CalendarSlot, slot_id) + if not slot: + raise SlotError("slot not found") + slot.description = description or None + await sess.flush() + return slot diff --git a/events/bp/calendar/services/visiblity.py b/events/bp/calendar/services/visiblity.py new file mode 100644 index 0000000..5c5776a --- /dev/null +++ b/events/bp/calendar/services/visiblity.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import CalendarEntry + + + +@dataclass +class VisibleEntries: + """ + Result of applying calendar visibility rules for a given period. + """ + user_entries: list[CalendarEntry] + confirmed_entries: list[CalendarEntry] + admin_other_entries: list[CalendarEntry] + merged_entries: list[CalendarEntry] # sorted, deduped + + +async def get_visible_entries_for_period( + sess: AsyncSession, + calendar_id: int, + period_start: datetime, + period_end: datetime, + user: Optional[object], + session_id: Optional[str], +) -> VisibleEntries: + """ + Visibility rules (same as your fixed month view): + + - Non-admin: + - sees all *confirmed* entries in the period (any user) + - sees all entries for current user/session in the period (any state) + - Admin: + - sees all confirmed + provisional + ordered entries in the period (all users) + - sees pending only for current user/session + """ + + user_id = user.id if user else None + is_admin = bool(user and getattr(user, "is_admin", False)) + + # --- Entries for current user/session (any state, in period) --- + user_entries: list[CalendarEntry] = [] + if user_id or session_id: + conditions_user = [ + CalendarEntry.calendar_id == calendar_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= period_start, + CalendarEntry.start_at < period_end, + ] + if user_id: + conditions_user.append(CalendarEntry.user_id == user_id) + elif session_id: + conditions_user.append(CalendarEntry.session_id == session_id) + + result_user = await sess.execute(select(CalendarEntry).where(*conditions_user)) + user_entries = result_user.scalars().all() + + # --- Confirmed entries for everyone in period --- + result_conf = await sess.execute( + select(CalendarEntry).where( + CalendarEntry.calendar_id == calendar_id, + CalendarEntry.state == "confirmed", + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= period_start, + CalendarEntry.start_at < period_end, + ) + ) + confirmed_entries = result_conf.scalars().all() + + # --- For admins: ordered + provisional for everyone in period --- + admin_other_entries: list[CalendarEntry] = [] + if is_admin: + result_admin = await sess.execute( + select(CalendarEntry).where( + CalendarEntry.calendar_id == calendar_id, + CalendarEntry.state.in_(("ordered", "provisional")), + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= period_start, + CalendarEntry.start_at < period_end, + ) + ) + admin_other_entries = result_admin.scalars().all() + + # --- Merge with de-duplication and keep chronological order --- + entries_by_id: dict[int, CalendarEntry] = {} + + # Everyone's confirmed + for e in confirmed_entries: + entries_by_id[e.id] = e + + # Admin-only: everyone's ordered/provisional + if is_admin: + for e in admin_other_entries: + entries_by_id[e.id] = e + + # Always include current user/session entries (includes their pending) + for e in user_entries: + entries_by_id[e.id] = e + + merged_entries = sorted( + entries_by_id.values(), + key=lambda e: e.start_at or period_start, + ) + + return VisibleEntries( + user_entries=user_entries, + confirmed_entries=confirmed_entries, + admin_other_entries=admin_other_entries, + merged_entries=merged_entries, + ) diff --git a/events/bp/calendar_entries/routes.py b/events/bp/calendar_entries/routes.py new file mode 100644 index 0000000..b4fdb31 --- /dev/null +++ b/events/bp/calendar_entries/routes.py @@ -0,0 +1,257 @@ +from __future__ import annotations +from datetime import datetime, timezone +from decimal import Decimal + +from quart import ( + request, render_template, render_template_string, make_response, + Blueprint, g, redirect, url_for, jsonify, +) + + +from sqlalchemy import update, func as sa_func + +from models.calendars import CalendarEntry + + +from .services.entries import ( + + add_entry as svc_add_entry, +) +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + + +from bp.calendar_entry.routes import register as register_calendar_entry + + +from models.calendars import CalendarSlot + +from sqlalchemy import select + + +def calculate_entry_cost(slot: CalendarSlot, start_at: datetime, end_at: datetime) -> Decimal: + """ + Calculate cost for an entry based on slot and time range. + - Fixed slot: use slot cost + - Flexible slot: prorate based on actual time vs slot time range + """ + if not slot.cost: + return Decimal('0') + + if not slot.flexible: + # Fixed slot: full cost + return Decimal(str(slot.cost)) + + # Flexible slot: calculate ratio + if not slot.time_end or not start_at or not end_at: + return Decimal('0') + + # Calculate durations in minutes + slot_start_minutes = slot.time_start.hour * 60 + slot.time_start.minute + slot_end_minutes = slot.time_end.hour * 60 + slot.time_end.minute + slot_duration = slot_end_minutes - slot_start_minutes + + actual_start_minutes = start_at.hour * 60 + start_at.minute + actual_end_minutes = end_at.hour * 60 + end_at.minute + actual_duration = actual_end_minutes - actual_start_minutes + + if slot_duration <= 0 or actual_duration <= 0: + return Decimal('0') + + ratio = Decimal(actual_duration) / Decimal(slot_duration) + return Decimal(str(slot.cost)) * ratio + + +def register(): + bp = Blueprint("calendar_entries", __name__, url_prefix='/entries') + + bp.register_blueprint( + register_calendar_entry() + ) + + @bp.post("/") + @clear_cache(tag="calendars", tag_scope="all") + async def add_entry(year: int, month: int, day: int, **kwargs): + form = await request.form + + def parse_time_to_dt(value: str | None, year: int, month: int, day: int): + if not value: + return None + try: + hour_str, minute_str = value.split(":", 1) + hour = int(hour_str) + minute = int(minute_str) + return datetime(year, month, day, hour, minute, tzinfo=timezone.utc) + except Exception: + return None + + name = (form.get("name") or "").strip() + start_at = parse_time_to_dt(form.get("start_time"), year, month, day) + end_at = parse_time_to_dt(form.get("end_time"), year, month, day) + + # NEW: slot_id + slot_id_raw = (form.get("slot_id") or "").strip() + slot_id = int(slot_id_raw) if slot_id_raw else None + + # Ticket configuration + ticket_price_str = (form.get("ticket_price") or "").strip() + ticket_price = None + if ticket_price_str: + try: + ticket_price = Decimal(ticket_price_str) + except Exception: + pass + + ticket_count_str = (form.get("ticket_count") or "").strip() + ticket_count = None + if ticket_count_str: + try: + ticket_count = int(ticket_count_str) + except Exception: + pass + + field_errors: dict[str, list[str]] = {} + + # Basic checks + if not name: + field_errors.setdefault("name", []).append("Please enter a name for the entry.") + + # Check slot first before validating times + slot = None + cost = Decimal('10') # default cost + + if slot_id is not None: + result = await g.s.execute( + select(CalendarSlot).where( + CalendarSlot.id == slot_id, + CalendarSlot.calendar_id == g.calendar.id, + CalendarSlot.deleted_at.is_(None), + ) + ) + slot = result.scalar_one_or_none() + if slot is None: + field_errors.setdefault("slot_id", []).append( + "Selected slot is no longer available." + ) + else: + # For inflexible slots, override the times with slot times + if not slot.flexible: + # Replace start/end with slot times + start_at = datetime(year, month, day, + slot.time_start.hour, + slot.time_start.minute, + tzinfo=timezone.utc) + if slot.time_end: + end_at = datetime(year, month, day, + slot.time_end.hour, + slot.time_end.minute, + tzinfo=timezone.utc) + else: + # Flexible: validate times are within slot band + # Only validate if times were provided + if not start_at: + field_errors.setdefault("start_time", []).append("Please select a start time.") + if end_at is None: + field_errors.setdefault("end_time", []).append("Please select an end time.") + + if start_at and end_at: + s_time = start_at.timetz() + e_time = end_at.timetz() + slot_start = slot.time_start + slot_end = slot.time_end + + if s_time.replace(tzinfo=None) < slot_start: + field_errors.setdefault("start_time", []).append( + f"Start time must be at or after {slot_start.strftime('%H:%M')}." + ) + if slot_end is not None and e_time.replace(tzinfo=None) > slot_end: + field_errors.setdefault("end_time", []).append( + f"End time must be at or before {slot_end.strftime('%H:%M')}." + ) + + # Calculate cost based on slot and times + if start_at and end_at: + cost = calculate_entry_cost(slot, start_at, end_at) + else: + field_errors.setdefault("slot_id", []).append( + "Please select a slot." + ) + + # Time ordering check (only if we have times) + if start_at and end_at and end_at < start_at: + field_errors.setdefault("end_time", []).append("End time must be after the start time.") + + if field_errors: + return jsonify( + { + "message": "Please fix the highlighted fields.", + "errors": field_errors, + } + ), 422 + + # Pass slot_id and calculated cost to the service + entry = await svc_add_entry( + g.s, + calendar_id=g.calendar.id, + name=name, + start_at=start_at, + end_at=end_at, + user_id=getattr(g, "user", None).id if getattr(g, "user", None) else None, + session_id=None, + slot_id=slot_id, + cost=cost, # Pass calculated cost + ) + + # Set ticket configuration + entry.ticket_price = ticket_price + entry.ticket_count = ticket_count + + # Count pending calendar entries from local session (sees the just-added entry) + user_id = getattr(g, "user", None) and g.user.id + cal_filters = [ + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "pending", + ] + if user_id: + cal_filters.append(CalendarEntry.user_id == user_id) + + cal_count = await g.s.scalar( + select(sa_func.count()).select_from(CalendarEntry).where(*cal_filters) + ) or 0 + + # Get product cart count via service (same DB, no HTTP needed) + from shared.infrastructure.cart_identity import current_cart_identity + from shared.services.registry import services + ident = current_cart_identity() + cart_summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + product_count = cart_summary.count + total_count = product_count + cal_count + + html = await render_template("_types/day/_main_panel.html") + mini_html = await render_template_string( + '{% from "_types/cart/_mini.html" import mini with context %}' + '{{ mini(oob="true") }}', + cart_count=total_count, + ) + return await make_response(html + mini_html, 200) + + @bp.get("/add/") + async def add_form(day: int, month: int, year: int, **kwargs): + html = await render_template( + "_types/day/_add.html", + ) + return await make_response(html) + + @bp.get("/add-button/") + async def add_button(day: int, month: int, year: int, **kwargs): + + html = await render_template( + "_types/day/_add_button.html", + ) + return await make_response(html) + + + + return bp diff --git a/events/bp/calendar_entries/services/entries.py b/events/bp/calendar_entries/services/entries.py new file mode 100644 index 0000000..c51345e --- /dev/null +++ b/events/bp/calendar_entries/services/entries.py @@ -0,0 +1,278 @@ +from __future__ import annotations +from datetime import datetime +from typing import Optional, Sequence +from decimal import Decimal + +from sqlalchemy import select, and_, or_ +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import Calendar, CalendarEntry + +from datetime import datetime + +from shared.browser.app.errors import AppError + +class CalendarError(AppError): + """Base error for calendar service operations.""" + status_code = 422 + + + +async def add_entry( + sess: AsyncSession, + calendar_id: int, + name: str, + start_at: Optional[datetime], + end_at: Optional[datetime], + user_id: int | None = None, + session_id: str | None = None, + slot_id: int | None = None, # NEW: accept slot_id + cost: Optional[Decimal] = None, # NEW: accept cost +) -> CalendarEntry: + """ + Add an entry to a calendar. + + Collects *all* validation errors and raises CalendarError([...]) + so the HTMX handler can show them as a list. + """ + errors: list[str] = [] + + # Normalise + name = (name or "").strip() + + # Name validation + if not name: + errors.append("Entry name must not be empty.") + + # start_at validation + if start_at is None: + errors.append("Start time is required.") + elif not isinstance(start_at, datetime): + errors.append("Start time is invalid.") + + # end_at validation + if end_at is not None and not isinstance(end_at, datetime): + errors.append("End time is invalid.") + + # Time ordering (only if we have sensible datetimes) + if isinstance(start_at, datetime) and isinstance(end_at, datetime): + if end_at < start_at: + errors.append("End time must be greater than or equal to the start time.") + + # If we have any validation errors, bail out now + if errors: + raise CalendarError(errors, status_code=422) + + # Calendar existence (this is more of a 404 than a validation issue) + cal = ( + await sess.execute( + select(Calendar).where( + Calendar.id == calendar_id, + Calendar.deleted_at.is_(None), + ) + ) + ).scalar_one_or_none() + + if not cal: + # Single-message CalendarError – still handled by the same error handler + raise CalendarError( + f"Calendar {calendar_id} does not exist or has been deleted.", + status_code=404, + ) + + # All good, create the entry + entry = CalendarEntry( + calendar_id=calendar_id, + name=name, + start_at=start_at, + end_at=end_at, + user_id=user_id, + session_id=session_id, + slot_id=slot_id, # NEW: save slot_id + state="pending", + cost=cost if cost is not None else Decimal('10'), # Use provided cost or default + ) + sess.add(entry) + await sess.flush() + + # Publish to federation inline + if entry.user_id: + from shared.services.federation_publish import try_publish + await try_publish( + sess, + user_id=entry.user_id, + activity_type="Create", + object_type="Event", + object_data={ + "name": entry.name or "", + "startTime": entry.start_at.isoformat() if entry.start_at else "", + "endTime": entry.end_at.isoformat() if entry.end_at else "", + }, + source_type="CalendarEntry", + source_id=entry.id, + ) + + return entry + + +async def list_entries( + sess: AsyncSession, + post_id: int, + calendar_slug: str, + from_: Optional[datetime] = None, + to: Optional[datetime] = None, +) -> Sequence[CalendarEntry]: + """ + List entries for a given post's calendar by name. + - Respects soft-deletes (only non-deleted calendar / entries). + - If a time window is provided, returns entries that overlap the window: + - If only from_ is given: entries where end_at is NULL or end_at >= from_ + - If only to is given: entries where start_at <= to + - If both given: entries where [start_at, end_at or +inf] overlaps [from_, to] + - Sorted by start_at ascending. + """ + calendar_slug = (calendar_slug or "").strip() + if not calendar_slug: + raise CalendarError("calendar_slug must not be empty.") + + cal = ( + await sess.execute( + select(Calendar.id) + .where( + Calendar.container_type == "page", + Calendar.container_id == post_id, + Calendar.slug == calendar_slug, + Calendar.deleted_at.is_(None), + ) + ) + ).scalar_one_or_none() + + if not cal: + # Return empty list instead of raising, so callers can treat absence as "no entries" + return [] + + # Base filter: not soft-deleted entries of this calendar + filters = [CalendarEntry.calendar_id == cal, CalendarEntry.deleted_at.is_(None)] + + # Time window logic + if from_ and to: + # Overlap condition: start <= to AND (end is NULL OR end >= from_) + filters.append(CalendarEntry.start_at <= to) + filters.append(or_(CalendarEntry.end_at.is_(None), CalendarEntry.end_at >= from_)) + elif from_: + # Anything that hasn't ended before from_ + filters.append(or_(CalendarEntry.end_at.is_(None), CalendarEntry.end_at >= from_)) + elif to: + # Anything that has started by 'to' + filters.append(CalendarEntry.start_at <= to) + + stmt = ( + select(CalendarEntry) + .where(and_(*filters)) + .order_by(CalendarEntry.start_at.asc(), CalendarEntry.id.asc()) + ) + + result = await sess.execute(stmt) + entries = list(result.scalars()) + + # Eagerly load slot relationships + for entry in entries: + await sess.refresh(entry, ['slot']) + + return entries + + +async def svc_update_entry( + sess: AsyncSession, + entry_id: int, + *, + name: str | None = None, + start_at: datetime | None = None, + end_at: datetime | None = None, + user_id: int | None = None, + session_id: str | None = None, + slot_id: int | None = None, # NEW: accept slot_id + cost: Decimal | None = None, # NEW: accept cost +) -> CalendarEntry: + """ + Update an existing CalendarEntry. + + - Performs the same validations as add_entry() + - Returns the updated CalendarEntry + - Raises CalendarError([...]) on validation issues + - Raises CalendarError(...) if entry does not exist + """ + + # Fetch entry + entry = ( + await sess.execute( + select(CalendarEntry).where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + ) + ) + ).scalar_one_or_none() + + if not entry: + raise CalendarError( + f"Entry {entry_id} does not exist or has been deleted.", + status_code=404, + ) + + errors: list[str] = [] + + # ----- Validation ----- # + + # Name validation only if updating it + if name is not None: + name = name.strip() + if not name: + errors.append("Entry name must not be empty.") + + # start_at type validation only if provided + if start_at is not None and not isinstance(start_at, datetime): + errors.append("Start time is invalid.") + + # end_at type validation + if end_at is not None and not isinstance(end_at, datetime): + errors.append("End time is invalid.") + + # Time ordering + effective_start = start_at if start_at is not None else entry.start_at + effective_end = end_at if end_at is not None else entry.end_at + + if isinstance(effective_start, datetime) and isinstance(effective_end, datetime): + if effective_end < effective_start: + errors.append("End time must be greater than or equal to the start time.") + + # Validation failures? + if errors: + raise CalendarError(errors, status_code=422) + + # ----- Apply Updates ----- # + + if name is not None: + entry.name = name + + if start_at is not None: + entry.start_at = start_at + + if end_at is not None: + entry.end_at = end_at + + if user_id is not None: + entry.user_id = user_id + + if session_id is not None: + entry.session_id = session_id + + if slot_id is not None: # NEW: update slot_id + entry.slot_id = slot_id + + if cost is not None: # NEW: update cost + entry.cost = cost + + entry.updated_at = datetime.utcnow() + + await sess.flush() + return entry \ No newline at end of file diff --git a/events/bp/calendar_entry/admin/routes.py b/events/bp/calendar_entry/admin/routes.py new file mode 100644 index 0000000..fb422a2 --- /dev/null +++ b/events/bp/calendar_entry/admin/routes.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from quart import ( + render_template, make_response, Blueprint +) + + +from shared.browser.app.authz import require_admin + + +def register(): + bp = Blueprint("admin", __name__, url_prefix='/admin') + + # ---------- Pages ---------- + @bp.get("/") + @require_admin + async def admin(entry_id: int, **kwargs): + from shared.browser.app.utils.htmx import is_htmx_request + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/entry/admin/index.html") + else: + html = await render_template("_types/entry/admin/_oob_elements.html") + + return await make_response(html) + return bp diff --git a/events/bp/calendar_entry/routes.py b/events/bp/calendar_entry/routes.py new file mode 100644 index 0000000..ab46095 --- /dev/null +++ b/events/bp/calendar_entry/routes.py @@ -0,0 +1,626 @@ +from __future__ import annotations + + +from sqlalchemy import select, update + +from models.calendars import CalendarEntry, CalendarSlot + +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + + +from sqlalchemy import select +from quart import ( + request, render_template, make_response, Blueprint, g, jsonify +) +from ..calendar_entries.services.entries import ( + svc_update_entry, + CalendarError, # <-- add this if you want to catch it explicitly +) +from .services.post_associations import ( + add_post_to_entry, + remove_post_from_entry, + get_entry_posts, + search_posts as svc_search_posts, +) +from datetime import datetime, timezone +import math +import logging + +from shared.infrastructure.fragments import fetch_fragment + +from ..ticket_types.routes import register as register_ticket_types + +from .admin.routes import register as register_admin + + +logger = logging.getLogger(__name__) + +def register(): + bp = Blueprint("calendar_entry", __name__, url_prefix='/') + + # Register tickets blueprint + bp.register_blueprint( + register_ticket_types() + ) + bp.register_blueprint( + register_admin() + ) + + @bp.before_request + async def load_entry(): + """Load the calendar entry from the URL parameter.""" + entry_id = request.view_args.get("entry_id") + if entry_id: + result = await g.s.execute( + select(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None) + ) + ) + g.entry = result.scalar_one_or_none() + + @bp.context_processor + async def inject_entry(): + """Make entry and date parameters available to all templates in this blueprint.""" + return { + "entry": getattr(g, "entry", None), + "year": request.view_args.get("year"), + "month": request.view_args.get("month"), + "day": request.view_args.get("day"), + } + + async def get_day_nav_oob(year: int, month: int, day: int): + """Helper to generate OOB update for day entries nav""" + from datetime import datetime, timezone, date, timedelta + from ..calendar.services import get_visible_entries_for_period + from quart import session as qsession + + # Get the calendar from g + calendar = getattr(g, "calendar", None) + if not calendar: + return "" + + # Build day date + try: + day_date = date(year, month, day) + except (ValueError, TypeError): + return "" + + # Period: this day only + period_start = datetime(year, month, day, tzinfo=timezone.utc) + period_end = period_start + timedelta(days=1) + + # Identity + user = getattr(g, "user", None) + session_id = qsession.get("calendar_sid") + + # Get confirmed entries for this day + visible = await get_visible_entries_for_period( + sess=g.s, + calendar_id=calendar.id, + period_start=period_start, + period_end=period_end, + user=user, + session_id=session_id, + ) + + # Render OOB template + nav_oob = await render_template( + "_types/day/admin/_nav_entries_oob.html", + confirmed_entries=visible.confirmed_entries, + post=g.post_data["post"], + calendar=calendar, + day_date=day_date, + ) + return nav_oob + + async def get_post_nav_oob(entry_id: int): + """Helper to generate OOB update for post entries nav when entry state changes""" + # Get the entry to find associated posts + entry = await g.s.scalar( + select(CalendarEntry).where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None) + ) + ) + if not entry: + return "" + + # Get all posts associated with this entry + from .services.post_associations import get_entry_posts + entry_posts = await get_entry_posts(g.s, entry_id) + + # Generate OOB updates for each post's nav + nav_oobs = [] + for post in entry_posts: + # Get associated entries for this post + from ..post.services.entry_associations import get_associated_entries + associated_entries = await get_associated_entries(g.s, post.id) + + # Load calendars for this post + from models.calendars import Calendar + calendars = ( + await g.s.execute( + select(Calendar) + .where(Calendar.container_type == "page", Calendar.container_id == post.id, Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + ).scalars().all() + + # Render OOB template for this post's nav + nav_oob = await render_template( + "_types/post/admin/_nav_entries_oob.html", + associated_entries=associated_entries, + calendars=calendars, + post=post, + ) + nav_oobs.append(nav_oob) + + return "".join(nav_oobs) + + @bp.context_processor + async def inject_root(): + from ..tickets.services.tickets import ( + get_available_ticket_count, + get_sold_ticket_count, + get_user_reserved_count, + ) + from shared.infrastructure.cart_identity import current_cart_identity + from sqlalchemy.orm import selectinload + + view_args = getattr(request, "view_args", {}) or {} + entry_id = view_args.get("entry_id") + calendar_entry = None + entry_posts = [] + ticket_remaining = None + ticket_sold_count = 0 + user_ticket_count = 0 + user_ticket_counts_by_type = {} + + stmt = ( + select(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + ) + .options(selectinload(CalendarEntry.ticket_types)) + ) + result = await g.s.execute(stmt) + calendar_entry = result.scalar_one_or_none() + + # Optional: also ensure it belongs to the current calendar, if g.calendar is set + if calendar_entry is not None and getattr(g, "calendar", None): + if calendar_entry.calendar_id != g.calendar.id: + calendar_entry = None + + # Refresh slot relationship if we have a valid entry + if calendar_entry is not None: + await g.s.refresh(calendar_entry, ['slot']) + # Fetch associated posts + entry_posts = await get_entry_posts(g.s, calendar_entry.id) + # Get ticket availability + ticket_remaining = await get_available_ticket_count(g.s, calendar_entry.id) + # Get sold count + ticket_sold_count = await get_sold_ticket_count(g.s, calendar_entry.id) + # Get current user's reserved count + ident = current_cart_identity() + user_ticket_count = await get_user_reserved_count( + g.s, calendar_entry.id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + # Per-type counts for multi-type entries + if calendar_entry.ticket_types: + for tt in calendar_entry.ticket_types: + if tt.deleted_at is None: + user_ticket_counts_by_type[tt.id] = await get_user_reserved_count( + g.s, calendar_entry.id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=tt.id, + ) + + # Fetch container nav from market (skip calendar — we're on a calendar page) + container_nav_html = "" + post_data = getattr(g, "post_data", None) + if post_data: + post_id = post_data["post"]["id"] + post_slug = post_data["post"]["slug"] + container_nav_html = await fetch_fragment("market", "container-nav", params={ + "container_type": "page", + "container_id": str(post_id), + "post_slug": post_slug, + }) + + return { + "entry": calendar_entry, + "entry_posts": entry_posts, + "ticket_remaining": ticket_remaining, + "ticket_sold_count": ticket_sold_count, + "user_ticket_count": user_ticket_count, + "user_ticket_counts_by_type": user_ticket_counts_by_type, + "container_nav_html": container_nav_html, + } + @bp.get("/") + @require_admin + async def get(entry_id: int, **rest): + from shared.browser.app.utils.htmx import is_htmx_request + + # Full template for both HTMX and normal requests + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/entry/index.html", + ) + else: + + html = await render_template( + "_types/entry/_oob_elements.html", + ) + + return await make_response(html, 200) + + @bp.get("/edit/") + @require_admin + async def get_edit(entry_id: int, **rest): + html = await render_template("_types/entry/_edit.html") + return await make_response(html, 200) + + @bp.put("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def put(year: int, month: int, day: int, entry_id: int, **rest): + form = await request.form + + def parse_time_to_dt(value: str | None, year: int, month: int, day: int): + """ + 'HH:MM' + (year, month, day) -> aware datetime in UTC. + Returns None if empty/invalid. + """ + if not value: + return None + try: + hour_str, minute_str = value.split(":", 1) + hour = int(hour_str) + minute = int(minute_str) + return datetime(year, month, day, hour, minute, tzinfo=timezone.utc) + except Exception: + return None + + name = (form.get("name") or "").strip() + start_at = parse_time_to_dt(form.get("start_at"), year, month, day) + end_at = parse_time_to_dt(form.get("end_at"), year, month, day) + + # NEW: slot_id + slot_id_raw = (form.get("slot_id") or "").strip() + slot_id = int(slot_id_raw) if slot_id_raw else None + + # Ticket configuration + ticket_price_str = (form.get("ticket_price") or "").strip() + ticket_price = None + if ticket_price_str: + try: + from decimal import Decimal + ticket_price = Decimal(ticket_price_str) + except Exception: + pass # Will be validated below if needed + + ticket_count_str = (form.get("ticket_count") or "").strip() + ticket_count = None + if ticket_count_str: + try: + ticket_count = int(ticket_count_str) + except Exception: + pass # Will be validated below if needed + + field_errors: dict[str, list[str]] = {} + + # --- Basic validation (slot-style) ------------------------- + + if not name: + field_errors.setdefault("name", []).append( + "Please enter a name for the entry." + ) + + # Check slot first before validating times + slot = None + if slot_id is not None: + result = await g.s.execute( + select(CalendarSlot).where( + CalendarSlot.id == slot_id, + CalendarSlot.calendar_id == g.calendar.id, + CalendarSlot.deleted_at.is_(None), + ) + ) + slot = result.scalar_one_or_none() + if slot is None: + field_errors.setdefault("slot_id", []).append( + "Selected slot is no longer available." + ) + else: + # For inflexible slots, override the times with slot times + if not slot.flexible: + # Replace start/end with slot times + start_at = datetime(year, month, day, + slot.time_start.hour, + slot.time_start.minute, + tzinfo=timezone.utc) + if slot.time_end: + end_at = datetime(year, month, day, + slot.time_end.hour, + slot.time_end.minute, + tzinfo=timezone.utc) + else: + # Flexible: validate times are within slot band + # Only validate if times were provided + if not start_at: + field_errors.setdefault("start_at", []).append( + "Please select a start time." + ) + if not end_at: + field_errors.setdefault("end_at", []).append( + "Please select an end time." + ) + + if start_at and end_at: + s_time = start_at.timetz() + e_time = end_at.timetz() + slot_start = slot.time_start + slot_end = slot.time_end + + if s_time.replace(tzinfo=None) < slot_start: + field_errors.setdefault("start_at", []).append( + f"Start time must be at or after {slot_start.strftime('%H:%M')}." + ) + if slot_end is not None and e_time.replace(tzinfo=None) > slot_end: + field_errors.setdefault("end_at", []).append( + f"End time must be at or before {slot_end.strftime('%H:%M')}." + ) + else: + field_errors.setdefault("slot_id", []).append( + "Please select a slot." + ) + + # Time ordering check (only if we have times and no slot override) + if start_at and end_at and end_at < start_at: + field_errors.setdefault("end_at", []).append( + "End time must be after the start time." + ) + + if field_errors: + return jsonify( + { + "message": "Please fix the highlighted fields.", + "errors": field_errors, + } + ), 422 + + # --- Service call & safety net for extra validation ------- + + try: + entry = await svc_update_entry( + g.s, + entry_id, + name=name, + start_at=start_at, + end_at=end_at, + slot_id=slot_id, # Pass slot_id to service + ) + + # Update ticket configuration + entry.ticket_price = ticket_price + entry.ticket_count = ticket_count + + except CalendarError as e: + # If the service still finds something wrong, surface it nicely. + msg = str(e) + return jsonify( + { + "message": "There was a problem updating the entry.", + "errors": {"__all__": [msg]}, + } + ), 422 + + # --- Success: re-render the entry block ------------------- + + # Get nav OOB update + nav_oob = await get_day_nav_oob(year, month, day) + + html = await render_template( + "_types/entry/index.html", + #entry=entry, + ) + return await make_response(html + nav_oob, 200) + + + @bp.post("/confirm/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def confirm_entry(entry_id: int, year: int, month: int, day: int, **rest): + await g.s.execute( + update(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "provisional", + ) + .values(state="confirmed") + ) + await g.s.flush() + + # Get nav OOB updates (both day and post navs) + day_nav_oob = await get_day_nav_oob(year, month, day) + post_nav_oob = await get_post_nav_oob(entry_id) + + # redirect back to calendar admin or order page as you prefer + html = await render_template("_types/entry/_optioned.html") + return await make_response(html + day_nav_oob + post_nav_oob, 200) + + @bp.post("/decline/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def decline_entry(entry_id: int, year: int, month: int, day: int, **rest): + await g.s.execute( + update(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "provisional", + ) + .values(state="declined") + ) + await g.s.flush() + + # Get nav OOB updates (both day and post navs) + day_nav_oob = await get_day_nav_oob(year, month, day) + post_nav_oob = await get_post_nav_oob(entry_id) + + # redirect back to calendar admin or order page as you prefer + html = await render_template("_types/entry/_optioned.html") + return await make_response(html + day_nav_oob + post_nav_oob, 200) + + @bp.post("/provisional/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def provisional_entry(entry_id: int, year: int, month: int, day: int, **rest): + await g.s.execute( + update(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "confirmed", + ) + .values(state="provisional") + ) + await g.s.flush() + + # Get nav OOB updates (both day and post navs) + day_nav_oob = await get_day_nav_oob(year, month, day) + post_nav_oob = await get_post_nav_oob(entry_id) + + # redirect back to calendar admin or order page as you prefer + html = await render_template("_types/entry/_optioned.html") + return await make_response(html + day_nav_oob + post_nav_oob, 200) + + @bp.post("/tickets/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def update_tickets(entry_id: int, **rest): + """Update ticket configuration for a calendar entry""" + from .services.ticket_operations import update_ticket_config + from decimal import Decimal + + form = await request.form + + # Parse ticket price + ticket_price_str = (form.get("ticket_price") or "").strip() + ticket_price = None + if ticket_price_str: + try: + ticket_price = Decimal(ticket_price_str) + except Exception: + return await make_response("Invalid ticket price", 400) + + # Parse ticket count + ticket_count_str = (form.get("ticket_count") or "").strip() + ticket_count = None + if ticket_count_str: + try: + ticket_count = int(ticket_count_str) + except Exception: + return await make_response("Invalid ticket count", 400) + + # Update ticket configuration + success, error = await update_ticket_config( + g.s, entry_id, ticket_price, ticket_count + ) + + if not success: + return await make_response(error, 400) + + await g.s.flush() + + # Return just the tickets fragment (targeted by hx-target="#entry-tickets-...") + html = await render_template("_types/entry/_tickets.html") + return await make_response(html, 200) + + @bp.get("/posts/search/") + @require_admin + async def search_posts(entry_id: int, **rest): + """Search for posts to associate with this entry""" + query = request.args.get("q", "").strip() + page = int(request.args.get("page", 1)) + per_page = 10 + + search_posts, total = await svc_search_posts(g.s, query, page, per_page) + total_pages = math.ceil(total / per_page) if total > 0 else 0 + + html = await render_template( + "_types/entry/_post_search_results.html", + search_posts=search_posts, + search_query=query, + page=page, + total_pages=total_pages, + ) + return await make_response(html, 200) + + @bp.post("/posts/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def add_post(entry_id: int, **rest): + """Add a post association to this entry""" + form = await request.form + post_id = form.get("post_id") + + if not post_id: + return await make_response("Post ID is required", 400) + + try: + post_id = int(post_id) + except ValueError: + return await make_response("Invalid post ID", 400) + + success, error = await add_post_to_entry(g.s, entry_id, post_id) + + if not success: + return await make_response(error, 400) + + await g.s.flush() + + # Reload entry_posts for nav update + entry_posts = await get_entry_posts(g.s, entry_id) + + # Return updated posts list + OOB nav update + html = await render_template("_types/entry/_posts.html") + nav_oob = await render_template( + "_types/entry/admin/_nav_posts_oob.html", + entry_posts=entry_posts, + ) + return await make_response(html + nav_oob, 200) + + @bp.delete("/posts//") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def remove_post(entry_id: int, post_id: int, **rest): + """Remove a post association from this entry""" + success, error = await remove_post_from_entry(g.s, entry_id, post_id) + + if not success: + return await make_response(error or "Association not found", 404) + + await g.s.flush() + + # Reload entry_posts for nav update + entry_posts = await get_entry_posts(g.s, entry_id) + + # Return updated posts list + OOB nav update + html = await render_template("_types/entry/_posts.html") + nav_oob = await render_template( + "_types/entry/admin/_nav_posts_oob.html", + entry_posts=entry_posts, + ) + return await make_response(html + nav_oob, 200) + + return bp diff --git a/events/bp/calendar_entry/services/post_associations.py b/events/bp/calendar_entry/services/post_associations.py new file mode 100644 index 0000000..d96cf7d --- /dev/null +++ b/events/bp/calendar_entry/services/post_associations.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from sqlalchemy.sql import func + +from models.calendars import CalendarEntry, CalendarEntryPost +from shared.services.registry import services + + +async def add_post_to_entry( + session: AsyncSession, + entry_id: int, + post_id: int +) -> tuple[bool, str | None]: + """ + Associate a post with a calendar entry. + Returns (success, error_message). + """ + # Check if entry exists + entry = await session.scalar( + select(CalendarEntry).where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None) + ) + ) + if not entry: + return False, "Calendar entry not found" + + # Check if post exists + post = await services.blog.get_post_by_id(session, post_id) + if not post: + return False, "Post not found" + + # Check if association already exists + existing = await session.scalar( + select(CalendarEntryPost).where( + CalendarEntryPost.entry_id == entry_id, + CalendarEntryPost.content_type == "post", + CalendarEntryPost.content_id == post_id, + CalendarEntryPost.deleted_at.is_(None) + ) + ) + + if existing: + return False, "Post is already associated with this entry" + + # Create association + association = CalendarEntryPost( + entry_id=entry_id, + content_type="post", + content_id=post_id + ) + session.add(association) + await session.flush() + + return True, None + + +async def remove_post_from_entry( + session: AsyncSession, + entry_id: int, + post_id: int +) -> tuple[bool, str | None]: + """ + Remove a post association from a calendar entry (soft delete). + Returns (success, error_message). + """ + # Find the association + association = await session.scalar( + select(CalendarEntryPost).where( + CalendarEntryPost.entry_id == entry_id, + CalendarEntryPost.content_type == "post", + CalendarEntryPost.content_id == post_id, + CalendarEntryPost.deleted_at.is_(None) + ) + ) + + if not association: + return False, "Association not found" + + # Soft delete + association.deleted_at = func.now() + await session.flush() + + return True, None + + +async def get_entry_posts( + session: AsyncSession, + entry_id: int +) -> list: + """ + Get all posts (as PostDTOs) associated with a calendar entry. + """ + result = await session.execute( + select(CalendarEntryPost.content_id).where( + CalendarEntryPost.entry_id == entry_id, + CalendarEntryPost.content_type == "post", + CalendarEntryPost.deleted_at.is_(None), + ) + ) + post_ids = list(result.scalars().all()) + if not post_ids: + return [] + posts = await services.blog.get_posts_by_ids(session, post_ids) + return sorted(posts, key=lambda p: (p.title or "")) + + +async def search_posts( + session: AsyncSession, + query: str, + page: int = 1, + per_page: int = 10 +) -> tuple[list, int]: + """ + Search for posts by title with pagination. + If query is empty, returns all posts in published order. + Returns (post_dtos, total_count). + """ + return await services.blog.search_posts(session, query, page, per_page) diff --git a/events/bp/calendar_entry/services/ticket_operations.py b/events/bp/calendar_entry/services/ticket_operations.py new file mode 100644 index 0000000..46fbdfb --- /dev/null +++ b/events/bp/calendar_entry/services/ticket_operations.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from typing import Optional +from decimal import Decimal + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import CalendarEntry + + + +async def update_ticket_config( + session: AsyncSession, + entry_id: int, + ticket_price: Optional[Decimal], + ticket_count: Optional[int], +) -> tuple[bool, Optional[str]]: + """ + Update ticket configuration for a calendar entry. + + Args: + session: Database session + entry_id: Calendar entry ID + ticket_price: Price per ticket (None = no tickets) + ticket_count: Total available tickets (None = unlimited) + + Returns: + (success, error_message) + """ + # Get the entry + entry = await session.scalar( + select(CalendarEntry).where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None) + ) + ) + + if not entry: + return False, "Calendar entry not found" + + # Validate inputs + if ticket_price is not None and ticket_price < 0: + return False, "Ticket price cannot be negative" + + if ticket_count is not None and ticket_count < 0: + return False, "Ticket count cannot be negative" + + # Update ticket configuration + entry.ticket_price = ticket_price + entry.ticket_count = ticket_count + + return True, None + + +async def get_available_tickets( + session: AsyncSession, + entry_id: int, +) -> tuple[Optional[int], Optional[str]]: + """ + Get the number of available tickets for a calendar entry. + + Returns: + (available_count, error_message) + - available_count is None if unlimited tickets + - available_count is the remaining count if limited + """ + entry = await session.scalar( + select(CalendarEntry).where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None) + ) + ) + + if not entry: + return None, "Calendar entry not found" + + # If no ticket configuration, return None (unlimited) + if entry.ticket_price is None: + return None, None + + # If ticket_count is None, unlimited tickets + if entry.ticket_count is None: + return None, None + + # Returns total count (booked tickets not yet subtracted) + return entry.ticket_count, None diff --git a/events/bp/calendars/routes.py b/events/bp/calendars/routes.py new file mode 100644 index 0000000..ebae1f7 --- /dev/null +++ b/events/bp/calendars/routes.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g +) +from sqlalchemy import select + +from models.calendars import Calendar + + +from .services.calendars import ( + create_calendar as svc_create_calendar, +) + +from ..calendar.routes import register as register_calendar + +from shared.browser.app.redis_cacher import cache_page, clear_cache + +from shared.browser.app.authz import require_admin +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("calendars", __name__, url_prefix='/calendars') + bp.register_blueprint( + register_calendar(), + ) + @bp.context_processor + async def inject_root(): + # Must always return a dict + return {} + + # ---------- Pages ---------- + + @bp.get("/") + @cache_page(tag="calendars") + async def home(**kwargs): + if not is_htmx_request(): + html = await render_template( + "_types/calendars/index.html", + ) + else: + html = await render_template( + "_types/calendars/_oob_elements.html", + ) + return await make_response(html) + + + @bp.post("/new/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def create_calendar(**kwargs): + form = await request.form + name = (form.get("name") or "").strip() + + # Get post_id from context if available (blog-embedded mode) + post_data = getattr(g, "post_data", None) + post_id = (post_data.get("post") or {}).get("id") if post_data else None + + if not post_id: + # Standalone mode: post_id from form (or None — calendar without post) + post_id = form.get("post_id") + if post_id: + post_id = int(post_id) + + try: + await svc_create_calendar(g.s, post_id, name) + except Exception as e: + return await make_response(f'
    {e}
    ', 422) + + html = await render_template( + "_types/calendars/index.html", + ) + + # Blog-embedded mode: also update post nav + if post_data: + from ..post.services.entry_associations import get_associated_entries + + cals = ( + await g.s.execute( + select(Calendar) + .where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + ).scalars().all() + + associated_entries = await get_associated_entries(g.s, post_id) + + nav_oob = await render_template( + "_types/post/admin/_nav_entries_oob.html", + associated_entries=associated_entries, + calendars=cals, + post=post_data["post"], + ) + + html = html + nav_oob + + return await make_response(html) + return bp diff --git a/events/bp/calendars/services/calendars.py b/events/bp/calendars/services/calendars.py new file mode 100644 index 0000000..2e8a94b --- /dev/null +++ b/events/bp/calendars/services/calendars.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import Calendar +from shared.services.registry import services +from shared.services.relationships import attach_child, detach_child +import unicodedata +import re + + +class CalendarError(ValueError): + """Base error for calendar service operations.""" + +from shared.browser.app.utils import ( + utcnow +) + +def slugify(value: str, max_len: int = 255) -> str: + """ + Make a URL-friendly slug: + - lowercase + - remove accents + - replace any non [a-z0-9]+ with '-' + - no forward slashes + - collapse multiple dashes + - trim leading/trailing dashes + """ + if value is None: + value = "" + # normalize accents -> ASCII + value = unicodedata.normalize("NFKD", value) + value = value.encode("ascii", "ignore").decode("ascii") + value = value.lower() + + # explicitly block forward slashes + value = value.replace("/", "-") + + # replace non-alnum with hyphen + value = re.sub(r"[^a-z0-9]+", "-", value) + # collapse multiple hyphens + value = re.sub(r"-{2,}", "-", value) + # trim hyphens and enforce length + value = value.strip("-")[:max_len].strip("-") + + # fallback if empty + return value or "calendar" + + +async def soft_delete(sess: AsyncSession, post_slug: str, calendar_slug: str) -> bool: + post = await services.blog.get_post_by_slug(sess, post_slug) + if not post: + return False + + cal = ( + await sess.execute( + select(Calendar).where( + Calendar.container_type == "page", + Calendar.container_id == post.id, + Calendar.slug == calendar_slug, + Calendar.deleted_at.is_(None), + ) + ) + ).scalar_one_or_none() + + if not cal: + return False + + cal.deleted_at = utcnow() + await sess.flush() + await detach_child(sess, "page", cal.container_id, "calendar", cal.id) + return True + +async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calendar: + """ + Create a calendar for a post. Name must be unique per post. + If a calendar with the same (post_id, name) exists but is soft-deleted, + it will be revived (deleted_at=None). + """ + name = (name or "").strip() + if not name: + raise CalendarError("Calendar name must not be empty.") + slug=slugify(name) + + # Ensure post exists (avoid silent FK errors in some DBs) + post = await services.blog.get_post_by_id(sess, post_id) + if not post: + raise CalendarError(f"Post {post_id} does not exist.") + + # Enforce: calendars can only be created on pages with the calendar feature + if not post.is_page: + raise CalendarError("Calendars can only be created on pages, not posts.") + + # Look for existing (including soft-deleted) + q = await sess.execute( + select(Calendar).where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.name == name) + ) + existing = q.scalar_one_or_none() + + if existing: + if existing.deleted_at is not None: + existing.deleted_at = None # revive + await sess.flush() + await attach_child(sess, "page", post_id, "calendar", existing.id) + return existing + raise CalendarError(f'Calendar with slug "{slug}" already exists for post {post_id}.') + + cal = Calendar(container_type="page", container_id=post_id, name=name, slug=slug) + sess.add(cal) + await sess.flush() + await attach_child(sess, "page", post_id, "calendar", cal.id) + return cal + + diff --git a/events/bp/day/admin/routes.py b/events/bp/day/admin/routes.py new file mode 100644 index 0000000..b14cfe7 --- /dev/null +++ b/events/bp/day/admin/routes.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from quart import ( + render_template, make_response, Blueprint +) + + +from shared.browser.app.authz import require_admin + + +def register(): + bp = Blueprint("admin", __name__, url_prefix='/admin') + + # ---------- Pages ---------- + @bp.get("/") + @require_admin + async def admin(year: int, month: int, day: int, **kwargs): + from shared.browser.app.utils.htmx import is_htmx_request + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/day/admin/index.html") + else: + html = await render_template("_types/day/admin/_oob_elements.html") + + return await make_response(html) + return bp diff --git a/events/bp/day/routes.py b/events/bp/day/routes.py new file mode 100644 index 0000000..7fbe550 --- /dev/null +++ b/events/bp/day/routes.py @@ -0,0 +1,154 @@ +from __future__ import annotations +from datetime import datetime, timezone, date, timedelta + +from quart import ( + request, render_template, make_response, Blueprint, g, abort, session as qsession +) + +from bp.calendar.services import get_visible_entries_for_period + +from bp.calendar_entries.routes import register as register_calendar_entries +from .admin.routes import register as register_admin + +from shared.browser.app.redis_cacher import cache_page +from shared.infrastructure.fragments import fetch_fragment + +from models.calendars import CalendarSlot # add this import + +from sqlalchemy import select + +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("day", __name__, url_prefix='/day///') + + bp.register_blueprint( + register_calendar_entries() + ) + bp.register_blueprint( + register_admin() + ) + + @bp.context_processor + async def inject_root(): + view_args = getattr(request, "view_args", {}) or {} + day = view_args.get("day") + month = view_args.get("month") + year = view_args.get("year") + + calendar = getattr(g, "calendar", None) + if not calendar: + return {} + + try: + day_date = date(year, month, day) + except (ValueError, TypeError): + return {} + + # Period: this day only + period_start = datetime(year, month, day, tzinfo=timezone.utc) + period_end = period_start + timedelta(days=1) + + # Identity & admin flag + user = getattr(g, "user", None) + session_id = qsession.get("calendar_sid") + + visible = await get_visible_entries_for_period( + sess=g.s, + calendar_id=calendar.id, + period_start=period_start, + period_end=period_end, + user=user, + session_id=session_id, + ) + + # --- NEW: slots for this weekday --- + weekday_attr = ["mon","tue","wed","thu","fri","sat","sun"][day_date.weekday()] + + stmt = ( + select(CalendarSlot) + .where( + CalendarSlot.calendar_id == calendar.id, + getattr(CalendarSlot, weekday_attr) == True, # noqa: E712 + CalendarSlot.deleted_at.is_(None), + ) + .order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc()) + ) + result = await g.s.execute(stmt) + day_slots = list(result.scalars()) + + # Fetch container nav from market (skip calendar — we're on a calendar page) + container_nav_html = "" + post_data = getattr(g, "post_data", None) + if post_data: + post_id = post_data["post"]["id"] + post_slug = post_data["post"]["slug"] + container_nav_html = await fetch_fragment("market", "container-nav", params={ + "container_type": "page", + "container_id": str(post_id), + "post_slug": post_slug, + }) + + return { + "qsession": qsession, + "day_date": day_date, + "day": day, + "year": year, + "month": month, + "day_entries": visible.merged_entries, + "user_entries": visible.user_entries, + "confirmed_entries": visible.confirmed_entries, + "day_slots": day_slots, + "container_nav_html": container_nav_html, + } + + + + @bp.get("/") + @cache_page(tag="calendars") + async def show_day(year: int, month: int, day: int, **kwargs): + """ + Show a detail view for a single calendar day. + + Visibility rules: + - Non-admin: + - all *confirmed* entries for that day (any user) + - all entries for current user/session (any state) for that day + (pending/ordered/provisional/confirmed) + - Admin: + - all confirmed + provisional + ordered entries for that day (all users) + - pending only for current user/session + """ + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/day/index.html", + ) + else: + + html = await render_template( + "_types/day/_oob_elements.html", + ) + return await make_response(html) + + @bp.get("/w//") + async def widget_paginate(widget_domain: str, **kwargs): + """Proxies paginated widget requests to the appropriate fragment provider.""" + page = int(request.args.get("page", 1)) + post_data = getattr(g, "post_data", None) + if not post_data: + abort(404) + post_id = post_data["post"]["id"] + post_slug = post_data["post"]["slug"] + + if widget_domain == "market": + html = await fetch_fragment("market", "container-nav", params={ + "container_type": "page", + "container_id": str(post_id), + "post_slug": post_slug, + }) + return await make_response(html or "") + abort(404) + + return bp diff --git a/events/bp/fragments/__init__.py b/events/bp/fragments/__init__.py new file mode 100644 index 0000000..a4af44b --- /dev/null +++ b/events/bp/fragments/__init__.py @@ -0,0 +1 @@ +from .routes import register as register_fragments diff --git a/events/bp/fragments/routes.py b/events/bp/fragments/routes.py new file mode 100644 index 0000000..293398a --- /dev/null +++ b/events/bp/fragments/routes.py @@ -0,0 +1,130 @@ +"""Events app fragment endpoints. + +Exposes HTML fragments at ``/internal/fragments/`` for consumption +by other coop apps via the fragment client. +""" + +from __future__ import annotations + +from quart import Blueprint, Response, g, render_template, request + +from shared.infrastructure.fragments import FRAGMENT_HEADER +from shared.services.registry import services + + +def register(): + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + _handlers: dict[str, object] = {} + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.get("/") + async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/html") + html = await handler() + return Response(html, status=200, content_type="text/html") + + # --- container-nav fragment: calendar entries + calendar links ----------- + + async def _container_nav_handler(): + container_type = request.args.get("container_type", "page") + container_id = int(request.args.get("container_id", 0)) + post_slug = request.args.get("post_slug", "") + paginate_url_base = request.args.get("paginate_url", "") + page = int(request.args.get("page", 1)) + exclude = request.args.get("exclude", "") + excludes = [e.strip() for e in exclude.split(",") if e.strip()] + + html_parts = [] + + # Calendar entries nav + if not any(e.startswith("calendar") for e in excludes): + entries, has_more = await services.calendar.associated_entries( + g.s, container_type, container_id, page, + ) + if entries: + html_parts.append(await render_template( + "fragments/container_nav_entries.html", + entries=entries, has_more=has_more, + page=page, post_slug=post_slug, + paginate_url_base=paginate_url_base, + )) + + # Calendar links nav + if not any(e.startswith("calendar") for e in excludes): + calendars = await services.calendar.calendars_for_container( + g.s, container_type, container_id, + ) + if calendars: + html_parts.append(await render_template( + "fragments/container_nav_calendars.html", + calendars=calendars, post_slug=post_slug, + )) + + return "\n".join(html_parts) + + _handlers["container-nav"] = _container_nav_handler + + # --- container-cards fragment: entries for blog listing cards ------------ + + async def _container_cards_handler(): + post_ids_raw = request.args.get("post_ids", "") + post_slugs_raw = request.args.get("post_slugs", "") + post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()] + post_slugs = [x.strip() for x in post_slugs_raw.split(",") if x.strip()] + if not post_ids: + return "" + + # Build post_id -> slug mapping + slug_map = {} + for i, pid in enumerate(post_ids): + slug_map[pid] = post_slugs[i] if i < len(post_slugs) else "" + + batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids) + return await render_template( + "fragments/container_cards_entries.html", + batch=batch, post_ids=post_ids, slug_map=slug_map, + ) + + _handlers["container-cards"] = _container_cards_handler + + # --- account-nav-item fragment: tickets + bookings links for account nav - + + async def _account_nav_item_handler(): + return await render_template("fragments/account_nav_items.html") + + _handlers["account-nav-item"] = _account_nav_item_handler + + # --- account-page fragment: tickets or bookings panel -------------------- + + async def _account_page_handler(): + slug = request.args.get("slug", "") + user_id = request.args.get("user_id", type=int) + if not user_id: + return "" + + if slug == "tickets": + tickets = await services.calendar.user_tickets(g.s, user_id=user_id) + return await render_template( + "fragments/account_page_tickets.html", + tickets=tickets, + ) + elif slug == "bookings": + bookings = await services.calendar.user_bookings(g.s, user_id=user_id) + return await render_template( + "fragments/account_page_bookings.html", + bookings=bookings, + ) + return "" + + _handlers["account-page"] = _account_page_handler + + bp._fragment_handlers = _handlers + + return bp diff --git a/events/bp/markets/__init__.py b/events/bp/markets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/markets/routes.py b/events/bp/markets/routes.py new file mode 100644 index 0000000..bac523f --- /dev/null +++ b/events/bp/markets/routes.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g +) + +from .services.markets import ( + create_market as svc_create_market, + soft_delete as svc_soft_delete, +) + +from shared.browser.app.redis_cacher import cache_page, clear_cache +from shared.browser.app.authz import require_admin +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("markets", __name__, url_prefix='/markets') + + @bp.context_processor + async def inject_root(): + return {} + + @bp.get("/") + async def home(**kwargs): + if not is_htmx_request(): + html = await render_template("_types/markets/index.html") + else: + html = await render_template("_types/markets/_oob_elements.html") + return await make_response(html) + + @bp.post("/new/") + @require_admin + async def create_market(**kwargs): + form = await request.form + name = (form.get("name") or "").strip() + + post_data = getattr(g, "post_data", None) + post_id = (post_data.get("post") or {}).get("id") if post_data else None + + if not post_id: + post_id = form.get("post_id") + if post_id: + post_id = int(post_id) + + try: + await svc_create_market(g.s, post_id, name) + except Exception as e: + return await make_response(f'
    {e}
    ', 422) + + html = await render_template("_types/markets/index.html") + return await make_response(html) + + @bp.delete("//") + @require_admin + async def delete_market(market_slug: str, **kwargs): + post_slug = getattr(g, "post_slug", None) + deleted = await svc_soft_delete(g.s, post_slug, market_slug) + if not deleted: + return await make_response("Market not found", 404) + + html = await render_template("_types/markets/index.html") + return await make_response(html) + + return bp diff --git a/events/bp/markets/services/__init__.py b/events/bp/markets/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/markets/services/markets.py b/events/bp/markets/services/markets.py new file mode 100644 index 0000000..7b0890a --- /dev/null +++ b/events/bp/markets/services/markets.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import re +import unicodedata + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.contracts.dtos import MarketPlaceDTO +from shared.services.registry import services + + +class MarketError(ValueError): + """Base error for market service operations.""" + + +def slugify(value: str, max_len: int = 255) -> str: + if value is None: + value = "" + value = unicodedata.normalize("NFKD", value) + value = value.encode("ascii", "ignore").decode("ascii") + value = value.lower() + value = value.replace("/", "-") + value = re.sub(r"[^a-z0-9]+", "-", value) + value = re.sub(r"-{2,}", "-", value) + value = value.strip("-")[:max_len].strip("-") + return value or "market" + + +async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPlaceDTO: + """ + Create a market for a page. Name must be unique per page. + If a market with the same (post_id, slug) exists but is soft-deleted, + it will be revived. + """ + name = (name or "").strip() + if not name: + raise MarketError("Market name must not be empty.") + slug = slugify(name) + + post = await services.blog.get_post_by_id(sess, post_id) + if not post: + raise MarketError(f"Post {post_id} does not exist.") + if not post.is_page: + raise MarketError("Markets can only be created on pages, not posts.") + + try: + return await services.market.create_marketplace(sess, "page", post_id, name, slug) + except ValueError as e: + raise MarketError(str(e)) from e + + +async def soft_delete(sess: AsyncSession, post_slug: str, market_slug: str) -> bool: + post = await services.blog.get_post_by_slug(sess, post_slug) + if not post: + return False + + return await services.market.soft_delete_marketplace(sess, "page", post.id, market_slug) diff --git a/events/bp/page/__init__.py b/events/bp/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/page/routes.py b/events/bp/page/routes.py new file mode 100644 index 0000000..da4fb74 --- /dev/null +++ b/events/bp/page/routes.py @@ -0,0 +1,129 @@ +""" +Page summary blueprint — shows upcoming events for a single page's calendars. + +Routes: + GET // — full page scoped to this page + GET //entries — HTMX fragment for infinite scroll + POST //tickets/adjust — adjust ticket quantity inline +""" +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, render_template_string, make_response + +from shared.browser.app.utils.htmx import is_htmx_request +from shared.infrastructure.cart_identity import current_cart_identity +from shared.services.registry import services + + +def register() -> Blueprint: + bp = Blueprint("page_summary", __name__) + + async def _load_entries(post_id, page, per_page=20): + """Load upcoming entries for this page + pending ticket counts.""" + entries, has_more = await services.calendar.upcoming_entries_for_container( + g.s, "page", post_id, page=page, per_page=per_page, + ) + + # Pending ticket counts keyed by entry_id + ident = current_cart_identity() + pending_tickets = {} + if entries: + tickets = await services.calendar.pending_tickets( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + for t in tickets: + if t.entry_id is not None: + pending_tickets[t.entry_id] = pending_tickets.get(t.entry_id, 0) + 1 + + return entries, has_more, pending_tickets + + @bp.get("/") + async def index(): + post = g.post_data["post"] + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets = await _load_entries(post["id"], page) + + ctx = dict( + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page_info={}, + page=page, + view=view, + ) + + if is_htmx_request(): + html = await render_template("_types/page_summary/_main_panel.html", **ctx) + else: + html = await render_template("_types/page_summary/index.html", **ctx) + + return await make_response(html, 200) + + @bp.get("/entries") + async def entries_fragment(): + post = g.post_data["post"] + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets = await _load_entries(post["id"], page) + + html = await render_template( + "_types/page_summary/_cards.html", + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page_info={}, + page=page, + view=view, + ) + return await make_response(html, 200) + + @bp.post("/tickets/adjust") + async def adjust_ticket(): + """Adjust ticket quantity, return updated widget + OOB cart-mini.""" + ident = current_cart_identity() + form = await request.form + entry_id = int(form.get("entry_id", 0)) + count = max(int(form.get("count", 0)), 0) + tt_raw = (form.get("ticket_type_id") or "").strip() + ticket_type_id = int(tt_raw) if tt_raw else None + + await services.calendar.adjust_ticket_quantity( + g.s, entry_id, count, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=ticket_type_id, + ) + + # Get updated ticket count for this entry + tickets = await services.calendar.pending_tickets( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + qty = sum(1 for t in tickets if t.entry_id == entry_id) + + # Load entry DTO for the widget template + entry = await services.calendar.entry_by_id(g.s, entry_id) + + # Updated cart count for OOB mini-cart + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + cart_count = summary.count + summary.calendar_count + summary.ticket_count + + # Render widget + OOB cart-mini + widget_html = await render_template( + "_types/page_summary/_ticket_widget.html", + entry=entry, + qty=qty, + ticket_url=f"/{g.post_slug}/tickets/adjust", + ) + mini_html = await render_template_string( + '{% from "_types/cart/_mini.html" import mini with context %}' + '{{ mini(oob="true") }}', + cart_count=cart_count, + ) + return await make_response(widget_html + mini_html, 200) + + return bp diff --git a/events/bp/payments/__init__.py b/events/bp/payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/payments/routes.py b/events/bp/payments/routes.py new file mode 100644 index 0000000..677bbc8 --- /dev/null +++ b/events/bp/payments/routes.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from quart import ( + render_template, make_response, Blueprint, g, request +) +from sqlalchemy import select + +from shared.models.page_config import PageConfig + +from shared.browser.app.authz import require_admin +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("payments", __name__, url_prefix='/payments') + + @bp.context_processor + async def inject_root(): + return {} + + async def _load_payment_ctx(): + """Load PageConfig SumUp data for the current page.""" + post = (getattr(g, "post_data", None) or {}).get("post", {}) + post_id = post.get("id") + if not post_id: + return {} + + pc = (await g.s.execute( + select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id) + )).scalar_one_or_none() + + return { + "sumup_configured": bool(pc and pc.sumup_api_key), + "sumup_merchant_code": (pc.sumup_merchant_code or "") if pc else "", + "sumup_checkout_prefix": (pc.sumup_checkout_prefix or "") if pc else "", + } + + @bp.get("/") + @require_admin + async def home(**kwargs): + ctx = await _load_payment_ctx() + if not is_htmx_request(): + html = await render_template("_types/payments/index.html", **ctx) + else: + html = await render_template("_types/payments/_oob_elements.html", **ctx) + return await make_response(html) + + @bp.put("/") + @require_admin + async def update_sumup(**kwargs): + """Update SumUp credentials for this page.""" + post = (getattr(g, "post_data", None) or {}).get("post", {}) + post_id = post.get("id") + if not post_id: + return await make_response("Post not found", 404) + + pc = (await g.s.execute( + select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id) + )).scalar_one_or_none() + if pc is None: + pc = PageConfig(container_type="page", container_id=post_id, features={}) + g.s.add(pc) + await g.s.flush() + + form = await request.form + merchant_code = (form.get("merchant_code") or "").strip() + api_key = (form.get("api_key") or "").strip() + checkout_prefix = (form.get("checkout_prefix") or "").strip() + + pc.sumup_merchant_code = merchant_code or None + pc.sumup_checkout_prefix = checkout_prefix or None + if api_key: + pc.sumup_api_key = api_key + + await g.s.flush() + + ctx = await _load_payment_ctx() + html = await render_template("_types/payments/_main_panel.html", **ctx) + return await make_response(html) + + return bp diff --git a/events/bp/slot/routes.py b/events/bp/slot/routes.py new file mode 100644 index 0000000..d3011fd --- /dev/null +++ b/events/bp/slot/routes.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g, jsonify +) +from sqlalchemy.exc import IntegrityError + + +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + +from .services.slot import ( + update_slot as svc_update_slot, + soft_delete_slot as svc_delete_slot, + get_slot as svc_get_slot, +) + +from ..slots.services.slots import ( + list_slots as svc_list_slots, +) + +from shared.browser.app.utils import ( + parse_time, + parse_cost +) +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("slot", __name__, url_prefix='/') + + # ---------- Pages ---------- + + @bp.get("/") + @require_admin + async def get(slot_id: int, **kwargs): + slot = await svc_get_slot(g.s, slot_id) + if not slot: + return await make_response("Not found", 404) + + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/slot/index.html", + slot=slot, + ) + else: + + html = await render_template( + "_types/slot/_oob_elements.html", + slot=slot, + ) + + return await make_response(html) + + + @bp.get("/edit/") + @require_admin + async def get_edit(slot_id: int, **kwargs): + slot = await svc_get_slot(g.s, slot_id) + if not slot: + return await make_response("Not found", 404) + html = await render_template( + "_types/slot/_edit.html", + slot=slot, + #post=g.post_data['post'], + #calendar=g.calendar, + ) + return await make_response(html) + + @bp.get("/view/") + @require_admin + async def get_view(slot_id: int, **kwargs): + slot = await svc_get_slot(g.s, slot_id) + if not slot: + return await make_response("Not found", 404) + html = await render_template( + "_types/slot/_main_panel.html", + slot=slot, + #post=g.post_data['post'], + #calendar=g.calendar, + ) + return await make_response(html) + + @bp.delete("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def slot_delete(slot_id: int, **kwargs): + await svc_delete_slot(g.s, slot_id) + slots = await svc_list_slots(g.s, g.calendar.id) + html = await render_template("_types/slots/_man_panel.html", calendar=g.calendar, slots=slots) + return await make_response(html) + + @bp.put("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def put(slot_id: int, **kwargs): + form = await request.form + + name = (form.get("name") or "").strip() + description = (form.get("description") or "").strip() or None + days = {k: form.get(k) for k in ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]} + time_start = parse_time(form.get("time_start")) + time_end = parse_time(form.get("time_end")) + cost = parse_cost(form.get("cost")) + + # NEW + flexible = bool(form.get("flexible")) + + field_errors: dict[str, list[str]] = {} + + # Basic validation... + if not name: + field_errors.setdefault("name", []).append("Please enter a name for the slot.") + + if not time_start: + field_errors.setdefault("time_start", []).append("Please select a start time.") + + if not time_end: + field_errors.setdefault("time_end", []).append("Please select an end time.") + + if time_start and time_end and time_end <= time_start: + field_errors.setdefault("time_end", []).append( + "End time must be after the start time." + ) + + if not any(form.get(d) for d in ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]): + field_errors.setdefault("days", []).append( + "Please select at least one day." + ) + + if field_errors: + return jsonify( + { + "message": "Please fix the highlighted fields.", + "errors": field_errors, + } + ), 422 + + # DB update + friendly duplicate handling + try: + slot = await svc_update_slot( + g.s, + slot_id, + name=name, + description=description, + days=days, + time_start=time_start, + time_end=time_end, + cost=cost, + flexible=flexible, # <--- NEW + ) + except IntegrityError as e: + msg = str(e.orig) if getattr(e, "orig", None) else str(e) + if "uq_calendar_slots_unique_band" in msg or "duplicate key value" in msg: + field_errors = { + "name": [f'A slot called “{name}” already exists on this calendar.'] + } + return jsonify( + { + "message": "That slot name is already in use.", + "errors": field_errors, + } + ), 422 + + return jsonify( + { + "message": "An unexpected error occurred while updating the slot.", + "errors": {"__all__": [msg]}, + } + ), 422 + + html = await render_template( + "_types/slot/_main_panel.html", + slot=slot, + oob=True, + ) + return await make_response(html) + + + + return bp diff --git a/events/bp/slot/services/slot.py b/events/bp/slot/services/slot.py new file mode 100644 index 0000000..169facd --- /dev/null +++ b/events/bp/slot/services/slot.py @@ -0,0 +1,91 @@ + +from __future__ import annotations +from datetime import time + +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import CalendarSlot + + +class SlotError(ValueError): + pass + +def _b(v): + if isinstance(v, bool): + return v + s = str(v).lower() + return s in {"1","true","t","yes","y","on"} + + +async def update_slot( + sess: AsyncSession, + slot_id: int, + *, + name: str | None = None, + description: str | None = None, + days: dict | None = None, + time_start: time | None = None, + time_end: time | None = None, + cost: float | None = None, + flexible: bool | None = None, # NEW +): + slot = await sess.get(CalendarSlot, slot_id) + if not slot or slot.deleted_at is not None: + raise SlotError("slot not found") + + if name is not None: + slot.name = name + + if description is not None: + slot.description = description or None + + if days is not None: + slot.mon = _b(days.get("mon", slot.mon)) + slot.tue = _b(days.get("tue", slot.tue)) + slot.wed = _b(days.get("wed", slot.wed)) + slot.thu = _b(days.get("thu", slot.thu)) + slot.fri = _b(days.get("fri", slot.fri)) + slot.sat = _b(days.get("sat", slot.sat)) + slot.sun = _b(days.get("sun", slot.sun)) + + if time_start is not None: + slot.time_start = time_start + if time_end is not None: + slot.time_end = time_end + + if (time_start or time_end) and slot.time_end <= slot.time_start: + raise SlotError("time range invalid") + + if cost is not None: + slot.cost = cost + + # NEW: update flexible flag only if explicitly provided + if flexible is not None: + slot.flexible = flexible + + await sess.flush() + return slot + +async def soft_delete_slot(sess: AsyncSession, slot_id: int): + slot = await sess.get(CalendarSlot, slot_id) + if not slot or slot.deleted_at is not None: + return + from datetime import datetime, timezone + slot.deleted_at = datetime.now(timezone.utc) + await sess.flush() + + +async def get_slot(sess: AsyncSession, slot_id: int) -> CalendarSlot | None: + return await sess.get(CalendarSlot, slot_id) + +async def update_slot_description( + sess: AsyncSession, + slot_id: int, + description: str | None, +) -> CalendarSlot: + slot = await sess.get(CalendarSlot, slot_id) + if not slot: + raise SlotError("slot not found") + slot.description = description or None + await sess.flush() + return slot diff --git a/events/bp/slots/routes.py b/events/bp/slots/routes.py new file mode 100644 index 0000000..cd655cb --- /dev/null +++ b/events/bp/slots/routes.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g, jsonify +) + +from sqlalchemy.exc import IntegrityError +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + +from .services.slots import ( + list_slots as svc_list_slots, + create_slot as svc_create_slot, +) + +from ..slot.routes import register as register_slot + +from shared.browser.app.utils import ( + parse_time, + parse_cost +) +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("slots", __name__, url_prefix='/slots') + + # ---------- Pages ---------- + + bp.register_blueprint( + register_slot() + ) + + + + @bp.context_processor + async def get_slots(): + calendar = getattr(g, "calendar", None) + if calendar: + return { + "slots": await svc_list_slots(g.s, calendar.id) + } + return {"slots": []} + + @bp.get("/") + async def get(**kwargs): + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/slots/index.html", + ) + else: + + html = await render_template( + "_types/slots/_oob_elements.html", + ) + return await make_response(html) + + + @bp.post("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def post(**kwargs): + form = await request.form + + name = (form.get("name") or "").strip() + description = (form.get("description") or "").strip() or None + days = {k: form.get(k) for k in ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]} + time_start = parse_time(form.get("time_start")) + time_end = parse_time(form.get("time_end")) + cost = parse_cost(form.get("cost")) + + # NEW: flexible flag from checkbox + flexible = bool(form.get("flexible")) + + field_errors: dict[str, list[str]] = {} + + if not name: + field_errors.setdefault("name", []).append("Please enter a name for the slot.") + + if not time_start: + field_errors.setdefault("time_start", []).append("Please select a start time.") + + if not time_end: + field_errors.setdefault("time_end", []).append("Please select an end time.") + + if time_start and time_end and time_end <= time_start: + field_errors.setdefault("time_end", []).append("End time must be after the start time.") + + if not any(form.get(d) for d in ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]): + field_errors.setdefault("days", []).append("Please select at least one day.") + + if field_errors: + return jsonify({ + "message": "Please fix the highlighted fields.", + "errors": field_errors, + }), 422 + + # DB insert with friendly duplicate detection + try: + await svc_create_slot( + g.s, + g.calendar.id, + name=name, + description=description, + days=days, + time_start=time_start, + time_end=time_end, + cost=cost, + flexible=flexible, # <<< NEW + ) + except IntegrityError as e: + # Improve duplicate detection: check constraint name or message + msg = str(e.orig) if getattr(e, "orig", None) else str(e) + if "uq_calendar_slots_unique_band" in msg or "duplicate key value" in msg: + field_errors = { + "name": [f"A slot called “{name}” already exists on this calendar."] + } + return jsonify({ + "message": "That slot name is already in use.", + "errors": field_errors, + }), 422 + + # Unknown DB error + return jsonify({ + "message": "An unexpected error occurred while saving the slot.", + "errors": {"__all__": [msg]}, + }), 422 + + # Success → re-render the slots table + html = await render_template("_types/slots/_main_panel.html") + return await make_response(html) + + + @bp.get("/add") + @require_admin + async def add_form(**kwargs): + html = await render_template( + "_types/slots/_add.html", + ) + return await make_response(html) + + @bp.get("/add-button") + @require_admin + async def add_button(**kwargs): + + html = await render_template( + "_types/slots/_add_button.html", + ) + return await make_response(html) + + return bp diff --git a/events/bp/slots/services/slots.py b/events/bp/slots/services/slots.py new file mode 100644 index 0000000..bd9827f --- /dev/null +++ b/events/bp/slots/services/slots.py @@ -0,0 +1,65 @@ + +from __future__ import annotations +from datetime import time +from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import CalendarSlot + + +class SlotError(ValueError): + pass + +def _b(v): + if isinstance(v, bool): + return v + s = str(v).lower() + return s in {"1","true","t","yes","y","on"} + +async def list_slots(sess: AsyncSession, calendar_id: int) -> Sequence[CalendarSlot]: + res = await sess.execute( + select(CalendarSlot) + .where(CalendarSlot.calendar_id == calendar_id, CalendarSlot.deleted_at.is_(None)) + .order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc()) + ) + return res.scalars().all() + +async def create_slot( + sess: AsyncSession, + calendar_id: int, + *, + name: str, + description: str | None, + days: dict, + time_start: time, + time_end: time, + cost: float | None, + flexible: bool = False, # NEW +): + if not name: + raise SlotError("name is required") + + if not time_start or not time_end or time_end <= time_start: + raise SlotError("time range invalid") + + slot = CalendarSlot( + calendar_id=calendar_id, + name=name, + description=(description or None), + mon=_b(days.get("mon")), + tue=_b(days.get("tue")), + wed=_b(days.get("wed")), + thu=_b(days.get("thu")), + fri=_b(days.get("fri")), + sat=_b(days.get("sat")), + sun=_b(days.get("sun")), + time_start=time_start, + time_end=time_end, + cost=cost, + flexible=flexible, # NEW + ) + sess.add(slot) + await sess.flush() + return slot diff --git a/events/bp/ticket_admin/__init__.py b/events/bp/ticket_admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/ticket_admin/routes.py b/events/bp/ticket_admin/routes.py new file mode 100644 index 0000000..3168a29 --- /dev/null +++ b/events/bp/ticket_admin/routes.py @@ -0,0 +1,166 @@ +""" +Ticket admin blueprint — check-in interface and ticket management. + +Routes: + GET /admin/tickets/ — Ticket dashboard (scan + list) + GET /admin/tickets/entry// — Tickets for a specific entry + POST /admin/tickets//checkin — Check in a ticket + GET /admin/tickets// — Ticket admin detail +""" +from __future__ import annotations + +import logging + +from quart import ( + Blueprint, g, request, render_template, make_response, jsonify, +) +from sqlalchemy import select, func +from sqlalchemy.orm import selectinload + +from models.calendars import CalendarEntry, Ticket, TicketType +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + +from ..tickets.services.tickets import ( + get_ticket_by_code, + get_tickets_for_entry, + checkin_ticket, +) + +logger = logging.getLogger(__name__) + + +def register() -> Blueprint: + bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets") + + @bp.get("/") + @require_admin + async def dashboard(): + """Ticket admin dashboard with QR scanner and recent tickets.""" + from shared.browser.app.utils.htmx import is_htmx_request + + # Get recent tickets + result = await g.s.execute( + select(Ticket) + .options( + selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), + selectinload(Ticket.ticket_type), + ) + .order_by(Ticket.created_at.desc()) + .limit(50) + ) + tickets = result.scalars().all() + + # Stats + total = await g.s.scalar(select(func.count(Ticket.id))) + confirmed = await g.s.scalar( + select(func.count(Ticket.id)).where(Ticket.state == "confirmed") + ) + checked_in = await g.s.scalar( + select(func.count(Ticket.id)).where(Ticket.state == "checked_in") + ) + reserved = await g.s.scalar( + select(func.count(Ticket.id)).where(Ticket.state == "reserved") + ) + + stats = { + "total": total or 0, + "confirmed": confirmed or 0, + "checked_in": checked_in or 0, + "reserved": reserved or 0, + } + + if not is_htmx_request(): + html = await render_template( + "_types/ticket_admin/index.html", + tickets=tickets, + stats=stats, + ) + else: + html = await render_template( + "_types/ticket_admin/_main_panel.html", + tickets=tickets, + stats=stats, + ) + + return await make_response(html, 200) + + @bp.get("/entry//") + @require_admin + async def entry_tickets(entry_id: int): + """List all tickets for a specific calendar entry.""" + from shared.browser.app.utils.htmx import is_htmx_request + + entry = await g.s.scalar( + select(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + ) + .options(selectinload(CalendarEntry.calendar)) + ) + if not entry: + return await make_response("Entry not found", 404) + + tickets = await get_tickets_for_entry(g.s, entry_id) + + html = await render_template( + "_types/ticket_admin/_entry_tickets.html", + entry=entry, + tickets=tickets, + ) + return await make_response(html, 200) + + @bp.get("/lookup/") + @require_admin + async def lookup(): + """Look up a ticket by code (used by scanner).""" + code = request.args.get("code", "").strip() + if not code: + return await make_response( + '
    Enter a ticket code
    ', + 200, + ) + + ticket = await get_ticket_by_code(g.s, code) + if not ticket: + html = await render_template( + "_types/ticket_admin/_lookup_result.html", + ticket=None, + error="Ticket not found", + ) + return await make_response(html, 200) + + html = await render_template( + "_types/ticket_admin/_lookup_result.html", + ticket=ticket, + error=None, + ) + return await make_response(html, 200) + + @bp.post("//checkin/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def do_checkin(code: str): + """Check in a ticket by its code.""" + success, error = await checkin_ticket(g.s, code) + + if not success: + html = await render_template( + "_types/ticket_admin/_checkin_result.html", + success=False, + error=error, + ticket=None, + ) + return await make_response(html, 200) + + ticket = await get_ticket_by_code(g.s, code) + html = await render_template( + "_types/ticket_admin/_checkin_result.html", + success=True, + error=None, + ticket=ticket, + ) + return await make_response(html, 200) + + return bp diff --git a/events/bp/ticket_admin/services/__init__.py b/events/bp/ticket_admin/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/ticket_type/routes.py b/events/bp/ticket_type/routes.py new file mode 100644 index 0000000..8f807b3 --- /dev/null +++ b/events/bp/ticket_type/routes.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g, jsonify +) + +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + +from .services.ticket import ( + get_ticket_type as svc_get_ticket_type, + update_ticket_type as svc_update_ticket_type, + soft_delete_ticket_type as svc_delete_ticket_type, +) + +from ..ticket_types.services.tickets import ( + list_ticket_types as svc_list_ticket_types, +) +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("ticket_type", __name__, url_prefix='/') + + @bp.get("/") + @require_admin + async def get(ticket_type_id: int, **kwargs): + """View a single ticket type.""" + ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) + if not ticket_type: + return await make_response("Not found", 404) + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/ticket_type/index.html", + ticket_type=ticket_type, + ) + else: + + html = await render_template( + "_types/ticket_type/_oob_elements.html", + ticket_type=ticket_type, + ) + + return await make_response(html) + + @bp.get("/edit/") + @require_admin + async def get_edit(ticket_type_id: int, **kwargs): + """Show the edit form for a ticket type.""" + ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) + if not ticket_type: + return await make_response("Not found", 404) + + html = await render_template( + "_types/ticket_type/_edit.html", + ticket_type=ticket_type, + ) + return await make_response(html) + + @bp.get("/view/") + @require_admin + async def get_view(ticket_type_id: int, **kwargs): + """Show the view for a ticket type.""" + ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) + if not ticket_type: + return await make_response("Not found", 404) + + html = await render_template( + "_types/ticket_type/_main_panel.html", + ticket_type=ticket_type, + ) + return await make_response(html) + + @bp.put("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def put(ticket_type_id: int, **kwargs): + """Update a ticket type.""" + form = await request.form + + name = (form.get("name") or "").strip() + cost_str = (form.get("cost") or "").strip() + count_str = (form.get("count") or "").strip() + + field_errors: dict[str, list[str]] = {} + + # Validate name + if not name: + field_errors.setdefault("name", []).append("Please enter a ticket type name.") + + # Validate cost + cost = None + if not cost_str: + field_errors.setdefault("cost", []).append("Please enter a cost.") + else: + try: + cost = float(cost_str) + if cost < 0: + field_errors.setdefault("cost", []).append("Cost must be positive.") + except ValueError: + field_errors.setdefault("cost", []).append("Please enter a valid number.") + + # Validate count + count = None + if not count_str: + field_errors.setdefault("count", []).append("Please enter a ticket count.") + else: + try: + count = int(count_str) + if count < 0: + field_errors.setdefault("count", []).append("Count must be positive.") + except ValueError: + field_errors.setdefault("count", []).append("Please enter a valid whole number.") + + if field_errors: + return jsonify({ + "message": "Please fix the highlighted fields.", + "errors": field_errors, + }), 422 + + # Update ticket type + ticket_type = await svc_update_ticket_type( + g.s, + ticket_type_id, + name=name, + cost=cost, + count=count, + ) + + if not ticket_type: + return await make_response("Not found", 404) + + # Return updated view with OOB flag + html = await render_template( + "_types/ticket_type/_main_panel.html", + ticket_type=ticket_type, + oob=True, + ) + return await make_response(html) + + @bp.delete("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def delete(ticket_type_id: int, **kwargs): + """Soft-delete a ticket type.""" + success = await svc_delete_ticket_type(g.s, ticket_type_id) + if not success: + return await make_response("Not found", 404) + + # Re-render the ticket types list + ticket_types = await svc_list_ticket_types(g.s, g.entry.id) + html = await render_template( + "_types/ticket_types/_main_panel.html", + ticket_types=ticket_types + ) + return await make_response(html) + + return bp diff --git a/events/bp/ticket_type/services/ticket.py b/events/bp/ticket_type/services/ticket.py new file mode 100644 index 0000000..b53a657 --- /dev/null +++ b/events/bp/ticket_type/services/ticket.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.calendars import TicketType + +from datetime import datetime, timezone + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +async def get_ticket_type(session: AsyncSession, ticket_type_id: int) -> TicketType | None: + """Get a single ticket type by ID (only if not soft-deleted).""" + result = await session.execute( + select(TicketType) + .where( + TicketType.id == ticket_type_id, + TicketType.deleted_at.is_(None) + ) + ) + return result.scalar_one_or_none() + + +async def update_ticket_type( + session: AsyncSession, + ticket_type_id: int, + *, + name: str, + cost: float, + count: int, +) -> TicketType | None: + """Update an existing ticket type.""" + ticket_type = await get_ticket_type(session, ticket_type_id) + if not ticket_type: + return None + + ticket_type.name = name + ticket_type.cost = cost + ticket_type.count = count + ticket_type.updated_at = utcnow() + + await session.flush() + return ticket_type + + +async def soft_delete_ticket_type(session: AsyncSession, ticket_type_id: int) -> bool: + """Soft-delete a ticket type.""" + ticket_type = await get_ticket_type(session, ticket_type_id) + if not ticket_type: + return False + + ticket_type.deleted_at = utcnow() + await session.flush() + return True diff --git a/events/bp/ticket_types/routes.py b/events/bp/ticket_types/routes.py new file mode 100644 index 0000000..0041eb1 --- /dev/null +++ b/events/bp/ticket_types/routes.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +from quart import ( + request, render_template, make_response, Blueprint, g, jsonify +) + +from shared.browser.app.authz import require_admin +from shared.browser.app.redis_cacher import clear_cache + +from .services.tickets import ( + list_ticket_types as svc_list_ticket_types, + create_ticket_type as svc_create_ticket_type, +) + +from ..ticket_type.routes import register as register_ticket_type + +from shared.browser.app.utils.htmx import is_htmx_request + + +def register(): + bp = Blueprint("ticket_types", __name__, url_prefix='/ticket-types') + + # Register individual ticket routes + bp.register_blueprint( + register_ticket_type() + ) + + @bp.context_processor + async def get_ticket_types(): + """Make ticket types available to all templates in this blueprint.""" + entry = getattr(g, "entry", None) + if entry: + return { + "ticket_types": await svc_list_ticket_types(g.s, entry.id) + } + return {"ticket_types": []} + + @bp.get("/") + async def get(**kwargs): + """List all ticket types for the current entry.""" + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template( + "_types/ticket_types/index.html", + ) + else: + + html = await render_template( + "_types/ticket_types/_oob_elements.html", + ) + + return await make_response(html) + + @bp.post("/") + @require_admin + @clear_cache(tag="calendars", tag_scope="all") + async def post(**kwargs): + """Create a new ticket type.""" + form = await request.form + + name = (form.get("name") or "").strip() + cost_str = (form.get("cost") or "").strip() + count_str = (form.get("count") or "").strip() + + field_errors: dict[str, list[str]] = {} + + # Validate name + if not name: + field_errors.setdefault("name", []).append("Please enter a ticket type name.") + + # Validate cost + cost = None + if not cost_str: + field_errors.setdefault("cost", []).append("Please enter a cost.") + else: + try: + cost = float(cost_str) + if cost < 0: + field_errors.setdefault("cost", []).append("Cost must be positive.") + except ValueError: + field_errors.setdefault("cost", []).append("Please enter a valid number.") + + # Validate count + count = None + if not count_str: + field_errors.setdefault("count", []).append("Please enter a ticket count.") + else: + try: + count = int(count_str) + if count < 0: + field_errors.setdefault("count", []).append("Count must be positive.") + except ValueError: + field_errors.setdefault("count", []).append("Please enter a valid whole number.") + + if field_errors: + return jsonify({ + "message": "Please fix the highlighted fields.", + "errors": field_errors, + }), 422 + + # Create ticket type + await svc_create_ticket_type( + g.s, + g.entry.id, + name=name, + cost=cost, + count=count, + ) + + # Success → re-render the ticket types table + html = await render_template("_types/ticket_types/_main_panel.html") + return await make_response(html) + + @bp.get("/add") + @require_admin + async def add_form(**kwargs): + """Show the add ticket type form.""" + html = await render_template( + "_types/ticket_types/_add.html", + ) + return await make_response(html) + + @bp.get("/add-button") + @require_admin + async def add_button(**kwargs): + """Show the add ticket type button.""" + html = await render_template( + "_types/ticket_types/_add_button.html", + ) + return await make_response(html) + + return bp diff --git a/events/bp/ticket_types/services/tickets.py b/events/bp/ticket_types/services/tickets.py new file mode 100644 index 0000000..0be361e --- /dev/null +++ b/events/bp/ticket_types/services/tickets.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from models.calendars import TicketType + +from datetime import datetime, timezone + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +async def list_ticket_types(session: AsyncSession, entry_id: int) -> list[TicketType]: + """Get all active ticket types for a calendar entry.""" + result = await session.execute( + select(TicketType) + .where( + TicketType.entry_id == entry_id, + TicketType.deleted_at.is_(None) + ) + .order_by(TicketType.name) + ) + return list(result.scalars().all()) + + +async def create_ticket_type( + session: AsyncSession, + entry_id: int, + *, + name: str, + cost: float, + count: int, +) -> TicketType: + """Create a new ticket type for a calendar entry.""" + ticket_type = TicketType( + entry_id=entry_id, + name=name, + cost=cost, + count=count, + created_at=utcnow(), + updated_at=utcnow(), + ) + session.add(ticket_type) + await session.flush() + return ticket_type diff --git a/events/bp/tickets/__init__.py b/events/bp/tickets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/tickets/routes.py b/events/bp/tickets/routes.py new file mode 100644 index 0000000..408eb06 --- /dev/null +++ b/events/bp/tickets/routes.py @@ -0,0 +1,308 @@ +""" +Tickets blueprint — user-facing ticket views and QR codes. + +Routes: + GET /tickets/ — My tickets list + GET /tickets// — Ticket detail with QR code + POST /tickets/buy/ — Purchase tickets for an entry + POST /tickets/adjust/ — Adjust ticket quantity (+/-) +""" +from __future__ import annotations + +import logging + +from quart import ( + Blueprint, g, request, render_template, make_response, +) +from sqlalchemy import select +from sqlalchemy.orm import selectinload + +from models.calendars import CalendarEntry +from shared.infrastructure.cart_identity import current_cart_identity +from shared.browser.app.redis_cacher import clear_cache + +from .services.tickets import ( + create_ticket, + get_ticket_by_code, + get_user_tickets, + get_available_ticket_count, + get_tickets_for_entry, + get_sold_ticket_count, + get_user_reserved_count, + cancel_latest_reserved_ticket, +) + +logger = logging.getLogger(__name__) + + +def register() -> Blueprint: + bp = Blueprint("tickets", __name__, url_prefix="/tickets") + + @bp.get("/") + async def my_tickets(): + """List all tickets for the current user/session.""" + from shared.browser.app.utils.htmx import is_htmx_request + + ident = current_cart_identity() + tickets = await get_user_tickets( + g.s, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + if not is_htmx_request(): + html = await render_template( + "_types/tickets/index.html", + tickets=tickets, + ) + else: + html = await render_template( + "_types/tickets/_main_panel.html", + tickets=tickets, + ) + + return await make_response(html, 200) + + @bp.get("//") + async def ticket_detail(code: str): + """View a single ticket with QR code.""" + from shared.browser.app.utils.htmx import is_htmx_request + + ticket = await get_ticket_by_code(g.s, code) + if not ticket: + return await make_response("Ticket not found", 404) + + # Verify ownership + ident = current_cart_identity() + if ident["user_id"] is not None: + if ticket.user_id != ident["user_id"]: + return await make_response("Ticket not found", 404) + elif ident["session_id"] is not None: + if ticket.session_id != ident["session_id"]: + return await make_response("Ticket not found", 404) + else: + return await make_response("Ticket not found", 404) + + if not is_htmx_request(): + html = await render_template( + "_types/tickets/detail.html", + ticket=ticket, + ) + else: + html = await render_template( + "_types/tickets/_detail_panel.html", + ticket=ticket, + ) + + return await make_response(html, 200) + + @bp.post("/buy/") + @clear_cache(tag="calendars", tag_scope="all") + async def buy_tickets(): + """ + Purchase tickets for a calendar entry. + Creates ticket records with state='reserved' (awaiting payment). + + Form fields: + entry_id — the calendar entry ID + ticket_type_id (optional) — specific ticket type + quantity — number of tickets (default 1) + """ + form = await request.form + + entry_id_raw = form.get("entry_id", "").strip() + if not entry_id_raw: + return await make_response("Entry ID required", 400) + + try: + entry_id = int(entry_id_raw) + except ValueError: + return await make_response("Invalid entry ID", 400) + + # Load entry + entry = await g.s.scalar( + select(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + ) + .options(selectinload(CalendarEntry.ticket_types)) + ) + if not entry: + return await make_response("Entry not found", 404) + + if entry.ticket_price is None: + return await make_response("Tickets not available for this entry", 400) + + # Check availability + available = await get_available_ticket_count(g.s, entry_id) + quantity = int(form.get("quantity", 1)) + if quantity < 1: + quantity = 1 + + if available is not None and quantity > available: + return await make_response( + f"Only {available} ticket(s) remaining", 400 + ) + + # Ticket type (optional) + ticket_type_id = None + tt_raw = form.get("ticket_type_id", "").strip() + if tt_raw: + try: + ticket_type_id = int(tt_raw) + except ValueError: + pass + + ident = current_cart_identity() + + # Create tickets + created = [] + for _ in range(quantity): + ticket = await create_ticket( + g.s, + entry_id=entry_id, + ticket_type_id=ticket_type_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + state="reserved", + ) + created.append(ticket) + + # Re-check availability for display + remaining = await get_available_ticket_count(g.s, entry_id) + all_tickets = await get_tickets_for_entry(g.s, entry_id) + + html = await render_template( + "_types/tickets/_buy_result.html", + entry=entry, + created_tickets=created, + remaining=remaining, + all_tickets=all_tickets, + ) + return await make_response(html, 200) + + @bp.post("/adjust/") + @clear_cache(tag="calendars", tag_scope="all") + async def adjust_quantity(): + """ + Adjust ticket quantity for a calendar entry (+/- pattern). + Creates or cancels tickets to reach the target count. + + Form fields: + entry_id — the calendar entry ID + ticket_type_id — (optional) specific ticket type + count — target quantity of reserved tickets + """ + form = await request.form + + entry_id_raw = form.get("entry_id", "").strip() + if not entry_id_raw: + return await make_response("Entry ID required", 400) + try: + entry_id = int(entry_id_raw) + except ValueError: + return await make_response("Invalid entry ID", 400) + + # Load entry + entry = await g.s.scalar( + select(CalendarEntry) + .where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + ) + .options(selectinload(CalendarEntry.ticket_types)) + ) + if not entry: + return await make_response("Entry not found", 404) + if entry.ticket_price is None: + return await make_response("Tickets not available for this entry", 400) + + # Ticket type (optional) + ticket_type_id = None + tt_raw = form.get("ticket_type_id", "").strip() + if tt_raw: + try: + ticket_type_id = int(tt_raw) + except ValueError: + pass + + target = max(int(form.get("count", 0)), 0) + ident = current_cart_identity() + + current = await get_user_reserved_count( + g.s, entry_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=ticket_type_id, + ) + + if target > current: + # Need to add tickets + to_add = target - current + available = await get_available_ticket_count(g.s, entry_id) + if available is not None and to_add > available: + return await make_response( + f"Only {available} ticket(s) remaining", 400 + ) + for _ in range(to_add): + await create_ticket( + g.s, + entry_id=entry_id, + ticket_type_id=ticket_type_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + state="reserved", + ) + elif target < current: + # Need to remove tickets + to_remove = current - target + for _ in range(to_remove): + await cancel_latest_reserved_ticket( + g.s, entry_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=ticket_type_id, + ) + + # Build context for re-rendering the buy form + ticket_remaining = await get_available_ticket_count(g.s, entry_id) + ticket_sold_count = await get_sold_ticket_count(g.s, entry_id) + user_ticket_count = await get_user_reserved_count( + g.s, entry_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + # Per-type counts for multi-type entries + user_ticket_counts_by_type = {} + if entry.ticket_types: + for tt in entry.ticket_types: + if tt.deleted_at is None: + user_ticket_counts_by_type[tt.id] = await get_user_reserved_count( + g.s, entry_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=tt.id, + ) + + # Compute cart count for OOB mini-cart update + from shared.services.registry import services + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + cart_count = summary.count + summary.calendar_count + summary.ticket_count + + html = await render_template( + "_types/tickets/_adjust_response.html", + entry=entry, + ticket_remaining=ticket_remaining, + ticket_sold_count=ticket_sold_count, + user_ticket_count=user_ticket_count, + user_ticket_counts_by_type=user_ticket_counts_by_type, + cart_count=cart_count, + ) + + return await make_response(html, 200) + + return bp diff --git a/events/bp/tickets/services/__init__.py b/events/bp/tickets/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/events/bp/tickets/services/tickets.py b/events/bp/tickets/services/tickets.py new file mode 100644 index 0000000..dab250c --- /dev/null +++ b/events/bp/tickets/services/tickets.py @@ -0,0 +1,313 @@ +""" +Ticket service layer — create, query, and manage tickets. +""" +from __future__ import annotations + +import uuid +from decimal import Decimal +from typing import Optional + +from sqlalchemy import select, update, func +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from models.calendars import Ticket, TicketType, CalendarEntry + + + +async def create_ticket( + session: AsyncSession, + *, + entry_id: int, + ticket_type_id: Optional[int] = None, + user_id: Optional[int] = None, + session_id: Optional[str] = None, + order_id: Optional[int] = None, + state: str = "reserved", +) -> Ticket: + """Create a single ticket with a unique code.""" + ticket = Ticket( + entry_id=entry_id, + ticket_type_id=ticket_type_id, + user_id=user_id, + session_id=session_id, + order_id=order_id, + code=uuid.uuid4().hex, + state=state, + ) + session.add(ticket) + await session.flush() + return ticket + + +async def create_tickets_for_order( + session: AsyncSession, + order_id: int, + user_id: Optional[int], + session_id: Optional[str], +) -> list[Ticket]: + """ + Create ticket records for all calendar entries in an order + that have ticket_price configured. + Called during checkout after calendar entries are transitioned to 'ordered'. + """ + # Find all ordered entries for this order that have ticket pricing + result = await session.execute( + select(CalendarEntry) + .where( + CalendarEntry.order_id == order_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.ticket_price.isnot(None), + ) + .options(selectinload(CalendarEntry.ticket_types)) + ) + entries = result.scalars().all() + + tickets = [] + for entry in entries: + if entry.ticket_types: + # Entry has specific ticket types — create one ticket per type + # (quantity handling can be added later) + for tt in entry.ticket_types: + if tt.deleted_at is None: + ticket = await create_ticket( + session, + entry_id=entry.id, + ticket_type_id=tt.id, + user_id=user_id, + session_id=session_id, + order_id=order_id, + state="reserved", + ) + tickets.append(ticket) + else: + # Simple ticket — one per entry + ticket = await create_ticket( + session, + entry_id=entry.id, + user_id=user_id, + session_id=session_id, + order_id=order_id, + state="reserved", + ) + tickets.append(ticket) + + return tickets + + +async def confirm_tickets_for_order( + session: AsyncSession, + order_id: int, +) -> int: + """ + Transition tickets from reserved → confirmed when payment succeeds. + Returns the number of tickets confirmed. + """ + result = await session.execute( + update(Ticket) + .where( + Ticket.order_id == order_id, + Ticket.state == "reserved", + ) + .values(state="confirmed") + ) + return result.rowcount + + +async def get_ticket_by_code( + session: AsyncSession, + code: str, +) -> Optional[Ticket]: + """Look up a ticket by its unique code.""" + result = await session.execute( + select(Ticket) + .where(Ticket.code == code) + .options( + selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), + selectinload(Ticket.ticket_type), + ) + ) + return result.scalar_one_or_none() + + +async def get_user_tickets( + session: AsyncSession, + user_id: Optional[int] = None, + session_id: Optional[str] = None, + state: Optional[str] = None, +) -> list[Ticket]: + """Get all tickets for a user or session.""" + filters = [] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return [] + + if state: + filters.append(Ticket.state == state) + else: + # Exclude cancelled by default + filters.append(Ticket.state != "cancelled") + + result = await session.execute( + select(Ticket) + .where(*filters) + .options( + selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), + selectinload(Ticket.ticket_type), + ) + .order_by(Ticket.created_at.desc()) + ) + return result.scalars().all() + + +async def get_tickets_for_entry( + session: AsyncSession, + entry_id: int, +) -> list[Ticket]: + """Get all non-cancelled tickets for a calendar entry.""" + result = await session.execute( + select(Ticket) + .where( + Ticket.entry_id == entry_id, + Ticket.state != "cancelled", + ) + .options( + selectinload(Ticket.ticket_type), + ) + .order_by(Ticket.created_at.asc()) + ) + return result.scalars().all() + + +async def get_sold_ticket_count( + session: AsyncSession, + entry_id: int, +) -> int: + """Count all non-cancelled tickets for an entry (total sold/reserved).""" + result = await session.scalar( + select(func.count(Ticket.id)).where( + Ticket.entry_id == entry_id, + Ticket.state != "cancelled", + ) + ) + return result or 0 + + +async def get_user_reserved_count( + session: AsyncSession, + entry_id: int, + user_id: Optional[int] = None, + session_id: Optional[str] = None, + ticket_type_id: Optional[int] = None, +) -> int: + """Count reserved tickets for a specific user/session + entry + optional type.""" + filters = [ + Ticket.entry_id == entry_id, + Ticket.state == "reserved", + ] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return 0 + if ticket_type_id is not None: + filters.append(Ticket.ticket_type_id == ticket_type_id) + result = await session.scalar( + select(func.count(Ticket.id)).where(*filters) + ) + return result or 0 + + +async def cancel_latest_reserved_ticket( + session: AsyncSession, + entry_id: int, + user_id: Optional[int] = None, + session_id: Optional[str] = None, + ticket_type_id: Optional[int] = None, +) -> bool: + """Cancel the most recently created reserved ticket. Returns True if one was cancelled.""" + filters = [ + Ticket.entry_id == entry_id, + Ticket.state == "reserved", + ] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return False + if ticket_type_id is not None: + filters.append(Ticket.ticket_type_id == ticket_type_id) + + ticket = await session.scalar( + select(Ticket) + .where(*filters) + .order_by(Ticket.created_at.desc()) + .limit(1) + ) + if ticket: + ticket.state = "cancelled" + await session.flush() + return True + return False + + +async def get_available_ticket_count( + session: AsyncSession, + entry_id: int, +) -> Optional[int]: + """ + Get number of remaining tickets for an entry. + Returns None if unlimited. + """ + entry = await session.scalar( + select(CalendarEntry).where( + CalendarEntry.id == entry_id, + CalendarEntry.deleted_at.is_(None), + ) + ) + if not entry or entry.ticket_price is None: + return None + if entry.ticket_count is None: + return None # Unlimited + + # Count non-cancelled tickets + sold = await session.scalar( + select(func.count(Ticket.id)).where( + Ticket.entry_id == entry_id, + Ticket.state != "cancelled", + ) + ) + return max(0, entry.ticket_count - (sold or 0)) + + +async def checkin_ticket( + session: AsyncSession, + code: str, +) -> tuple[bool, Optional[str]]: + """ + Check in a ticket by its code. + Returns (success, error_message). + """ + from datetime import datetime, timezone + + ticket = await get_ticket_by_code(session, code) + if not ticket: + return False, "Ticket not found" + + if ticket.state == "checked_in": + return False, "Ticket already checked in" + + if ticket.state == "cancelled": + return False, "Ticket is cancelled" + + if ticket.state not in ("confirmed", "reserved"): + return False, f"Ticket in unexpected state: {ticket.state}" + + ticket.state = "checked_in" + ticket.checked_in_at = datetime.now(timezone.utc) + return True, None diff --git a/events/config/app-config.yaml b/events/config/app-config.yaml new file mode 100644 index 0000000..3aa6a76 --- /dev/null +++ b/events/config/app-config.yaml @@ -0,0 +1,84 @@ +# App-wide settings +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: Rose Ash +market_root: /market +market_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + blog: "http://localhost:8000" + market: "http://localhost:8001" + cart: "http://localhost:8002" + events: "http://localhost:8003" + federation: "http://localhost:8004" +cache: + fs_root: _snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/wines + - branded-goods/ciders + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + - ---just-lem-just-wholefoods-jelly-crystals-lemon-12-x-85g-vf067-2-html + product-details: + - General Information + - A Note About Prices + +# SumUp payment settings (fill these in for live usage) +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + checkout_reference_prefix: 'dev-' + diff --git a/events/entrypoint.sh b/events/entrypoint.sh new file mode 100644 index 0000000..9d2720e --- /dev/null +++ b/events/entrypoint.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional: wait for Postgres to be reachable +if [[ -n "${DATABASE_HOST:-}" && -n "${DATABASE_PORT:-}" ]]; then + echo "Waiting for Postgres at ${DATABASE_HOST}:${DATABASE_PORT}..." + for i in {1..60}; do + (echo > /dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}) >/dev/null 2>&1 && break || true + sleep 1 + done +fi + +# NOTE: Events app does NOT run Alembic migrations. +# Migrations are managed by the blog app which owns the shared database schema. + +# Clear Redis page cache on deploy +if [[ -n "${REDIS_URL:-}" && "${REDIS_URL}" != "no" ]]; then + echo "Flushing Redis cache..." + python3 -c " +import redis, os +r = redis.from_url(os.environ['REDIS_URL']) +r.flushall() +print('Redis cache cleared.') +" || echo "Redis flush failed (non-fatal), continuing..." +fi + +# Start the app +echo "Starting Hypercorn (${APP_MODULE:-app:app})..." +PYTHONUNBUFFERED=1 exec hypercorn "${APP_MODULE:-app:app}" --bind 0.0.0.0:${PORT:-8000} diff --git a/events/models/__init__.py b/events/models/__init__.py new file mode 100644 index 0000000..4006b10 --- /dev/null +++ b/events/models/__init__.py @@ -0,0 +1,4 @@ +from .calendars import ( + Calendar, CalendarEntry, CalendarSlot, + TicketType, Ticket, CalendarEntryPost, +) diff --git a/events/models/calendars.py b/events/models/calendars.py new file mode 100644 index 0000000..02025ff --- /dev/null +++ b/events/models/calendars.py @@ -0,0 +1,4 @@ +from shared.models.calendars import ( # noqa: F401 + Calendar, CalendarEntry, CalendarSlot, + TicketType, Ticket, CalendarEntryPost, +) diff --git a/events/path_setup.py b/events/path_setup.py new file mode 100644 index 0000000..c7166f7 --- /dev/null +++ b/events/path_setup.py @@ -0,0 +1,9 @@ +import sys +import os + +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p) diff --git a/events/services/__init__.py b/events/services/__init__.py new file mode 100644 index 0000000..e7ddf54 --- /dev/null +++ b/events/services/__init__.py @@ -0,0 +1,29 @@ +"""Events app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the events app. + + Events owns: Calendar, CalendarEntry, CalendarSlot, TicketType, + Ticket, CalendarEntryPost. + Standard deployment registers all 4 services as real DB impls + (shared DB). For composable deployments, swap non-owned services + with stubs from shared.services.stubs. + """ + from shared.services.registry import services + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + services.calendar = SqlCalendarService() + if not services.has("blog"): + services.blog = SqlBlogService() + if not services.has("market"): + services.market = SqlMarketService() + if not services.has("cart"): + services.cart = SqlCartService() + if not services.has("federation"): + from shared.services.federation_impl import SqlFederationService + services.federation = SqlFederationService() diff --git a/events/templates/_types/all_events/_card.html b/events/templates/_types/all_events/_card.html new file mode 100644 index 0000000..0005563 --- /dev/null +++ b/events/templates/_types/all_events/_card.html @@ -0,0 +1,62 @@ +{# List card for all events — one entry #} +{% set pi = page_info.get(entry.calendar_container_id, {}) %} +{% set page_slug = pi.get('slug', '') %} +{% set page_title = pi.get('title') %} +
    +
    + {# Left: event info #} +
    + {% if page_slug %} + {% set day_href = events_url('/' ~ page_slug ~ '/calendars/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %} + {% else %} + {% set day_href = '' %} + {% endif %} + {% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' if day_href else '' %} + {% if entry_href %} + +

    {{ entry.name }}

    +
    + {% else %} +

    {{ entry.name }}

    + {% endif %} + +
    + {% if page_title %} + + {{ page_title }} + + {% endif %} + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} +
    + +
    + {% if day_href %} + {{ entry.start_at.strftime('%a %-d %b') }} · + {% else %} + {{ entry.start_at.strftime('%a %-d %b') }} · + {% endif %} + {{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    + + {% if entry.cost %} +
    + £{{ '%.2f'|format(entry.cost) }} +
    + {% endif %} +
    + + {# Right: ticket widget #} + {% if entry.ticket_price is not none %} +
    + {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = url_for('all_events.adjust_ticket') %} + {% include '_types/page_summary/_ticket_widget.html' %} +
    + {% endif %} +
    +
    diff --git a/events/templates/_types/all_events/_card_tile.html b/events/templates/_types/all_events/_card_tile.html new file mode 100644 index 0000000..3f8855f --- /dev/null +++ b/events/templates/_types/all_events/_card_tile.html @@ -0,0 +1,60 @@ +{# Tile card for all events — compact event tile #} +{% set pi = page_info.get(entry.calendar_container_id, {}) %} +{% set page_slug = pi.get('slug', '') %} +{% set page_title = pi.get('title') %} +
    + {% if page_slug %} + {% set day_href = events_url('/' ~ page_slug ~ '/calendars/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %} + {% else %} + {% set day_href = '' %} + {% endif %} + {% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' if day_href else '' %} +
    + {% if entry_href %} + +

    {{ entry.name }}

    +
    + {% else %} +

    {{ entry.name }}

    + {% endif %} + +
    + {% if page_title %} + + {{ page_title }} + + {% endif %} + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} +
    + +
    + {% if day_href %} + {{ entry.start_at.strftime('%a %-d %b') }} + {% else %} + {{ entry.start_at.strftime('%a %-d %b') }} + {% endif %} + · + {{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    + + {% if entry.cost %} +
    + £{{ '%.2f'|format(entry.cost) }} +
    + {% endif %} +
    + + {# Ticket widget below card #} + {% if entry.ticket_price is not none %} +
    + {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = url_for('all_events.adjust_ticket') %} + {% include '_types/page_summary/_ticket_widget.html' %} +
    + {% endif %} +
    diff --git a/events/templates/_types/all_events/_cards.html b/events/templates/_types/all_events/_cards.html new file mode 100644 index 0000000..0e3c6b8 --- /dev/null +++ b/events/templates/_types/all_events/_cards.html @@ -0,0 +1,31 @@ +{% for entry in entries %} + {% if view == 'tile' %} + {% include "_types/all_events/_card_tile.html" %} + {% else %} + {# Date header when date changes (list view only) #} + {% set entry_date = entry.start_at.strftime('%A %-d %B %Y') %} + {% if loop.first or entry_date != entries[loop.index0 - 1].start_at.strftime('%A %-d %B %Y') %} +
    +

    + {{ entry_date }} +

    +
    + {% endif %} + {% include "_types/all_events/_card.html" %} + {% endif %} +{% endfor %} +{% if has_more %} + {# Infinite scroll sentinel #} + {% set entries_url = url_for('all_events.entries_fragment', page=page + 1, view=view if view != 'list' else '')|host %} + +{% endif %} diff --git a/events/templates/_types/all_events/_main_panel.html b/events/templates/_types/all_events/_main_panel.html new file mode 100644 index 0000000..0130973 --- /dev/null +++ b/events/templates/_types/all_events/_main_panel.html @@ -0,0 +1,54 @@ +{# View toggle bar - desktop only #} + + +{# Cards container - list or grid based on view #} +{% if entries %} + {% if view == 'tile' %} +
    + {% include "_types/all_events/_cards.html" %} +
    + {% else %} +
    + {% include "_types/all_events/_cards.html" %} +
    + {% endif %} +{% else %} +
    + +

    No upcoming events

    +
    +{% endif %} +
    diff --git a/events/templates/_types/all_events/index.html b/events/templates/_types/all_events/index.html new file mode 100644 index 0000000..00a9696 --- /dev/null +++ b/events/templates/_types/all_events/index.html @@ -0,0 +1,7 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block content %} + {% include '_types/all_events/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/calendar/_description.html b/events/templates/_types/calendar/_description.html new file mode 100644 index 0000000..0f04f3a --- /dev/null +++ b/events/templates/_types/calendar/_description.html @@ -0,0 +1,12 @@ +{% macro description(calendar, oob=False) %} +
    + {{ calendar.description or ''}} +
    + +{% endmacro %} \ No newline at end of file diff --git a/events/templates/_types/calendar/_main_panel.html b/events/templates/_types/calendar/_main_panel.html new file mode 100644 index 0000000..7c0ffde --- /dev/null +++ b/events/templates/_types/calendar/_main_panel.html @@ -0,0 +1,170 @@ +
    +
    + + {# Month / year navigation #} + +
    + + {# Calendar grid #} +
    + {# Weekday header: only show on sm+ (desktop/tablet) #} + + + {# On mobile: 1 column; on sm+: 7 columns #} +
    + {% for week in weeks %} + {% for day in week %} +
    +
    +
    + + {{ day.date.strftime('%a') }} + + + {# Clickable day number: goes to day detail view #} + + {{ day.date.day }} + +
    +
    + {# Entries for this day: merged, chronological #} +
    + {# Build a list of entries for this specific day. + month_entries is already sorted by start_at in Python. #} + {% for e in month_entries %} + {% if e.start_at.date() == day.date %} + {# Decide colour: highlight "mine" differently if you want #} + {% set is_mine = (g.user and e.user_id == g.user.id) + or (not g.user and e.session_id == qsession.get('calendar_sid')) %} +
    + + {{ e.name }} + + + {{ (e.state or 'pending')|replace('_', ' ') }} + +
    + {% endif %} + {% endfor %} + +
    +
    + {% endfor %} + {% endfor %} +
    +
    diff --git a/events/templates/_types/calendar/_nav.html b/events/templates/_types/calendar/_nav.html new file mode 100644 index 0000000..d3ef2cd --- /dev/null +++ b/events/templates/_types/calendar/_nav.html @@ -0,0 +1,18 @@ + +{% import 'macros/links.html' as links %} +{% call links.link( + url_for('calendars.calendar.slots.get', calendar_slug=calendar.slug), + hx_select_search, + select_colours, + True, + aclass=styles.nav_button +) %} + +
    + Slots +
    +{% endcall %} +{% if g.rights.admin %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{ admin_nav_item(url_for('calendars.calendar.admin.admin', calendar_slug=calendar.slug)) }} +{% endif %} \ No newline at end of file diff --git a/events/templates/_types/calendar/_oob_elements.html b/events/templates/_types/calendar/_oob_elements.html new file mode 100644 index 0000000..1447e24 --- /dev/null +++ b/events/templates/_types/calendar/_oob_elements.html @@ -0,0 +1,22 @@ +{% extends "oob_elements.html" %} +{# OOB elements for post admin page #} + + + + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-header-child', 'calendar-header-child', '_types/calendar/header/_header.html')}} + + {% from '_types/post/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} +{% include '_types/calendar/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/calendar/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/calendar/admin/_description.html b/events/templates/_types/calendar/admin/_description.html new file mode 100644 index 0000000..46d99cb --- /dev/null +++ b/events/templates/_types/calendar/admin/_description.html @@ -0,0 +1,32 @@ +
    + {% if calendar.description %} +

    + {{ calendar.description }} +

    + {% else %} +

    + No description yet. +

    + {% endif %} + + +
    + +{% if oob %} + + {% from '_types/calendar/_description.html' import description %} + {{description(calendar, oob=True)}} +{% endif %} + + diff --git a/events/templates/_types/calendar/admin/_description_edit.html b/events/templates/_types/calendar/admin/_description_edit.html new file mode 100644 index 0000000..4ab7a7b --- /dev/null +++ b/events/templates/_types/calendar/admin/_description_edit.html @@ -0,0 +1,41 @@ +
    +
    + + + + +
    + + + +
    +
    +
    diff --git a/events/templates/_types/calendar/admin/_main_panel.html b/events/templates/_types/calendar/admin/_main_panel.html new file mode 100644 index 0000000..9c3e1a6 --- /dev/null +++ b/events/templates/_types/calendar/admin/_main_panel.html @@ -0,0 +1,45 @@ + +
    + +
    +

    Calendar configuration

    +
    +
    + + {% include '_types/calendar/admin/_description.html' %} +
    + + +
    + +
    + +
    diff --git a/events/templates/_types/calendar/admin/_nav.html b/events/templates/_types/calendar/admin/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/calendar/admin/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/calendar/admin/_oob_elements.html b/events/templates/_types/calendar/admin/_oob_elements.html new file mode 100644 index 0000000..ec6244c --- /dev/null +++ b/events/templates/_types/calendar/admin/_oob_elements.html @@ -0,0 +1,25 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for calendar admin page #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('calendar-header-child', 'calendar-admin-header-child', '_types/calendar/admin/header/_header.html')}} + + {% from '_types/calendar/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/calendar/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/calendar/admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/calendar/admin/header/_header.html b/events/templates/_types/calendar/admin/header/_header.html new file mode 100644 index 0000000..a138229 --- /dev/null +++ b/events/templates/_types/calendar/admin/header/_header.html @@ -0,0 +1,13 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='calendar-admin-row', oob=oob) %} + {% call links.link( + hx_select_search + ) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/calendar/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/events/templates/_types/calendar/admin/index.html b/events/templates/_types/calendar/admin/index.html new file mode 100644 index 0000000..c27d6d2 --- /dev/null +++ b/events/templates/_types/calendar/admin/index.html @@ -0,0 +1,24 @@ +{% extends '_types/calendar/index.html' %} +{% import 'macros/layout.html' as layout %} + +{% block calendar_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/calendar/admin/header/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block calendar_admin_header_child %} + {% endblock %} +
    + {% endcall %} +{% endblock %} + + + +{% block _main_mobile_menu %} + {% include '_types/calendar/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/calendar/admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/calendar/header/_header.html b/events/templates/_types/calendar/header/_header.html new file mode 100644 index 0000000..2f4ecf0 --- /dev/null +++ b/events/templates/_types/calendar/header/_header.html @@ -0,0 +1,23 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='calendar-row', oob=oob) %} + {% call links.link(url_for('calendars.calendar.get', calendar_slug=calendar.slug), hx_select_search) %} +
    +
    + +
    + {{ calendar.name }} +
    +
    + {% from '_types/calendar/_description.html' import description %} + {{description(calendar)}} +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/calendar/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/calendar/index.html b/events/templates/_types/calendar/index.html new file mode 100644 index 0000000..bdd0b49 --- /dev/null +++ b/events/templates/_types/calendar/index.html @@ -0,0 +1,26 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %} + {% call index_row('calendar-header-child', '_types/calendar/header/_header.html') %} + {% block calendar_header_child %} + {% endblock %} + {% endcall %} + {% endcall %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {% include '_types/calendar/_nav.html' %} +{% endblock %} + + + +{% block content %} + {% include '_types/calendar/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/calendars/_calendars_list.html b/events/templates/_types/calendars/_calendars_list.html new file mode 100644 index 0000000..87d482c --- /dev/null +++ b/events/templates/_types/calendars/_calendars_list.html @@ -0,0 +1,44 @@ + {% for row in calendars %} + {% set cal = row %} +
    +
    + + {% set calendar_href = url_for('calendars.calendar.get', calendar_slug=cal.slug)|host %} + +

    {{ cal.name }}

    +

    /{{ cal.slug }}/

    +
    + + + + +
    +
    + {% else %} +

    No calendars yet. Create one above.

    + {% endfor %} diff --git a/events/templates/_types/calendars/_main_panel.html b/events/templates/_types/calendars/_main_panel.html new file mode 100644 index 0000000..7b9aa7c --- /dev/null +++ b/events/templates/_types/calendars/_main_panel.html @@ -0,0 +1,27 @@ +
    + {% if has_access('calendars.create_calendar') %} + +
    + +
    + +
    + + +
    + +
    + {% endif %} + +
    + {% include "_types/calendars/_calendars_list.html" %} +
    +
    \ No newline at end of file diff --git a/events/templates/_types/calendars/_nav.html b/events/templates/_types/calendars/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/calendars/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/calendars/_oob_elements.html b/events/templates/_types/calendars/_oob_elements.html new file mode 100644 index 0000000..6de3bea --- /dev/null +++ b/events/templates/_types/calendars/_oob_elements.html @@ -0,0 +1,28 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'calendars-header-child', '_types/calendars/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/calendars/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/calendars/_main_panel.html" %} +{% endblock %} + + diff --git a/events/templates/_types/calendars/header/_header.html b/events/templates/_types/calendars/header/_header.html new file mode 100644 index 0000000..047e8a3 --- /dev/null +++ b/events/templates/_types/calendars/header/_header.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='calendars-row', oob=oob) %} + {% call links.link(url_for('calendars.home'), hx_select_search) %} + +
    + Calendars +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/calendars/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/events/templates/_types/calendars/index.html b/events/templates/_types/calendars/index.html new file mode 100644 index 0000000..d958a0c --- /dev/null +++ b/events/templates/_types/calendars/index.html @@ -0,0 +1,26 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %} + {% call index_row('calendars-header-child', '_types/calendars/header/_header.html') %} + {% block calendars_header_child %} + {% endblock %} + {% endcall %} + {% endcall %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {% include '_types/calendars/_nav.html' %} +{% endblock %} + + + +{% block content %} + {% include '_types/calendars/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/day/_add.html b/events/templates/_types/day/_add.html new file mode 100644 index 0000000..ed08280 --- /dev/null +++ b/events/templates/_types/day/_add.html @@ -0,0 +1,299 @@ +
    + +
    + + + {# 1) Entry name #} + + + {# 2) Slot picker for this weekday (required) #} + {% if day_slots %} + + {% else %} +
    + No slots defined for this day. +
    + {% endif %} + + {# 3) Time entry + cost display #} +
    + {# Time inputs — hidden until a flexible slot is selected #} + + + {# Cost display — shown when a slot is selected #} + + + {# Summary of fixed times — shown for non-flexible slots #} + +
    + + {# Ticket Configuration #} +
    +

    Ticket Configuration (Optional)

    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + + + +
    +
    + +{# --- Behaviour: lock / unlock times based on slot.flexible --- #} + \ No newline at end of file diff --git a/events/templates/_types/day/_add_button.html b/events/templates/_types/day/_add_button.html new file mode 100644 index 0000000..e92a174 --- /dev/null +++ b/events/templates/_types/day/_add_button.html @@ -0,0 +1,16 @@ + + diff --git a/events/templates/_types/day/_main_panel.html b/events/templates/_types/day/_main_panel.html new file mode 100644 index 0000000..0eea6f0 --- /dev/null +++ b/events/templates/_types/day/_main_panel.html @@ -0,0 +1,28 @@ +
    + + + + + + + + + + + + + {% for entry in day_entries %} + {% include '_types/day/_row.html' %} + {% else %} + + {% endfor %} + + + +
    NameSlot/TimeStateCostTicketsActions
    No entries yet.
    + +
    + {% include '_types/day/_add_button.html' %} +
    + +
    diff --git a/events/templates/_types/day/_nav.html b/events/templates/_types/day/_nav.html new file mode 100644 index 0000000..41d541f --- /dev/null +++ b/events/templates/_types/day/_nav.html @@ -0,0 +1,39 @@ +{% import 'macros/links.html' as links %} + +{# Confirmed Entries - vertical on mobile, horizontal with arrows on desktop #} +
    + {% from 'macros/scrolling_menu.html' import scrolling_menu with context %} + {% call(entry) scrolling_menu('day-entries-container', confirmed_entries) %} + +
    +
    {{ entry.name }}
    +
    + {{ entry.start_at.strftime('%H:%M') }} + {% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    +
    +
    + {% endcall %} +
    + +{# Admin link #} +{% if g.rights.admin %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{admin_nav_item( + url_for( + 'calendars.calendar.day.admin.admin', + calendar_slug=calendar.slug, + year=day_date.year, + month=day_date.month, + day=day_date.day + ) + )}} +{% endif %} \ No newline at end of file diff --git a/events/templates/_types/day/_oob_elements.html b/events/templates/_types/day/_oob_elements.html new file mode 100644 index 0000000..812e6b0 --- /dev/null +++ b/events/templates/_types/day/_oob_elements.html @@ -0,0 +1,18 @@ +{% extends "oob_elements.html" %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('calendar-header-child', 'day-header-child', '_types/day/header/_header.html')}} + + {% from '_types/calendar/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/day/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/day/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/day/_row.html b/events/templates/_types/day/_row.html new file mode 100644 index 0000000..1c3138d --- /dev/null +++ b/events/templates/_types/day/_row.html @@ -0,0 +1,74 @@ +{% import 'macros/links.html' as links %} + + +
    + {% call links.link( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.get', + calendar_slug=calendar.slug, + day=day, + month=month, + year=year, + entry_id=entry.id + ), + hx_select_search, + aclass=styles.pill + ) %} + {{ entry.name }} + {% endcall %} +
    + + + {% if entry.slot %} +
    + {% call links.link( + url_for( + 'calendars.calendar.slots.slot.get', + calendar_slug=calendar.slug, + slot_id=entry.slot.id + ), + hx_select_search, + aclass=styles.pill + ) %} + {{ entry.slot.name }} + {% endcall %} + + ({{ entry.slot.time_start.strftime('%H:%M') }}{% if entry.slot.time_end %} → {{ entry.slot.time_end.strftime('%H:%M') }}{% endif %}) + +
    + {% else %} +
    + {% include '_types/entry/_times.html' %} +
    + {% endif %} + + +
    + {% include '_types/entry/_state.html' %} +
    + + + + £{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }} + + + + {% if entry.ticket_price is not none %} +
    +
    £{{ ('%.2f'|format(entry.ticket_price)) }}
    +
    + {% if entry.ticket_count is not none %} + {{ entry.ticket_count }} tickets + {% else %} + Unlimited + {% endif %} +
    +
    + {% else %} + No tickets + {% endif %} + + + {% include '_types/entry/_options.html' %} + + \ No newline at end of file diff --git a/events/templates/_types/day/admin/_main_panel.html b/events/templates/_types/day/admin/_main_panel.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/day/admin/_main_panel.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/day/admin/_nav.html b/events/templates/_types/day/admin/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/day/admin/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/day/admin/_nav_entries_oob.html b/events/templates/_types/day/admin/_nav_entries_oob.html new file mode 100644 index 0000000..c8be72c --- /dev/null +++ b/events/templates/_types/day/admin/_nav_entries_oob.html @@ -0,0 +1,33 @@ +{# OOB swap for day confirmed entries nav when entries are edited #} +{% import 'macros/links.html' as links %} + +{# Confirmed Entries - vertical on mobile, horizontal with arrows on desktop #} +{% if confirmed_entries %} +
    + {% from 'macros/scrolling_menu.html' import scrolling_menu with context %} + {% call(entry) scrolling_menu('day-entries-container', confirmed_entries) %} + +
    +
    {{ entry.name }}
    +
    + {{ entry.start_at.strftime('%H:%M') }} + {% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    +
    +
    + {% endcall %} +
    +{% else %} + {# Empty placeholder to remove nav entries when none are confirmed #} +
    +{% endif %} diff --git a/events/templates/_types/day/admin/_oob_elements.html b/events/templates/_types/day/admin/_oob_elements.html new file mode 100644 index 0000000..20986bf --- /dev/null +++ b/events/templates/_types/day/admin/_oob_elements.html @@ -0,0 +1,25 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for calendar admin page #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('day-header-child', 'day-admin-header-child', '_types/day/admin/header/_header.html')}} + + {% from '_types/calendar/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/day/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/day/admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/day/admin/header/_header.html b/events/templates/_types/day/admin/header/_header.html new file mode 100644 index 0000000..f3af170 --- /dev/null +++ b/events/templates/_types/day/admin/header/_header.html @@ -0,0 +1,20 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='day-admin-row', oob=oob) %} + {% call links.link( + url_for( + 'calendars.calendar.day.admin.admin', + calendar_slug=calendar.slug, + year=day_date.year, + month=day_date.month, + day=day_date.day + ), + hx_select_search + ) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/day/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/events/templates/_types/day/admin/index.html b/events/templates/_types/day/admin/index.html new file mode 100644 index 0000000..f4f37b5 --- /dev/null +++ b/events/templates/_types/day/admin/index.html @@ -0,0 +1,24 @@ +{% extends '_types/day/index.html' %} +{% import 'macros/layout.html' as layout %} +{% import 'macros/links.html' as links %} + + +{% block day_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/day/admin/header/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block day_admin_header_child %} + {% endblock %} +
    + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/day/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/day/admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/day/header/_header.html b/events/templates/_types/day/header/_header.html new file mode 100644 index 0000000..5774492 --- /dev/null +++ b/events/templates/_types/day/header/_header.html @@ -0,0 +1,26 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='day-row', oob=oob) %} + {% call links.link( + url_for( + 'calendars.calendar.day.show_day', + calendar_slug=calendar.slug, + year=day_date.year, + month=day_date.month, + day=day_date.day + ), + hx_select_search, + ) %} +
    + + {{ day_date.strftime('%A %d %B %Y') }} +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/day/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/day/index.html b/events/templates/_types/day/index.html new file mode 100644 index 0000000..655ee55 --- /dev/null +++ b/events/templates/_types/day/index.html @@ -0,0 +1,18 @@ +{% extends '_types/calendar/index.html' %} + +{% block calendar_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('day-header-child', '_types/day/header/_header.html') %} + {% block day_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/day/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include '_types/day/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/entry/_edit.html b/events/templates/_types/entry/_edit.html new file mode 100644 index 0000000..5467179 --- /dev/null +++ b/events/templates/_types/entry/_edit.html @@ -0,0 +1,332 @@ +
    + + +
    + +
    + + + + +
    + + +
    + + +
    + + {% if day_slots %} + + {% else %} +
    + No slots defined for this day. +
    + {% endif %} +
    + + + + + + + + + + + +
    +

    Ticket Configuration

    + +
    +
    + + +

    Leave empty if no tickets needed

    +
    + +
    + + +

    Leave empty for unlimited

    +
    +
    +
    + +
    + + + + + + + +
    + +
    +
    + +{# --- Behaviour: lock / unlock times based on slot.flexible --- #} + \ No newline at end of file diff --git a/events/templates/_types/entry/_main_panel.html b/events/templates/_types/entry/_main_panel.html new file mode 100644 index 0000000..902ffa4 --- /dev/null +++ b/events/templates/_types/entry/_main_panel.html @@ -0,0 +1,128 @@ +
    + + +
    +
    + Name +
    +
    + {{ entry.name }} +
    +
    + + +
    +
    + Slot +
    +
    + {% if entry.slot %} + + {{ entry.slot.name }} + + {% if entry.slot.flexible %} + (flexible) + {% else %} + (fixed) + {% endif %} + {% else %} + No slot assigned + {% endif %} +
    +
    + + +
    +
    + Time Period +
    +
    + {{ entry.start_at.strftime('%H:%M') }} + {% if entry.end_at %} + – {{ entry.end_at.strftime('%H:%M') }} + {% else %} + – open-ended + {% endif %} +
    +
    + + +
    +
    + State +
    +
    +
    + {% include '_types/entry/_state.html' %} +
    +
    +
    + + +
    +
    + Cost +
    +
    + + £{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }} + +
    +
    + + +
    +
    + Tickets +
    +
    + {% include '_types/entry/_tickets.html' %} +
    +
    + + + {% include '_types/tickets/_buy_form.html' %} + + +
    +
    + Date +
    +
    + {{ entry.start_at.strftime('%A, %B %d, %Y') }} +
    +
    + + +
    +
    + Associated Posts +
    +
    + {% include '_types/entry/_posts.html' %} +
    +
    + + +
    + {% include '_types/entry/_options.html' %} + + +
    + +
    \ No newline at end of file diff --git a/events/templates/_types/entry/_nav.html b/events/templates/_types/entry/_nav.html new file mode 100644 index 0000000..bdfe325 --- /dev/null +++ b/events/templates/_types/entry/_nav.html @@ -0,0 +1,39 @@ +{% import 'macros/links.html' as links %} + +{# Associated Posts - vertical on mobile, horizontal with arrows on desktop #} +
    + {% from 'macros/scrolling_menu.html' import scrolling_menu with context %} + {% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %} + + {% if entry_post.feature_image %} + {{ entry_post.title }} + {% else %} +
    + {% endif %} +
    +
    {{ entry_post.title }}
    +
    +
    + {% endcall %} +
    + +{# Admin link #} +{% if g.rights.admin %} + + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{admin_nav_item( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.admin.admin', + calendar_slug=calendar.slug, + day=day, + month=month, + year=year, + entry_id=entry.id + ) + )}} +{% endif %} diff --git a/events/templates/_types/entry/_oob_elements.html b/events/templates/_types/entry/_oob_elements.html new file mode 100644 index 0000000..8981fa1 --- /dev/null +++ b/events/templates/_types/entry/_oob_elements.html @@ -0,0 +1,18 @@ +{% extends "oob_elements.html" %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('day-header-child', 'entry-header-child', '_types/entry/header/_header.html')}} + + {% from '_types/day/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/entry/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/entry/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/entry/_optioned.html b/events/templates/_types/entry/_optioned.html new file mode 100644 index 0000000..ba23391 --- /dev/null +++ b/events/templates/_types/entry/_optioned.html @@ -0,0 +1,9 @@ + +{% include '_types/entry/_options.html' %} +
    + {% include '_types/entry/_title.html' %} +
    + +
    + {% include '_types/entry/_state.html' %} +
    \ No newline at end of file diff --git a/events/templates/_types/entry/_options.html b/events/templates/_types/entry/_options.html new file mode 100644 index 0000000..d33ae4c --- /dev/null +++ b/events/templates/_types/entry/_options.html @@ -0,0 +1,95 @@ +
    + {% if entry.state == 'provisional' %} +
    + + +
    +
    + + +
    + {% endif %} + {% if entry.state == 'confirmed' %} +
    + + + +
    + {% endif %} +
    \ No newline at end of file diff --git a/events/templates/_types/entry/_post_search_results.html b/events/templates/_types/entry/_post_search_results.html new file mode 100644 index 0000000..297cd70 --- /dev/null +++ b/events/templates/_types/entry/_post_search_results.html @@ -0,0 +1,105 @@ +{% for search_post in search_posts %} +
    + + + +
    +{% endfor %} + +{# Infinite scroll sentinel #} +{% if page < total_pages|int %} + +{% elif search_posts %} +
    + End of results +
    +{% endif %} diff --git a/events/templates/_types/entry/_posts.html b/events/templates/_types/entry/_posts.html new file mode 100644 index 0000000..122442e --- /dev/null +++ b/events/templates/_types/entry/_posts.html @@ -0,0 +1,72 @@ + +
    + {% if entry_posts %} +
    + {% for entry_post in entry_posts %} +
    + {% if entry_post.feature_image %} + {{ entry_post.title }} + {% else %} +
    + {% endif %} + {{ entry_post.title }} + +
    + {% endfor %} +
    + {% else %} +

    No posts associated

    + {% endif %} + + +
    + + +
    +
    +
    diff --git a/events/templates/_types/entry/_state.html b/events/templates/_types/entry/_state.html new file mode 100644 index 0000000..b67254a --- /dev/null +++ b/events/templates/_types/entry/_state.html @@ -0,0 +1,15 @@ +{% if entry.state %} + + {{ entry.state|capitalize }} + + {% endif %} \ No newline at end of file diff --git a/events/templates/_types/entry/_tickets.html b/events/templates/_types/entry/_tickets.html new file mode 100644 index 0000000..3d9613a --- /dev/null +++ b/events/templates/_types/entry/_tickets.html @@ -0,0 +1,104 @@ +{% if entry.ticket_price is not none %} + {# Tickets are configured #} +
    +
    + Price: + + £{{ ('%.2f'|format(entry.ticket_price)) }} + +
    +
    + Available: + + {% if entry.ticket_count is not none %} + {{ entry.ticket_count }} tickets + {% else %} + Unlimited + {% endif %} + +
    + +
    +{% else %} + {# No tickets configured #} +
    + No tickets configured + +
    +{% endif %} + +{# Ticket configuration form (hidden by default) #} +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    diff --git a/events/templates/_types/entry/_times.html b/events/templates/_types/entry/_times.html new file mode 100644 index 0000000..3543fe4 --- /dev/null +++ b/events/templates/_types/entry/_times.html @@ -0,0 +1,5 @@ +{% from 'macros/date.html' import t %} +
    + {{ t(entry.start_at) }} + {% if entry.end_at %} → {{ t(entry.end_at) }}{% endif %} +
    \ No newline at end of file diff --git a/events/templates/_types/entry/_title.html b/events/templates/_types/entry/_title.html new file mode 100644 index 0000000..3c1dc63 --- /dev/null +++ b/events/templates/_types/entry/_title.html @@ -0,0 +1,3 @@ + + {{ entry.name }} + {% include '_types/entry/_state.html' %} diff --git a/events/templates/_types/entry/admin/_main_panel.html b/events/templates/_types/entry/admin/_main_panel.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/entry/admin/_main_panel.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/entry/admin/_nav.html b/events/templates/_types/entry/admin/_nav.html new file mode 100644 index 0000000..9db8ac0 --- /dev/null +++ b/events/templates/_types/entry/admin/_nav.html @@ -0,0 +1,17 @@ +{% import 'macros/links.html' as links %} +{% call links.link( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.get', + calendar_slug=calendar.slug, + entry_id=entry.id, + year=year, + month=month, + day=day + ), + hx_select_search, + select_colours, + True, + aclass=styles.nav_button, +)%} + ticket_types +{% endcall %} diff --git a/events/templates/_types/entry/admin/_nav_posts_oob.html b/events/templates/_types/entry/admin/_nav_posts_oob.html new file mode 100644 index 0000000..25ef1f1 --- /dev/null +++ b/events/templates/_types/entry/admin/_nav_posts_oob.html @@ -0,0 +1,31 @@ +{# OOB swap for entry posts nav when posts are associated/disassociated #} +{% import 'macros/links.html' as links %} + +{# Associated Posts - vertical on mobile, horizontal with arrows on desktop #} +{% if entry_posts %} +
    + {% from 'macros/scrolling_menu.html' import scrolling_menu with context %} + {% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %} + + {% if entry_post.feature_image %} + {{ entry_post.title }} + {% else %} +
    + {% endif %} +
    +
    {{ entry_post.title }}
    +
    +
    + {% endcall %} +
    +{% else %} + {# Empty placeholder to remove nav posts when all are disassociated #} +
    +{% endif %} diff --git a/events/templates/_types/entry/admin/_oob_elements.html b/events/templates/_types/entry/admin/_oob_elements.html new file mode 100644 index 0000000..bcf2255 --- /dev/null +++ b/events/templates/_types/entry/admin/_oob_elements.html @@ -0,0 +1,25 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for calendar admin page #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('entry-header-child', 'entry-admin-header-child', '_types/entry/admin/header/_header.html')}} + + {% from '_types/entry/header/_header.html' import header_row with context %} + {{header_row(oob=True)}} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/entry/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/entry/admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/entry/admin/header/_header.html b/events/templates/_types/entry/admin/header/_header.html new file mode 100644 index 0000000..952e215 --- /dev/null +++ b/events/templates/_types/entry/admin/header/_header.html @@ -0,0 +1,21 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='entry-admin-row', oob=oob) %} + {% call links.link( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.admin.admin', + calendar_slug=calendar.slug, + day=day, + month=month, + year=year, + entry_id=entry.id + ), + hx_select_search + ) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/entry/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/events/templates/_types/entry/admin/index.html b/events/templates/_types/entry/admin/index.html new file mode 100644 index 0000000..caa100c --- /dev/null +++ b/events/templates/_types/entry/admin/index.html @@ -0,0 +1,24 @@ +{% extends '_types/entry/index.html' %} +{% import 'macros/layout.html' as layout %} +{% import 'macros/links.html' as links %} + + +{% block entry_header_child %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% from '_types/entry/admin/header/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block entry_admin_header_child %} + {% endblock %} +
    + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/entry/admin/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/entry/admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/entry/header/_header.html b/events/templates/_types/entry/header/_header.html new file mode 100644 index 0000000..5e1a5cc --- /dev/null +++ b/events/templates/_types/entry/header/_header.html @@ -0,0 +1,27 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='entry-row', oob=oob) %} + {% call links.link( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.get', + calendar_slug=calendar.slug, + day=day, + month=month, + year=year, + entry_id=entry.id + ), + hx_select_search, + ) %} +
    + {% include '_types/entry/_title.html' %} + {% include '_types/entry/_times.html' %} +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/entry/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/entry/index.html b/events/templates/_types/entry/index.html new file mode 100644 index 0000000..a980f46 --- /dev/null +++ b/events/templates/_types/entry/index.html @@ -0,0 +1,20 @@ +{% extends '_types/day/index.html' %} + +{% block day_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('entry-header-child', '_types/entry/header/_header.html') %} + {% block entry_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {% include '_types/entry/_nav.html' %} +{% endblock %} + + + +{% block content %} +{% include '_types/entry/_main_panel.html' %} +{% endblock %} \ No newline at end of file diff --git a/events/templates/_types/markets/_main_panel.html b/events/templates/_types/markets/_main_panel.html new file mode 100644 index 0000000..7168712 --- /dev/null +++ b/events/templates/_types/markets/_main_panel.html @@ -0,0 +1,25 @@ +
    + {% if has_access('markets.create_market') %} +
    + +
    + +
    + + +
    + +
    + {% endif %} +
    + {% include "_types/markets/_markets_list.html" %} +
    +
    diff --git a/events/templates/_types/markets/_markets_list.html b/events/templates/_types/markets/_markets_list.html new file mode 100644 index 0000000..2ac5143 --- /dev/null +++ b/events/templates/_types/markets/_markets_list.html @@ -0,0 +1,37 @@ + {% for m in markets %} +
    +
    + + {% set market_href = market_url('/' + post.slug + '/' + m.slug + '/') %} + +

    {{ m.name }}

    +

    /{{ m.slug }}/

    +
    + + + +
    +
    + {% else %} +

    No markets yet. Create one above.

    + {% endfor %} diff --git a/events/templates/_types/markets/_nav.html b/events/templates/_types/markets/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/markets/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/markets/_oob_elements.html b/events/templates/_types/markets/_oob_elements.html new file mode 100644 index 0000000..93ec6d7 --- /dev/null +++ b/events/templates/_types/markets/_oob_elements.html @@ -0,0 +1,19 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'markets-header-child', '_types/markets/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block mobile_menu %} + {% include '_types/markets/_nav.html' %} +{% endblock %} + +{% block content %} + {% include "_types/markets/_main_panel.html" %} +{% endblock %} diff --git a/events/templates/_types/markets/header/_header.html b/events/templates/_types/markets/header/_header.html new file mode 100644 index 0000000..6ae008d --- /dev/null +++ b/events/templates/_types/markets/header/_header.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='markets-row', oob=oob) %} + {% call links.link(url_for('markets.home'), hx_select_search) %} + +
    + Markets +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/markets/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/events/templates/_types/markets/index.html b/events/templates/_types/markets/index.html new file mode 100644 index 0000000..cb05b12 --- /dev/null +++ b/events/templates/_types/markets/index.html @@ -0,0 +1,23 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %} + {% call index_row('markets-header-child', '_types/markets/header/_header.html') %} + {% block markets_header_child %} + {% endblock %} + {% endcall %} + {% endcall %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/markets/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/markets/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/page_summary/_card.html b/events/templates/_types/page_summary/_card.html new file mode 100644 index 0000000..27f12cb --- /dev/null +++ b/events/templates/_types/page_summary/_card.html @@ -0,0 +1,49 @@ +{# List card for page summary — one entry #} +{% set pi = page_info.get(entry.calendar_container_id, {}) %} +{% set page_slug = pi.get('slug', post.slug) %} +{% set page_title = pi.get('title') %} +
    +
    + {# Left: event info #} +
    + {% set day_href = events_url('/' ~ page_slug ~ '/calendars/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %} + {% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' %} + +

    {{ entry.name }}

    +
    + +
    + {% if page_title and page_title != post.title %} + + {{ page_title }} + + {% endif %} + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} +
    + +
    + {{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    + + {% if entry.cost %} +
    + £{{ '%.2f'|format(entry.cost) }} +
    + {% endif %} +
    + + {# Right: ticket widget #} + {% if entry.ticket_price is not none %} +
    + {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = url_for('page_summary.adjust_ticket') %} + {% include '_types/page_summary/_ticket_widget.html' %} +
    + {% endif %} +
    +
    diff --git a/events/templates/_types/page_summary/_card_tile.html b/events/templates/_types/page_summary/_card_tile.html new file mode 100644 index 0000000..7d13cca --- /dev/null +++ b/events/templates/_types/page_summary/_card_tile.html @@ -0,0 +1,48 @@ +{# Tile card for page summary — compact event tile #} +{% set pi = page_info.get(entry.calendar_container_id, {}) %} +{% set page_slug = pi.get('slug', post.slug) %} +{% set page_title = pi.get('title') %} +
    + {% set day_href = events_url('/' ~ page_slug ~ '/calendars/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %} + {% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' %} +
    + +

    {{ entry.name }}

    +
    + +
    + {% if page_title and page_title != post.title %} + + {{ page_title }} + + {% endif %} + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} +
    + +
    + {{ entry.start_at.strftime('%a %-d %b') }} + · + {{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    + + {% if entry.cost %} +
    + £{{ '%.2f'|format(entry.cost) }} +
    + {% endif %} +
    + + {# Ticket widget below card #} + {% if entry.ticket_price is not none %} +
    + {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = url_for('page_summary.adjust_ticket') %} + {% include '_types/page_summary/_ticket_widget.html' %} +
    + {% endif %} +
    diff --git a/events/templates/_types/page_summary/_cards.html b/events/templates/_types/page_summary/_cards.html new file mode 100644 index 0000000..b6958ab --- /dev/null +++ b/events/templates/_types/page_summary/_cards.html @@ -0,0 +1,31 @@ +{% for entry in entries %} + {% if view == 'tile' %} + {% include "_types/page_summary/_card_tile.html" %} + {% else %} + {# Date header when date changes (list view only) #} + {% set entry_date = entry.start_at.strftime('%A %-d %B %Y') %} + {% if loop.first or entry_date != entries[loop.index0 - 1].start_at.strftime('%A %-d %B %Y') %} +
    +

    + {{ entry_date }} +

    +
    + {% endif %} + {% include "_types/page_summary/_card.html" %} + {% endif %} +{% endfor %} +{% if has_more %} + {# Infinite scroll sentinel #} + {% set entries_url = url_for('page_summary.entries_fragment', page=page + 1, view=view if view != 'list' else '')|host %} + +{% endif %} diff --git a/events/templates/_types/page_summary/_main_panel.html b/events/templates/_types/page_summary/_main_panel.html new file mode 100644 index 0000000..ab1a8b4 --- /dev/null +++ b/events/templates/_types/page_summary/_main_panel.html @@ -0,0 +1,54 @@ +{# View toggle bar - desktop only #} + + +{# Cards container - list or grid based on view #} +{% if entries %} + {% if view == 'tile' %} +
    + {% include "_types/page_summary/_cards.html" %} +
    + {% else %} +
    + {% include "_types/page_summary/_cards.html" %} +
    + {% endif %} +{% else %} +
    + +

    No upcoming events

    +
    +{% endif %} +
    diff --git a/events/templates/_types/page_summary/_ticket_widget.html b/events/templates/_types/page_summary/_ticket_widget.html new file mode 100644 index 0000000..6e90871 --- /dev/null +++ b/events/templates/_types/page_summary/_ticket_widget.html @@ -0,0 +1,63 @@ +{# Inline ticket +/- widget for page summary cards. + Variables: entry, qty, ticket_url + Wrapped in a div with stable ID for HTMX targeting. #} +
    + £{{ '%.2f'|format(entry.ticket_price) }} + + {% if qty == 0 %} +
    + + + + +
    + {% else %} +
    + + + + +
    + + + + + + {{ qty }} + + + + +
    + + + + +
    + {% endif %} +
    diff --git a/events/templates/_types/page_summary/index.html b/events/templates/_types/page_summary/index.html new file mode 100644 index 0000000..d084317 --- /dev/null +++ b/events/templates/_types/page_summary/index.html @@ -0,0 +1,15 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% block post_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/page_summary/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/payments/_main_panel.html b/events/templates/_types/payments/_main_panel.html new file mode 100644 index 0000000..42f4141 --- /dev/null +++ b/events/templates/_types/payments/_main_panel.html @@ -0,0 +1,70 @@ +
    +
    +

    + + SumUp Payment +

    +

    + Configure per-page SumUp credentials. Leave blank to use the global merchant account. +

    + +
    + + +
    + + +
    + +
    + + + {% if sumup_configured %} +

    Key is set. Leave blank to keep current key.

    + {% endif %} +
    + +
    + + +
    + + + + {% if sumup_configured %} + + Connected + + {% endif %} +
    +
    +
    diff --git a/events/templates/_types/payments/_nav.html b/events/templates/_types/payments/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/payments/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/payments/_oob_elements.html b/events/templates/_types/payments/_oob_elements.html new file mode 100644 index 0000000..5232f7e --- /dev/null +++ b/events/templates/_types/payments/_oob_elements.html @@ -0,0 +1,19 @@ +{% extends 'oob_elements.html' %} + +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-admin-header-child', 'payments-header-child', '_types/payments/header/_header.html')}} + + {% from '_types/post/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + +{% block mobile_menu %} + {% include '_types/payments/_nav.html' %} +{% endblock %} + +{% block content %} + {% include "_types/payments/_main_panel.html" %} +{% endblock %} diff --git a/events/templates/_types/payments/header/_header.html b/events/templates/_types/payments/header/_header.html new file mode 100644 index 0000000..282aac6 --- /dev/null +++ b/events/templates/_types/payments/header/_header.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='payments-row', oob=oob) %} + {% call links.link(url_for('payments.home'), hx_select_search) %} + +
    + Payments +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/payments/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/events/templates/_types/payments/index.html b/events/templates/_types/payments/index.html new file mode 100644 index 0000000..721145c --- /dev/null +++ b/events/templates/_types/payments/index.html @@ -0,0 +1,23 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %} + {% call index_row('payments-header-child', '_types/payments/header/_header.html') %} + {% block payments_header_child %} + {% endblock %} + {% endcall %} + {% endcall %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/payments/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/payments/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/post/_nav.html b/events/templates/_types/post/_nav.html new file mode 100644 index 0000000..8a75650 --- /dev/null +++ b/events/templates/_types/post/_nav.html @@ -0,0 +1,14 @@ +{% import 'macros/links.html' as links %} +{% if calendars %} + {% for calendar in calendars %} + {% call links.link(url_for('calendars.calendar.get', calendar_slug=calendar.slug), hx_select_search, select_colours, True, aclass=styles.nav_button_less_pad) %} + +
    {{ calendar.name }}
    + {% endcall %} + {% endfor %} +{% endif %} +{% if g.rights.admin %} + + + +{% endif %} diff --git a/events/templates/_types/post/admin/_associated_entries.html b/events/templates/_types/post/admin/_associated_entries.html new file mode 100644 index 0000000..d9fe853 --- /dev/null +++ b/events/templates/_types/post/admin/_associated_entries.html @@ -0,0 +1,50 @@ +
    +

    Associated Entries

    + {% if associated_entry_ids %} +
    + {% for calendar in all_calendars %} + {% for entry in calendar.entries %} + {% if entry.id in associated_entry_ids and entry.deleted_at is none %} + + {% endif %} + {% endfor %} + {% endfor %} +
    + {% else %} +
    No entries associated yet. Browse calendars below to add entries.
    + {% endif %} +
    diff --git a/events/templates/_types/post/admin/_nav.html b/events/templates/_types/post/admin/_nav.html new file mode 100644 index 0000000..c0237d6 --- /dev/null +++ b/events/templates/_types/post/admin/_nav.html @@ -0,0 +1,36 @@ +{% import 'macros/links.html' as links %} + + + + + + + diff --git a/events/templates/_types/post/admin/header/_header.html b/events/templates/_types/post/admin/header/_header.html new file mode 100644 index 0000000..9056d09 --- /dev/null +++ b/events/templates/_types/post/admin/header/_header.html @@ -0,0 +1,12 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post-admin-row', oob=oob) %} + + {{ links.admin() }} + + {% call links.desktop_nav() %} + {% include '_types/post/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} diff --git a/events/templates/_types/post/header/_header.html b/events/templates/_types/post/header/_header.html new file mode 100644 index 0000000..6655eb5 --- /dev/null +++ b/events/templates/_types/post/header/_header.html @@ -0,0 +1,28 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post-row', oob=oob) %} + {% call links.link(blog_url('/' + post.slug + '/'), hx_select_search ) %} + {% if post.feature_image %} + + {% endif %} + + {{ post.title | truncate(160, True, '…') }} + + {% endcall %} + {% call links.desktop_nav() %} + {% if page_cart_count is defined and page_cart_count > 0 %} + + + {{ page_cart_count }} + + {% endif %} + {% include '_types/post/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/events/templates/_types/post_entries/_main_panel.html b/events/templates/_types/post_entries/_main_panel.html new file mode 100644 index 0000000..3a938ba --- /dev/null +++ b/events/templates/_types/post_entries/_main_panel.html @@ -0,0 +1,47 @@ +
    + + {# Associated Entries List #} + {% include '_types/post/admin/_associated_entries.html' %} + + {# Calendars Browser #} +
    +

    Browse Calendars

    + {% for calendar in all_calendars %} +
    + + {% if calendar.post.feature_image %} + {{ calendar.post.title }} + {% else %} +
    + {% endif %} +
    +
    + + {{ calendar.name }} +
    +
    + {{ calendar.post.title }} +
    +
    +
    +
    +
    Loading calendar...
    +
    +
    + {% else %} +
    No calendars found.
    + {% endfor %} +
    +
    \ No newline at end of file diff --git a/events/templates/_types/post_entries/_nav.html b/events/templates/_types/post_entries/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/post_entries/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/post_entries/header/_header.html b/events/templates/_types/post_entries/header/_header.html new file mode 100644 index 0000000..18859eb --- /dev/null +++ b/events/templates/_types/post_entries/header/_header.html @@ -0,0 +1,17 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post_entries-row', oob=oob) %} + {% call links.link(blog_url('/' + post.slug + '/admin/entries/'), hx_select_search) %} + +
    + entries +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/post_entries/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/slot/__description.html b/events/templates/_types/slot/__description.html new file mode 100644 index 0000000..7897fd2 --- /dev/null +++ b/events/templates/_types/slot/__description.html @@ -0,0 +1,13 @@ +{% macro description(slot, oob=False) %} +
    + {{ slot.description or ''}} +
    + +{% endmacro %} diff --git a/events/templates/_types/slot/_description.html b/events/templates/_types/slot/_description.html new file mode 100644 index 0000000..32e28e6 --- /dev/null +++ b/events/templates/_types/slot/_description.html @@ -0,0 +1,5 @@ +

    + {% if slot.description %} + {{ slot.description }} + {% endif %} +

    diff --git a/events/templates/_types/slot/_edit.html b/events/templates/_types/slot/_edit.html new file mode 100644 index 0000000..e591e74 --- /dev/null +++ b/events/templates/_types/slot/_edit.html @@ -0,0 +1,180 @@ +
    + +
    +
    + + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + Days + + + {# pre-check "All" if every day is true on this slot #} + {% set all_days_checked = + slot|getattr('mon') + and slot|getattr('tue') + and slot|getattr('wed') + and slot|getattr('thu') + and slot|getattr('fri') + and slot|getattr('sat') + and slot|getattr('sun') %} + +
    + {# "All" toggle – no name so it’s not submitted #} + + + {# Individual days, with data-day like the add form #} + {% for key, label in [ + ('mon','Mon'),('tue','Tue'),('wed','Wed'),('thu','Thu'), + ('fri','Fri'),('sat','Sat'),('sun','Sun') + ] %} + {% set is_checked = slot|getattr(key) %} + + {% endfor %} +
    +
    + + +
    + + +
    + +
    + + + +
    +
    +
    diff --git a/events/templates/_types/slot/_main_panel.html b/events/templates/_types/slot/_main_panel.html new file mode 100644 index 0000000..2b4cb17 --- /dev/null +++ b/events/templates/_types/slot/_main_panel.html @@ -0,0 +1,72 @@ +
    + +
    +
    + Days +
    +
    + {% set days = slot.days_display.split(', ') %} + {% if days and days[0] != "—" %} +
    + {% for day in days %} + + {{ day }} + + {% endfor %} +
    + {% else %} + No days + {% endif %} +
    +
    + + +
    +
    + Flexible +
    +
    + {{ 'yes' if slot.flexible else 'no' }} +
    +
    + + +
    +
    +
    + Time +
    +
    + {{ slot.time_start.strftime('%H:%M') }} — {{ slot.time_end.strftime('%H:%M') }} +
    +
    + +
    +
    + Cost +
    +
    + {{ ('%.2f'|format(slot.cost)) if slot.cost is not none else '' }} +
    +
    +
    + +
    + +{% if oob %} + {% from '_types/slot/__description.html' import description %} + {{description(slot, oob=True)}} + +{% endif %} \ No newline at end of file diff --git a/events/templates/_types/slot/_oob_elements.html b/events/templates/_types/slot/_oob_elements.html new file mode 100644 index 0000000..3b82170 --- /dev/null +++ b/events/templates/_types/slot/_oob_elements.html @@ -0,0 +1,15 @@ +{% extends "oob_elements.html" %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('slots-header-child', 'slot-header-child', '_types/slot/header/_header.html')}} + + {% from '_types/slots/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + + +{% block content %} + {% include '_types/slot/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/slot/header/_header.html b/events/templates/_types/slot/header/_header.html new file mode 100644 index 0000000..fc5381d --- /dev/null +++ b/events/templates/_types/slot/header/_header.html @@ -0,0 +1,25 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='slot-row', oob=oob) %} + {% call links.link( + hx_select_search, + ) %} +
    +
    + +
    + {{ slot.name }} +
    +
    + {% from '_types/slot/__description.html' import description %} + {{description(slot)}} +
    + {% endcall %} + {% call links.desktop_nav() %} + {#% include '_types/slot/_nav.html' %#} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/slot/index.html b/events/templates/_types/slot/index.html new file mode 100644 index 0000000..265be24 --- /dev/null +++ b/events/templates/_types/slot/index.html @@ -0,0 +1,20 @@ +{% extends '_types/slots/index.html' %} +{% import 'macros/layout.html' as layout %} + + +{% block slots_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('slot-header-child', '_types/slot/header/_header.html') %} + {% block slot_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {#% include '_types/slot/_nav.html' %#} +{% endblock %} + +{% block content %} + {% include '_types/slot/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/slots/_add.html b/events/templates/_types/slots/_add.html new file mode 100644 index 0000000..8a0f6df --- /dev/null +++ b/events/templates/_types/slots/_add.html @@ -0,0 +1,123 @@ +
    +
    +
    + + +
    + +
    + + +
    + +
    + +
    + {# "All" toggle – no name so it’s not submitted #} + + + {# Individual days #} + {% for key, label in [ + ('mon','Mon'),('tue','Tue'),('wed','Wed'),('thu','Thu'), + ('fri','Fri'),('sat','Sat'),('sun','Sun') + ] %} + + {% endfor %} +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + {# NEW: flexible flag #} +
    + + +
    +
    + +
    + + + +
    +
    diff --git a/events/templates/_types/slots/_add_button.html b/events/templates/_types/slots/_add_button.html new file mode 100644 index 0000000..6bb7e5d --- /dev/null +++ b/events/templates/_types/slots/_add_button.html @@ -0,0 +1,11 @@ + + diff --git a/events/templates/_types/slots/_main_panel.html b/events/templates/_types/slots/_main_panel.html new file mode 100644 index 0000000..a2ac263 --- /dev/null +++ b/events/templates/_types/slots/_main_panel.html @@ -0,0 +1,26 @@ +
    + + + + + + + + + + + + + {% for s in slots %} + {% include '_types/slots/_row.html' %} + {% else %} + + {% endfor %} + +
    NameFlexibleDaysTimeCostActions
    No slots yet.
    + + +
    + {% include '_types/slots/_add_button.html' %} +
    +
    diff --git a/events/templates/_types/slots/_oob_elements.html b/events/templates/_types/slots/_oob_elements.html new file mode 100644 index 0000000..acf0d05 --- /dev/null +++ b/events/templates/_types/slots/_oob_elements.html @@ -0,0 +1,15 @@ +{% extends "oob_elements.html" %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('calendar-header-child', 'slots-header-child', '_types/slots/header/_header.html')}} + + {% from '_types/calendar/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + + +{% block content %} + {% include '_types/slots/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/slots/_row.html b/events/templates/_types/slots/_row.html new file mode 100644 index 0000000..1a9965f --- /dev/null +++ b/events/templates/_types/slots/_row.html @@ -0,0 +1,61 @@ +{% import 'macros/links.html' as links %} + + +
    + {% call links.link( + hx_select_search, + aclass=styles.pill + ) %} + {{ s.name }} + {% endcall %} +
    + {% set slot = s %} + {% include '_types/slot/_description.html' %} + + + {{ 'yes' if s.flexible else 'no' }} + + + {% set days = s.days_display.split(', ') %} + {% if days and days[0] != "—" %} +
    + {% for day in days %} + + {{ day }} + + {% endfor %} +
    + {% else %} + No days + {% endif %} + + + {{ s.time_start.strftime('%H:%M') }} - {{ s.time_end.strftime('%H:%M') }} + + + {{ ('%.2f'|format(s.cost)) if s.cost is not none else '' }} + + + + + diff --git a/events/templates/_types/slots/header/_header.html b/events/templates/_types/slots/header/_header.html new file mode 100644 index 0000000..eb34edb --- /dev/null +++ b/events/templates/_types/slots/header/_header.html @@ -0,0 +1,18 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='slots-row', oob=oob) %} + {% call links.link( + hx_select_search, + ) %} + +
    + slots +
    + {% endcall %} + {% call links.desktop_nav() %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/slots/index.html b/events/templates/_types/slots/index.html new file mode 100644 index 0000000..453ba5f --- /dev/null +++ b/events/templates/_types/slots/index.html @@ -0,0 +1,19 @@ +{% extends '_types/calendar/index.html' %} + +{% block calendar_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('slots-header-child', '_types/slots/header/_header.html') %} + {% block slots_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {#% include '_types/calendar/_nav.html' %#} +{% endblock %} + + + +{% block content %} + {% include '_types/slots/_main_panel.html' %} +{% endblock %} \ No newline at end of file diff --git a/events/templates/_types/ticket_admin/_checkin_result.html b/events/templates/_types/ticket_admin/_checkin_result.html new file mode 100644 index 0000000..4d6447e --- /dev/null +++ b/events/templates/_types/ticket_admin/_checkin_result.html @@ -0,0 +1,39 @@ +{# Check-in result — replaces ticket row or action area #} +{% if success and ticket %} + + + {{ ticket.code[:12] }}... + + +
    {{ ticket.entry.name if ticket.entry else '—' }}
    + {% if ticket.entry and ticket.entry.start_at %} +
    + {{ ticket.entry.start_at.strftime('%d %b %Y, %H:%M') }} +
    + {% endif %} + + + {{ ticket.ticket_type.name if ticket.ticket_type else '—' }} + + + + Checked in + + + + + + {% if ticket.checked_in_at %} + {{ ticket.checked_in_at.strftime('%H:%M') }} + {% else %} + Just now + {% endif %} + + + +{% elif not success %} +
    + + {{ error or 'Check-in failed' }} +
    +{% endif %} diff --git a/events/templates/_types/ticket_admin/_entry_tickets.html b/events/templates/_types/ticket_admin/_entry_tickets.html new file mode 100644 index 0000000..6599b2a --- /dev/null +++ b/events/templates/_types/ticket_admin/_entry_tickets.html @@ -0,0 +1,75 @@ +{# Tickets for a specific calendar entry — admin view #} +
    +
    +

    + Tickets for: {{ entry.name }} +

    + + {{ tickets|length }} ticket{{ 's' if tickets|length != 1 else '' }} + +
    + + {% if tickets %} +
    + + + + + + + + + + + {% for ticket in tickets %} + + + + + + + {% endfor %} + +
    CodeTypeStateActions
    {{ ticket.code[:12] }}...{{ ticket.ticket_type.name if ticket.ticket_type else '—' }} + + {{ ticket.state|replace('_', ' ')|capitalize }} + + + {% if ticket.state in ('confirmed', 'reserved') %} +
    + + +
    + {% elif ticket.state == 'checked_in' %} + + + {% if ticket.checked_in_at %}{{ ticket.checked_in_at.strftime('%H:%M') }}{% endif %} + + {% endif %} +
    +
    + {% else %} +
    + No tickets for this entry +
    + {% endif %} +
    diff --git a/events/templates/_types/ticket_admin/_lookup_result.html b/events/templates/_types/ticket_admin/_lookup_result.html new file mode 100644 index 0000000..5ea17eb --- /dev/null +++ b/events/templates/_types/ticket_admin/_lookup_result.html @@ -0,0 +1,82 @@ +{# Ticket lookup result — rendered into #lookup-result #} +{% if error %} +
    + + {{ error }} +
    +{% elif ticket %} +
    +
    +
    +
    + {{ ticket.entry.name if ticket.entry else 'Unknown event' }} +
    + {% if ticket.ticket_type %} +
    {{ ticket.ticket_type.name }}
    + {% endif %} + {% if ticket.entry and ticket.entry.start_at %} +
    + {{ ticket.entry.start_at.strftime('%A, %B %d, %Y at %H:%M') }} +
    + {% endif %} + {% if ticket.entry and ticket.entry.calendar %} +
    + {{ ticket.entry.calendar.name }} +
    + {% endif %} +
    + + {{ ticket.state|replace('_', ' ')|capitalize }} + + {{ ticket.code }} +
    + {% if ticket.checked_in_at %} +
    + Checked in: {{ ticket.checked_in_at.strftime('%B %d, %Y at %H:%M') }} +
    + {% endif %} +
    + +
    + {% if ticket.state in ('confirmed', 'reserved') %} +
    + + +
    + {% elif ticket.state == 'checked_in' %} +
    + +
    Checked In
    +
    + {% elif ticket.state == 'cancelled' %} +
    + +
    Cancelled
    +
    + {% endif %} +
    +
    +
    +{% endif %} diff --git a/events/templates/_types/ticket_admin/_main_panel.html b/events/templates/_types/ticket_admin/_main_panel.html new file mode 100644 index 0000000..43f367b --- /dev/null +++ b/events/templates/_types/ticket_admin/_main_panel.html @@ -0,0 +1,148 @@ +
    +

    Ticket Admin

    + + {# Stats row #} +
    +
    +
    {{ stats.total }}
    +
    Total
    +
    +
    +
    {{ stats.confirmed }}
    +
    Confirmed
    +
    +
    +
    {{ stats.checked_in }}
    +
    Checked In
    +
    +
    +
    {{ stats.reserved }}
    +
    Reserved
    +
    +
    + + {# Scanner section #} +
    +

    + + Scan / Look Up Ticket +

    + +
    + + +
    + +
    +
    + Enter a ticket code to look it up +
    +
    +
    + + {# Recent tickets table #} +
    +

    + Recent Tickets +

    + + {% if tickets %} +
    + + + + + + + + + + + + {% for ticket in tickets %} + + + + + + + + {% endfor %} + +
    CodeEventTypeStateActions
    + {{ ticket.code[:12] }}... + +
    {{ ticket.entry.name if ticket.entry else '—' }}
    + {% if ticket.entry and ticket.entry.start_at %} +
    + {{ ticket.entry.start_at.strftime('%d %b %Y, %H:%M') }} +
    + {% endif %} +
    + {{ ticket.ticket_type.name if ticket.ticket_type else '—' }} + + + {{ ticket.state|replace('_', ' ')|capitalize }} + + + {% if ticket.state in ('confirmed', 'reserved') %} +
    + + +
    + {% elif ticket.state == 'checked_in' %} + + + {% if ticket.checked_in_at %} + {{ ticket.checked_in_at.strftime('%H:%M') }} + {% endif %} + + {% endif %} +
    +
    + {% else %} +
    + No tickets yet +
    + {% endif %} +
    +
    diff --git a/events/templates/_types/ticket_admin/index.html b/events/templates/_types/ticket_admin/index.html new file mode 100644 index 0000000..47ecb0a --- /dev/null +++ b/events/templates/_types/ticket_admin/index.html @@ -0,0 +1,8 @@ +{% extends '_types/root/index.html' %} + +{% block _main_mobile_menu %} +{% endblock %} + +{% block content %} +{% include '_types/ticket_admin/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/ticket_type/_edit.html b/events/templates/_types/ticket_type/_edit.html new file mode 100644 index 0000000..67cec9e --- /dev/null +++ b/events/templates/_types/ticket_type/_edit.html @@ -0,0 +1,101 @@ +
    + +
    +
    + + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + +
    + + + +
    +
    +
    diff --git a/events/templates/_types/ticket_type/_main_panel.html b/events/templates/_types/ticket_type/_main_panel.html new file mode 100644 index 0000000..7805dc3 --- /dev/null +++ b/events/templates/_types/ticket_type/_main_panel.html @@ -0,0 +1,49 @@ +
    + +
    +
    +
    + Name +
    +
    + {{ ticket_type.name }} +
    +
    + +
    +
    + Cost +
    +
    + £{{ ('%.2f'|format(ticket_type.cost)) if ticket_type.cost is not none else '0.00' }} +
    +
    + +
    +
    + Count +
    +
    + {{ ticket_type.count }} +
    +
    +
    + + +
    diff --git a/events/templates/_types/ticket_type/_nav.html b/events/templates/_types/ticket_type/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/ticket_type/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/ticket_type/_oob_elements.html b/events/templates/_types/ticket_type/_oob_elements.html new file mode 100644 index 0000000..824e62a --- /dev/null +++ b/events/templates/_types/ticket_type/_oob_elements.html @@ -0,0 +1,18 @@ +{% extends "oob_elements.html" %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('ticket_types-header-child', 'ticket_type-header-child', '_types/ticket_type/header/_header.html')}} + + {% from '_types/ticket_types/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/ticket_type/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/ticket_type/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/ticket_type/header/_header.html b/events/templates/_types/ticket_type/header/_header.html new file mode 100644 index 0000000..9496cbc --- /dev/null +++ b/events/templates/_types/ticket_type/header/_header.html @@ -0,0 +1,32 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='ticket_type-row', oob=oob) %} + {% call links.link( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get', + calendar_slug=calendar.slug, + year=year, + month=month, + day=day, + entry_id=entry.id, + ticket_type_id=ticket_type.id + ), + hx_select_search, + ) %} +
    +
    + +
    + {{ ticket_type.name }} +
    +
    +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/ticket_type/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/ticket_type/index.html b/events/templates/_types/ticket_type/index.html new file mode 100644 index 0000000..245992c --- /dev/null +++ b/events/templates/_types/ticket_type/index.html @@ -0,0 +1,19 @@ +{% extends '_types/ticket_types/index.html' %} +{% import 'macros/layout.html' as layout %} + +{% block ticket_types_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('ticket_types-header-child', '_types/ticket_type/header/_header.html') %} + {% block ticket_type_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {#% include '_types/ticket_type/_nav.html' %#} +{% endblock %} + +{% block content %} + {% include '_types/ticket_type/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/ticket_types/_add.html b/events/templates/_types/ticket_types/_add.html new file mode 100644 index 0000000..cbea211 --- /dev/null +++ b/events/templates/_types/ticket_types/_add.html @@ -0,0 +1,85 @@ +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + + + +
    +
    diff --git a/events/templates/_types/ticket_types/_add_button.html b/events/templates/_types/ticket_types/_add_button.html new file mode 100644 index 0000000..6deeea9 --- /dev/null +++ b/events/templates/_types/ticket_types/_add_button.html @@ -0,0 +1,15 @@ + diff --git a/events/templates/_types/ticket_types/_main_panel.html b/events/templates/_types/ticket_types/_main_panel.html new file mode 100644 index 0000000..2afaa7a --- /dev/null +++ b/events/templates/_types/ticket_types/_main_panel.html @@ -0,0 +1,24 @@ +
    + + + + + + + + + + + {% for tt in ticket_types %} + {% include '_types/ticket_types/_row.html' %} + {% else %} + + {% endfor %} + +
    NameCostCountActions
    No ticket types yet.
    + + +
    + {% include '_types/ticket_types/_add_button.html' %} +
    +
    diff --git a/events/templates/_types/ticket_types/_nav.html b/events/templates/_types/ticket_types/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/events/templates/_types/ticket_types/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/events/templates/_types/ticket_types/_oob_elements.html b/events/templates/_types/ticket_types/_oob_elements.html new file mode 100644 index 0000000..a746f17 --- /dev/null +++ b/events/templates/_types/ticket_types/_oob_elements.html @@ -0,0 +1,18 @@ +{% extends "oob_elements.html" %} + +{% block oobs %} + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('entry-admin-header-child', 'ticket_types-header-child', '_types/ticket_types/header/_header.html')}} + + {% from '_types/entry/admin/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/ticket_types/_nav.html' %} +{% endblock %} + +{% block content %} + {% include '_types/ticket_types/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/ticket_types/_row.html b/events/templates/_types/ticket_types/_row.html new file mode 100644 index 0000000..0864844 --- /dev/null +++ b/events/templates/_types/ticket_types/_row.html @@ -0,0 +1,55 @@ +{% import 'macros/links.html' as links %} + + +
    + {% call links.link( + url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get', + calendar_slug=calendar.slug, + year=year, + month=month, + day=day, + entry_id=entry.id, + ticket_type_id=tt.id + ), + hx_select_search, + aclass=styles.pill + ) %} + {{ tt.name }} + {% endcall %} +
    + + + £{{ ('%.2f'|format(tt.cost)) if tt.cost is not none else '0.00' }} + + + {{ tt.count }} + + + + + diff --git a/events/templates/_types/ticket_types/header/_header.html b/events/templates/_types/ticket_types/header/_header.html new file mode 100644 index 0000000..2a95316 --- /dev/null +++ b/events/templates/_types/ticket_types/header/_header.html @@ -0,0 +1,24 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='ticket_types-row', oob=oob) %} + {% call links.link(url_for( + 'calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.get', + calendar_slug=calendar.slug, + entry_id=entry.id, + year=year, + month=month, + day=day + ), hx_select_search) %} + +
    + ticket types +
    + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/ticket_types/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/events/templates/_types/ticket_types/index.html b/events/templates/_types/ticket_types/index.html new file mode 100644 index 0000000..9d0362a --- /dev/null +++ b/events/templates/_types/ticket_types/index.html @@ -0,0 +1,20 @@ +{% extends '_types/entry/admin/index.html' %} + +{% block entry_admin_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('ticket_type-header-child', '_types/ticket_types/header/_header.html') %} + {% block ticket_types_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {% include '_types/ticket_types/_nav.html' %} +{% endblock %} + + + +{% block content %} + {% include '_types/ticket_types/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/tickets/_adjust_response.html b/events/templates/_types/tickets/_adjust_response.html new file mode 100644 index 0000000..24ec2fc --- /dev/null +++ b/events/templates/_types/tickets/_adjust_response.html @@ -0,0 +1,4 @@ +{# Response for ticket adjust — buy form + OOB cart-mini update #} +{% from 'macros/cart_icon.html' import cart_icon %} +{{ cart_icon(count=cart_count, oob='true') }} +{% include '_types/tickets/_buy_form.html' %} diff --git a/events/templates/_types/tickets/_buy_form.html b/events/templates/_types/tickets/_buy_form.html new file mode 100644 index 0000000..3cb981a --- /dev/null +++ b/events/templates/_types/tickets/_buy_form.html @@ -0,0 +1,206 @@ +{# Ticket purchase form — shown on entry detail when tickets are available #} +{% if entry.ticket_price is not none and entry.state == 'confirmed' %} +
    +

    + + Tickets +

    + + {# Sold / remaining info #} +
    + {% if ticket_sold_count is defined and ticket_sold_count %} + {{ ticket_sold_count }} sold + {% endif %} + {% if ticket_remaining is not none %} + {{ ticket_remaining }} remaining + {% endif %} + {% if user_ticket_count is defined and user_ticket_count %} + + + {{ user_ticket_count }} in basket + + {% endif %} +
    + + {% if entry.ticket_types %} + {# Multiple ticket types #} +
    + {% for tt in entry.ticket_types %} + {% if tt.deleted_at is none %} + {% set type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type is defined else 0 %} +
    +
    +
    {{ tt.name }}
    +
    + £{{ '%.2f'|format(tt.cost) }} +
    +
    + + {% if type_count == 0 %} + {# Add to basket button #} +
    + + + + + +
    + {% else %} + {# +/- controls #} +
    +
    + + + + + +
    + + + + + + + {{ type_count }} + + + + + +
    + + + + + +
    +
    + {% endif %} +
    + {% endif %} + {% endfor %} +
    + + {% else %} + {# Simple ticket (single price) #} +
    +
    + + £{{ '%.2f'|format(entry.ticket_price) }} + + per ticket +
    +
    + + {% set qty = user_ticket_count if user_ticket_count is defined else 0 %} + + {% if qty == 0 %} + {# Add to basket button #} +
    + + + + +
    + {% else %} + {# +/- controls #} +
    +
    + + + + +
    + + + + + + + {{ qty }} + + + + + +
    + + + + +
    +
    + {% endif %} + {% endif %} +
    +{% elif entry.ticket_price is not none %} + {# Tickets configured but entry not confirmed yet #} +
    + + Tickets available once this event is confirmed. +
    +{% endif %} diff --git a/events/templates/_types/tickets/_buy_result.html b/events/templates/_types/tickets/_buy_result.html new file mode 100644 index 0000000..900b098 --- /dev/null +++ b/events/templates/_types/tickets/_buy_result.html @@ -0,0 +1,43 @@ +{# Shown after ticket purchase — replaces the buy form #} +{# OOB: refresh cart badge to reflect new ticket count #} +{% from 'macros/cart_icon.html' import cart_icon %} +{{ cart_icon(count=cart_count|default(0), oob='true') }} + +
    +
    + + + {{ created_tickets|length }} ticket{{ 's' if created_tickets|length != 1 else '' }} reserved + +
    + +
    + {% for ticket in created_tickets %} + +
    + + {{ ticket.code[:12] }}... +
    + View ticket +
    + {% endfor %} +
    + + {% if remaining is not none %} +

    + {{ remaining }} ticket{{ 's' if remaining != 1 else '' }} remaining +

    + {% endif %} + + +
    diff --git a/events/templates/_types/tickets/_detail_panel.html b/events/templates/_types/tickets/_detail_panel.html new file mode 100644 index 0000000..75cde1a --- /dev/null +++ b/events/templates/_types/tickets/_detail_panel.html @@ -0,0 +1,124 @@ +
    + + {# Back link #} + + + Back to my tickets + + + {# Ticket card #} +
    + {# Header with state #} +
    +
    +

    + {{ ticket.entry.name if ticket.entry else 'Ticket' }} +

    + + {{ ticket.state|replace('_', ' ')|capitalize }} + +
    + {% if ticket.ticket_type %} +
    + {{ ticket.ticket_type.name }} +
    + {% endif %} +
    + + {# QR Code #} +
    +
    + {# QR code rendered via JavaScript #} +
    +

    + {{ ticket.code }} +

    +
    + + {# Event details #} +
    + {% if ticket.entry %} +
    + +
    +
    + {{ ticket.entry.start_at.strftime('%A, %B %d, %Y') }} +
    +
    + {{ ticket.entry.start_at.strftime('%H:%M') }} + {% if ticket.entry.end_at %} + – {{ ticket.entry.end_at.strftime('%H:%M') }} + {% endif %} +
    +
    +
    + + {% if ticket.entry.calendar %} +
    + +
    + {{ ticket.entry.calendar.name }} +
    +
    + {% endif %} + {% endif %} + + {% if ticket.ticket_type and ticket.ticket_type.cost %} +
    + +
    + {{ ticket.ticket_type.name }} — £{{ '%.2f'|format(ticket.ticket_type.cost) }} +
    +
    + {% endif %} + + {% if ticket.checked_in_at %} +
    + +
    + Checked in: {{ ticket.checked_in_at.strftime('%B %d, %Y at %H:%M') }} +
    +
    + {% endif %} +
    +
    + + {# QR code generation script #} + + +
    diff --git a/events/templates/_types/tickets/_main_panel.html b/events/templates/_types/tickets/_main_panel.html new file mode 100644 index 0000000..15b40d9 --- /dev/null +++ b/events/templates/_types/tickets/_main_panel.html @@ -0,0 +1,65 @@ +
    +

    My Tickets

    + + {% if tickets %} + + {% else %} +
    + +

    No tickets yet

    +

    Tickets will appear here after you purchase them.

    +
    + {% endif %} +
    diff --git a/events/templates/_types/tickets/detail.html b/events/templates/_types/tickets/detail.html new file mode 100644 index 0000000..31c9319 --- /dev/null +++ b/events/templates/_types/tickets/detail.html @@ -0,0 +1,8 @@ +{% extends '_types/root/index.html' %} + +{% block _main_mobile_menu %} +{% endblock %} + +{% block content %} +{% include '_types/tickets/_detail_panel.html' %} +{% endblock %} diff --git a/events/templates/_types/tickets/index.html b/events/templates/_types/tickets/index.html new file mode 100644 index 0000000..908be8b --- /dev/null +++ b/events/templates/_types/tickets/index.html @@ -0,0 +1,8 @@ +{% extends '_types/root/index.html' %} + +{% block _main_mobile_menu %} +{% endblock %} + +{% block content %} +{% include '_types/tickets/_main_panel.html' %} +{% endblock %} diff --git a/events/templates/fragments/account_nav_items.html b/events/templates/fragments/account_nav_items.html new file mode 100644 index 0000000..f1d9ca3 --- /dev/null +++ b/events/templates/fragments/account_nav_items.html @@ -0,0 +1,23 @@ +{# Account nav items: tickets + bookings links for the account dashboard #} + + diff --git a/events/templates/fragments/account_page_bookings.html b/events/templates/fragments/account_page_bookings.html new file mode 100644 index 0000000..28f8280 --- /dev/null +++ b/events/templates/fragments/account_page_bookings.html @@ -0,0 +1,44 @@ +
    +
    + +

    Bookings

    + + {% if bookings %} +
    + {% for booking in bookings %} +
    +
    +
    +

    {{ booking.name }}

    +
    + {{ booking.start_at.strftime('%d %b %Y, %H:%M') }} + {% if booking.end_at %} + – {{ booking.end_at.strftime('%H:%M') }} + {% endif %} + {% if booking.calendar_name %} + · {{ booking.calendar_name }} + {% endif %} + {% if booking.cost %} + · £{{ booking.cost }} + {% endif %} +
    +
    +
    + {% if booking.state == 'confirmed' %} + confirmed + {% elif booking.state == 'provisional' %} + provisional + {% else %} + {{ booking.state }} + {% endif %} +
    +
    +
    + {% endfor %} +
    + {% else %} +

    No bookings yet.

    + {% endif %} + +
    +
    diff --git a/events/templates/fragments/account_page_tickets.html b/events/templates/fragments/account_page_tickets.html new file mode 100644 index 0000000..69f7596 --- /dev/null +++ b/events/templates/fragments/account_page_tickets.html @@ -0,0 +1,44 @@ +
    +
    + +

    Tickets

    + + {% if tickets %} +
    + {% for ticket in tickets %} +
    +
    +
    + + {{ ticket.entry_name }} + +
    + {{ ticket.entry_start_at.strftime('%d %b %Y, %H:%M') }} + {% if ticket.calendar_name %} + · {{ ticket.calendar_name }} + {% endif %} + {% if ticket.ticket_type_name %} + · {{ ticket.ticket_type_name }} + {% endif %} +
    +
    +
    + {% if ticket.state == 'checked_in' %} + checked in + {% elif ticket.state == 'confirmed' %} + confirmed + {% else %} + {{ ticket.state }} + {% endif %} +
    +
    +
    + {% endfor %} +
    + {% else %} +

    No tickets yet.

    + {% endif %} + +
    +
    diff --git a/events/templates/fragments/container_cards_entries.html b/events/templates/fragments/container_cards_entries.html new file mode 100644 index 0000000..53ce49f --- /dev/null +++ b/events/templates/fragments/container_cards_entries.html @@ -0,0 +1,33 @@ +{# Calendar entries for blog listing cards — served as fragment from events app. + Each post's entries are delimited by comment markers so the consumer can + extract per-post HTML via simple string splitting. #} +{% for post_id in post_ids %} + +{% set widget_entries = batch.get(post_id, []) %} +{% if widget_entries %} +
    +

    Events:

    +
    +
    + {% for entry in widget_entries %} + {% set _post_slug = slug_map.get(post_id, '') %} + {% set _entry_path = '/' + _post_slug + '/calendars/' + entry.calendar_slug + '/' + entry.start_at.year|string + '/' + entry.start_at.month|string + '/' + entry.start_at.day|string + '/entries/' + entry.id|string + '/' %} + +
    {{ entry.name }}
    +
    + {{ entry.start_at.strftime('%a, %b %d') }} +
    +
    + {{ entry.start_at.strftime('%H:%M') }} + {% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    +
    + {% endfor %} +
    +
    +
    +{% endif %} + +{% endfor %} diff --git a/events/templates/fragments/container_nav_calendars.html b/events/templates/fragments/container_nav_calendars.html new file mode 100644 index 0000000..cdf50e3 --- /dev/null +++ b/events/templates/fragments/container_nav_calendars.html @@ -0,0 +1,10 @@ +{# Calendar links nav — served as fragment from events app #} +{% for calendar in calendars %} + {% set local_href=events_url('/' + post_slug + '/calendars/' + calendar.slug + '/') %} + + +
    {{calendar.name}}
    +
    +{% endfor %} diff --git a/events/templates/fragments/container_nav_entries.html b/events/templates/fragments/container_nav_entries.html new file mode 100644 index 0000000..d217565 --- /dev/null +++ b/events/templates/fragments/container_nav_entries.html @@ -0,0 +1,28 @@ +{# Calendar entries nav — served as fragment from events app #} +{% for entry in entries %} + {% set _entry_path = '/' + post_slug + '/calendars/' + entry.calendar_slug + '/' + entry.start_at.year|string + '/' + entry.start_at.month|string + '/' + entry.start_at.day|string + '/entries/' + entry.id|string + '/' %} + +
    +
    +
    {{ entry.name }}
    +
    + {{ entry.start_at.strftime('%b %d, %Y at %H:%M') }} + {% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
    +
    +
    +{% endfor %} + +{# Infinite scroll sentinel — URL points back to the consumer app #} +{% if has_more and paginate_url_base %} +
    +
    +{% endif %} diff --git a/events/templates/macros/date.html b/events/templates/macros/date.html new file mode 100644 index 0000000..5954f28 --- /dev/null +++ b/events/templates/macros/date.html @@ -0,0 +1,7 @@ +{% macro dt(d) -%} +{{ d.astimezone().strftime('%-d %b %Y, %H:%M') if d.tzinfo else d.strftime('%-d %b %Y, %H:%M') }} +{%- endmacro %} + +{% macro t(d) -%} +{{ d.astimezone().strftime('%H:%M') if d.tzinfo else d.strftime('%H:%M') }} +{%- endmacro %} diff --git a/federation/.gitignore b/federation/.gitignore new file mode 100644 index 0000000..87d616e --- /dev/null +++ b/federation/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +*.pyo +.env +node_modules/ +*.egg-info/ +dist/ +build/ +.venv/ diff --git a/federation/Dockerfile b/federation/Dockerfile new file mode 100644 index 0000000..e961f11 --- /dev/null +++ b/federation/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# ---------- Python application ---------- +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + PIP_NO_CACHE_DIR=1 \ + APP_PORT=8000 \ + APP_MODULE=app:app + +WORKDIR /app + +# Install system deps + psql client +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +COPY shared/requirements.txt ./requirements.txt +RUN pip install -r requirements.txt + +# Shared code (replaces submodule) +COPY shared/ ./shared/ + +# App code +COPY federation/ ./ + +# Sibling models for cross-domain SQLAlchemy imports +COPY blog/__init__.py ./blog/__init__.py +COPY blog/models/ ./blog/models/ +COPY market/__init__.py ./market/__init__.py +COPY market/models/ ./market/models/ +COPY cart/__init__.py ./cart/__init__.py +COPY cart/models/ ./cart/models/ +COPY events/__init__.py ./events/__init__.py +COPY events/models/ ./events/models/ +COPY account/__init__.py ./account/__init__.py +COPY account/models/ ./account/models/ + +# ---------- Runtime setup ---------- +COPY federation/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +RUN useradd -m -u 10001 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE ${APP_PORT} +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/federation/__init__.py b/federation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/federation/app.py b/federation/app.py new file mode 100644 index 0000000..ac8c6d0 --- /dev/null +++ b/federation/app.py @@ -0,0 +1,84 @@ +from __future__ import annotations +import path_setup # noqa: F401 # adds shared/ to sys.path +from pathlib import Path + +from quart import g, request +from jinja2 import FileSystemLoader, ChoiceLoader + +from shared.infrastructure.factory import create_base_app +from shared.services.registry import services + +from bp import ( + register_identity_bp, + register_social_bp, + register_fragments, +) + + +async def federation_context() -> dict: + """Federation app context processor.""" + from shared.infrastructure.context import base_context + from shared.services.navigation import get_navigation_tree + from shared.infrastructure.cart_identity import current_cart_identity + from shared.infrastructure.fragments import fetch_fragment + + ctx = await base_context() + + ctx["nav_tree_html"] = await fetch_fragment( + "blog", "nav-tree", + params={"app_name": "federation", "path": request.path}, + ) + # Fallback for _nav.html when nav-tree fragment fetch fails + ctx["menu_items"] = await get_navigation_tree(g.s) + + # Cart data (consistent with all other apps) + ident = current_cart_identity() + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count + ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total) + + # Actor profile for logged-in users + if g.get("user"): + actor = await services.federation.get_actor_by_user_id(g.s, g.user.id) + ctx["actor"] = actor + else: + ctx["actor"] = None + + return ctx + + +def create_app() -> "Quart": + from services import register_domain_services + + app = create_base_app( + "federation", + context_fn=federation_context, + domain_services_fn=register_domain_services, + ) + + # App-specific templates override shared templates + app_templates = str(Path(__file__).resolve().parent / "templates") + app.jinja_loader = ChoiceLoader([ + FileSystemLoader(app_templates), + app.jinja_loader, + ]) + + # --- blueprints --- + # Well-known + actors (webfinger, inbox, outbox, etc.) are now handled + # by the shared AP blueprint registered in create_base_app(). + app.register_blueprint(register_identity_bp()) + app.register_blueprint(register_social_bp()) + app.register_blueprint(register_fragments()) + + # --- home page --- + @app.get("/") + async def home(): + from quart import render_template + return await render_template("_types/federation/index.html") + + return app + + +app = create_app() diff --git a/federation/bp/__init__.py b/federation/bp/__init__.py new file mode 100644 index 0000000..1be06bb --- /dev/null +++ b/federation/bp/__init__.py @@ -0,0 +1,3 @@ +from .identity.routes import register as register_identity_bp +from .social.routes import register as register_social_bp +from .fragments import register_fragments diff --git a/federation/bp/auth/__init__.py b/federation/bp/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/federation/bp/auth/routes.py b/federation/bp/auth/routes.py new file mode 100644 index 0000000..6b33175 --- /dev/null +++ b/federation/bp/auth/routes.py @@ -0,0 +1,232 @@ +"""Authentication routes for the federation app. + +Owns magic link login/logout + OAuth2 authorization server endpoint. +Account pages (newsletters, widget pages) have moved to the account app. +""" +from __future__ import annotations + +import secrets +from datetime import datetime, timezone, timedelta + +from quart import ( + Blueprint, + request, + render_template, + redirect, + url_for, + session as qsession, + g, + current_app, +) +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError + +from shared.db.session import get_session +from shared.models import User +from shared.models.oauth_code import OAuthCode +from shared.infrastructure.urls import federation_url, app_url +from shared.infrastructure.cart_identity import current_cart_identity +from shared.events import emit_activity + +from .services import ( + pop_login_redirect_target, + store_login_redirect_target, + send_magic_email, + find_or_create_user, + create_magic_link, + validate_magic_link, + validate_email, +) + +SESSION_USER_KEY = "uid" + +ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "account"} + + +def register(url_prefix="/auth"): + auth_bp = Blueprint("auth", __name__, url_prefix=url_prefix) + + # --- OAuth2 authorize endpoint ------------------------------------------- + + @auth_bp.get("/oauth/authorize") + @auth_bp.get("/oauth/authorize/") + async def oauth_authorize(): + client_id = request.args.get("client_id", "") + redirect_uri = request.args.get("redirect_uri", "") + state = request.args.get("state", "") + + if client_id not in ALLOWED_CLIENTS: + return "Invalid client_id", 400 + + expected_redirect = app_url(client_id, "/auth/callback") + if redirect_uri != expected_redirect: + return "Invalid redirect_uri", 400 + + # Not logged in — bounce to magic link login, then back here + if not g.get("user"): + # Preserve the full authorize URL so we return here after login + authorize_path = request.full_path # includes query string + store_login_redirect_target() + return redirect(url_for("auth.login_form", next=authorize_path)) + + # Logged in — issue authorization code + code = secrets.token_urlsafe(48) + now = datetime.now(timezone.utc) + expires = now + timedelta(minutes=5) + + async with get_session() as s: + async with s.begin(): + oauth_code = OAuthCode( + code=code, + user_id=g.user.id, + client_id=client_id, + redirect_uri=redirect_uri, + expires_at=expires, + ) + s.add(oauth_code) + + sep = "&" if "?" in redirect_uri else "?" + return redirect(f"{redirect_uri}{sep}code={code}&state={state}") + + # --- Magic link login flow ----------------------------------------------- + + @auth_bp.get("/login/") + async def login_form(): + store_login_redirect_target() + cross_cart_sid = request.args.get("cart_sid") + if cross_cart_sid: + qsession["cart_sid"] = cross_cart_sid + if g.get("user"): + # If there's a pending redirect (e.g. OAuth authorize), follow it + redirect_url = pop_login_redirect_target() + return redirect(redirect_url) + return await render_template("auth/login.html") + + @auth_bp.post("/start/") + async def start_login(): + form = await request.form + email_input = form.get("email") or "" + + is_valid, email = validate_email(email_input) + if not is_valid: + return ( + await render_template( + "auth/login.html", + error="Please enter a valid email address.", + email=email_input, + ), + 400, + ) + + user = await find_or_create_user(g.s, email) + token, expires = await create_magic_link(g.s, user.id) + + from shared.utils import host_url + magic_url = host_url(url_for("auth.magic", token=token)) + + email_error = None + try: + await send_magic_email(email, magic_url) + except Exception as e: + current_app.logger.error("EMAIL SEND FAILED: %r", e) + email_error = ( + "We couldn't send the email automatically. " + "Please try again in a moment." + ) + + return await render_template( + "auth/check_email.html", + email=email, + email_error=email_error, + ) + + @auth_bp.get("/magic//") + async def magic(token: str): + now = datetime.now(timezone.utc) + user_id: int | None = None + + try: + async with get_session() as s: + async with s.begin(): + user, error = await validate_magic_link(s, token) + + if error: + return ( + await render_template("auth/login.html", error=error), + 400, + ) + user_id = user.id + + except Exception: + return ( + await render_template( + "auth/login.html", + error="Could not sign you in right now. Please try again.", + ), + 502, + ) + + assert user_id is not None + + ident = current_cart_identity() + anon_session_id = ident.get("session_id") + + try: + async with get_session() as s: + async with s.begin(): + u2 = await s.get(User, user_id) + if u2: + u2.last_login_at = now + if anon_session_id: + await emit_activity( + s, + activity_type="rose:Login", + actor_uri="internal:system", + object_type="Person", + object_data={ + "user_id": user_id, + "session_id": anon_session_id, + }, + ) + except SQLAlchemyError: + current_app.logger.exception( + "[auth] non-fatal DB update for user_id=%s", user_id + ) + + qsession[SESSION_USER_KEY] = user_id + + redirect_url = pop_login_redirect_target() + resp = redirect(redirect_url, 303) + resp.set_cookie( + "sso_hint", "1", + domain=".rose-ash.com", max_age=30 * 24 * 3600, + secure=True, samesite="Lax", httponly=True, + ) + return resp + + @auth_bp.post("/logout/") + async def logout(): + qsession.pop(SESSION_USER_KEY, None) + resp = redirect(federation_url("/")) + resp.delete_cookie("sso_hint", domain=".rose-ash.com", path="/") + return resp + + @auth_bp.get("/clear/") + async def clear(): + """One-time migration helper: clear all session cookies.""" + qsession.clear() + resp = redirect(federation_url("/")) + resp.delete_cookie("blog_session", domain=".rose-ash.com", path="/") + resp.delete_cookie("sso_hint", domain=".rose-ash.com", path="/") + return resp + + @auth_bp.get("/sso-logout/") + async def sso_logout(): + """SSO logout: clear federation session + sso_hint, redirect to blog.""" + qsession.pop(SESSION_USER_KEY, None) + from shared.infrastructure.urls import blog_url + resp = redirect(blog_url("/")) + resp.delete_cookie("sso_hint", domain=".rose-ash.com", path="/") + return resp + + return auth_bp diff --git a/federation/bp/auth/services/__init__.py b/federation/bp/auth/services/__init__.py new file mode 100644 index 0000000..648f87d --- /dev/null +++ b/federation/bp/auth/services/__init__.py @@ -0,0 +1,24 @@ +from .login_redirect import pop_login_redirect_target, store_login_redirect_target +from .auth_operations import ( + get_app_host, + get_app_root, + send_magic_email, + load_user_by_id, + find_or_create_user, + create_magic_link, + validate_magic_link, + validate_email, +) + +__all__ = [ + "pop_login_redirect_target", + "store_login_redirect_target", + "get_app_host", + "get_app_root", + "send_magic_email", + "load_user_by_id", + "find_or_create_user", + "create_magic_link", + "validate_magic_link", + "validate_email", +] diff --git a/federation/bp/auth/services/auth_operations.py b/federation/bp/auth/services/auth_operations.py new file mode 100644 index 0000000..d9f4487 --- /dev/null +++ b/federation/bp/auth/services/auth_operations.py @@ -0,0 +1,157 @@ +"""Auth operations for the federation app. + +Copied from blog/bp/auth/services/auth_operations.py to avoid cross-app +import chains. The logic is identical — shared models, shared config. +""" +from __future__ import annotations + +import os +import secrets +from datetime import datetime, timedelta, timezone +from typing import Optional, Tuple + +from quart import current_app, render_template, request, g +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from shared.models import User, MagicLink +from shared.config import config + + +def get_app_host() -> str: + host = ( + config().get("host") or os.getenv("APP_HOST") or "http://localhost:8000" + ).rstrip("/") + return host + + +def get_app_root() -> str: + root = (g.root).rstrip("/") + return root + + +async def send_magic_email(to_email: str, link_url: str) -> None: + host = os.getenv("SMTP_HOST") + port = int(os.getenv("SMTP_PORT") or "587") + username = os.getenv("SMTP_USER") + password = os.getenv("SMTP_PASS") + mail_from = os.getenv("MAIL_FROM") or "no-reply@example.com" + + site_name = config().get("title", "Rose Ash") + subject = f"Your sign-in link \u2014 {site_name}" + + tpl_vars = dict(site_name=site_name, link_url=link_url) + text_body = await render_template("_email/magic_link.txt", **tpl_vars) + html_body = await render_template("_email/magic_link.html", **tpl_vars) + + if not host or not username or not password: + current_app.logger.warning( + "SMTP not configured. Printing magic link to console for %s: %s", + to_email, + link_url, + ) + print(f"[DEV] Magic link for {to_email}: {link_url}") + return + + import aiosmtplib + from email.message import EmailMessage + + msg = EmailMessage() + msg["From"] = mail_from + msg["To"] = to_email + msg["Subject"] = subject + msg.set_content(text_body) + msg.add_alternative(html_body, subtype="html") + + is_secure = port == 465 + if is_secure: + smtp = aiosmtplib.SMTP( + hostname=host, port=port, use_tls=True, + username=username, password=password, + ) + else: + smtp = aiosmtplib.SMTP( + hostname=host, port=port, start_tls=True, + username=username, password=password, + ) + + async with smtp: + await smtp.send_message(msg) + + +async def load_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]: + stmt = ( + select(User) + .options(selectinload(User.labels)) + .where(User.id == user_id) + ) + result = await session.execute(stmt) + return result.scalar_one_or_none() + + +async def find_or_create_user(session: AsyncSession, email: str) -> User: + result = await session.execute(select(User).where(User.email == email)) + user = result.scalar_one_or_none() + + if user is None: + user = User(email=email) + session.add(user) + await session.flush() + + return user + + +async def create_magic_link( + session: AsyncSession, + user_id: int, + purpose: str = "signin", + expires_minutes: int = 15, +) -> Tuple[str, datetime]: + token = secrets.token_urlsafe(32) + expires = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes) + + ml = MagicLink( + token=token, + user_id=user_id, + purpose=purpose, + expires_at=expires, + ip=request.headers.get("x-forwarded-for", request.remote_addr), + user_agent=request.headers.get("user-agent"), + ) + session.add(ml) + + return token, expires + + +async def validate_magic_link( + session: AsyncSession, + token: str, +) -> Tuple[Optional[User], Optional[str]]: + now = datetime.now(timezone.utc) + + ml = await session.scalar( + select(MagicLink) + .where(MagicLink.token == token) + .with_for_update() + ) + + if not ml or ml.purpose != "signin": + return None, "Invalid or expired link." + + if ml.used_at or ml.expires_at < now: + return None, "This link has expired. Please request a new one." + + user = await session.get(User, ml.user_id) + if not user: + return None, "User not found." + + ml.used_at = now + return user, None + + +def validate_email(email: str) -> Tuple[bool, str]: + email = email.strip().lower() + if not email or "@" not in email: + return False, email + return True, email diff --git a/federation/bp/auth/services/login_redirect.py b/federation/bp/auth/services/login_redirect.py new file mode 100644 index 0000000..aff43d9 --- /dev/null +++ b/federation/bp/auth/services/login_redirect.py @@ -0,0 +1,45 @@ +from urllib.parse import urlparse +from quart import session + +from shared.infrastructure.urls import federation_url + + +LOGIN_REDIRECT_SESSION_KEY = "login_redirect_to" + + +def store_login_redirect_target() -> None: + from quart import request + + target = request.args.get("next") + if not target: + ref = request.referrer or "" + try: + parsed = urlparse(ref) + target = parsed.path or "" + except Exception: + target = "" + + if not target: + return + + # Accept both relative paths and absolute URLs (cross-app redirects) + if target.startswith("http://") or target.startswith("https://"): + session[LOGIN_REDIRECT_SESSION_KEY] = target + elif target.startswith("/") and not target.startswith("//"): + session[LOGIN_REDIRECT_SESSION_KEY] = target + + +def pop_login_redirect_target() -> str: + path = session.pop(LOGIN_REDIRECT_SESSION_KEY, None) + if not path or not isinstance(path, str): + return federation_url("/") + + # Absolute URL: return as-is (cross-app redirect) + if path.startswith("http://") or path.startswith("https://"): + return path + + # Relative path: must start with / and not // + if path.startswith("/") and not path.startswith("//"): + return federation_url(path) + + return federation_url("/") diff --git a/federation/bp/fragments/__init__.py b/federation/bp/fragments/__init__.py new file mode 100644 index 0000000..a4af44b --- /dev/null +++ b/federation/bp/fragments/__init__.py @@ -0,0 +1 @@ +from .routes import register as register_fragments diff --git a/federation/bp/fragments/routes.py b/federation/bp/fragments/routes.py new file mode 100644 index 0000000..d4e20d1 --- /dev/null +++ b/federation/bp/fragments/routes.py @@ -0,0 +1,34 @@ +"""Federation app fragment endpoints. + +Exposes HTML fragments at ``/internal/fragments/`` for consumption +by other coop apps via the fragment client. +""" + +from __future__ import annotations + +from quart import Blueprint, Response, request + +from shared.infrastructure.fragments import FRAGMENT_HEADER + + +def register(): + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + _handlers: dict[str, object] = {} + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.get("/") + async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/html") + html = await handler() + return Response(html, status=200, content_type="text/html") + + bp._fragment_handlers = _handlers + + return bp diff --git a/federation/bp/identity/__init__.py b/federation/bp/identity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/federation/bp/identity/routes.py b/federation/bp/identity/routes.py new file mode 100644 index 0000000..b445eda --- /dev/null +++ b/federation/bp/identity/routes.py @@ -0,0 +1,108 @@ +"""Username selection flow. + +Users must choose a preferred_username before they can publish. +This creates their ActorProfile with RSA keys. +""" +from __future__ import annotations + +import re + +from quart import ( + Blueprint, request, render_template, redirect, url_for, g, abort, +) + +from shared.services.registry import services + + +# Username rules: 3-32 chars, lowercase alphanumeric + underscores +USERNAME_RE = re.compile(r"^[a-z][a-z0-9_]{2,31}$") + +# Reserved usernames +RESERVED = frozenset({ + "admin", "administrator", "root", "system", "moderator", "mod", + "support", "help", "info", "postmaster", "webmaster", "abuse", + "federation", "activitypub", "api", "static", "media", "assets", + "well-known", "nodeinfo", "inbox", "outbox", "followers", "following", +}) + + +def register(url_prefix="/identity"): + bp = Blueprint("identity", __name__, url_prefix=url_prefix) + + @bp.get("/choose-username") + async def choose_username_form(): + if not g.get("user"): + return redirect(url_for("auth.login_form")) + + # Already has a username? + actor = await services.federation.get_actor_by_user_id(g.s, g.user.id) + if actor: + return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username)) + + return await render_template("federation/choose_username.html") + + @bp.post("/choose-username") + async def choose_username(): + if not g.get("user"): + abort(401) + + # Already has a username? + existing = await services.federation.get_actor_by_user_id(g.s, g.user.id) + if existing: + return redirect(url_for("activitypub.actor_profile", username=existing.preferred_username)) + + form = await request.form + username = (form.get("username") or "").strip().lower() + + # Validate format + error = None + if not USERNAME_RE.match(username): + error = ( + "Username must be 3-32 characters, start with a letter, " + "and contain only lowercase letters, numbers, and underscores." + ) + elif username in RESERVED: + error = "This username is reserved." + elif not await services.federation.username_available(g.s, username): + error = "This username is already taken." + + if error: + return await render_template( + "federation/choose_username.html", + error=error, + username=username, + ), 400 + + # Create ActorProfile with RSA keys + display_name = g.user.name or username + actor = await services.federation.create_actor( + g.s, g.user.id, username, + display_name=display_name, + ) + + # Redirect to where they were going, or their new profile + next_url = request.args.get("next") + if next_url: + return redirect(next_url) + return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username)) + + @bp.get("/check-username") + async def check_username(): + """HTMX endpoint to check username availability.""" + username = (request.args.get("username") or "").strip().lower() + + if not username: + return "" + + if not USERNAME_RE.match(username): + return 'Invalid format' + + if username in RESERVED: + return 'Reserved' + + available = await services.federation.username_available(g.s, username) + if available: + return 'Available' + return 'Taken' + + return bp diff --git a/federation/bp/social/__init__.py b/federation/bp/social/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/federation/bp/social/routes.py b/federation/bp/social/routes.py new file mode 100644 index 0000000..7878156 --- /dev/null +++ b/federation/bp/social/routes.py @@ -0,0 +1,499 @@ +"""Social fediverse routes: timeline, compose, search, follow, interactions, notifications.""" +from __future__ import annotations + +import logging +from datetime import datetime + +from quart import Blueprint, request, g, redirect, url_for, abort, render_template, Response + +from shared.services.registry import services + +log = logging.getLogger(__name__) + + +def _require_actor(): + """Return actor context or abort 403.""" + actor = g.get("ctx", {}).get("actor") if hasattr(g, "ctx") else None + if not actor: + actor = getattr(g, "_social_actor", None) + if not actor: + abort(403, "You need to choose a federation username first") + return actor + + +def register(url_prefix="/social"): + bp = Blueprint("social", __name__, url_prefix=url_prefix) + + @bp.before_request + async def load_actor(): + """Load actor profile for authenticated users.""" + if g.get("user"): + actor = await services.federation.get_actor_by_user_id(g.s, g.user.id) + g._social_actor = actor + + # -- Timeline ------------------------------------------------------------- + + @bp.get("/") + async def home_timeline(): + if not g.get("user"): + return redirect(url_for("auth.login_form")) + actor = _require_actor() + items = await services.federation.get_home_timeline(g.s, actor.id) + return await render_template( + "federation/timeline.html", + items=items, + timeline_type="home", + actor=actor, + ) + + @bp.get("/timeline") + async def home_timeline_page(): + actor = _require_actor() + before_str = request.args.get("before") + before = None + if before_str: + try: + before = datetime.fromisoformat(before_str) + except ValueError: + pass + items = await services.federation.get_home_timeline( + g.s, actor.id, before=before, + ) + return await render_template( + "federation/_timeline_items.html", + items=items, + timeline_type="home", + actor=actor, + ) + + @bp.get("/public") + async def public_timeline(): + items = await services.federation.get_public_timeline(g.s) + actor = getattr(g, "_social_actor", None) + return await render_template( + "federation/timeline.html", + items=items, + timeline_type="public", + actor=actor, + ) + + @bp.get("/public/timeline") + async def public_timeline_page(): + before_str = request.args.get("before") + before = None + if before_str: + try: + before = datetime.fromisoformat(before_str) + except ValueError: + pass + items = await services.federation.get_public_timeline(g.s, before=before) + actor = getattr(g, "_social_actor", None) + return await render_template( + "federation/_timeline_items.html", + items=items, + timeline_type="public", + actor=actor, + ) + + # -- Compose -------------------------------------------------------------- + + @bp.get("/compose") + async def compose_form(): + actor = _require_actor() + reply_to = request.args.get("reply_to") + return await render_template( + "federation/compose.html", + actor=actor, + reply_to=reply_to, + ) + + @bp.post("/compose") + async def compose_submit(): + actor = _require_actor() + form = await request.form + content = form.get("content", "").strip() + if not content: + return redirect(url_for("social.compose_form")) + + visibility = form.get("visibility", "public") + in_reply_to = form.get("in_reply_to") or None + + await services.federation.create_local_post( + g.s, actor.id, + content=content, + visibility=visibility, + in_reply_to=in_reply_to, + ) + return redirect(url_for("social.home_timeline")) + + @bp.post("/delete/") + async def delete_post(post_id: int): + actor = _require_actor() + await services.federation.delete_local_post(g.s, actor.id, post_id) + return redirect(url_for("social.home_timeline")) + + # -- Search + Follow ------------------------------------------------------ + + @bp.get("/search") + async def search(): + actor = getattr(g, "_social_actor", None) + query = request.args.get("q", "").strip() + actors = [] + total = 0 + followed_urls: set[str] = set() + if query: + actors, total = await services.federation.search_actors(g.s, query) + if actor: + following, _ = await services.federation.get_following( + g.s, actor.preferred_username, page=1, per_page=1000, + ) + followed_urls = {a.actor_url for a in following} + return await render_template( + "federation/search.html", + query=query, + actors=actors, + total=total, + page=1, + followed_urls=followed_urls, + actor=actor, + ) + + @bp.get("/search/page") + async def search_page(): + actor = getattr(g, "_social_actor", None) + query = request.args.get("q", "").strip() + page = request.args.get("page", 1, type=int) + actors = [] + total = 0 + followed_urls: set[str] = set() + if query: + actors, total = await services.federation.search_actors( + g.s, query, page=page, + ) + if actor: + following, _ = await services.federation.get_following( + g.s, actor.preferred_username, page=1, per_page=1000, + ) + followed_urls = {a.actor_url for a in following} + return await render_template( + "federation/_search_results.html", + actors=actors, + total=total, + page=page, + query=query, + followed_urls=followed_urls, + actor=actor, + ) + + @bp.post("/follow") + async def follow(): + actor = _require_actor() + form = await request.form + remote_actor_url = form.get("actor_url", "") + if remote_actor_url: + await services.federation.send_follow( + g.s, actor.preferred_username, remote_actor_url, + ) + if request.headers.get("HX-Request"): + return await _actor_card_response(actor, remote_actor_url, is_followed=True) + return redirect(request.referrer or url_for("social.search")) + + @bp.post("/unfollow") + async def unfollow(): + actor = _require_actor() + form = await request.form + remote_actor_url = form.get("actor_url", "") + if remote_actor_url: + await services.federation.unfollow( + g.s, actor.preferred_username, remote_actor_url, + ) + if request.headers.get("HX-Request"): + return await _actor_card_response(actor, remote_actor_url, is_followed=False) + return redirect(request.referrer or url_for("social.search")) + + async def _actor_card_response(actor, remote_actor_url, is_followed): + """Re-render a single actor card after follow/unfollow via HTMX.""" + remote_dto = await services.federation.get_or_fetch_remote_actor( + g.s, remote_actor_url, + ) + if not remote_dto: + return Response("", status=200) + followed_urls = {remote_actor_url} if is_followed else set() + # Detect list context from referer + referer = request.referrer or "" + if "/followers" in referer: + list_type = "followers" + else: + list_type = "following" + return await render_template( + "federation/_actor_list_items.html", + actors=[remote_dto], + total=0, + page=1, + list_type=list_type, + followed_urls=followed_urls, + actor=actor, + ) + + # -- Interactions --------------------------------------------------------- + + @bp.post("/like") + async def like(): + actor = _require_actor() + form = await request.form + object_id = form.get("object_id", "") + author_inbox = form.get("author_inbox", "") + await services.federation.like_post(g.s, actor.id, object_id, author_inbox) + # Return updated buttons for HTMX + return await _interaction_buttons_response(actor, object_id, author_inbox) + + @bp.post("/unlike") + async def unlike(): + actor = _require_actor() + form = await request.form + object_id = form.get("object_id", "") + author_inbox = form.get("author_inbox", "") + await services.federation.unlike_post(g.s, actor.id, object_id, author_inbox) + return await _interaction_buttons_response(actor, object_id, author_inbox) + + @bp.post("/boost") + async def boost(): + actor = _require_actor() + form = await request.form + object_id = form.get("object_id", "") + author_inbox = form.get("author_inbox", "") + await services.federation.boost_post(g.s, actor.id, object_id, author_inbox) + return await _interaction_buttons_response(actor, object_id, author_inbox) + + @bp.post("/unboost") + async def unboost(): + actor = _require_actor() + form = await request.form + object_id = form.get("object_id", "") + author_inbox = form.get("author_inbox", "") + await services.federation.unboost_post(g.s, actor.id, object_id, author_inbox) + return await _interaction_buttons_response(actor, object_id, author_inbox) + + async def _interaction_buttons_response(actor, object_id, author_inbox): + """Re-render interaction buttons after a like/boost action.""" + from shared.models.federation import APInteraction, APRemotePost, APActivity + from sqlalchemy import select + from shared.services.federation_impl import SqlFederationService + + svc = services.federation + post_type, post_id = await svc._resolve_post(g.s, object_id) + + like_count = 0 + boost_count = 0 + liked_by_me = False + boosted_by_me = False + + if post_type: + from sqlalchemy import func as sa_func + like_count = (await g.s.execute( + select(sa_func.count(APInteraction.id)).where( + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "like", + ) + )).scalar() or 0 + boost_count = (await g.s.execute( + select(sa_func.count(APInteraction.id)).where( + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "boost", + ) + )).scalar() or 0 + liked_by_me = bool((await g.s.execute( + select(APInteraction.id).where( + APInteraction.actor_profile_id == actor.id, + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "like", + ).limit(1) + )).scalar()) + boosted_by_me = bool((await g.s.execute( + select(APInteraction.id).where( + APInteraction.actor_profile_id == actor.id, + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "boost", + ).limit(1) + )).scalar()) + + return await render_template( + "federation/_interaction_buttons.html", + item_object_id=object_id, + item_author_inbox=author_inbox, + like_count=like_count, + boost_count=boost_count, + liked_by_me=liked_by_me, + boosted_by_me=boosted_by_me, + actor=actor, + ) + + # -- Following / Followers ------------------------------------------------ + + @bp.get("/following") + async def following_list(): + actor = _require_actor() + actors, total = await services.federation.get_following( + g.s, actor.preferred_username, + ) + return await render_template( + "federation/following.html", + actors=actors, + total=total, + page=1, + actor=actor, + ) + + @bp.get("/following/page") + async def following_list_page(): + actor = _require_actor() + page = request.args.get("page", 1, type=int) + actors, total = await services.federation.get_following( + g.s, actor.preferred_username, page=page, + ) + return await render_template( + "federation/_actor_list_items.html", + actors=actors, + total=total, + page=page, + list_type="following", + followed_urls=set(), + actor=actor, + ) + + @bp.get("/followers") + async def followers_list(): + actor = _require_actor() + actors, total = await services.federation.get_followers_paginated( + g.s, actor.preferred_username, + ) + # Build set of followed actor URLs to show Follow Back vs Unfollow + following, _ = await services.federation.get_following( + g.s, actor.preferred_username, page=1, per_page=1000, + ) + followed_urls = {a.actor_url for a in following} + return await render_template( + "federation/followers.html", + actors=actors, + total=total, + page=1, + followed_urls=followed_urls, + actor=actor, + ) + + @bp.get("/followers/page") + async def followers_list_page(): + actor = _require_actor() + page = request.args.get("page", 1, type=int) + actors, total = await services.federation.get_followers_paginated( + g.s, actor.preferred_username, page=page, + ) + following, _ = await services.federation.get_following( + g.s, actor.preferred_username, page=1, per_page=1000, + ) + followed_urls = {a.actor_url for a in following} + return await render_template( + "federation/_actor_list_items.html", + actors=actors, + total=total, + page=page, + list_type="followers", + followed_urls=followed_urls, + actor=actor, + ) + + @bp.get("/actor/") + async def actor_timeline(id: int): + actor = getattr(g, "_social_actor", None) + # Get remote actor info + from shared.models.federation import RemoteActor + from sqlalchemy import select as sa_select + remote = ( + await g.s.execute( + sa_select(RemoteActor).where(RemoteActor.id == id) + ) + ).scalar_one_or_none() + if not remote: + abort(404) + from shared.services.federation_impl import _remote_actor_to_dto + remote_dto = _remote_actor_to_dto(remote) + items = await services.federation.get_actor_timeline(g.s, id) + # Check if we follow this actor + is_following = False + if actor: + from shared.models.federation import APFollowing + existing = ( + await g.s.execute( + sa_select(APFollowing).where( + APFollowing.actor_profile_id == actor.id, + APFollowing.remote_actor_id == id, + ) + ) + ).scalar_one_or_none() + is_following = existing is not None + return await render_template( + "federation/actor_timeline.html", + remote_actor=remote_dto, + items=items, + is_following=is_following, + actor=actor, + ) + + @bp.get("/actor//timeline") + async def actor_timeline_page(id: int): + actor = getattr(g, "_social_actor", None) + before_str = request.args.get("before") + before = None + if before_str: + try: + before = datetime.fromisoformat(before_str) + except ValueError: + pass + items = await services.federation.get_actor_timeline( + g.s, id, before=before, + ) + return await render_template( + "federation/_timeline_items.html", + items=items, + timeline_type="actor", + actor_id=id, + actor=actor, + ) + + # -- Notifications -------------------------------------------------------- + + @bp.get("/notifications") + async def notifications(): + actor = _require_actor() + items = await services.federation.get_notifications(g.s, actor.id) + await services.federation.mark_notifications_read(g.s, actor.id) + return await render_template( + "federation/notifications.html", + notifications=items, + actor=actor, + ) + + @bp.get("/notifications/count") + async def notification_count(): + actor = getattr(g, "_social_actor", None) + if not actor: + return Response("0", content_type="text/plain") + count = await services.federation.unread_notification_count(g.s, actor.id) + if count > 0: + return Response( + f'{count}', + content_type="text/html", + ) + return Response("", content_type="text/html") + + @bp.post("/notifications/read") + async def mark_read(): + actor = _require_actor() + await services.federation.mark_notifications_read(g.s, actor.id) + return redirect(url_for("social.notifications")) + + return bp diff --git a/federation/config/app-config.yaml b/federation/config/app-config.yaml new file mode 100644 index 0000000..3aa6a76 --- /dev/null +++ b/federation/config/app-config.yaml @@ -0,0 +1,84 @@ +# App-wide settings +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: Rose Ash +market_root: /market +market_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + blog: "http://localhost:8000" + market: "http://localhost:8001" + cart: "http://localhost:8002" + events: "http://localhost:8003" + federation: "http://localhost:8004" +cache: + fs_root: _snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/wines + - branded-goods/ciders + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + - ---just-lem-just-wholefoods-jelly-crystals-lemon-12-x-85g-vf067-2-html + product-details: + - General Information + - A Note About Prices + +# SumUp payment settings (fill these in for live usage) +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + checkout_reference_prefix: 'dev-' + diff --git a/federation/entrypoint.sh b/federation/entrypoint.sh new file mode 100755 index 0000000..05d9e3d --- /dev/null +++ b/federation/entrypoint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional: wait for Postgres to be reachable +if [[ -n "${DATABASE_HOST:-}" && -n "${DATABASE_PORT:-}" ]]; then + echo "Waiting for Postgres at ${DATABASE_HOST}:${DATABASE_PORT}..." + for i in {1..60}; do + (echo > /dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}) >/dev/null 2>&1 && break || true + sleep 1 + done +fi + +# Federation can optionally run migrations (set RUN_MIGRATIONS=true) +if [[ "${RUN_MIGRATIONS:-}" == "true" ]]; then + echo "Running Alembic migrations..." + (cd shared && alembic upgrade head) +fi + +# Clear Redis page cache on deploy +if [[ -n "${REDIS_URL:-}" && "${REDIS_URL}" != "no" ]]; then + echo "Flushing Redis cache..." + python3 -c " +import redis, os +r = redis.from_url(os.environ['REDIS_URL']) +r.flushall() +print('Redis cache cleared.') +" || echo "Redis flush failed (non-fatal), continuing..." +fi + +# Start the app +echo "Starting Hypercorn (${APP_MODULE:-app:app})..." +PYTHONUNBUFFERED=1 exec hypercorn "${APP_MODULE:-app:app}" --bind 0.0.0.0:${PORT:-8000} diff --git a/federation/models/__init__.py b/federation/models/__init__.py new file mode 100644 index 0000000..7d27499 --- /dev/null +++ b/federation/models/__init__.py @@ -0,0 +1,9 @@ +"""Re-export federation models from shared.models.""" +from shared.models.federation import ( # noqa: F401 + ActorProfile, + APActivity, + APFollower, + APInboxItem, + APAnchor, + IPFSPin, +) diff --git a/federation/path_setup.py b/federation/path_setup.py new file mode 100644 index 0000000..c7166f7 --- /dev/null +++ b/federation/path_setup.py @@ -0,0 +1,9 @@ +import sys +import os + +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p) diff --git a/federation/services/__init__.py b/federation/services/__init__.py new file mode 100644 index 0000000..e6794e2 --- /dev/null +++ b/federation/services/__init__.py @@ -0,0 +1,27 @@ +"""Federation app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the federation app. + + Federation owns: ActorProfile, APActivity, APFollower, APInboxItem, + APAnchor, IPFSPin. + Standard deployment registers all services as real DB impls (shared DB). + """ + from shared.services.registry import services + from shared.services.federation_impl import SqlFederationService + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + services.federation = SqlFederationService() + if not services.has("blog"): + services.blog = SqlBlogService() + if not services.has("calendar"): + services.calendar = SqlCalendarService() + if not services.has("market"): + services.market = SqlMarketService() + if not services.has("cart"): + services.cart = SqlCartService() diff --git a/federation/templates/_email/magic_link.html b/federation/templates/_email/magic_link.html new file mode 100644 index 0000000..3c1eac6 --- /dev/null +++ b/federation/templates/_email/magic_link.html @@ -0,0 +1,33 @@ + + + + + + +
    + + +
    +

    {{ site_name }}

    +

    Sign in to your account

    +

    + Click the button below to sign in. This link will expire in 15 minutes. +

    +
    + + Sign in + +
    +

    Or copy and paste this link into your browser:

    +

    + {{ link_url }} +

    +
    +

    + If you did not request this email, you can safely ignore it. +

    +
    +
    + + diff --git a/federation/templates/_email/magic_link.txt b/federation/templates/_email/magic_link.txt new file mode 100644 index 0000000..28a2efb --- /dev/null +++ b/federation/templates/_email/magic_link.txt @@ -0,0 +1,8 @@ +Hello, + +Click this link to sign in: +{{ link_url }} + +This link will expire in 15 minutes. + +If you did not request this, you can ignore this email. diff --git a/federation/templates/_types/federation/index.html b/federation/templates/_types/federation/index.html new file mode 100644 index 0000000..e2caacb --- /dev/null +++ b/federation/templates/_types/federation/index.html @@ -0,0 +1,3 @@ +{% extends '_types/root/_index.html' %} +{% block meta %}{% endblock %} +{% block content %}{% endblock %} diff --git a/federation/templates/_types/social/header/_header.html b/federation/templates/_types/social/header/_header.html new file mode 100644 index 0000000..3bc55e3 --- /dev/null +++ b/federation/templates/_types/social/header/_header.html @@ -0,0 +1,52 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='social-row', oob=oob) %} + + {% endcall %} +{% endmacro %} diff --git a/federation/templates/_types/social/index.html b/federation/templates/_types/social/index.html new file mode 100644 index 0000000..8eeed33 --- /dev/null +++ b/federation/templates/_types/social/index.html @@ -0,0 +1,10 @@ +{% extends '_types/root/_index.html' %} +{% block meta %}{% endblock %} +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('social-header-child', '_types/social/header/_header.html') %} + {% endcall %} +{% endblock %} +{% block content %} + {% block social_content %}{% endblock %} +{% endblock %} diff --git a/federation/templates/auth/check_email.html b/federation/templates/auth/check_email.html new file mode 100644 index 0000000..5eb1b61 --- /dev/null +++ b/federation/templates/auth/check_email.html @@ -0,0 +1,19 @@ +{% extends "_types/root/_index.html" %} +{% block meta %}{% endblock %} +{% block title %}Check your email — Rose Ash{% endblock %} +{% block content %} +
    +

    Check your email

    +

    + We sent a sign-in link to {{ email }}. +

    +

    + Click the link in the email to sign in. The link expires in 15 minutes. +

    + {% if email_error %} +
    + {{ email_error }} +
    + {% endif %} +
    +{% endblock %} diff --git a/federation/templates/auth/login.html b/federation/templates/auth/login.html new file mode 100644 index 0000000..79031e5 --- /dev/null +++ b/federation/templates/auth/login.html @@ -0,0 +1,36 @@ +{% extends "_types/root/_index.html" %} +{% block meta %}{% endblock %} +{% block title %}Login — Rose Ash{% endblock %} +{% block content %} +
    +

    Sign in

    + + {% if error %} +
    + {{ error }} +
    + {% endif %} + +
    + +
    + + +
    + +
    +
    +{% endblock %} diff --git a/federation/templates/federation/_actor_list_items.html b/federation/templates/federation/_actor_list_items.html new file mode 100644 index 0000000..13b18aa --- /dev/null +++ b/federation/templates/federation/_actor_list_items.html @@ -0,0 +1,63 @@ +{% for a in actors %} +
    + {% if a.icon_url %} + + {% else %} +
    + {{ (a.display_name or a.preferred_username)[0] | upper }} +
    + {% endif %} + +
    + {% if list_type == "following" and a.id %} + + {{ a.display_name or a.preferred_username }} + + {% else %} + + {{ a.display_name or a.preferred_username }} + + {% endif %} +
    @{{ a.preferred_username }}@{{ a.domain }}
    + {% if a.summary %} +
    {{ a.summary | striptags }}
    + {% endif %} +
    + + {% if actor %} +
    + {% if list_type == "following" or a.actor_url in (followed_urls or []) %} +
    + + + +
    + {% else %} +
    + + + +
    + {% endif %} +
    + {% endif %} +
    +{% endfor %} + +{% if actors | length >= 20 %} +
    +
    +{% endif %} diff --git a/federation/templates/federation/_interaction_buttons.html b/federation/templates/federation/_interaction_buttons.html new file mode 100644 index 0000000..5551732 --- /dev/null +++ b/federation/templates/federation/_interaction_buttons.html @@ -0,0 +1,61 @@ +{% set oid = item.object_id if item is defined and item.object_id is defined else item_object_id | default('') %} +{% set ainbox = item.author_inbox if item is defined and item.author_inbox is defined else item_author_inbox | default('') %} +{% set lcount = item.like_count if item is defined and item.like_count is defined else like_count | default(0) %} +{% set bcount = item.boost_count if item is defined and item.boost_count is defined else boost_count | default(0) %} +{% set liked = item.liked_by_me if item is defined and item.liked_by_me is defined else liked_by_me | default(false) %} +{% set boosted = item.boosted_by_me if item is defined and item.boosted_by_me is defined else boosted_by_me | default(false) %} + +
    + {% if liked %} +
    + + + + +
    + {% else %} +
    + + + + +
    + {% endif %} + + {% if boosted %} +
    + + + + +
    + {% else %} +
    + + + + +
    + {% endif %} + + {% if oid %} + Reply + {% endif %} +
    diff --git a/federation/templates/federation/_notification.html b/federation/templates/federation/_notification.html new file mode 100644 index 0000000..d18ef4d --- /dev/null +++ b/federation/templates/federation/_notification.html @@ -0,0 +1,42 @@ +
    +
    + {% if notif.from_actor_icon %} + + {% else %} +
    + {{ notif.from_actor_name[0] | upper if notif.from_actor_name else '?' }} +
    + {% endif %} + +
    +
    + {{ notif.from_actor_name }} + + @{{ notif.from_actor_username }}{% if notif.from_actor_domain %}@{{ notif.from_actor_domain }}{% endif %} + + + {% if notif.notification_type == "follow" %} + followed you + {% elif notif.notification_type == "like" %} + liked your post + {% elif notif.notification_type == "boost" %} + boosted your post + {% elif notif.notification_type == "mention" %} + mentioned you + {% elif notif.notification_type == "reply" %} + replied to your post + {% endif %} +
    + + {% if notif.target_content_preview %} +
    + {{ notif.target_content_preview }} +
    + {% endif %} + +
    + {{ notif.created_at.strftime('%b %d, %H:%M') }} +
    +
    +
    +
    diff --git a/federation/templates/federation/_post_card.html b/federation/templates/federation/_post_card.html new file mode 100644 index 0000000..33102ca --- /dev/null +++ b/federation/templates/federation/_post_card.html @@ -0,0 +1,52 @@ +
    + {% if item.boosted_by %} +
    + Boosted by {{ item.boosted_by }} +
    + {% endif %} + +
    + {% if item.actor_icon %} + + {% else %} +
    + {{ item.actor_name[0] | upper if item.actor_name else '?' }} +
    + {% endif %} + +
    +
    + {{ item.actor_name }} + + @{{ item.actor_username }}{% if item.actor_domain %}@{{ item.actor_domain }}{% endif %} + + + {% if item.published %} + {{ item.published.strftime('%b %d, %H:%M') }} + {% endif %} + +
    + + {% if item.summary %} +
    + CW: {{ item.summary }} +
    {{ item.content | safe }}
    +
    + {% else %} +
    {{ item.content | safe }}
    + {% endif %} + + {% if item.url and item.post_type == "remote" %} + + original + + {% endif %} + + {% if actor %} +
    + {% include "federation/_interaction_buttons.html" with context %} +
    + {% endif %} +
    +
    +
    diff --git a/federation/templates/federation/_search_results.html b/federation/templates/federation/_search_results.html new file mode 100644 index 0000000..ca8c248 --- /dev/null +++ b/federation/templates/federation/_search_results.html @@ -0,0 +1,61 @@ +{% for a in actors %} +
    + {% if a.icon_url %} + + {% else %} +
    + {{ (a.display_name or a.preferred_username)[0] | upper }} +
    + {% endif %} + +
    + {% if a.id %} + + {{ a.display_name or a.preferred_username }} + + {% else %} + {{ a.display_name or a.preferred_username }} + {% endif %} +
    @{{ a.preferred_username }}@{{ a.domain }}
    + {% if a.summary %} +
    {{ a.summary | striptags }}
    + {% endif %} +
    + + {% if actor %} +
    + {% if a.actor_url in (followed_urls or []) %} +
    + + + +
    + {% else %} +
    + + + +
    + {% endif %} +
    + {% endif %} +
    +{% endfor %} + +{% if actors | length >= 20 %} +
    +
    +{% endif %} diff --git a/federation/templates/federation/_timeline_items.html b/federation/templates/federation/_timeline_items.html new file mode 100644 index 0000000..c004743 --- /dev/null +++ b/federation/templates/federation/_timeline_items.html @@ -0,0 +1,18 @@ +{% for item in items %} + {% include "federation/_post_card.html" %} +{% endfor %} + +{% if items %} + {% set last = items[-1] %} + {% if timeline_type == "actor" %} +
    +
    + {% else %} +
    +
    + {% endif %} +{% endif %} diff --git a/federation/templates/federation/account.html b/federation/templates/federation/account.html new file mode 100644 index 0000000..ef7f7d6 --- /dev/null +++ b/federation/templates/federation/account.html @@ -0,0 +1,27 @@ +{% extends "_types/social/index.html" %} +{% block title %}Account — Rose Ash{% endblock %} +{% block social_content %} +
    +

    Account

    + +
    +

    Email: {{ g.user.email }}

    + {% if actor %} +

    Username: @{{ actor.preferred_username }}

    +

    + + View profile + +

    + {% else %} +

    + + Choose a username to start publishing + +

    + {% endif %} +
    +
    +{% endblock %} diff --git a/federation/templates/federation/actor_card.html b/federation/templates/federation/actor_card.html new file mode 100644 index 0000000..cd97c70 --- /dev/null +++ b/federation/templates/federation/actor_card.html @@ -0,0 +1,45 @@ +
    +
    + {% if result.icon_url %} + + {% else %} +
    + {{ result.preferred_username[0] | upper }} +
    + {% endif %} + +
    +
    + {{ result.display_name or result.preferred_username }} + @{{ result.preferred_username }}@{{ result.domain }} +
    + + {% if result.summary %} +
    + {{ result.summary | safe }} +
    + {% endif %} + + {% if actor %} +
    +
    + + + +
    +
    + + + +
    +
    + {% endif %} +
    +
    +
    diff --git a/federation/templates/federation/actor_timeline.html b/federation/templates/federation/actor_timeline.html new file mode 100644 index 0000000..0c69f8a --- /dev/null +++ b/federation/templates/federation/actor_timeline.html @@ -0,0 +1,53 @@ +{% extends "_types/social/index.html" %} + +{% block title %}{{ remote_actor.display_name or remote_actor.preferred_username }} — Rose Ash{% endblock %} + +{% block social_content %} +
    +
    + {% if remote_actor.icon_url %} + + {% else %} +
    + {{ (remote_actor.display_name or remote_actor.preferred_username)[0] | upper }} +
    + {% endif %} + +
    +

    {{ remote_actor.display_name or remote_actor.preferred_username }}

    +
    @{{ remote_actor.preferred_username }}@{{ remote_actor.domain }}
    + {% if remote_actor.summary %} +
    {{ remote_actor.summary | safe }}
    + {% endif %} +
    + + {% if actor %} +
    + {% if is_following %} +
    + + + +
    + {% else %} +
    + + + +
    + {% endif %} +
    + {% endif %} +
    +
    + +
    + {% set timeline_type = "actor" %} + {% set actor_id = remote_actor.id %} + {% include "federation/_timeline_items.html" %} +
    +{% endblock %} diff --git a/federation/templates/federation/choose_username.html b/federation/templates/federation/choose_username.html new file mode 100644 index 0000000..259afb2 --- /dev/null +++ b/federation/templates/federation/choose_username.html @@ -0,0 +1,54 @@ +{% extends "_types/social/index.html" %} +{% block title %}Choose Username — Rose Ash{% endblock %} +{% block social_content %} +
    +

    Choose your username

    +

    + This will be your identity on the fediverse: + @username@{{ config.get('ap_domain', 'rose-ash.com') }} +

    + + {% if error %} +
    + {{ error }} +
    + {% endif %} + +
    + +
    + +
    + @ + +
    +
    +

    + 3-32 characters. Lowercase letters, numbers, underscores. Must start with a letter. +

    +
    + + +
    +
    +{% endblock %} diff --git a/federation/templates/federation/compose.html b/federation/templates/federation/compose.html new file mode 100644 index 0000000..d82a031 --- /dev/null +++ b/federation/templates/federation/compose.html @@ -0,0 +1,34 @@ +{% extends "_types/social/index.html" %} + +{% block title %}Compose — Rose Ash{% endblock %} + +{% block social_content %} +

    Compose

    + +
    + + {% if reply_to %} + +
    + Replying to {{ reply_to }} +
    + {% endif %} + + + +
    + + + +
    +
    +{% endblock %} diff --git a/federation/templates/federation/followers.html b/federation/templates/federation/followers.html new file mode 100644 index 0000000..07eb862 --- /dev/null +++ b/federation/templates/federation/followers.html @@ -0,0 +1,12 @@ +{% extends "_types/social/index.html" %} + +{% block title %}Followers — Rose Ash{% endblock %} + +{% block social_content %} +

    Followers ({{ total }})

    + +
    + {% set list_type = "followers" %} + {% include "federation/_actor_list_items.html" %} +
    +{% endblock %} diff --git a/federation/templates/federation/following.html b/federation/templates/federation/following.html new file mode 100644 index 0000000..ca900e4 --- /dev/null +++ b/federation/templates/federation/following.html @@ -0,0 +1,13 @@ +{% extends "_types/social/index.html" %} + +{% block title %}Following — Rose Ash{% endblock %} + +{% block social_content %} +

    Following ({{ total }})

    + +
    + {% set list_type = "following" %} + {% set followed_urls = [] %} + {% include "federation/_actor_list_items.html" %} +
    +{% endblock %} diff --git a/federation/templates/federation/notifications.html b/federation/templates/federation/notifications.html new file mode 100644 index 0000000..11eb3f8 --- /dev/null +++ b/federation/templates/federation/notifications.html @@ -0,0 +1,17 @@ +{% extends "_types/social/index.html" %} + +{% block title %}Notifications — Rose Ash{% endblock %} + +{% block social_content %} +

    Notifications

    + +{% if not notifications %} +

    No notifications yet.

    +{% endif %} + +
    + {% for notif in notifications %} + {% include "federation/_notification.html" %} + {% endfor %} +
    +{% endblock %} diff --git a/federation/templates/federation/profile.html b/federation/templates/federation/profile.html new file mode 100644 index 0000000..2e21a08 --- /dev/null +++ b/federation/templates/federation/profile.html @@ -0,0 +1,32 @@ +{% extends "_types/social/index.html" %} +{% block title %}@{{ actor.preferred_username }} — Rose Ash{% endblock %} +{% block social_content %} +
    +
    +

    {{ actor.display_name or actor.preferred_username }}

    +

    @{{ actor.preferred_username }}@{{ config.get('ap_domain', 'rose-ash.com') }}

    + {% if actor.summary %} +

    {{ actor.summary }}

    + {% endif %} +
    + +

    Activities ({{ total }})

    + {% if activities %} +
    + {% for a in activities %} +
    +
    + {{ a.activity_type }} + {{ a.published.strftime('%Y-%m-%d %H:%M') if a.published }} +
    + {% if a.object_type %} + {{ a.object_type }} + {% endif %} +
    + {% endfor %} +
    + {% else %} +

    No activities yet.

    + {% endif %} +
    +{% endblock %} diff --git a/federation/templates/federation/search.html b/federation/templates/federation/search.html new file mode 100644 index 0000000..62c33dc --- /dev/null +++ b/federation/templates/federation/search.html @@ -0,0 +1,32 @@ +{% extends "_types/social/index.html" %} + +{% block title %}Search — Rose Ash{% endblock %} + +{% block social_content %} +

    Search

    + +
    +
    + + +
    +
    + +{% if query and total %} +

    {{ total }} result{{ 's' if total != 1 }} for {{ query }}

    +{% elif query %} +

    No results found for {{ query }}

    +{% endif %} + +
    + {% include "federation/_search_results.html" %} +
    +{% endblock %} diff --git a/federation/templates/federation/timeline.html b/federation/templates/federation/timeline.html new file mode 100644 index 0000000..74861f3 --- /dev/null +++ b/federation/templates/federation/timeline.html @@ -0,0 +1,19 @@ +{% extends "_types/social/index.html" %} + +{% block title %}{{ "Home" if timeline_type == "home" else "Public" }} Timeline — Rose Ash{% endblock %} + +{% block social_content %} +
    +

    {{ "Home" if timeline_type == "home" else "Public" }} Timeline

    + {% if actor %} + + Compose + + {% endif %} +
    + +
    + {% include "federation/_timeline_items.html" %} +
    +{% endblock %} diff --git a/market/.gitignore b/market/.gitignore new file mode 100644 index 0000000..1e06fbc --- /dev/null +++ b/market/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.env +node_modules/ +_snapshot/ +_debug/ +*.egg-info/ +dist/ +build/ +.venv/ +venv/ diff --git a/market/Dockerfile b/market/Dockerfile new file mode 100644 index 0000000..836fa1c --- /dev/null +++ b/market/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# ---------- Python application ---------- +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + PIP_NO_CACHE_DIR=1 \ + APP_PORT=8000 \ + APP_MODULE=app:app + +WORKDIR /app + +# Install system deps + psql client +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +COPY shared/requirements.txt ./requirements.txt +RUN pip install -r requirements.txt + +# Shared code (replaces submodule) +COPY shared/ ./shared/ + +# App code +COPY market/ ./ + +# Sibling models for cross-domain SQLAlchemy imports +COPY blog/__init__.py ./blog/__init__.py +COPY blog/models/ ./blog/models/ +COPY cart/__init__.py ./cart/__init__.py +COPY cart/models/ ./cart/models/ +COPY events/__init__.py ./events/__init__.py +COPY events/models/ ./events/models/ +COPY federation/__init__.py ./federation/__init__.py +COPY federation/models/ ./federation/models/ +COPY account/__init__.py ./account/__init__.py +COPY account/models/ ./account/models/ + +# ---------- Runtime setup ---------- +COPY market/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +RUN useradd -m -u 10001 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE ${APP_PORT} +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/market/README.md b/market/README.md new file mode 100644 index 0000000..6d9a840 --- /dev/null +++ b/market/README.md @@ -0,0 +1,56 @@ +# Market App + +Product browsing and marketplace service for the Rose Ash cooperative. Displays products scraped from Suma Wholesale. + +## Architecture + +One of five Quart microservices sharing a single PostgreSQL database: + +| App | Port | Domain | +|-----|------|--------| +| blog (coop) | 8000 | Auth, blog, admin, menus, snippets | +| **market** | 8001 | Product browsing, Suma scraping | +| cart | 8002 | Shopping cart, checkout, orders | +| events | 8003 | Calendars, bookings, tickets | +| federation | 8004 | ActivityPub, fediverse social | + +## Structure + +``` +app.py # Application factory (create_base_app + blueprints) +path_setup.py # Adds project root + app dir to sys.path +config/app-config.yaml # App URLs, feature flags +models/ # Market-domain models (+ re-export stubs) +bp/ # Blueprints + market/ # Market root, navigation, category listing + browse/ # Product browsing with filters and infinite scroll + product/ # Product detail pages + cart/ # Page-scoped cart views + api/ # Product sync API (used by scraper) +scrape/ # Suma Wholesale scraper +services/ # register_domain_services() — wires market + cart +shared/ # Submodule -> git.rose-ash.com/coop/shared.git +``` + +## Cross-Domain Communication + +- `services.cart.*` — cart summary via CartService protocol +- `services.federation.*` — AP publishing via FederationService protocol +- `shared.services.navigation` — site navigation tree + +## Scraping + +```bash +bash scrape.sh # Full Suma Wholesale catalogue +bash scrape-test.sh # Limited test scrape +``` + +## Running + +```bash +export DATABASE_URL_ASYNC=postgresql+asyncpg://user:pass@localhost/coop +export REDIS_URL=redis://localhost:6379/0 +export SECRET_KEY=your-secret-key + +hypercorn app:app --bind 0.0.0.0:8001 +``` diff --git a/market/__init__.py b/market/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/app.py b/market/app.py new file mode 100644 index 0000000..bbfcbcf --- /dev/null +++ b/market/app.py @@ -0,0 +1,188 @@ +from __future__ import annotations +import path_setup # noqa: F401 # adds shared/ to sys.path + +from pathlib import Path + +from quart import g, abort, request +from jinja2 import FileSystemLoader, ChoiceLoader +from sqlalchemy import select + +from shared.infrastructure.factory import create_base_app +from shared.config import config + +from bp import register_market_bp, register_all_markets, register_page_markets, register_fragments + + +async def market_context() -> dict: + """ + Market app context processor. + + - nav_tree_html: fetched from blog as fragment + - cart_count/cart_total: via cart service (includes calendar entries) + - cart: direct ORM query (templates need .product relationship) + """ + from shared.infrastructure.context import base_context + from shared.services.navigation import get_navigation_tree + from shared.services.registry import services + from shared.infrastructure.cart_identity import current_cart_identity + from shared.infrastructure.fragments import fetch_fragment + from shared.models.market import CartItem + from sqlalchemy.orm import selectinload + + ctx = await base_context() + + ctx["nav_tree_html"] = await fetch_fragment( + "blog", "nav-tree", + params={"app_name": "market", "path": request.path}, + ) + # Fallback for _nav.html when nav-tree fragment fetch fails + ctx["menu_items"] = await get_navigation_tree(g.s) + + ident = current_cart_identity() + + # cart_count/cart_total via service (consistent with blog/events apps) + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + ctx["cart_count"] = summary.count + summary.calendar_count + ctx["cart_total"] = float(summary.total + summary.calendar_total) + + # ORM cart items for product templates (need .product relationship) + filters = [CartItem.deleted_at.is_(None)] + if ident["user_id"] is not None: + filters.append(CartItem.user_id == ident["user_id"]) + elif ident["session_id"] is not None: + filters.append(CartItem.session_id == ident["session_id"]) + else: + ctx["cart"] = [] + return ctx + + result = await g.s.execute( + select(CartItem).where(*filters).options(selectinload(CartItem.product)) + ) + ctx["cart"] = list(result.scalars().all()) + + return ctx + + +def create_app() -> "Quart": + from models.market_place import MarketPlace + from shared.services.registry import services + from services import register_domain_services + + app = create_base_app( + "market", + context_fn=market_context, + domain_services_fn=register_domain_services, + ) + + # App-specific templates override shared templates + app_templates = str(Path(__file__).resolve().parent / "templates") + app.jinja_loader = ChoiceLoader([ + FileSystemLoader(app_templates), + app.jinja_loader, + ]) + + # All markets: / — global view across all pages + app.register_blueprint( + register_all_markets(), + url_prefix="/", + ) + + # Page markets: // — markets for a single page + app.register_blueprint( + register_page_markets(), + url_prefix="/", + ) + + # Market blueprint nested under post slug: /// + app.register_blueprint( + register_market_bp( + url_prefix="/", + title=config()["market_title"], + ), + url_prefix="//", + ) + + app.register_blueprint(register_fragments()) + + # --- Auto-inject slugs into url_for() calls --- + @app.url_value_preprocessor + def pull_slugs(endpoint, values): + if values: + # page_markets blueprint uses "slug" + if "slug" in values: + g.post_slug = values.pop("slug") + # market blueprint uses "page_slug" / "market_slug" + if "page_slug" in values: + g.post_slug = values.pop("page_slug") + if "market_slug" in values: + g.market_slug = values.pop("market_slug") + + @app.url_defaults + def inject_slugs(endpoint, values): + slug = g.get("post_slug") + if slug: + for param in ("slug", "page_slug"): + if param not in values and app.url_map.is_endpoint_expecting(endpoint, param): + values[param] = slug + market_slug = g.get("market_slug") + if market_slug and "market_slug" not in values: + if app.url_map.is_endpoint_expecting(endpoint, "market_slug"): + values["market_slug"] = market_slug + + # --- Load post and market data --- + @app.before_request + async def hydrate_market(): + post_slug = getattr(g, "post_slug", None) + market_slug = getattr(g, "market_slug", None) + if not post_slug: + return + + # Load post by slug via blog service + post = await services.blog.get_post_by_slug(g.s, post_slug) + if not post: + abort(404) + + g.post_data = { + "post": { + "id": post.id, + "title": post.title, + "slug": post.slug, + "feature_image": post.feature_image, + "html": post.html, + "status": post.status, + "visibility": post.visibility, + "is_page": post.is_page, + }, + } + + # Only load market when market_slug is present (///) + if not market_slug: + return + + market = ( + await g.s.execute( + select(MarketPlace).where( + MarketPlace.slug == market_slug, + MarketPlace.container_type == "page", + MarketPlace.container_id == post.id, + MarketPlace.deleted_at.is_(None), + ) + ) + ).scalar_one_or_none() + if not market: + abort(404) + g.market = market + + @app.context_processor + async def inject_post(): + post_data = getattr(g, "post_data", None) + if not post_data: + return {} + return {**post_data} + + return app + + +app = create_app() diff --git a/market/bp/__init__.py b/market/bp/__init__.py new file mode 100644 index 0000000..b62b4b6 --- /dev/null +++ b/market/bp/__init__.py @@ -0,0 +1,5 @@ +from .market.routes import register as register_market_bp +from .product.routes import register as register_product +from .all_markets.routes import register as register_all_markets +from .page_markets.routes import register as register_page_markets +from .fragments import register_fragments diff --git a/market/bp/all_markets/__init__.py b/market/bp/all_markets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/bp/all_markets/routes.py b/market/bp/all_markets/routes.py new file mode 100644 index 0000000..0ce086d --- /dev/null +++ b/market/bp/all_markets/routes.py @@ -0,0 +1,74 @@ +""" +All-markets blueprint — shows markets across ALL pages. + +Mounted at / (root of market app). No slug context. + +Routes: + GET / — full page with first page of markets + GET /all-markets — HTMX fragment for infinite scroll +""" +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, make_response + +from shared.browser.app.utils.htmx import is_htmx_request +from shared.services.registry import services + + +def register() -> Blueprint: + bp = Blueprint("all_markets", __name__) + + async def _load_markets(page, per_page=20): + """Load all markets + page info for container badges.""" + markets, has_more = await services.market.list_marketplaces( + g.s, page=page, per_page=per_page, + ) + + # Batch-load page info for container_ids + page_info = {} + if markets: + post_ids = list({ + m.container_id for m in markets + if m.container_type == "page" + }) + if post_ids: + posts = await services.blog.get_posts_by_ids(g.s, post_ids) + for p in posts: + page_info[p.id] = {"title": p.title, "slug": p.slug} + + return markets, has_more, page_info + + @bp.get("/") + async def index(): + page = int(request.args.get("page", 1)) + markets, has_more, page_info = await _load_markets(page) + + ctx = dict( + markets=markets, + has_more=has_more, + page_info=page_info, + page=page, + ) + + if is_htmx_request(): + html = await render_template("_types/all_markets/_main_panel.html", **ctx) + else: + html = await render_template("_types/all_markets/index.html", **ctx) + + return await make_response(html, 200) + + @bp.get("/all-markets") + async def markets_fragment(): + page = int(request.args.get("page", 1)) + markets, has_more, page_info = await _load_markets(page) + + html = await render_template( + "_types/all_markets/_cards.html", + markets=markets, + has_more=has_more, + page_info=page_info, + page=page, + ) + return await make_response(html, 200) + + return bp diff --git a/market/bp/api/__init__.py b/market/bp/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/bp/api/routes.py b/market/bp/api/routes.py new file mode 100644 index 0000000..e83824d --- /dev/null +++ b/market/bp/api/routes.py @@ -0,0 +1,432 @@ +# products_api_async.py +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal +from typing import Any, Dict, List, Tuple, Iterable, Optional + +from quart import Blueprint, request, jsonify, g +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from scrape.persist_snapshot.log_product_result import _log_product_result +from scrape.persist_snapshot.save_nav import _save_nav +from scrape.persist_snapshot.capture_listing import _capture_listing +from scrape.persist_snapshot.save_subcategory_redirects import _save_subcategory_redirects + +# ⬇️ Import your models (names match your current file) +from models.market import ( + Product, + ProductImage, + ProductSection, + ProductLabel, + ProductSticker, + ProductAttribute, + ProductNutrition, + ProductAllergen, +) + +from shared.browser.app.redis_cacher import clear_cache +from shared.browser.app.csrf import csrf_exempt + + +products_api = Blueprint("products_api", __name__, url_prefix="/api/products") + +# ---- Comparison config (matches your schema) -------------------------------- + +PRODUCT_FIELDS: List[str] = [ + "slug", + "title", + "image", + "description_short", + "description_html", + "suma_href", + "brand", + "rrp", "rrp_currency", "rrp_raw", + "price_per_unit", "price_per_unit_currency", "price_per_unit_raw", + "special_price", "special_price_currency", "special_price_raw", + "regular_price", "regular_price_currency", "regular_price_raw", + "oe_list_price", + "case_size_count", "case_size_item_qty", "case_size_item_unit", "case_size_raw", + "ean", "sku", "unit_size", "pack_size", +] + +# rel_name -> (Model, fields_to_compare, key_for_orderless_compare) +CHILD_SPECS: Dict[str, Tuple[Any, List[str], str]] = { + "images": (ProductImage, ["url", "position", "kind"], "url"), + "sections": (ProductSection, ["title", "html"], "title"), + "labels": (ProductLabel, ["name"], "name"), + "stickers": (ProductSticker, ["name"], "name"), + "attributes": (ProductAttribute, ["key", "value"], "key"), + "nutrition": (ProductNutrition, ["key", "value", "unit"], "key"), + "allergens": (ProductAllergen, ["name", "contains"], "name"), +} + +def _now_utc(): + return datetime.now(timezone.utc) + +def _norm_scalar(v: Any) -> Any: + if isinstance(v, Decimal): + s = format(v.normalize(), "f") + return "0" if s in ("-0", "-0.0") else s + if isinstance(v, bool): + return bool(v) + if isinstance(v, (int, float, str)) or v is None: + return v + return str(v) + +def _normalize_row(obj: Dict[str, Any], keep: List[str]) -> Dict[str, Any]: + out: Dict[str, Any] = {} + for f in keep: + val = obj.get(f) + if isinstance(val, str): + val = val.strip() + out[f] = _norm_scalar(val) + return out + +def _list_to_index(items: Iterable[Dict[str, Any]], uniq: str) -> Dict[Any, Dict[str, Any]]: + ix: Dict[Any, Dict[str, Any]] = {} + for it in items or []: + key = it.get(uniq) + if key is None: + continue + ix[key] = it + return ix + +def _serialize_product_for_compare(p: Product) -> Dict[str, Any]: + root: Dict[str, Any] = {f: _norm_scalar(getattr(p, f)) for f in PRODUCT_FIELDS} + for rel_name, (_Model, fields, uniq) in CHILD_SPECS.items(): + rows: List[Dict[str, Any]] = [] + for child in getattr(p, rel_name) or []: + rows.append({f: _norm_scalar(getattr(child, f)) for f in fields}) + root[rel_name] = _list_to_index(rows, uniq) + return root + +def _serialize_payload_for_compare(payload: Dict[str, Any]) -> Dict[str, Any]: + root = _normalize_row(payload, PRODUCT_FIELDS) + for rel_name, (_Model, fields, uniq) in CHILD_SPECS.items(): + rows = payload.get(rel_name) or [] + rows = [r for r in rows if isinstance(r, dict)] + root[rel_name] = _list_to_index([_normalize_row(r, fields) for r in rows], uniq) + return root + +from decimal import Decimal, InvalidOperation + +def _is_numeric_like(x) -> bool: + if isinstance(x, bool): + return False + if isinstance(x, (int, float, Decimal)): + return True + if isinstance(x, str): + s = x.strip() + if not s: + return False + try: + Decimal(s) + return True + except InvalidOperation: + return False + return False + +def _to_decimal(x) -> Decimal: + if isinstance(x, Decimal): + return x + if isinstance(x, bool) or x is None: + raise InvalidOperation + if isinstance(x, (int, str)): + return Decimal(str(x).strip()) + if isinstance(x, float): + return Decimal(str(x)) # avoid float fp artifacts + # last resort: string-coerce + return Decimal(str(x).strip()) + +def values_different(av, bv) -> bool: + # match original None semantics first + if bv is None: + return av is not None + if av is None: + return True + + if _is_numeric_like(bv): + try: + return _to_decimal(av) != _to_decimal(bv) + except InvalidOperation: + # av isn't numeric-parsable → different + return True + else: + # non-numeric: compare as strings (like original) + return f"{av}" != f"{bv}" + +import re + +_cf_a_re = re.compile(r']+/cdn-cgi/l/email-protection#[^"]+"[^>]*>(.*?)', re.I | re.S) +_cf_span_re = re.compile(r']*class="__cf_email__"[^>]*>(.*?)', re.I | re.S) +_cf_data_attr_re = re.compile(r'\sdata-cfemail="[^"]+"', re.I) +_ws_re = re.compile(r'\s+') + +def normalize_cf_email(html: str) -> str: + if not isinstance(html, str): + return html + s = html + # Replace CF spans with their inner text + s = _cf_span_re.sub(r'\1', s) + # Replace CF protection anchors with their inner text + s = _cf_a_re.sub(r'\1', s) + # Drop the data-cfemail attribute if any remains + s = _cf_data_attr_re.sub('', s) + # Optional: collapse whitespace + s = _ws_re.sub(' ', s).strip() + return s + + +def _deep_equal(a: Dict[str, Any], b: Dict[str, Any]) -> bool: + # keys must match at this level + if a.keys() != b.keys(): + return False + + for k in a.keys(): + av, bv = a[k], b[k] + + # Dicts: recurse, but don't return early unless it's False + if isinstance(av, dict) and isinstance(bv, dict): + if not _deep_equal(av, bv): + # log_diff(k, av, bv) # optional + return False + continue + + # Lists/Tuples: compare length then elements (order-sensitive here) + if isinstance(av, (list, tuple)) and isinstance(bv, (list, tuple)): + if len(av) != len(bv): + # log_diff(k, av, bv) + return False + for i, (ai, bi) in enumerate(zip(av, bv)): + # nested dicts within lists + if isinstance(ai, dict) and isinstance(bi, dict): + if not _deep_equal(ai, bi): + return False + else: + if values_different(normalize_cf_email(ai), normalize_cf_email(bi)): + return False + continue + + # Scalars / everything else + if values_different(normalize_cf_email(av), normalize_cf_email(bv)): + # print('!!deep', k, av, bv) + return False + + return True + +# ---- Mutation helpers ------------------------------------------------------- + +def _apply_product_fields(p: Product, payload: Dict[str, Any]) -> None: + for f in PRODUCT_FIELDS: + setattr(p, f, payload.get(f)) + p.updated_at = _now_utc() + +def _replace_children(p: Product, payload: Dict[str, Any]) -> None: + # replace each relation wholesale (delete-orphan takes care of removal) + #p.images.clear() + for row in payload.get("images") or []: + p.images.append(ProductImage( + url=row.get("url"), + position=row.get("position") or 0, + kind=row.get("kind") or "gallery", + created_at=_now_utc(), updated_at=_now_utc(), + )) + + #p.sections.clear() + for row in payload.get("sections") or []: + p.sections.append(ProductSection( + title=row.get("title") or "", + html=row.get("html") or "", + created_at=_now_utc(), updated_at=_now_utc(), + )) + + #p.labels.clear() + for row in payload.get("labels") or []: + p.labels.append(ProductLabel( + name=row.get("name") or "", + created_at=_now_utc(), updated_at=_now_utc(), + )) + + #p.stickers.clear() + for row in payload.get("stickers") or []: + p.stickers.append(ProductSticker( + name=row.get("name") or "", + created_at=_now_utc(), updated_at=_now_utc(), + )) + + #p.attributes.clear() + for row in payload.get("attributes") or []: + p.attributes.append(ProductAttribute( + key=row.get("key") or "", + value=row.get("value"), + created_at=_now_utc(), updated_at=_now_utc(), + )) + + #p.nutrition.clear() + for row in payload.get("nutrition") or []: + p.nutrition.append(ProductNutrition( + key=row.get("key") or "", + value=row.get("value"), + unit=row.get("unit"), + created_at=_now_utc(), updated_at=_now_utc(), + )) + + #p.allergens.clear() + for row in payload.get("allergens") or []: + p.allergens.append(ProductAllergen( + name=row.get("name") or "", + contains=bool(row.get("contains", False)), + created_at=_now_utc(), updated_at=_now_utc(), + )) + +async def _create_product_from_payload(session: AsyncSession, payload: Dict[str, Any]) -> Product: + p = Product() + _apply_product_fields(p, payload) + p.created_at = _now_utc() + p.deleted_at = None + session.add(p) + #await session.flush() # get p.id + _replace_children(p, payload) + await session.flush() + + # Publish to federation inline + from shared.services.federation_publish import try_publish + await try_publish( + session, + user_id=getattr(p, "user_id", None), + activity_type="Create", + object_type="Object", + object_data={ + "name": p.title or "", + "summary": getattr(p, "description", "") or "", + }, + source_type="Product", + source_id=p.id, + ) + + return p + +# ---- API -------------------------------------------------------------------- + + +@csrf_exempt +@products_api.post("/listing/") +@clear_cache(tag='browse') +async def capture_lsting(): + data: Dict[str, Any] = await request.get_json(force=True, silent=False) + url = data['url'] + items = data['items'] + total_pages = data['total_pages'] + await _capture_listing(g.s, url,items, total_pages) + return {"ok": True} + + + +@csrf_exempt +@products_api.post("/log/") +@clear_cache(tag='browse') +async def log_product(): + data: Dict[str, Any] = await request.get_json(force=True, silent=False) + ok = bool(data["ok"]) + + payload = data.get("payload") or {} + try: + await _log_product_result(g.s, ok, payload) + return {"ok": True} + except Exception as e: + return {"ok": False} + + +@csrf_exempt +@products_api.post("/redirects/") +@clear_cache(tag='browse') +async def rediects(): + data: Dict[str, str] = await request.get_json(force=True, silent=False) + await _save_subcategory_redirects(g.s, data) + return {"ok": True} + + +@csrf_exempt +@products_api.post("/nav/") +@clear_cache(tag='browse') +async def save_nav(): + data: Dict[str, Any] = await request.get_json(force=True, silent=False) + market = getattr(g, "market", None) + market_id = market.id if market else None + await _save_nav(g.s, data, market_id=market_id) + return {"ok": True} + + +@csrf_exempt +@products_api.post("/sync/") +@clear_cache(tag='browse') +async def sync_product(): + """ + POST /api/products/sync + Body includes top-level fields and child arrays like: + { + "slug": "my-product", + "title": "...", + "images": [{"url":"https://..","position":0,"kind":"gallery"}], + "sections": [{"title":"Details","html":"

    ..

    "}], + "labels": [{"name":"Vegan"}], + "stickers": [{"name":"Sale"}], + "attributes": [{"key":"Country","value":"UK"}], + "nutrition": [{"key":"Energy","value":"100","unit":"kcal"}], + "allergens": [{"name":"Nuts","contains":true}] + } + """ + payload = await request.get_json(force=True, silent=False) + if not isinstance(payload, dict): + return jsonify({"error": "Invalid JSON"}), 400 + + slug = payload.get("slug") + if not isinstance(slug, str) or not slug: + return jsonify({"error": "Missing 'slug'"}), 400 + + + # find undeleted row by slug + #stmt = select(Product).where(Product.slug == slug, Product.deleted_at.is_(None)) + + stmt = ( + select(Product) + .where(Product.slug == slug, Product.deleted_at.is_(None)) + .options( + selectinload(Product.images), + selectinload(Product.sections), + selectinload(Product.labels), + selectinload(Product.stickers), + selectinload(Product.attributes), + selectinload(Product.nutrition), + selectinload(Product.allergens), + ) + ) + existing: Optional[Product] = (await g.s.execute(stmt)).scalars().first() + + incoming_norm = _serialize_payload_for_compare(payload) + + if existing: + db_norm = _serialize_product_for_compare(existing) + + if _deep_equal(db_norm, incoming_norm): + # Exactly equal → just touch updated_at + existing.updated_at = _now_utc() + await g.s.flush() + return jsonify({"id": existing.id, "action": "touched"}), 200 + + # Different → soft delete old + create a new row + existing.deleted_at = _now_utc() + await g.s.flush() # ensure the soft-delete is persisted before inserting the new row + + new_p = await _create_product_from_payload(g.s, payload) + await g.s.flush() + return jsonify({"id": new_p.id, "action": "replaced"}), 201 + + # Not found → create + new_p = await _create_product_from_payload(g.s, payload) + await g.s.flush() + return jsonify({"id": new_p.id, "action": "created"}), 201 + diff --git a/market/bp/browse/__init__.py b/market/bp/browse/__init__.py new file mode 100644 index 0000000..85fd1a5 --- /dev/null +++ b/market/bp/browse/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +# create the blueprint at package import time +from .routes import register # = Blueprint("browse_bp", __name__) + +# import routes AFTER browse_bp is defined so routes can attach to it +from . import routes # noqa: F401 diff --git a/market/bp/browse/routes.py b/market/bp/browse/routes.py new file mode 100644 index 0000000..750b816 --- /dev/null +++ b/market/bp/browse/routes.py @@ -0,0 +1,163 @@ +from __future__ import annotations + + +from quart import ( + g, + Blueprint, + abort, + render_template, + render_template_string, + make_response, + current_app, +) +from shared.config import config +from .services.nav import category_context, get_nav +from .services.blacklist.category import is_category_blocked + +from .services import ( + _hx_fragment_request, + _productInfo, + _vary, + _current_url_without_page, +) + +from shared.browser.app.redis_cacher import cache_page +from shared.browser.app.utils.htmx import is_htmx_request + +def register(): + browse_bp = Blueprint("browse", __name__) + + from .. import register_product + browse_bp.register_blueprint( + register_product(), + ) + + @browse_bp.get("/") + @cache_page(tag="browse") + async def home(): + """ + Market landing page. + Uses the post data hydrated by the app-level before_request (g.post_data). + """ + p_data = getattr(g, "post_data", None) or {} + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/market/index.html", **p_data) + else: + # HTMX request: main panel + OOB elements + html = await render_template("_types/market/_oob_elements.html", **p_data) + + return await make_response(html) + + @browse_bp.get("/all/") + @cache_page(tag="browse") + async def browse_all(): + """ + Browse all products across all categories. + Renders full page or just product cards (HTMX pagination fragment). + """ + market = getattr(g, "market", None) + market_id = market.id if market else None + nav = await get_nav(g.s, market_id=market_id) + ctx = { + "category_label": "All Products", + "top_slug": "all", + "sub_slug": None, + } + + product_info = await _productInfo() + full_context = {**product_info, **ctx} + + # Determine which template to use based on request type and pagination + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/browse/index.html", **full_context) + elif product_info["page"] > 1: + # HTMX pagination: just product cards + sentinel + html = await render_template("_types/browse/_product_cards.html", **product_info) + else: + # HTMX navigation (page 1): main panel + OOB elements + html = await render_template("_types/browse/_oob_elements.html", **full_context) + + resp = await make_response(html) + resp.headers["Hx-Push-Url"] = _current_url_without_page() + return _vary(resp) + + + @browse_bp.get("//") + @cache_page(tag="browse") + async def browse_top(top_slug: str): + """ + Browse by top-level category (e.g. /fruit). + 404 if category not in allowed list or is blocked. + """ + REVERSE_CATEGORY = {v: k for k, v in config()["categories"]["allow"].items()} + if top_slug not in REVERSE_CATEGORY: + abort(404) + if is_category_blocked(top_slug): + abort(404) + + market = getattr(g, "market", None) + market_id = market.id if market else None + nav = await get_nav(g.s, market_id=market_id) + ctx = category_context(top_slug, None, nav) + + product_info = await _productInfo(top_slug) + full_context = {**product_info, **ctx} + + # Determine which template to use based on request type and pagination + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/browse/index.html", **full_context) + elif product_info["page"] > 1: + # HTMX pagination: just product cards + sentinel + html = await render_template("_types/browse/_product_cards.html", **product_info) + else: + html = await render_template("_types/browse/_oob_elements.html", **full_context) + + resp = await make_response(html) + resp.headers["Hx-Push-Url"] = _current_url_without_page() + return _vary(resp) + + + @browse_bp.get("///") + @cache_page(tag="browse") + async def browse_sub(top_slug: str, sub_slug: str): + """ + Browse by subcategory (e.g. /fruit/citrus). + 404 if blocked or unknown. + """ + REVERSE_CATEGORY = {v: k for k, v in config()["categories"]["allow"].items()} + if top_slug not in REVERSE_CATEGORY: + abort(404) + if is_category_blocked(top_slug, sub_slug): + abort(404) + + market = getattr(g, "market", None) + market_id = market.id if market else None + nav = await get_nav(g.s, market_id=market_id) + ctx = category_context(top_slug, sub_slug, nav) + + product_info = await _productInfo(top_slug, sub_slug) + full_context = {**product_info, **ctx} + + # Determine which template to use based on request type and pagination + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/browse/index.html", **full_context) + elif product_info["page"] > 1: + # HTMX pagination: just product cards + sentinel + html = await render_template("_types/browse/_product_cards.html", **product_info) + else: + # HTMX navigation (page 1): main panel + OOB elements + html = await render_template("_types/browse/_oob_elements.html", **full_context) + + resp = await make_response(html) + resp.headers["Hx-Push-Url"] = _current_url_without_page() + return _vary(resp) + + + + return browse_bp \ No newline at end of file diff --git a/market/bp/browse/services/__init__.py b/market/bp/browse/services/__init__.py new file mode 100644 index 0000000..70d11d0 --- /dev/null +++ b/market/bp/browse/services/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations +from quart import Blueprint + + +from .services import ( + _hx_fragment_request, + _productInfo, + _order_brands_selected_first, + _massage_product, + _vary, + _current_url_without_page, + _is_liked +) diff --git a/market/bp/browse/services/blacklist/category.py b/market/bp/browse/services/blacklist/category.py new file mode 100644 index 0000000..87aceda --- /dev/null +++ b/market/bp/browse/services/blacklist/category.py @@ -0,0 +1,12 @@ +# suma_browser/category_blacklist.py +from __future__ import annotations +from typing import Optional +from shared.config import config + +def _norm(s: str) -> str: + return (s or "").strip().lower().strip("/") + +def is_category_blocked(top_slug: str, sub_slug: Optional[str] = None) -> bool: + if sub_slug: + return is_category_blocked(top_slug) or _norm(f"{top_slug}/{sub_slug}") in config()["blacklist"]["category"] + return _norm(top_slug) in config()["blacklist"]["category"] diff --git a/market/bp/browse/services/blacklist/product.py b/market/bp/browse/services/blacklist/product.py new file mode 100644 index 0000000..d7d298b --- /dev/null +++ b/market/bp/browse/services/blacklist/product.py @@ -0,0 +1,15 @@ +from typing import Set, Optional +from ..slugs import canonical_html_slug +from shared.config import config + +_blocked: Set[str] = set() +_mtime: Optional[float] = None + +def _norm(slug: str) -> str: + slug = (slug or "").strip().strip("/").lower() + if slug.startswith("product/"): + slug = slug.split("/", 1)[1] + return canonical_html_slug(slug) + +def is_product_blocked(slug: str) -> bool: + return _norm(slug) in config()["blacklist"]["product"] diff --git a/market/bp/browse/services/blacklist/product_details.py b/market/bp/browse/services/blacklist/product_details.py new file mode 100644 index 0000000..7a2244a --- /dev/null +++ b/market/bp/browse/services/blacklist/product_details.py @@ -0,0 +1,11 @@ +import re +from shared.config import config + +def _norm_title_key(t: str) -> str: + t = (t or "").strip().lower() + t = re.sub(r":\s*$", "", t) + t = re.sub(r"\s+", " ", t) + return t + +def is_blacklisted_heading(title: str) -> bool: + return _norm_title_key(title) in [s.lower() for s in config()["blacklist"]["product-details"]] diff --git a/market/bp/browse/services/cache_backend.py b/market/bp/browse/services/cache_backend.py new file mode 100644 index 0000000..00a0f77 --- /dev/null +++ b/market/bp/browse/services/cache_backend.py @@ -0,0 +1,367 @@ +from __future__ import annotations +import os, json +from typing import List, Optional +from shared.config import config +from .blacklist.product import is_product_blocked + + +def _json(path: str): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def fs_nav(): + path = os.path.join(config()["cache"]["fs_root"], "nav.json") + return _json(path) + + +def _brand_of(item: dict) -> str: + b = (item.get("brand") or "").strip() + if b: + return b + try: + return (item.get("info_table", {}).get("Brand") or "").strip() + except Exception: + return "" + + +def _stickers_of(item: dict) -> List[str]: + vals = item.get("stickers") or [] + out = [] + for v in vals: + s = (str(v) or "").strip().lower() + if s: + out.append(s) + return out + + +def fs_product_by_slug(slug: str): + slug = (slug or "").strip() + if slug.endswith(".json"): + path = os.path.join(config()["cache"]["fs_root"], "products", slug) + else: + path = os.path.join(config()["cache"]["fs_root"], "products", f"{slug}.json") + return _json(path) + + +def fs_count_products_in_sub(top_slug: str, sub_slug: Optional[str]) -> int: + """ + Return how many products are in the listing for (top_slug, sub_slug), + after filtering out blocked products. + + If sub_slug is None, that's the top-level category listing. + """ + fs_root = config()["cache"]["fs_root"] + + # Build path to listings/.../items.json just like fs_products does + parts = ["listings", top_slug] + if sub_slug: + parts.append(sub_slug) + parts.append("items.json") + + path = os.path.join(fs_root, *parts) + if not os.path.exists(path): + return 0 + + try: + all_slugs = _json(path) + except Exception: + return 0 + + # Filter out blocked products + allowed = [ + slug for slug in all_slugs + if not is_product_blocked(slug) + ] + return len(allowed) + + +def fs_products( + top_slug: str | None, + sub_slug: str | None, + selected_brands: Optional[List[str]] = None, + selected_stickers: Optional[List[str]] = None, + selected_labels: Optional[List[str]] = None, + page: int = 1, + search: Optional[str] = None, + sort: Optional[str] = None, + page_size: int = 20, + + # NEW: only include products the current user has liked + liked_slugs: Optional[List[str]] = None, + liked: bool = None, +): + """ + Returns: + { + "total_pages": int, + "items": [product dict ...], # filtered + paginated (sorted) + "brands": [{"name": str, "count": int}], + "stickers": [{"name": str, "count": int}], + "labels": [{"name": str, "count": int}], + } + + Filters: + - top_slug / sub_slug scope + - selected_brands + - selected_stickers + - selected_labels + - search + - liked_slugs (if provided) + """ + + import os + from typing import List, Dict + + fs_root = config()["cache"]["fs_root"] + + # ---------- Collect slugs ---------- + slugs: List[str] = [] + if top_slug: # normal listing path + parts = ["listings", top_slug] + if sub_slug: + parts.append(sub_slug) + parts.append("items.json") + path = os.path.join(fs_root, *parts) + if os.path.exists(path): + try: + slugs = [s for s in _json(path) if not is_product_blocked(s)] + except Exception: + slugs = [] + else: + # No top slug: include ALL products from /products/*.json + products_dir = os.path.join(fs_root, "products") + try: + for fname in os.listdir(products_dir): + if not fname.endswith(".json"): + continue + slug = fname[:-5] # strip .json + if not is_product_blocked(slug): + slugs.append(slug) + except FileNotFoundError: + slugs = [] + + # ---------- Load product dicts ---------- + all_items: List[dict] = [] + for slug in slugs: + try: + item = fs_product_by_slug(slug) + if isinstance(item, dict): + all_items.append(item) + except Exception: + continue + + # Stable deterministic ordering when aggregating everything (name ASC) + def _title_key(it: dict) -> tuple: + title = (it.get("title") or it.get("name") or it.get("slug") or "").strip().lower() + return (title, it.get("slug") or "") + + all_items.sort(key=_title_key) + + # ---------- Helpers for filters & counts ---------- + def _brand_of_local(item: dict) -> str: + b = item.get("brand") or (item.get("info_table") or {}).get("Brand") + return (b or "").strip() + + def _stickers_of_local(item: dict) -> List[str]: + vals = item.get("stickers") or [] + out = [] + for s in vals: + if isinstance(s, str): + s2 = s.strip().lower() + if s2: + out.append(s2) + return out + + def _labels_of_local(item: dict) -> List[str]: + vals = item.get("labels") or [] + out = [] + for s in vals: + if isinstance(s, str): + s2 = s.strip().lower() + if s2: + out.append(s2) + return out + + sel_brands = [ + (s or "").strip().lower() + for s in (selected_brands or []) + if (s or "").strip() + ] + sel_stickers = [ + (s or "").strip().lower() + for s in (selected_stickers or []) + if (s or "").strip() + ] + sel_labels = [ + (s or "").strip().lower() + for s in (selected_labels or []) + if (s or "").strip() + ] + search_q = (search or "").strip().lower() or None + + liked_set = { + (slug or "").strip().lower() + for slug in (liked_slugs or [] if liked else []) + if (slug or "").strip() + } + + real_liked_set = { + (slug or "").strip().lower() + for slug in (liked_slugs or []) + if (slug or "").strip() + } + + def matches_brand(item: dict) -> bool: + if not sel_brands: + return True + return _brand_of_local(item).strip().lower() in sel_brands + + def has_all_selected_stickers(item: dict) -> bool: + if not sel_stickers: + return True + tags = set(_stickers_of_local(item)) + return all(s in tags for s in sel_stickers) + + def has_all_selected_labels(item: dict) -> bool: + if not sel_labels: + return True + tags = set(_labels_of_local(item)) + return all(s in tags for s in sel_labels) + + def matches_search(item: dict) -> bool: + if not search_q: + return True + desc = (item.get("description_short") or "").strip().lower() + return search_q in desc + + def is_liked(item: dict) -> bool: + """ + True if this item should be shown under the liked filter. + If liked_set is empty, treat everything as allowed. + """ + slug_val = (item.get("slug") or "").strip().lower() + return slug_val in real_liked_set + + # ---------- Counts (dependent on other filters + search + liked) ---------- + brand_counts: Dict[str, int] = {} + for b in (selected_brands or []): + brand_counts[b] = 0 + + for it in all_items: + b = _brand_of_local(it) + if not b: + continue + brand_counts[b] = brand_counts.get(b, 0) + 1 + + sticker_counts: Dict[str, int] = {} + for s in (selected_stickers or []): + sticker_counts[s] = 0 + for it in all_items: + for s in _stickers_of_local(it): + sticker_counts[s] = sticker_counts.get(s, 0) + 1 + + label_counts: Dict[str, int] = {} + for s in (selected_labels or []): + label_counts[s] = 0 + for it in all_items: + for s in _labels_of_local(it): + label_counts[s] = label_counts.get(s, 0) + 1 + + liked_count = 0 + for it in all_items: + if is_liked(it): + liked_count += 1 + + search_count=0 + for it in all_items: + if matches_search(it): + search_count += 1 + + + # ---------- Apply filters ---------- + filtered = [ + it + for it in all_items + if matches_brand(it) + and has_all_selected_stickers(it) + and has_all_selected_labels(it) + and matches_search(it) + and (not liked or is_liked(it)) + ] + + # ---------- Sorting ---------- + sort_mode = (sort or "az").strip().lower() + + def _price_key(item: dict): + p = item["regular_price"] + title, slug = _title_key(item) + return (0 if p is not None else 1, p if p is not None else 0, title, slug) + + def _price_key_desc(item: dict): + p = item["regular_price"] + title, slug = _title_key(item) + return ( + 0 if p is not None else 1, + -(p if p is not None else 0), + title, + slug, + ) + + if sort_mode in ("az",): + filtered.sort(key=_title_key) + elif sort_mode in ("za",): + filtered.sort(key=_title_key, reverse=True) + elif sort_mode in ( + "price-asc", "price_asc", "price-low", "price-low-high", "low-high", "lo-hi" + ): + filtered.sort(key=_price_key) + elif sort_mode in ( + "price-desc", "price_desc", "price-high", "price-high-low", "high-low", "hi-lo" + ): + filtered.sort(key=_price_key_desc) + else: + filtered.sort(key=_title_key) + + # ---------- Pagination ---------- + total_pages = max(1, (len(filtered) + page_size - 1) // page_size) + page = max(1, page) + start = (page - 1) * page_size + end = start + page_size + page_items = filtered[start:end] + # ---------- Format counts lists ---------- + brands_list = sorted( + [{"name": k, "count": v} for k, v in brand_counts.items()], + key=lambda x: (-x["count"], x["name"].lower()), + ) + stickers_list = sorted( + [{"name": k, "count": v} for k, v in sticker_counts.items()], + key=lambda x: (-x["count"], x["name"]), + ) + labels_list = sorted( + [{"name": k, "count": v} for k, v in label_counts.items()], + key=lambda x: (-x["count"], x["name"]), + ) + return { + "total_pages": total_pages, + "items": page_items, + "brands": brands_list, + "stickers": stickers_list, + "labels": labels_list, + "liked_count": liked_count, + "search_count": search_count + } + +# async wrappers (unchanged) +async def read_nav(): + return fs_nav() + +async def read_listing(top_slug: str, sub_slug: str | None, page: int): + return fs_products(top_slug, sub_slug, None, None, page) + +async def read_product(slug_or_path: str): + slug = (slug_or_path or "").strip() + if "/" in slug: + slug = slug.rsplit("/", 1)[-1] + slug = slug.split("?", 1)[0] + return fs_product_by_slug(slug) diff --git a/market/bp/browse/services/db_backend.py b/market/bp/browse/services/db_backend.py new file mode 100644 index 0000000..dab83b2 --- /dev/null +++ b/market/bp/browse/services/db_backend.py @@ -0,0 +1,714 @@ +from __future__ import annotations +from typing import Dict, List, Optional + +from sqlalchemy import select, and_ +from sqlalchemy.orm import selectinload + +from shared.config import config # if unused elsewhere, you can remove this import + +# ORM models +from models.market import ( + Product, ProductImage, ProductSection, + Listing, ListingItem, + NavTop, NavSub, + ProductSticker, ProductLabel, + ProductAttribute, ProductNutrition, ProductAllergen, ProductLike + +) +from sqlalchemy import func, case + + +# ---------- helpers ---------- +def _regular_price_of(p: Product) -> Optional[float]: + try: + return ( + float(p.regular_price) + if p.regular_price is not None + else ( + float(p.special_price) + if p.special_price is not None + else None + ) + ) + except Exception: + return None + +# ---------- NAV ---------- +async def db_nav(session, market_id=None) -> Dict: + top_q = select(NavTop).where(NavTop.deleted_at.is_(None)) + if market_id is not None: + top_q = top_q.where(NavTop.market_id == market_id) + tops = (await session.execute(top_q)).scalars().all() + + top_ids = [t.id for t in tops] + if top_ids: + subs = (await session.execute( + select(NavSub).where(NavSub.top_id.in_(top_ids), NavSub.deleted_at.is_(None)) + )).scalars().all() + else: + subs = [] + + subs_by_top: Dict[int, List[Dict]] = {} + for s in subs: + sub_name = (s.label or s.slug or "").strip() + subs_by_top.setdefault(s.top_id, []).append({ + "label": s.label, + "name": sub_name, # back-compat for callers expecting "name" + "slug": s.slug, + "href": s.href, + }) + + cats: Dict[str, Dict] = {} + for t in tops: + top_label = (t.label or t.slug or "").strip() + cats[top_label] = { + "label": t.label, + "name": top_label, # back-compat + "slug": t.slug, + "subs": sorted(subs_by_top.get(t.id, []), key=lambda x: (x["name"] or "").lower()), + } + return {"cats": cats} + + +async def db_product_full(session, slug: str, user_id=0) -> Optional[dict]: + + liked_product_ids_subq = ( + select(ProductLike.product_slug) + .where( + and_( + ProductLike.user_id == user_id, + ProductLike.deleted_at.is_(None) + ) + ) + ) + + is_liked_case = case( + (and_( + (Product.slug.in_(liked_product_ids_subq)), + Product.deleted_at.is_(None) + ), True), + else_=False + ).label("is_liked") + + q = ( + select(Product, is_liked_case) + .where(Product.slug == slug, Product.deleted_at.is_(None)) + .options( + selectinload(Product.images.and_(ProductImage.deleted_at.is_(None))), + selectinload(Product.sections.and_(ProductSection.deleted_at.is_(None))), + selectinload(Product.labels.and_(ProductLabel.deleted_at.is_(None))), + selectinload(Product.stickers.and_(ProductSticker.deleted_at.is_(None))), + selectinload(Product.attributes.and_(ProductAttribute.deleted_at.is_(None))), + selectinload(Product.nutrition.and_(ProductNutrition.deleted_at.is_(None))), + selectinload(Product.allergens.and_(ProductAllergen.deleted_at.is_(None))), + ) + ) + result = await session.execute(q) + + row = result.first() if result is not None else None + p, is_liked = row if row else (None, None) + if not p: + return None + + gallery = [ + img.url + for img in sorted(p.images, key=lambda i: (i.kind or "gallery", i.position or 0)) + if (img.kind or "gallery") == "gallery" + ] + embedded = [ + img.url + for img in sorted(p.images, key=lambda i: i.position or 0) + if (img.kind or "") == "embedded" + ] + all_imgs = [ + img.url + for img in sorted(p.images, key=lambda i: i.position or 0) + if (img.kind or "") == "all" + ] + return { + "id": p.id, + "slug": p.slug, + "title": p.title, + "brand": p.brand, + "image": p.image, + "description_short": p.description_short, + "description_html": p.description_html, + "suma_href": p.suma_href, + "rrp": float(p.rrp) if p.rrp is not None else None, + "special_price": float(p.special_price) if p.special_price is not None else None, + "special_price_raw": p.special_price_raw, + "special_price_currency": p.special_price_currency, + "regular_price": _regular_price_of(p), + "regular_price_raw": p.regular_price_raw, + "regular_price_currency": p.regular_price_currency, + "rrp_raw": p.rrp_raw, + "rrp_currency": p.rrp_currency, + "price_per_unit_raw": p.price_per_unit_raw, + "price_per_unit": p.price_per_unit, + "price_per_unit_currency": p.price_per_unit_currency, + "oe_list_price": p.oe_list_price, + "images": gallery, + "embedded_image_urls": embedded, + "all_image_urls": all_imgs, + "sections": [{"title": s.title, "html": s.html} for s in p.sections], + "stickers": [v.name.strip().lower() for v in p.stickers if v.name], + "labels": [v.name for v in p.labels if v.name], + "ean": p.ean, + "sku": p.sku, + "unit_size": p.unit_size, + "pack_size": p.pack_size, + "case_size_raw": p.case_size_raw, + "case_size_count": p.case_size_count, + "case_size_item_qty": p.case_size_item_qty, + "case_size_item_unit": p.case_size_item_unit, + "info_table": {a.key: a.value for a in p.attributes if a.key}, + "nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in p.nutrition if n.key], + "allergens": [{"name": a.name, "contains": a.contains} for a in p.allergens if a.name], + "is_liked": is_liked, + "deleted_at": p.deleted_at + } + + +async def db_product_full_id(session, id:int, user_id=0) -> Optional[dict]: + liked_product_ids_subq = ( + select(ProductLike.product_slug) + .where( + and_( + ProductLike.user_id == user_id, + ProductLike.deleted_at.is_(None) + ) + ) + ) + + is_liked_case = case( + ( + (Product.slug.in_(liked_product_ids_subq)), + True + ), + else_=False + ).label("is_liked") + + q = ( + select(Product, is_liked_case) + .where(Product.id == id) + .options( + selectinload(Product.images.and_(ProductImage.deleted_at.is_(None))), + selectinload(Product.sections.and_(ProductSection.deleted_at.is_(None))), + selectinload(Product.labels.and_(ProductLabel.deleted_at.is_(None))), + selectinload(Product.stickers.and_(ProductSticker.deleted_at.is_(None))), + selectinload(Product.attributes.and_(ProductAttribute.deleted_at.is_(None))), + selectinload(Product.nutrition.and_(ProductNutrition.deleted_at.is_(None))), + selectinload(Product.allergens.and_(ProductAllergen.deleted_at.is_(None))), + ) + ) + result = await session.execute(q) + + row = result.first() if result is not None else None + p, is_liked = row if row else (None, None) + if not p: + return None + + gallery = [ + img.url + for img in sorted(p.images, key=lambda i: (i.kind or "gallery", i.position or 0)) + if (img.kind or "gallery") == "gallery" + ] + embedded = [ + img.url + for img in sorted(p.images, key=lambda i: i.position or 0) + if (img.kind or "") == "embedded" + ] + all_imgs = [ + img.url + for img in sorted(p.images, key=lambda i: i.position or 0) + if (img.kind or "") == "all" + ] + return { + "id": p.id, + "slug": p.slug, + "title": p.title, + "brand": p.brand, + "image": p.image, + "description_short": p.description_short, + "description_html": p.description_html, + "suma_href": p.suma_href, + "rrp": float(p.rrp) if p.rrp is not None else None, + "special_price": float(p.special_price) if p.special_price is not None else None, + "special_price_raw": p.special_price_raw, + "special_price_currency": p.special_price_currency, + "regular_price": _regular_price_of(p), + "regular_price_raw": p.regular_price_raw, + "regular_price_currency": p.regular_price_currency, + "rrp_raw": p.rrp_raw, + "rrp_currency": p.rrp_currency, + "price_per_unit_raw": p.price_per_unit_raw, + "price_per_unit": p.price_per_unit, + "price_per_unit_currency": p.price_per_unit_currency, + "oe_list_price": p.oe_list_price, + "images": gallery, + "embedded_image_urls": embedded, + "all_image_urls": all_imgs, + "sections": [{"title": s.title, "html": s.html} for s in p.sections], + "stickers": [v.name.strip().lower() for v in p.stickers if v.name], + "labels": [v.name for v in p.labels if v.name], + "ean": p.ean, + "sku": p.sku, + "unit_size": p.unit_size, + "pack_size": p.pack_size, + "case_size_raw": p.case_size_raw, + "case_size_count": p.case_size_count, + "case_size_item_qty": p.case_size_item_qty, + "case_size_item_unit": p.case_size_item_unit, + "info_table": {a.key: a.value for a in p.attributes if a.key}, + "nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in p.nutrition if n.key], + "allergens": [{"name": a.name, "contains": a.contains} for a in p.allergens if a.name], + "is_liked": is_liked, + "deleted_at": p.deleted_at + } + + + + + +# ---------- PRODUCTS LISTING ---------- + +async def db_products_nocounts( + session, + top_slug: str | None, + sub_slug: str | None, + selected_brands: Optional[List[str]] = None, + selected_stickers: Optional[List[str]] = None, + selected_labels: Optional[List[str]] = None, + page: int = 1, + search: Optional[str] = None, + sort: Optional[str] = None, + page_size: int = 20, + liked: bool = None, + user_id: int=0, + market_id: int | None = None, +) -> Dict: + BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or [])) + base_conditions = [] + if BLOCKED_SLUGS: + base_conditions.append( + ~Product.slug.in_(BLOCKED_SLUGS), + ) + + if top_slug: + q_list_conditions = [ + Listing.deleted_at.is_(None), + NavTop.deleted_at.is_(None), + NavTop.slug == top_slug, + NavSub.deleted_at.is_(None), + NavSub.slug == sub_slug if sub_slug else Listing.sub_id.is_(None), + ] + if market_id is not None: + q_list_conditions.append(NavTop.market_id == market_id) + + q_list = ( + select(Listing.id) + .join(NavTop, Listing.top) + .outerjoin(NavSub, Listing.sub) + .where(*q_list_conditions) + ) + + listing_id = (await session.execute(q_list)).scalars().first() + if not listing_id: + return {"total_pages": 1, "items": []} + + base_conditions.append(Product.slug.in_( + select(ListingItem.slug).where(ListingItem.listing_id == listing_id, ListingItem.deleted_at.is_(None)) + )) + elif market_id is not None: + # Browse all within a specific market: filter products through market's nav hierarchy + market_product_slugs = ( + select(ListingItem.slug) + .join(Listing, ListingItem.listing_id == Listing.id) + .join(NavTop, Listing.top_id == NavTop.id) + .where( + ListingItem.deleted_at.is_(None), + Listing.deleted_at.is_(None), + NavTop.deleted_at.is_(None), + NavTop.market_id == market_id, + ) + ) + base_conditions.append(Product.slug.in_(market_product_slugs)) + + base_ids_subq = select(Product.id).where(*base_conditions, Product.deleted_at.is_(None)) + base_ids = (await session.execute(base_ids_subq)).scalars().all() + + if not base_ids: + return {"total_pages": 1, "items": []} + sel_brands = [(b or "").strip().lower() for b in (selected_brands or []) if (b or "").strip()] + sel_stickers = [(s or "").strip().lower() for s in (selected_stickers or []) if (s or "").strip()] + sel_labels = [(l or "").strip().lower() for l in (selected_labels or []) if (l or "").strip()] + search_q = (search or "").strip().lower() + + filter_conditions = [] + if sel_brands: + filter_conditions.append(func.lower(Product.brand).in_(sel_brands)) + for sticker_name in sel_stickers: + filter_conditions.append( + Product.stickers.any( + and_( + func.lower(ProductSticker.name) == sticker_name, + ProductSticker.deleted_at.is_(None) + ) + ) + ) + for label_name in sel_labels: + filter_conditions.append( + Product.labels.any( + and_( + func.lower(ProductLabel.name) == label_name, + ProductLabel.deleted_at.is_(None), + ) + ) + ) + if search_q: + filter_conditions.append(func.lower(Product.description_short).contains(search_q)) + if liked: + liked_subq = liked_subq = ( + select(ProductLike.product_slug) + .where( + and_( + ProductLike.user_id == user_id, + ProductLike.deleted_at.is_(None) + ) + ) + .subquery() + ) + filter_conditions.append(Product.slug.in_(liked_subq)) + + filtered_count_query = select(func.count(Product.id)).where(Product.id.in_(base_ids), *filter_conditions) + total_filtered = (await session.execute(filtered_count_query)).scalars().one() + total_pages = max(1, (total_filtered + page_size - 1) // page_size) + page = max(1, page) + + + liked_product_slugs_subq = ( + select(ProductLike.product_slug) + .where( + and_( + ProductLike.user_id == user_id, + ProductLike.deleted_at.is_(None) + ) + ) + ) + is_liked_case = case( + (Product.slug.in_(liked_product_slugs_subq), True), + else_=False + ).label("is_liked") + + q_filtered = select(Product, is_liked_case).where(Product.id.in_(base_ids), *filter_conditions).options( + selectinload(Product.images), + selectinload(Product.sections), + selectinload(Product.labels), + selectinload(Product.stickers), + selectinload(Product.attributes), + selectinload(Product.nutrition), + selectinload(Product.allergens), + ) + + sort_mode = (sort or "az").strip().lower() + if sort_mode == "az": + q_filtered = q_filtered.order_by(func.lower(Product.title), Product.slug) + elif sort_mode == "za": + q_filtered = q_filtered.order_by(func.lower(Product.title).desc(), Product.slug.desc()) + elif sort_mode in ("price-asc", "price_asc", "price-low", "price-low-high", "low-high", "lo-hi"): + q_filtered = q_filtered.order_by( + case((Product.regular_price.is_(None), 1), else_=0), + Product.regular_price.asc(), + func.lower(Product.title), + Product.slug + ) + elif sort_mode in ("price-desc", "price_desc", "price-high", "price-high-low", "high-low", "hi-lo"): + q_filtered = q_filtered.order_by( + case((Product.regular_price.is_(None), 1), else_=0), + Product.regular_price.desc(), + func.lower(Product.title), + Product.slug + ) + else: + q_filtered = q_filtered.order_by(func.lower(Product.title), Product.slug) + + offset_val = (page - 1) * page_size + q_filtered = q_filtered.offset(offset_val).limit(page_size) + products_page = (await session.execute(q_filtered)).all() + + items: List[Dict] = [] + for p, is_liked in products_page: + gallery_imgs = sorted((img for img in p.images), key=lambda i: (i.kind or "gallery", i.position or 0)) + gallery = [img.url for img in gallery_imgs if (img.kind or "gallery") == "gallery"] + embedded = [img.url for img in sorted(p.images, key=lambda i: i.position or 0) if (img.kind or "") == "embedded"] + all_imgs = [img.url for img in sorted(p.images, key=lambda i: i.position or 0) if (img.kind or "") == "all"] + + items.append({ + "slug": p.slug, + "title": p.title, + "brand": p.brand, + "description_short": p.description_short, + "description_html": p.description_html, + "image": p.image, + "rrp": float(p.rrp) if p.rrp is not None else None, + "special_price": float(p.special_price) if p.special_price is not None else None, + "special_price_raw": p.special_price_raw, + "special_price_currency": p.special_price_currency, + "regular_price": _regular_price_of(p), + "regular_price_raw": p.regular_price_raw, + "regular_price_currency": p.regular_price_currency, + "rrp_raw": p.rrp_raw, + "rrp_currency": p.rrp_currency, + "price_per_unit_raw": p.price_per_unit_raw, + "price_per_unit": p.price_per_unit, + "price_per_unit_currency": p.price_per_unit_currency, + "images": gallery, + "embedded_image_urls": embedded, + "all_image_urls": all_imgs, + "sections": [{"title": s.title, "html": s.html} for s in p.sections], + "labels": [l.name for l in p.labels if l.name], + "stickers": [s.name.strip().lower() for s in p.stickers if s.name], + "info_table": {a.key: a.value for a in p.attributes if a.key}, + "nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in p.nutrition if n.key], + "allergens": [{"name": a.name, "contains": a.contains} for a in p.allergens if a.name], + "ean": p.ean, + "sku": p.sku, + "unit_size": p.unit_size, + "pack_size": p.pack_size, + "is_liked": is_liked, + }) + + return { + "total_pages": total_pages, + "items": items, + } + + +async def db_products_counts( + session, + top_slug: str | None, + sub_slug: str | None, + search: Optional[str] = None, + user_id: int=0, + market_id: int | None = None, +) -> Dict: + BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or [])) + base_conditions = [] + + if top_slug: + q_list_conditions = [ + Listing.deleted_at.is_(None), + Listing.top.has(slug=top_slug), + Listing.sub.has(slug=sub_slug) if sub_slug else Listing.sub_id.is_(None), + ] + if market_id is not None: + q_list_conditions.append(Listing.top.has(market_id=market_id)) + q_list = select(Listing.id).where(*q_list_conditions) + listing_id = (await session.execute(q_list)).scalars().first() + if not listing_id: + return { + "brands": [], + "stickers": [], + "labels": [], + "liked_count": 0, + "search_count": 0, + } + + listing_slug_subquery = select(ListingItem.slug).where(ListingItem.listing_id == listing_id, ListingItem.deleted_at.is_(None)) + + if BLOCKED_SLUGS: + base_conditions.append( + and_( + Product.slug.in_(listing_slug_subquery), + ~Product.slug.in_(BLOCKED_SLUGS), + ) + ) + else: + base_conditions.append(Product.slug.in_(listing_slug_subquery)) + else: + if market_id is not None: + # Browse all within a specific market + market_product_slugs = ( + select(ListingItem.slug) + .join(Listing, ListingItem.listing_id == Listing.id) + .join(NavTop, Listing.top_id == NavTop.id) + .where( + ListingItem.deleted_at.is_(None), + Listing.deleted_at.is_(None), + NavTop.deleted_at.is_(None), + NavTop.market_id == market_id, + ) + ) + if BLOCKED_SLUGS: + base_conditions.append( + and_( + Product.slug.in_(market_product_slugs), + ~Product.slug.in_(BLOCKED_SLUGS), + ) + ) + else: + base_conditions.append(Product.slug.in_(market_product_slugs)) + elif BLOCKED_SLUGS: + base_conditions.append(~Product.slug.in_(BLOCKED_SLUGS)) + base_ids = (await session.execute(select(Product.id).where(*base_conditions, Product.deleted_at.is_(None)))).scalars().all() + if base_ids: + base_products_slugs = (await session.execute( + select(Product.slug).where(Product.id.in_(base_ids), Product.deleted_at.is_(None)) + )).scalars().all() + if not base_products_slugs: + return { + "brands": [], + "stickers": [], + "labels": [], + "liked_count": 0, + "search_count": 0, + } + base_ids = (await session.execute( + select(Product.id).where(Product.slug.in_(base_products_slugs), Product.deleted_at.is_(None)) + )).scalars().all() + else: + return { + "brands": [], + "stickers": [], + "labels": [], + "liked_count": 0, + "search_count": 0, + } + + brands_list: List[Dict] = [] + stickers_list: List[Dict] = [] + labels_list: List[Dict] = [] + liked_count = 0 + search_count = 0 + liked_product_slugs_subq = ( + select(ProductLike.product_slug) + .where(ProductLike.user_id == user_id, ProductLike.deleted_at.is_(None)) + ) + liked_count = await session.scalar( + select(func.count(Product.id)) + .where( + Product.id.in_(base_ids), + Product.slug.in_(liked_product_slugs_subq), + Product.deleted_at.is_(None) + ) + ) + + liked_count = (await session.execute( + select(func.count()) + .select_from(ProductLike) + .where( + ProductLike.user_id == user_id, + ProductLike.product_slug.in_( + select(Product.slug).where(Product.id.in_(base_ids)) + ), + ProductLike.deleted_at.is_(None) + ) + )).scalar_one() if user_id else 0 + + # Brand counts + brand_count_rows = await session.execute( + select(Product.brand, func.count(Product.id)) + .where(Product.id.in_(base_ids), + Product.brand.is_not(None), + func.trim(Product.brand) != "", + Product.deleted_at.is_(None) + ) + .group_by(Product.brand) + ) + for brand_name, count in brand_count_rows: + brands_list.append({"name": brand_name, "count": count}) + brands_list.sort(key=lambda x: (-x["count"], x["name"].lower())) + + # Sticker counts + sticker_count_rows = await session.execute( + select(ProductSticker.name, func.count(ProductSticker.product_id)) + .where( + ProductSticker.product_id.in_(base_ids), + ProductSticker.deleted_at.is_(None) + ) + .group_by(ProductSticker.name) + ) + for sticker_name, count in sticker_count_rows: + if sticker_name: + stickers_list.append({"name": sticker_name.strip().lower(), "count": count}) + stickers_list.sort(key=lambda x: (-x["count"], x["name"])) + + # Label counts + label_count_rows = await session.execute( + select(ProductLabel.name, func.count(ProductLabel.product_id)) + .where( + ProductLabel.product_id.in_(base_ids), + ProductLabel.deleted_at.is_(None) + ) + .group_by(ProductLabel.name) + ) + for label_name, count in label_count_rows: + if label_name: + labels_list.append({"name": label_name, "count": count}) + labels_list.sort(key=lambda x: (-x["count"], x["name"])) + + + # Search count + search_q = (search or "").strip().lower() + if search_q: + search_count = (await session.execute( + select(func.count(Product.id)) + .where( + Product.id.in_(base_ids), + func.lower(Product.description_short).contains(search_q), + Product.deleted_at.is_(None) + ) + )).scalars().one() + else: + search_count = len(base_ids) + + return { + "brands": brands_list, + "stickers": stickers_list, + "labels": labels_list, + "liked_count": liked_count, + "search_count": search_count, + } + +async def db_products( + session, + top_slug: str | None, + sub_slug: str | None, + selected_brands: Optional[List[str]] = None, + selected_stickers: Optional[List[str]] = None, + selected_labels: Optional[List[str]] = None, + page: int = 1, + search: Optional[str] = None, + sort: Optional[str] = None, + page_size: int = 20, + liked: bool = None, + user_id: int=0, + market_id: int | None = None, +) -> Dict: + return { + **(await db_products_nocounts( + session, + top_slug=top_slug, + sub_slug=sub_slug, + selected_brands=selected_brands, + selected_stickers=selected_stickers, + selected_labels=selected_labels, + page=page, + search=search, + sort=sort, + page_size=page_size, + liked=liked, + user_id=user_id, + market_id=market_id, + )), + **(await db_products_counts( + session, + top_slug=top_slug, + sub_slug=sub_slug, + search=search, + user_id=user_id, + market_id=market_id, + )), + } + + diff --git a/market/bp/browse/services/nav.py b/market/bp/browse/services/nav.py new file mode 100644 index 0000000..bdef674 --- /dev/null +++ b/market/bp/browse/services/nav.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import time +import re +from typing import Dict, List, Tuple, Optional +from urllib.parse import urlparse, urljoin + +from shared.config import config +from . import db_backend as cb +from .blacklist.category import is_category_blocked # Reverse map: slug -> label + +# ------------------ Caches ------------------ +_nav_cache: Dict = {} +_nav_cache_ts: float = 0.0 +_nav_ttl_seconds = 60 * 60 * 6 # 6 hours + + +def _now() -> float: + try: + return now() # type: ignore[name-defined] + except Exception: + return time.time() + + +def extract_sub_slug(href: str, top_slug: str) -> Optional[str]: + p = urlparse(href) + parts = [x for x in (p.path or "").split("/") if x] + if len(parts) >= 2 and parts[0].lower() == top_slug.lower(): + sub = parts[1] + if sub.lower().endswith((".html", ".htm")): + sub = re.sub(r"\.(html?|HTML?)$", "", sub) + return sub + return None + + +def group_by_category(slug_to_links: Dict[str, List[Tuple[str, str]]]) -> Dict[str, Dict]: + nav = {"cats": {}} + for label, slug in config()["categories"]["allow"].items(): + top_href = urljoin(config()["base_url"], f"/{slug}") + subs = [] + for text, href in slug_to_links.get(slug, []): + sub_slug = extract_sub_slug(href, slug) + if sub_slug: + subs.append({ + "name": text, + "href": href, + "slug": sub_slug, + # no count here yet in this path + }) + subs.sort(key=lambda x: x["name"].lower()) + nav["cats"][label] = {"href": top_href, "slug": slug, "subs": subs} + nav = _apply_category_blacklist(nav) + return nav + + +async def get_nav(session, market_id=None) -> Dict[str, Dict]: + """ + Return navigation structure; annotate each sub with product counts. + Uses snapshot for offline behaviour. + """ + global _nav_cache, _nav_cache_ts + now_ts = _now() + + # load from snapshot + nav = await cb.db_nav(session, market_id=market_id) + + # inject counts for each subcategory (and for top-level too if you like) + for label, cat in (nav.get("cats") or {}).items(): + top_slug = cat.get("slug") + if not top_slug: + continue + + + # Counts for subs + new_subs = [] + for s in cat.get("subs", []): + s.get("slug") + #if not sub_slug: + # s_count = 0 + #else: + # s_count = await cb.db_count_products_in_sub(session,top_slug, sub_slug) + #print('sub', s_count) + new_subs.append({ + **s, + #"count": s_count, + }) + cat["subs"] = new_subs + + _nav_cache = nav + _nav_cache_ts = now_ts + + nav = _apply_category_blacklist(nav) + return nav + + +def category_context(top_slug: Optional[str], sub_slug: Optional[str], nav: Dict[str, Dict]): + """Build template context for a category/subcategory page.""" + def _order_subs_selected_first(subs, sub_slug: str | None): + """Return subs with the selected subcategory (by slug) first.""" + if not subs or not sub_slug: + return subs + head = [s for s in subs if sub_slug and sub_slug.lower() == s['slug']] + tail = [s for s in subs if not (sub_slug and sub_slug.lower() == s['slug'])] + return head + tail + + REVERSE_CATEGORY = {v: k for k, v in config()["categories"]["allow"].items()} + label = REVERSE_CATEGORY.get(top_slug) + cat = nav["cats"].get(label) or {} + + top_suma_href = cat.get("href") or urljoin(config()["base_url"], f"/{top_slug}") + top_local_href = f"{top_slug}" + + # total products in this top-level category (all subs combined / top-level listing) + top_count = cat.get("count", 0) + + subs = [] + for s in cat.get("subs", []): + subs.append({ + "name": s["name"], + "slug": s.get("slug"), + "local_href": f"{top_slug}/{s.get('slug')}", + "suma_href": s["href"], + "count": s.get("count", 0), # per-subcategory product count + }) + + current_local_href = ( + f"{top_slug}/{sub_slug}" if sub_slug + else f"{top_slug}" if top_slug + else "" + ) + + return { + "category_label": label, + "top_slug": top_slug, + "sub_slug": sub_slug, + "top_suma_href": top_suma_href, + "top_local_href": top_local_href, + + # 👇 expose total count for the parent category + "top_count": top_count, + + # list of subcategories, each with its own count + "subs_local": _order_subs_selected_first(subs, sub_slug), + + #"current_local_href": current_local_href, + } + +def _apply_category_blacklist(nav: Dict[str, Dict]) -> Dict[str, Dict]: + cats = nav.get("cats", {}) + out = {"cats": {}} + for label, data in cats.items(): + top = (data or {}).get("slug") + if not top or is_category_blocked(top): + continue + # filter subs + subs = [] + for s in (data.get("subs") or []): + sub_slug = s.get("slug") + if sub_slug and not is_category_blocked(top, sub_slug): + subs.append(s) + # keep everything else (including counts) + out["cats"][label] = {**data, "subs": subs} + return out diff --git a/market/bp/browse/services/products.py b/market/bp/browse/services/products.py new file mode 100644 index 0000000..f9a7be3 --- /dev/null +++ b/market/bp/browse/services/products.py @@ -0,0 +1,118 @@ +# products.py +from __future__ import annotations +from typing import List, Optional +from urllib.parse import urlparse + +from .state import KNOWN_PRODUCT_SLUGS +from .blacklist.category import is_category_blocked +from . import db_backend as cb + +# NEW IMPORT: +from quart import g + +async def products( + list_url: str, + selected_brands: Optional[List[str]] = None, + selected_stickers: Optional[List[str]] = None, + selected_labels: Optional[List[str]] = None, + page: int = 1, + search: Optional[str] = None, + sort: Optional[str] = None, + liked: Optional[bool] = None, + user_id: Optional[int] = None, + market_id: int | None = None, +): + p = urlparse(list_url) + parts = [x for x in (p.path or "").split("/") if x] + top = parts[0] if parts else None + sub = parts[1] if len(parts) >= 2 else None + + if is_category_blocked(top, sub): + return [], [], [], [], 1 # <- note: 5 values now, keep shape consistent below + data = await cb.db_products( + g.s, + top, + sub, + selected_brands, + selected_stickers, + selected_labels, + page, + search, + sort, + liked=liked, + user_id = g.user.id if g.user else 0, + market_id=market_id, + ) + items = data.get("items", []) or [] + brands = data.get("brands", []) or [] + stickers = data.get("stickers", []) or [] + labels = data.get("labels", []) or [] + total_pages = int(data.get("total_pages", 1) or 1) + + # Track known product slugs + for it in items: + try: + slug = it.get("slug") + if slug: + KNOWN_PRODUCT_SLUGS.add(slug) + except Exception: + pass + + # --- NEW BIT: mark which are liked by this user --- + + + # Return same shape you were already returning: + # items, brands, stickers, labels, total_pages + return items, brands, stickers, labels, total_pages, data.get("liked_count"), data.get("search_count") + + +async def products_nocounts( + session, + list_url: str, + selected_brands: Optional[List[str]] = None, + selected_stickers: Optional[List[str]] = None, + selected_labels: Optional[List[str]] = None, + page: int = 1, + search: Optional[str] = None, + sort: Optional[str] = None, + liked: Optional[bool] = None, + user_id: Optional[int] = None, + market_id: int | None = None, +): + p = urlparse(list_url) + parts = [x for x in (p.path or "").split("/") if x] + top = parts[0] if parts else None + sub = parts[1] if len(parts) >= 2 else None + + if is_category_blocked(top, sub): + return [], [], [], [], 1 # <- note: 5 values now, keep shape consistent below + data = await cb.db_products_nocounts( + session, + top, + sub, + selected_brands, + selected_stickers, + selected_labels, + page, + search, + sort, + liked=liked, + user_id = g.user.id if g.user else 0, + market_id=market_id, + ) + items = data.get("items", []) or [] + total_pages = int(data.get("total_pages", 1) or 1) + + # Track known product slugs + for it in items: + try: + slug = it.get("slug") + if slug: + KNOWN_PRODUCT_SLUGS.add(slug) + except Exception: + pass + + + # Return same shape you were already returning: + # items, brands, stickers, labels, total_pages + return items, total_pages diff --git a/market/bp/browse/services/services.py b/market/bp/browse/services/services.py new file mode 100644 index 0000000..dbdcaad --- /dev/null +++ b/market/bp/browse/services/services.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +from urllib.parse import urljoin + +from quart import ( + g, + request, +) +from shared.config import config +from .products import products, products_nocounts +from .blacklist.product_details import is_blacklisted_heading + +from shared.utils import host_url + + +from sqlalchemy import select +from models import ProductLike +from ...market.filters.qs import decode + + +def _hx_fragment_request() -> bool: + return request.headers.get("HX-Request", "").lower() == "true" + +async def _productInfo(top_slug=None, sub_slug=None): + """ + Shared query logic for home / category / subcategory pages. + Pulls filters from qs.decode(), queries products(), and orders brands/stickers/etc. + """ + + q = decode() + page, search, sort = q.page, q.search, q.sort + selected_brands, selected_stickers, selected_labels = q.selected_brands, q.selected_stickers, q.selected_labels + liked = q.liked + + # Get market_id from hydrated market context + market = getattr(g, "market", None) + market_id = market.id if market else None + + if top_slug is not None and sub_slug is not None: + list_url = urljoin(config()["base_url"], f"/{top_slug}/{sub_slug}") + else: + if top_slug is not None: + list_url = top_slug + else: + list_url = "" + if not _hx_fragment_request() or page==1: + items, brands, stickers, labels, total_pages, liked_count, search_count = await products( + list_url, + selected_brands=selected_brands, + selected_stickers=selected_stickers, + selected_labels=selected_labels, + page=page, + search=search, + sort=sort, + user_id=g.user.id if g.user else None, + liked = liked, + market_id=market_id, + ) + + brands_ordered = _order_brands_selected_first(brands, selected_brands) + + return { + "products": items, + "page": page, + "search": search, + "sort": sort, + "total_pages": int(total_pages or 1), + "brands": brands_ordered, + "selected_brands": selected_brands, + "stickers": stickers, + "selected_stickers": selected_stickers, + "labels": labels, + "selected_labels": selected_labels, + "liked": liked, + "liked_count": liked_count, + "search_count": search_count + } + else: + items, total_pages = await products_nocounts( + g.s, + list_url, + selected_brands=selected_brands, + selected_stickers=selected_stickers, + selected_labels=selected_labels, + page=page, + search=search, + sort=sort, + user_id=g.user.id if g.user else None, + liked = liked, + market_id=market_id, + ) + return { + "products": items, + "page": page, + "search": search, + "sort": sort, + "total_pages": int(total_pages or 1), + } + + +def _order_brands_selected_first(brands, selected): + """Return brands with the selected brand(s) first.""" + if not brands or not selected: + return brands + sel = [(s or "").strip() for s in selected] + head = [s for s in brands if (s.get("name") or "").strip() in sel] + tail = [s for s in brands if (s.get("name") or "").strip() not in sel] + return head + tail + + +def _order_stickers_selected_first( + stickers: list[dict], selected_stickers: list[str] | None +): + if not stickers or not selected_stickers: + return stickers + sel = [(s or "").strip().lower() for s in selected_stickers] + head = [s for s in stickers if (s.get("name") or "").strip().lower() in sel] + tail = [ + s + for s in stickers + if (s.get("name") or "").strip().lower() not in sel + ] + return head + tail + + +def _order_labels_selected_first( + labels: list[dict], selected_labels: list[str] | None +): + if not labels or not selected_labels: + return labels + sel = [(s or "").strip().lower() for s in selected_labels] + head = [s for s in labels if (s.get("name") or "").strip().lower() in sel] + tail = [ + s + for s in labels + if (s.get("name") or "").strip().lower() not in sel + ] + return head + tail + +def _massage_product(d): + """ + Normalise the product dict for templates: + - inject APP_ROOT into HTML + - drop blacklisted sections + """ + massaged = { + **d, + "description_html": d["description_html"].replace( + "[**__APP_ROOT__**]", g.root + ), + "sections": [ + { + **section, + "html": section["html"].replace( + "[**__APP_ROOT__**]", g.root + ), + } + for section in d["sections"] + if not is_blacklisted_heading(section["title"]) + ], + } + return massaged + + +# Re-export from canonical shared location +from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page + +async def _is_liked(user_id: int | None, slug: str) -> bool: + """ + Check if this user has liked this product. + """ + if not user_id: + return False + # because ProductLike has composite PK (user_id, product_slug), + # we can fetch it by primary key dict: + row = await g.s.execute( + select(ProductLike).where( + ProductLike.user_id == user_id, + ProductLike.product_slug == slug, + ) + ) + row.scalar_one_or_none() + return row is not None + + diff --git a/market/bp/browse/services/slugs.py b/market/bp/browse/services/slugs.py new file mode 100644 index 0000000..f45a258 --- /dev/null +++ b/market/bp/browse/services/slugs.py @@ -0,0 +1,24 @@ +import re +from urllib.parse import urljoin, urlparse +from shared.config import config + +def product_slug_from_href(href: str) -> str: + p = urlparse(href) + parts = [x for x in p.path.split("/") if x] + if not parts: + return "" + last = parts[-1] + if last.endswith(".html"): + last = last[:-5] + elif last.endswith(".htm"): + last = last[:-4] + last = re.sub(r"-(html|htm)+$", "", last, flags=re.I) + return f"{last}-html" + +def canonical_html_slug(slug: str) -> str: + base = re.sub(r"-(html|htm)+$", "", slug, flags=re.I) + return f"{base}-html" + +def suma_href_from_html_slug(slug: str) -> str: + canon = canonical_html_slug(slug) + return urljoin(config()["base_url"], f"/{canon}.html") diff --git a/market/bp/browse/services/state.py b/market/bp/browse/services/state.py new file mode 100644 index 0000000..2ad0495 --- /dev/null +++ b/market/bp/browse/services/state.py @@ -0,0 +1,21 @@ +from typing import Dict, Tuple, List +import time + +_nav_cache: dict = {} +_nav_cache_ts: float = 0.0 +_nav_ttl_seconds = 60 * 60 * 6 + +_detail_cache: Dict[str, Dict] = {} +_detail_cache_ts: Dict[str, float] = {} +_detail_ttl_seconds = 60 * 60 * 6 + +KNOWN_PRODUCT_SLUGS: set[str] = set() + +_listing_variant_cache: Dict[str, Tuple[str, float]] = {} +_listing_variant_ttl = 60 * 60 * 6 + +_listing_page_cache: Dict[str, Tuple[Tuple[List[Dict], int], float]] = {} +_listing_page_ttl = 60 * 30 + +def now() -> float: + return time.time() diff --git a/market/bp/cart/__init__.py b/market/bp/cart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/bp/cart/services/__init__.py b/market/bp/cart/services/__init__.py new file mode 100644 index 0000000..2643d81 --- /dev/null +++ b/market/bp/cart/services/__init__.py @@ -0,0 +1,2 @@ +from .total import total +from .identity import CartIdentity, current_cart_identity diff --git a/market/bp/cart/services/identity.py b/market/bp/cart/services/identity.py new file mode 100644 index 0000000..50ecb70 --- /dev/null +++ b/market/bp/cart/services/identity.py @@ -0,0 +1,4 @@ +# Re-export from canonical shared location +from shared.infrastructure.cart_identity import CartIdentity, current_cart_identity + +__all__ = ["CartIdentity", "current_cart_identity"] diff --git a/market/bp/cart/services/total.py b/market/bp/cart/services/total.py new file mode 100644 index 0000000..15e074f --- /dev/null +++ b/market/bp/cart/services/total.py @@ -0,0 +1,6 @@ +def total(cart): + return sum( + (item.product.special_price or item.product.regular_price) * item.quantity + for item in cart + if (item.product.special_price or item.product.regular_price) is not None + ) diff --git a/market/bp/fragments/__init__.py b/market/bp/fragments/__init__.py new file mode 100644 index 0000000..a4af44b --- /dev/null +++ b/market/bp/fragments/__init__.py @@ -0,0 +1 @@ +from .routes import register as register_fragments diff --git a/market/bp/fragments/routes.py b/market/bp/fragments/routes.py new file mode 100644 index 0000000..bd2bdde --- /dev/null +++ b/market/bp/fragments/routes.py @@ -0,0 +1,54 @@ +"""Market app fragment endpoints. + +Exposes HTML fragments at ``/internal/fragments/`` for consumption +by other coop apps via the fragment client. +""" + +from __future__ import annotations + +from quart import Blueprint, Response, g, render_template, request + +from shared.infrastructure.fragments import FRAGMENT_HEADER +from shared.services.registry import services + + +def register(): + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + _handlers: dict[str, object] = {} + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.get("/") + async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/html") + html = await handler() + return Response(html, status=200, content_type="text/html") + + # --- container-nav fragment: market links -------------------------------- + + async def _container_nav_handler(): + container_type = request.args.get("container_type", "page") + container_id = int(request.args.get("container_id", 0)) + post_slug = request.args.get("post_slug", "") + + markets = await services.market.marketplaces_for_container( + g.s, container_type, container_id, + ) + if not markets: + return "" + return await render_template( + "fragments/container_nav_markets.html", + markets=markets, post_slug=post_slug, + ) + + _handlers["container-nav"] = _container_nav_handler + + bp._fragment_handlers = _handlers + + return bp diff --git a/market/bp/market/__init__.py b/market/bp/market/__init__.py new file mode 100644 index 0000000..85fd1a5 --- /dev/null +++ b/market/bp/market/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +# create the blueprint at package import time +from .routes import register # = Blueprint("browse_bp", __name__) + +# import routes AFTER browse_bp is defined so routes can attach to it +from . import routes # noqa: F401 diff --git a/market/bp/market/admin/__init__.py b/market/bp/market/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/bp/market/admin/routes.py b/market/bp/market/admin/routes.py new file mode 100644 index 0000000..0b8478a --- /dev/null +++ b/market/bp/market/admin/routes.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from quart import ( + render_template, make_response, Blueprint +) + + +from shared.browser.app.authz import require_admin + + +def register(): + bp = Blueprint("admin", __name__, url_prefix='/admin') + + # ---------- Pages ---------- + @bp.get("/") + @require_admin + async def admin(): + from shared.browser.app.utils.htmx import is_htmx_request + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/market/admin/index.html") + else: + html = await render_template("_types/market/admin/_oob_elements.html") + + return await make_response(html) + return bp diff --git a/market/bp/market/filters/__init__.py b/market/bp/market/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/bp/market/filters/qs.py b/market/bp/market/filters/qs.py new file mode 100644 index 0000000..d5a9950 --- /dev/null +++ b/market/bp/market/filters/qs.py @@ -0,0 +1,101 @@ +from quart import request + +from typing import Iterable, Optional, Union + +from shared.browser.app.filters.qs_base import ( + KEEP, _norm, make_filter_set, build_qs, +) +from shared.browser.app.filters.query_types import MarketQuery + + +def decode() -> MarketQuery: + page = int(request.args.get("page", 1)) + search = request.args.get("search") + sort = request.args.get("sort") + liked = request.args.get("liked") + + selected_brands = tuple(s.strip() for s in request.args.getlist("brand") if s.strip()) + selected_stickers = tuple(s.strip().lower() for s in request.args.getlist("sticker") if s.strip()) + selected_labels = tuple(s.strip().lower() for s in request.args.getlist("label") if s.strip()) + + return MarketQuery(page, search, sort, selected_brands, selected_stickers, selected_labels, liked) + + +def makeqs_factory(): + """ + Build a makeqs(...) that starts from the current filters + page. + Auto-resets page to 1 when filters change unless you pass page explicitly. + """ + q = decode() + base_stickers = [s for s in q.selected_stickers if (s or "").strip()] + base_labels = [s for s in q.selected_labels if (s or "").strip()] + base_brands = [s for s in q.selected_brands if (s or "").strip()] + base_search = q.search or None + base_liked = q.liked or None + base_sort = q.sort or None + base_page = int(q.page or 1) + + def makeqs( + *, + clear_filters: bool = False, + add_sticker: Union[str, Iterable[str], None] = None, + remove_sticker: Union[str, Iterable[str], None] = None, + add_label: Union[str, Iterable[str], None] = None, + remove_label: Union[str, Iterable[str], None] = None, + add_brand: Union[str, Iterable[str], None] = None, + remove_brand: Union[str, Iterable[str], None] = None, + search: Union[str, None, object] = KEEP, + sort: Union[str, None, object] = KEEP, + page: Union[int, None, object] = None, + extra: Optional[Iterable[tuple]] = None, + leading_q: bool = True, + liked: Union[bool, None, object] = KEEP, + ) -> str: + stickers = make_filter_set(base_stickers, add_sticker, remove_sticker, clear_filters) + labels = make_filter_set(base_labels, add_label, remove_label, clear_filters) + brands = make_filter_set(base_brands, add_brand, remove_brand, clear_filters) + + final_search = None if clear_filters else base_search if search is KEEP else ((search or "").strip() or None) + final_sort = base_sort if sort is KEEP else (sort or None) + final_liked = None if clear_filters else base_liked if liked is KEEP else liked + + # Did filters change? + filters_changed = ( + set(map(_norm, stickers)) != set(map(_norm, base_stickers)) + or set(map(_norm, labels)) != set(map(_norm, base_labels)) + or set(map(_norm, brands)) != set(map(_norm, base_brands)) + or final_search != base_search + or final_sort != base_sort + or final_liked != base_liked + ) + + # Page logic + if page is KEEP: + final_page = 1 if filters_changed else base_page + else: + final_page = page + + # Build params + params = [] + for s in stickers: + params.append(("sticker", s)) + for s in labels: + params.append(("label", s)) + for s in brands: + params.append(("brand", s)) + if final_search: + params.append(("search", final_search)) + if final_liked is not None: + params.append(("liked", final_liked)) + if final_sort: + params.append(("sort", final_sort)) + if final_page is not None: + params.append(("page", str(final_page))) + if extra: + for k, v in extra: + if v is not None: + params.append((k, str(v))) + + return build_qs(params, leading_q=leading_q) + + return makeqs diff --git a/market/bp/market/routes.py b/market/bp/market/routes.py new file mode 100644 index 0000000..2eefecc --- /dev/null +++ b/market/bp/market/routes.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from quart import Blueprint, g, render_template, make_response, url_for + + +from ..browse.routes import register as register_browse_bp + +from .filters.qs import makeqs_factory +from ..browse.services.nav import get_nav +from ..api.routes import products_api +from .admin.routes import register as register_admin + + + +def register(url_prefix, title): + bp = Blueprint("market", __name__, url_prefix) + + @bp.before_request + def route(): + g.makeqs_factory = makeqs_factory + + + @bp.context_processor + async def inject_root(): + market = getattr(g, "market", None) + market_id = market.id if market else None + post_data = getattr(g, "post_data", None) or {} + return { + **post_data, + "market_title": market.name if market else title, + "categories": (await get_nav(g.s, market_id=market_id))["cats"], + "qs": makeqs_factory()(), + "market": market, + } + + bp.register_blueprint( + register_browse_bp(), + ) + bp.register_blueprint( + products_api, + ) + bp.register_blueprint( + register_admin(), + ) + + + + return bp + diff --git a/market/bp/page_markets/__init__.py b/market/bp/page_markets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/bp/page_markets/routes.py b/market/bp/page_markets/routes.py new file mode 100644 index 0000000..e18a616 --- /dev/null +++ b/market/bp/page_markets/routes.py @@ -0,0 +1,65 @@ +""" +Page-markets blueprint — shows markets for a single page. + +Mounted at / (page-scoped). Requires g.post_data from hydrate_post. + +Routes: + GET // — full page scoped to this page + GET //page-markets — HTMX fragment for infinite scroll +""" +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, make_response + +from shared.browser.app.utils.htmx import is_htmx_request +from shared.services.registry import services + + +def register() -> Blueprint: + bp = Blueprint("page_markets", __name__) + + async def _load_markets(post_id, page, per_page=20): + """Load markets for this page's container.""" + markets, has_more = await services.market.list_marketplaces( + g.s, "page", post_id, page=page, per_page=per_page, + ) + return markets, has_more + + @bp.get("/") + async def index(): + post = g.post_data["post"] + page = int(request.args.get("page", 1)) + + markets, has_more = await _load_markets(post["id"], page) + + ctx = dict( + markets=markets, + has_more=has_more, + page_info={}, + page=page, + ) + + if is_htmx_request(): + html = await render_template("_types/page_markets/_main_panel.html", **ctx) + else: + html = await render_template("_types/page_markets/index.html", **ctx) + + return await make_response(html, 200) + + @bp.get("/page-markets") + async def markets_fragment(): + post = g.post_data["post"] + page = int(request.args.get("page", 1)) + + markets, has_more = await _load_markets(post["id"], page) + + html = await render_template( + "_types/page_markets/_cards.html", + markets=markets, + has_more=has_more, + page_info={}, + page=page, + ) + return await make_response(html, 200) + + return bp diff --git a/market/bp/product/routes.py b/market/bp/product/routes.py new file mode 100644 index 0000000..19e76c4 --- /dev/null +++ b/market/bp/product/routes.py @@ -0,0 +1,269 @@ +from __future__ import annotations + +from quart import ( + g, + Blueprint, + abort, + redirect, + render_template, + make_response, +) +from sqlalchemy import select, func, update + +from models.market import Product, ProductLike +from ..browse.services.slugs import canonical_html_slug +from ..browse.services.blacklist.product import is_product_blocked +from ..browse.services import db_backend as cb +from ..browse.services import _massage_product +from shared.utils import host_url +from shared.browser.app.redis_cacher import cache_page, clear_cache +from ..cart.services import total +from .services.product_operations import toggle_product_like, massage_full_product + + +def register(): + bp = Blueprint("product", __name__, url_prefix="/product/") + @bp.url_value_preprocessor + def pull_product_slug(endpoint, values): + # product_slug is distinct from the app-level "slug"/"page_slug" params, + # so it won't be popped by the app-level preprocessor in app.py. + g.product_slug = values.pop("product_slug", None) + + # ───────────────────────────────────────────────────────────── + # BEFORE REQUEST: Slug or numeric ID resolver + # ───────────────────────────────────────────────────────────── + @bp.before_request + async def resolve_product(): + from quart import request as req + + raw_slug = g.product_slug = getattr(g, "product_slug", None) + if raw_slug is None: + return + + is_post = req.method == "POST" + + # 1. If slug is INT → load product by ID + if raw_slug.isdigit(): + product_id = int(raw_slug) + + product = await cb.db_product_full_id( + g.s, product_id, user_id=g.user.id if g.user else 0 + ) + + if not product: + abort(404) + + # If product is deleted → SHOW as-is + if product["deleted_at"]: + d = product + g.item_data = {"d": d, "slug": product["slug"], "liked": False} + return + + # Not deleted → redirect to canonical slug (GET only) + if not is_post: + canon = canonical_html_slug(product["slug"]) + return redirect( + host_url(url_for("market.browse.product.product_detail", product_slug=canon)) + ) + + g.item_data = {"d": product, "slug": product["slug"], "liked": False} + return + + # 2. Normal slug-based behaviour + if is_product_blocked(raw_slug): + abort(404) + + canon = canonical_html_slug(raw_slug) + if canon != raw_slug and not is_post: + return redirect( + host_url(url_for("market.browse.product.product_detail", product_slug=canon)) + ) + + # hydrate full product + d = await cb.db_product_full( + g.s, canon, user_id=g.user.id if g.user else 0 + ) + if not d: + abort(404) + g.item_data = {"d": d, "slug": canon, "liked": d.get("is_liked", False)} + + @bp.context_processor + def context(): + item_data = getattr(g, "item_data", None) + + if item_data: + return { + **item_data, + } + else: + return {} + + # ───────────────────────────────────────────────────────────── + # RENDER PRODUCT + # ───────────────────────────────────────────────────────────── + @bp.get("/") + @cache_page(tag="browse") + async def product_detail(): + from shared.browser.app.utils.htmx import is_htmx_request + + # Determine which template to use based on request type + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/product/index.html") + else: + # HTMX request: main panel + OOB elements + html = await render_template("_types/product/_oob_elements.html") + + return html + + @bp.post("/like/toggle/") + @clear_cache(tag="browse", tag_scope="user") + async def like_toggle(): + product_slug = g.product_slug + + if not g.user: + html = await render_template( + "_types/browse/like/button.html", + slug=product_slug, + liked=False, + ) + resp = make_response(html, 403) + return resp + + user_id = g.user.id + + liked, error = await toggle_product_like(g.s, user_id, product_slug) + + if error: + resp = make_response(error, 404) + return resp + + html = await render_template( + "_types/browse/like/button.html", + slug=product_slug, + liked=liked, + ) + return html + + + + @bp.get("/admin/") + async def admin(): + from shared.browser.app.utils.htmx import is_htmx_request + + if not is_htmx_request(): + # Normal browser request: full page with layout + html = await render_template("_types/product/admin/index.html") + else: + # HTMX request: main panel + OOB elements + html = await render_template("_types/product/admin/_oob_elements.html") + + return await make_response(html) + + + from bp.cart.services.identity import current_cart_identity + #from bp.cart.routes import view_cart + from models.market import CartItem + from quart import request, url_for + + @bp.post("/cart/") + @clear_cache(tag="browse", tag_scope="user") + async def cart(): + slug = g.product_slug + # make sure product exists (we *allow* deleted_at != None later if you want) + product_id = await g.s.scalar( + select(Product.id).where( + Product.slug == slug, + Product.deleted_at.is_(None), + ) + ) + + product = await g.s.scalar( + select(Product).where(Product.id == product_id) + ) + if not product: + return await make_response("Product not found", 404) + + # --- NEW: read `count` from body (JSON or form), default to 1 --- + count = 1 + try: + if request.is_json: + data = await request.get_json() + if data is not None and "count" in data: + count = int(data["count"]) + else: + form = await request.form + if "count" in form: + count = int(form["count"]) + except (ValueError, TypeError): + # if parsing fails, just fall back to 1 + count = 1 + # --- END NEW --- + + ident = current_cart_identity() + + # Load cart items for current user/session + from sqlalchemy.orm import selectinload + cart_filters = [CartItem.deleted_at.is_(None)] + if ident["user_id"] is not None: + cart_filters.append(CartItem.user_id == ident["user_id"]) + else: + cart_filters.append(CartItem.session_id == ident["session_id"]) + cart_result = await g.s.execute( + select(CartItem) + .where(*cart_filters) + .order_by(CartItem.created_at.desc()) + .options( + selectinload(CartItem.product), + selectinload(CartItem.market_place), + ) + ) + g.cart = list(cart_result.scalars().all()) + + ci = next( + (item for item in g.cart if item.product_id == product_id), + None, + ) + + # --- NEW: set quantity based on `count` --- + if ci: + if count > 0: + ci.quantity = count + else: + # count <= 0 → remove from cart entirely + ci.quantity=0 + g.cart.remove(ci) + await g.s.delete(ci) + + else: + if count > 0: + ci = CartItem( + user_id=ident["user_id"], + session_id=ident["session_id"], + product_id=product.id, + product=product, + quantity=count, + market_place_id=getattr(g, "market", None) and g.market.id, + ) + g.cart.append(ci) + g.s.add(ci) + # if count <= 0 and no existing item, do nothing + # --- END NEW --- + + # no explicit commit; your session middleware should handle it + + # htmx response: OOB-swap mini cart + product buttons + if request.headers.get("HX-Request") == "true": + return await render_template( + "_types/product/_added.html", + cart=g.cart, + item=ci, + ) + + # normal POST: go to cart page + from shared.infrastructure.urls import cart_url + return redirect(cart_url("/")) + + + + return bp diff --git a/market/bp/product/services/__init__.py b/market/bp/product/services/__init__.py new file mode 100644 index 0000000..ce711a7 --- /dev/null +++ b/market/bp/product/services/__init__.py @@ -0,0 +1,3 @@ +from .product_operations import toggle_product_like, massage_full_product + +__all__ = ["toggle_product_like", "massage_full_product"] diff --git a/market/bp/product/services/product_operations.py b/market/bp/product/services/product_operations.py new file mode 100644 index 0000000..343be8e --- /dev/null +++ b/market/bp/product/services/product_operations.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.market import Product, ProductLike + + +def massage_full_product(product: Product) -> dict: + """ + Convert a Product ORM model to a dictionary with all fields. + Used for rendering product detail pages. + """ + from bp.browse.services import _massage_product + + gallery = [] + if product.image: + gallery.append(product.image) + + d = { + "id": product.id, + "slug": product.slug, + "title": product.title, + "brand": product.brand, + "image": product.image, + "description_short": product.description_short, + "description_html": product.description_html or "", + "suma_href": product.suma_href, + "rrp": float(product.rrp) if product.rrp else None, + "special_price": float(product.special_price) if product.special_price else None, + "regular_price": float(product.regular_price) if product.regular_price else None, + "images": gallery or [img.url for img in product.images], + "all_image_urls": gallery or [img.url for img in product.images], + "sections": [{"title": s.title, "html": s.html} for s in product.sections], + "stickers": [s.name.lower() for s in product.stickers], + "labels": [l.name for l in product.labels], + "nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in product.nutrition], + "allergens": [{"name": a.name, "contains": a.contains} for a in product.allergens], + "is_liked": False, + } + + return _massage_product(d) + + +async def toggle_product_like( + session: AsyncSession, + user_id: int, + product_slug: str, +) -> tuple[bool, Optional[str]]: + """ + Toggle a product like for a given user using soft deletes. + Returns (liked_state, error_message). + - If error_message is not None, an error occurred. + - liked_state indicates whether product is now liked (True) or unliked (False). + """ + from sqlalchemy import func, update + + # Get product_id from slug + product_id = await session.scalar( + select(Product.id).where(Product.slug == product_slug, Product.deleted_at.is_(None)) + ) + if not product_id: + return False, "Product not found" + + # Check if like exists (not deleted) + existing = await session.scalar( + select(ProductLike).where( + ProductLike.user_id == user_id, + ProductLike.product_slug == product_slug, + ProductLike.deleted_at.is_(None), + ) + ) + + if existing: + # Unlike: soft delete the like + await session.execute( + update(ProductLike) + .where( + ProductLike.user_id == user_id, + ProductLike.product_slug == product_slug, + ProductLike.deleted_at.is_(None), + ) + .values(deleted_at=func.now()) + ) + return False, None + else: + # Like: add a new like + new_like = ProductLike( + user_id=user_id, + product_slug=product_slug, + ) + session.add(new_like) + return True, None diff --git a/market/config/app-config.yaml b/market/config/app-config.yaml new file mode 100644 index 0000000..3aa6a76 --- /dev/null +++ b/market/config/app-config.yaml @@ -0,0 +1,84 @@ +# App-wide settings +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: Rose Ash +market_root: /market +market_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + blog: "http://localhost:8000" + market: "http://localhost:8001" + cart: "http://localhost:8002" + events: "http://localhost:8003" + federation: "http://localhost:8004" +cache: + fs_root: _snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/wines + - branded-goods/ciders + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + - ---just-lem-just-wholefoods-jelly-crystals-lemon-12-x-85g-vf067-2-html + product-details: + - General Information + - A Note About Prices + +# SumUp payment settings (fill these in for live usage) +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + checkout_reference_prefix: 'dev-' + diff --git a/market/entrypoint.sh b/market/entrypoint.sh new file mode 100644 index 0000000..320acdf --- /dev/null +++ b/market/entrypoint.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional: wait for Postgres to be reachable +if [[ -n "${DATABASE_HOST:-}" && -n "${DATABASE_PORT:-}" ]]; then + echo "Waiting for Postgres at ${DATABASE_HOST}:${DATABASE_PORT}..." + for i in {1..60}; do + (echo > /dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}) >/dev/null 2>&1 && break || true + sleep 1 + done +fi + +# NOTE: Market app does NOT run Alembic migrations. +# Migrations are managed by the blog app which owns the shared database schema. + +# Clear Redis page cache on deploy +if [[ -n "${REDIS_URL:-}" && "${REDIS_URL}" != "no" ]]; then + echo "Flushing Redis cache..." + python3 -c " +import redis, os +r = redis.from_url(os.environ['REDIS_URL']) +r.flushall() +print('Redis cache cleared.') +" || echo "Redis flush failed (non-fatal), continuing..." +fi + +# Start the app +echo "Starting Hypercorn (${APP_MODULE:-app:app})..." +PYTHONUNBUFFERED=1 exec hypercorn "${APP_MODULE:-app:app}" --bind 0.0.0.0:${PORT:-8000} diff --git a/market/models/__init__.py b/market/models/__init__.py new file mode 100644 index 0000000..9ca9e79 --- /dev/null +++ b/market/models/__init__.py @@ -0,0 +1,8 @@ +from .market import ( + Product, ProductLike, ProductImage, ProductSection, + NavTop, NavSub, Listing, ListingItem, + LinkError, LinkExternal, SubcategoryRedirect, ProductLog, + ProductLabel, ProductSticker, ProductAttribute, ProductNutrition, ProductAllergen, + CartItem, +) +from .market_place import MarketPlace diff --git a/market/models/market.py b/market/models/market.py new file mode 100644 index 0000000..65511e1 --- /dev/null +++ b/market/models/market.py @@ -0,0 +1,7 @@ +from shared.models.market import ( # noqa: F401 + Product, ProductLike, ProductImage, ProductSection, + NavTop, NavSub, Listing, ListingItem, + LinkError, LinkExternal, SubcategoryRedirect, ProductLog, + ProductLabel, ProductSticker, ProductAttribute, ProductNutrition, ProductAllergen, + CartItem, +) diff --git a/market/models/market_place.py b/market/models/market_place.py new file mode 100644 index 0000000..ca65447 --- /dev/null +++ b/market/models/market_place.py @@ -0,0 +1 @@ +from shared.models.market_place import MarketPlace # noqa: F401 diff --git a/market/path_setup.py b/market/path_setup.py new file mode 100644 index 0000000..c7166f7 --- /dev/null +++ b/market/path_setup.py @@ -0,0 +1,9 @@ +import sys +import os + +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p) diff --git a/market/scrape-test.sh b/market/scrape-test.sh new file mode 100644 index 0000000..c6e299f --- /dev/null +++ b/market/scrape-test.sh @@ -0,0 +1,6 @@ +. .env +source venv/bin/activate +rm -rf _debug/* +python test_scrape_detail.py --out ./_debug --slug sum-saag-suma-aloo-saag-12-x-400g-vf270-2-html +#git -C _debug status +#git -C _debug diff diff --git a/market/scrape.sh b/market/scrape.sh new file mode 100644 index 0000000..639cba8 --- /dev/null +++ b/market/scrape.sh @@ -0,0 +1,5 @@ +. .env +echo sumauser: $SUMA_USER +source .venv/bin/activate # was venv/bin/a +python scrape_to_snapshot.py --out ./_snapshot --max-pages 50 --max-products 200000 --concurrency 8 + diff --git a/market/scrape/__init__.py b/market/scrape/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/market/scrape/build_snapshot/__init__.py b/market/scrape/build_snapshot/__init__.py new file mode 100644 index 0000000..1eec55e --- /dev/null +++ b/market/scrape/build_snapshot/__init__.py @@ -0,0 +1 @@ +from .build_snapshot import build_snapshot diff --git a/market/scrape/build_snapshot/build_snapshot.py b/market/scrape/build_snapshot/build_snapshot.py new file mode 100644 index 0000000..3b7f623 --- /dev/null +++ b/market/scrape/build_snapshot/build_snapshot.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +from typing import Dict, Set + +from ..http_client import configure_cookies +from ..get_auth import login + +from shared.config import config + +from shared.utils import log + +# DB: persistence helpers + +from .tools import ( + _resolve_sub_redirects, + valid_subs, + candidate_subs, + rewrite_nav, + capture_product_slugs, + fetch_and_upsert_products, +) + +from ..nav import nav_scrape + +# ------------------------ core ------------------------ +async def build_snapshot( + concurrency: int, + user: str, + password: str, + save_nav, + capture_listing, + upsert_product, + log_product_result, + save_subcategory_redirects, + save_link_reports = None, +) -> None: + # NOTE: we keep ensure_dir for listings iteration but no longer write JSON files. + + # Make project importable + import sys + sys.path.insert(0, os.path.abspath(".")) + + + cookies = await login(username=user, password=password) + await configure_cookies(cookies) + for k, v in dict(cookies).items(): + print("logged in with", k, v) + + # 1) NAV + log("Fetching nav…") + nav = await nav_scrape() + + # Build valid subs per top from nav + valid_subs_by_top: Dict[str, Set[str]] = valid_subs(nav) + + # Resolve redirects for all subs in nav first + nav_sub_candidates = candidate_subs(nav) + nav_redirects = await _resolve_sub_redirects( + base_url=config()["base_url"], + candidates=nav_sub_candidates, + allowed_tops=set(config()["categories"]["allow"].values()), + valid_subs_by_top=valid_subs_by_top, + ) + rewrite_nav(nav, nav_redirects) + + # DB: save nav + await save_nav(nav) + + product_slugs: Set[str] = await capture_product_slugs( + nav, + capture_listing + ) + unknown_sub_paths: Set[str] = set() + + # 3) PRODUCTS (fetch details) + await fetch_and_upsert_products( + upsert_product, + log_product_result, + save_link_reports, + concurrency, + product_slugs, + valid_subs_by_top, + unknown_sub_paths + ) + + # Subcategory redirects from HTML + log("Resolving subcategory redirects…") + html_redirects = await _resolve_sub_redirects( + base_url=config()["base_url"], + candidates=unknown_sub_paths, + allowed_tops=set(config()["categories"]["allow"].values()), + valid_subs_by_top=valid_subs_by_top, + ) + sub_redirects: Dict[str, str] = dict(nav_redirects) + sub_redirects.update(html_redirects) + + # DB: persist redirects + await save_subcategory_redirects(sub_redirects) + + log("Snapshot build complete (to Postgres).") + + diff --git a/market/scrape/build_snapshot/tools/APP_ROOT_PLACEHOLDER.py b/market/scrape/build_snapshot/tools/APP_ROOT_PLACEHOLDER.py new file mode 100644 index 0000000..3291777 --- /dev/null +++ b/market/scrape/build_snapshot/tools/APP_ROOT_PLACEHOLDER.py @@ -0,0 +1 @@ +APP_ROOT_PLACEHOLDER = "[**__APP_ROOT__**]" diff --git a/market/scrape/build_snapshot/tools/__init__.py b/market/scrape/build_snapshot/tools/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/market/scrape/build_snapshot/tools/__init__.py @@ -0,0 +1 @@ + diff --git a/market/scrape/build_snapshot/tools/_anchor_text.py b/market/scrape/build_snapshot/tools/_anchor_text.py new file mode 100644 index 0000000..fd3ce6d --- /dev/null +++ b/market/scrape/build_snapshot/tools/_anchor_text.py @@ -0,0 +1,6 @@ +def _anchor_text(a) -> str: + try: + txt = " ".join((a.get_text(" ") or "").split()) + return txt[:200] + except Exception: + return "" diff --git a/market/scrape/build_snapshot/tools/_collect_html_img_srcs.py b/market/scrape/build_snapshot/tools/_collect_html_img_srcs.py new file mode 100644 index 0000000..c5feaef --- /dev/null +++ b/market/scrape/build_snapshot/tools/_collect_html_img_srcs.py @@ -0,0 +1,16 @@ +from bs4 import BeautifulSoup +from typing import List, Optional + +def _collect_html_img_srcs(html: Optional[str]) -> List[str]: + urls: List[str] = [] + if not html: + return urls + try: + soup = BeautifulSoup(html, "lxml") + for img in soup.find_all("img"): + src = img.get("src") + if src: + urls.append(src) + except Exception: + pass + return urls diff --git a/market/scrape/build_snapshot/tools/_dedupe_preserve_order.py b/market/scrape/build_snapshot/tools/_dedupe_preserve_order.py new file mode 100644 index 0000000..492cb5a --- /dev/null +++ b/market/scrape/build_snapshot/tools/_dedupe_preserve_order.py @@ -0,0 +1,14 @@ + +from typing import Iterable, List, Set + +def _dedupe_preserve_order(urls: Iterable[str]) -> List[str]: + seen: Set[str] = set() + out: List[str] = [] + for u in urls: + if not u or not isinstance(u, str): + continue + if u in seen: + continue + seen.add(u) + out.append(u) + return out diff --git a/market/scrape/build_snapshot/tools/_product_dict_is_cf.py b/market/scrape/build_snapshot/tools/_product_dict_is_cf.py new file mode 100644 index 0000000..5802af7 --- /dev/null +++ b/market/scrape/build_snapshot/tools/_product_dict_is_cf.py @@ -0,0 +1,32 @@ +from typing import Dict,Optional, Tuple + +_CF_TOKENS = ( + "One moment, please...", + "Please wait while your request is being verified", + "/cdn-cgi/challenge-platform/", + "rocket-loader.min.js", +) + +def _looks_like_cf_html(html: Optional[str]) -> Tuple[bool, Optional[str]]: + if not html: + return False, None + for tok in _CF_TOKENS: + if tok in html: + return True, tok + return False, None + +def _product_dict_is_cf(d: Dict) -> Tuple[bool, Optional[str]]: + title = (d.get("title") or "").strip() + if title.lower() == "one moment, please...": + return True, "One moment, please..." + ok, tok = _looks_like_cf_html(d.get("description_html")) + if ok: + return True, tok + for sec in d.get("sections") or []: + if isinstance(sec, dict) and sec.get("html"): + ok2, tok2 = _looks_like_cf_html(sec["html"]) + if ok2: + return True, tok2 + if not d.get("images") and not d.get("description_html") and not d.get("sections"): + return True, "all_empty_heuristic" + return False, None diff --git a/market/scrape/build_snapshot/tools/_resolve_sub_redirects.py b/market/scrape/build_snapshot/tools/_resolve_sub_redirects.py new file mode 100644 index 0000000..c3e4f43 --- /dev/null +++ b/market/scrape/build_snapshot/tools/_resolve_sub_redirects.py @@ -0,0 +1,34 @@ +from typing import Dict, Set +from urllib.parse import urlparse, urljoin +import httpx + + +async def _resolve_sub_redirects( + base_url: str, + candidates: Set[str], + allowed_tops: Set[str], + valid_subs_by_top: Dict[str, Set[str]], +) -> Dict[str, str]: + mapping: Dict[str, str] = {} + if not candidates: + return mapping + timeout = httpx.Timeout(20.0, connect=10.0) + async with httpx.AsyncClient(follow_redirects=True, timeout=timeout, http2=True) as client: + for path in sorted(candidates): + try: + url = urljoin(base_url, path) + r = await client.get(url) + final = str(r.url) + p = urlparse(final) + parts = [x for x in (p.path or "").split("/") if x] + if len(parts) >= 2: + top_new = parts[0].lower() + sub_new = parts[1].lower().removesuffix(".html").removesuffix(".htm") + if top_new in allowed_tops: + new_path = f"/{top_new}/{sub_new}" + if new_path != path: + mapping[path] = new_path + valid_subs_by_top.setdefault(top_new, set()).add(sub_new) + except Exception: + continue + return mapping diff --git a/market/scrape/build_snapshot/tools/_rewrite_links_fragment.py b/market/scrape/build_snapshot/tools/_rewrite_links_fragment.py new file mode 100644 index 0000000..2d3a816 --- /dev/null +++ b/market/scrape/build_snapshot/tools/_rewrite_links_fragment.py @@ -0,0 +1,100 @@ +from typing import Dict, List, Optional, Set +from bs4 import BeautifulSoup +from urllib.parse import urlparse, urljoin + +from ._anchor_text import _anchor_text +from bp.browse.services.slugs import product_slug_from_href +from .APP_ROOT_PLACEHOLDER import APP_ROOT_PLACEHOLDER + +def _rewrite_links_fragment( + html: Optional[str], + base_url: str, + known_slugs: Set[str], + category_allow_values: Set[str], + valid_subs_by_top: Dict[str, Set[str]], + current_product_slug: str, + link_errors: List[Dict], + link_externals: List[Dict], + unknown_sub_paths: Set[str], +) -> str: + if not html: + return "" + soup = BeautifulSoup(html, "lxml") + base_host = urlparse(base_url).netloc + + for a in soup.find_all("a", href=True): + raw = (a.get("href") or "").strip() + if not raw: + continue + low = raw.lower() + if low.startswith(("mailto:", "tel:", "javascript:", "data:")) or low.startswith("#"): + continue + abs_href = urljoin(base_url, raw) + p = urlparse(abs_href) + if not p.scheme or not p.netloc: + continue + if p.netloc != base_host: + link_externals.append({ + "product": current_product_slug, + "href": abs_href, + "text": _anchor_text(a), + "host": p.netloc, + }) + continue + parts = [x for x in (p.path or "").split("/") if x] + if not parts: + continue + last = parts[-1].lower() + if last.endswith((".html", ".htm")): + target_slug = product_slug_from_href(abs_href) + if target_slug and target_slug in known_slugs: + a["href"] = f"{APP_ROOT_PLACEHOLDER}/product/{target_slug}" + else: + link_errors.append({ + "product": current_product_slug, + "href": abs_href, + "text": _anchor_text(a), + "top": None, + "sub": None, + "target_slug": target_slug or None, + "type": "suma_product_unknown", + }) + continue + top = parts[0].lower() + if top in category_allow_values: + if len(parts) == 1: + a["href"] = f"{APP_ROOT_PLACEHOLDER}/{top}" + else: + sub = parts[1] + if sub.lower().endswith((".html", ".htm")): + sub = sub.rsplit(".", 1)[0] + if sub in (valid_subs_by_top.get(top) or set()): + a["href"] = f"{APP_ROOT_PLACEHOLDER}/{top}/{sub}" + else: + unknown_path = f"/{top}/{sub}" + unknown_sub_paths.add(unknown_path) + a["href"] = f"{APP_ROOT_PLACEHOLDER}{unknown_path}" + link_errors.append({ + "product": current_product_slug, + "href": abs_href, + "text": _anchor_text(a), + "top": top, + "sub": sub, + "target_slug": None, + "type": "suma_category_invalid_sub_pending", + }) + else: + link_errors.append({ + "product": current_product_slug, + "href": abs_href, + "text": _anchor_text(a), + "top": top, + "sub": parts[1] if len(parts) > 1 else None, + "target_slug": None, + "type": "suma_other", + }) + + for t in soup.find_all(["html", "body"]): + t.unwrap() + return "".join(str(c) for c in soup.contents).strip() + diff --git a/market/scrape/build_snapshot/tools/candidate_subs.py b/market/scrape/build_snapshot/tools/candidate_subs.py new file mode 100644 index 0000000..b7853b8 --- /dev/null +++ b/market/scrape/build_snapshot/tools/candidate_subs.py @@ -0,0 +1,14 @@ +from typing import Dict, Set + +def candidate_subs(nav: Dict[str, Dict])-> Set[str]: + nav_sub_candidates: Set[str] = set() + for label, data in (nav.get("cats") or {}).items(): + top_slug = (data or {}).get("slug") + if not top_slug: + continue + for s in (data.get("subs") or []): + sub_slug = (s.get("slug") or "").strip() + if sub_slug: + nav_sub_candidates.add(f"/{top_slug}/{sub_slug}") + return nav_sub_candidates + diff --git a/market/scrape/build_snapshot/tools/capture_category.py b/market/scrape/build_snapshot/tools/capture_category.py new file mode 100644 index 0000000..84e51e7 --- /dev/null +++ b/market/scrape/build_snapshot/tools/capture_category.py @@ -0,0 +1,18 @@ +from urllib.parse import urljoin +from shared.config import config +from shared.utils import log +from ...listings import scrape_products + +async def capture_category( + slug: str, +): + list_url = urljoin(config()["base_url"], f"/{slug}") + log(f"[{slug}] page 1…") + items, total_pages = await scrape_products(list_url, page=1) + + pmax = int(total_pages or 1) + for p in range(2, pmax + 1): + log(f"[{slug}] page {p}…") + items_p, _tp = await scrape_products(list_url, page=p) + items.extend(items_p) + return (list_url, items, total_pages) diff --git a/market/scrape/build_snapshot/tools/capture_product_slugs.py b/market/scrape/build_snapshot/tools/capture_product_slugs.py new file mode 100644 index 0000000..1592e1e --- /dev/null +++ b/market/scrape/build_snapshot/tools/capture_product_slugs.py @@ -0,0 +1,25 @@ +from typing import Dict, Set +from .capture_category import capture_category +from .capture_sub import capture_sub +from shared.config import config + + +async def capture_product_slugs( + nav: Dict[str, Dict], + capture_listing, +): + product_slugs: Set[str] = set() + for label, slug in config()["categories"]["allow"].items(): + lpars = await capture_category( slug) + await capture_listing(*lpars) + (_, items, __) = lpars + for slug_ in items: + product_slugs.add(slug_) + for sub in (nav["cats"].get(label, {}).get("subs", []) or []): + lpars = await capture_sub(sub, slug) + await capture_listing(*lpars) + (_, items, __) = lpars + for slug_ in items: + product_slugs.add(slug_) + return product_slugs + diff --git a/market/scrape/build_snapshot/tools/capture_sub.py b/market/scrape/build_snapshot/tools/capture_sub.py new file mode 100644 index 0000000..5c14ca7 --- /dev/null +++ b/market/scrape/build_snapshot/tools/capture_sub.py @@ -0,0 +1,22 @@ +from urllib.parse import urljoin +from urllib.parse import urljoin +from shared.config import config +from shared.utils import log +from ...listings import scrape_products + +async def capture_sub( + sub, + slug, +): + sub_slug = sub.get("slug") + if not sub_slug: + return + sub_url = urljoin(config()["base_url"], f"/{slug}/{sub_slug}") + log(f"[{slug}/{sub_slug}] page 1…") + items_s, total_pages_s = await scrape_products(sub_url, page=1) + spmax = int(total_pages_s or 1) + for p in range(2, spmax + 1): + log(f"[{slug}/{sub_slug}] page {p}…") + items_ps, _ = await scrape_products(sub_url, page=p) + items_s.extend(items_ps) + return (sub_url, items_s, total_pages_s) diff --git a/market/scrape/build_snapshot/tools/fetch_and_upsert_product.py b/market/scrape/build_snapshot/tools/fetch_and_upsert_product.py new file mode 100644 index 0000000..0fb625c --- /dev/null +++ b/market/scrape/build_snapshot/tools/fetch_and_upsert_product.py @@ -0,0 +1,106 @@ + +import asyncio +from typing import List + +import httpx + + +from ...html_utils import to_fragment +from bp.browse.services.slugs import suma_href_from_html_slug + + +from shared.config import config + +from shared.utils import log + +# DB: persistence helpers +from ...product.product_detail import scrape_product_detail +from ._product_dict_is_cf import _product_dict_is_cf +from ._rewrite_links_fragment import _rewrite_links_fragment +from ._dedupe_preserve_order import _dedupe_preserve_order +from ._collect_html_img_srcs import _collect_html_img_srcs + + +async def fetch_and_upsert_product( + upsert_product, + log_product_result, + sem: asyncio.Semaphore, + slug: str, + product_slugs, + category_values, + valid_subs_by_top, + link_errors, + link_externals, + unknown_sub_paths +) -> bool: + href = suma_href_from_html_slug(slug) + try: + async with sem: + d = await scrape_product_detail(href) + + is_cf, cf_token = _product_dict_is_cf(d) + if is_cf: + payload = { + "slug": slug, + "href_tried": href, + "error_type": "CloudflareChallengeDetected", + "error_message": f"Detected Cloudflare interstitial via token: {cf_token}", + "cf_token": cf_token, + } + await log_product_result(ok=False, payload=payload) + log(f" ! CF challenge detected: {slug} ({cf_token})") + return False + + # Rewrite embedded links; collect reports + if d.get("description_html"): + d["description_html"] = _rewrite_links_fragment( + d["description_html"], config()["base_url"], product_slugs, category_values, + valid_subs_by_top, slug, link_errors, link_externals, unknown_sub_paths + ) + d["description_html"] = to_fragment(d["description_html"]) + if d.get("sections"): + for sec in d["sections"]: + if isinstance(sec, dict) and sec.get("html"): + sec["html"] = _rewrite_links_fragment( + sec["html"], config()["base_url"], product_slugs, category_values, + valid_subs_by_top, slug, link_errors, link_externals, unknown_sub_paths + ) + sec["html"] = to_fragment(sec["html"]) + + # Images + gallery = _dedupe_preserve_order(d.get("images") or []) + embedded: List[str] = [] + if d.get("description_html"): + embedded += _collect_html_img_srcs(d["description_html"]) + for sec in d.get("sections", []) or []: + if isinstance(sec, dict) and sec.get("html"): + embedded += _collect_html_img_srcs(sec["html"]) + embedded = _dedupe_preserve_order(embedded) + all_imgs = _dedupe_preserve_order(list(gallery) + list(embedded)) + + d["images"] = gallery + d["embedded_image_urls"] = embedded + d["all_image_urls"] = all_imgs + await upsert_product(slug, href, d) + # DB: upsert product + success log + return True + except Exception as e: + payload = { + "slug": slug, + "href_tried": href, + "error_type": e.__class__.__name__, + "error_message": str(e), + } + try: + if isinstance(e, httpx.HTTPStatusError): + payload["http_status"] = getattr(e.response, "status_code", None) + req = getattr(e, "request", None) + if req is not None and getattr(req, "url", None) is not None: + payload["final_url"] = str(req.url) + elif isinstance(e, httpx.TransportError): + payload["transport_error"] = True + except Exception: + pass + await log_product_result(ok=False, payload=payload) + log(f" ! product failed: {slug} ({e})") + return False diff --git a/market/scrape/build_snapshot/tools/fetch_and_upsert_products.py b/market/scrape/build_snapshot/tools/fetch_and_upsert_products.py new file mode 100644 index 0000000..836dde0 --- /dev/null +++ b/market/scrape/build_snapshot/tools/fetch_and_upsert_products.py @@ -0,0 +1,49 @@ +import asyncio +from typing import Dict, List, Set +from shared.config import config +from shared.utils import log +from .fetch_and_upsert_product import fetch_and_upsert_product + + +async def fetch_and_upsert_products( + upsert_product, + log_product_result, + save_link_reports = None, + concurrency: int=8, + product_slugs: Set[str] = set(), + valid_subs_by_top: Dict[str, Set[str]] = {}, + unknown_sub_paths: Set[str] = set() +): + sem = asyncio.Semaphore(max(1, concurrency)) + link_errors: List[Dict] = [] + link_externals: List[Dict] = [] + + category_values: Set[str] = set(config()["categories"]["allow"].values()) + to_fetch = sorted(list(product_slugs)) + log(f"Fetching {len(to_fetch)} product details (concurrency={concurrency})…") + tasks = [asyncio.create_task( + fetch_and_upsert_product( + upsert_product, + log_product_result, + sem, + s, + product_slugs, + category_values, + valid_subs_by_top, + link_errors, + link_externals, + unknown_sub_paths + ) + ) for s in to_fetch] + done = 0 + ok_count = 0 + for coro in asyncio.as_completed(tasks): + ok = await coro + done += 1 + if ok: + ok_count += 1 + if done % 50 == 0 or done == len(tasks): + log(f" …{done}/{len(tasks)} saved (ok={ok_count})") + if save_link_reports: + await save_link_reports(link_errors, link_externals) + \ No newline at end of file diff --git a/market/scrape/build_snapshot/tools/rewrite_nav.py b/market/scrape/build_snapshot/tools/rewrite_nav.py new file mode 100644 index 0000000..aaa03da --- /dev/null +++ b/market/scrape/build_snapshot/tools/rewrite_nav.py @@ -0,0 +1,24 @@ + +from typing import Dict +from urllib.parse import urljoin +from shared.config import config + +def rewrite_nav(nav: Dict[str, Dict], nav_redirects:Dict[str, str]): + if nav_redirects: + for label, data in (nav.get("cats") or {}).items(): + top_slug = (data or {}).get("slug") + if not top_slug: + continue + new_subs = [] + for s in (data.get("subs") or []): + old_sub = (s.get("slug") or "").strip() + if not old_sub: + continue + old_path = f"/{top_slug}/{old_sub}" + canonical_path = nav_redirects.get(old_path, old_path) + parts = [x for x in canonical_path.split("/") if x] + top2, sub2 = parts[0], parts[1] + s["slug"] = sub2 + s["href"] = urljoin(config()["base_url"], f"/{top2}/{sub2}") + new_subs.append(s) + data["subs"] = new_subs diff --git a/market/scrape/build_snapshot/tools/valid_subs.py b/market/scrape/build_snapshot/tools/valid_subs.py new file mode 100644 index 0000000..8939a10 --- /dev/null +++ b/market/scrape/build_snapshot/tools/valid_subs.py @@ -0,0 +1,16 @@ +from typing import Dict, Set + +# make valid subs for ewch top in nav +def valid_subs(nav: Dict[str, Dict])->Dict[str, Set[str]] : + valid_subs_by_top: Dict[str, Set[str]] = {} + for label, data in (nav.get("cats") or {}).items(): + top_slug = (data or {}).get("slug") + if not top_slug: + continue + subs_set = { + (s.get("slug") or "").strip() + for s in (data.get("subs") or []) + if s.get("slug") + } + valid_subs_by_top[top_slug] = subs_set + return valid_subs_by_top diff --git a/market/scrape/get_auth.py b/market/scrape/get_auth.py new file mode 100644 index 0000000..a57b66c --- /dev/null +++ b/market/scrape/get_auth.py @@ -0,0 +1,244 @@ +from typing import Optional, Dict, Any, List +from urllib.parse import urljoin +import httpx +from bs4 import BeautifulSoup +from shared.config import config + +class LoginFailed(Exception): + def __init__(self, message: str, *, debug: Dict[str, Any]): + super().__init__(message) + self.debug = debug + +def _ff_headers(referer: Optional[str] = None, origin: Optional[str] = None) -> Dict[str, str]: + h = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-GB,en;q=0.5", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + "DNT": "1", + "Sec-GPC": "1", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + } + if referer: + h["Referer"] = referer + if origin: + h["Origin"] = origin + return h + +def _cookie_header_from_jar(jar: httpx.Cookies, domain: str, path: str = "/") -> str: + pairs: List[str] = [] + for c in jar.jar: + if not c.name or c.value is None: + continue + dom = (c.domain or "").lstrip(".") + if not dom: + continue + if not (domain == dom or domain.endswith("." + dom) or dom.endswith("." + domain)): + continue + if not (path.startswith(c.path or "/")): + continue + pairs.append(f"{c.name}={c.value}") + return "; ".join(pairs) + +def _extract_magento_errors(html_text: str) -> list[str]: + msgs: list[str] = [] + try: + soup = BeautifulSoup(html_text or "", "lxml") + for sel in [ + ".message-error", + ".messages .message-error", + ".page.messages .message-error", + "[data-ui-id='message-error']", + ".message.warning", + ".message.notice", + ]: + for box in soup.select(sel): + t = " ".join((box.get_text(" ") or "").split()) + if t and t not in msgs: + msgs.append(t) + except Exception: + pass + return msgs + +def _looks_like_login_page(html_text: str) -> bool: + try: + s = BeautifulSoup(html_text or "", "lxml") + if s.select_one("form#login-form.form-login"): + return True + title = (s.title.get_text() if s.title else "").strip().lower() + if "customer login" in title: + return True + except Exception: + pass + return False + +def _chrome_headers(referer=None, origin=None): + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + if referer: + headers["Referer"] = referer + if origin: + headers["Origin"] = origin + return headers + +async def login( + username: str, + password: str, + *, + extra_cookies = {}, # ok to pass cf_clearance etc., but NOT form_key + timeout: float = 30.0, +) -> httpx.Cookies: + """ + Attempt login and return an authenticated cookie jar. + + Success criteria (strict): + 1) /customer/section/load?sections=customer reports is_logged_in == True + OR + 2) GET /customer/account/ resolves to an account page (not the login page). + + Otherwise raises LoginFailed with debug info. + """ + limits = httpx.Limits(max_connections=10, max_keepalive_connections=6) + cookies = httpx.Cookies() + for k, v in { + **extra_cookies, + "pr-cookie-consent": '["all"]', + "user_allowed_save_cookie": '{"1":1}', + }.items(): + if k.lower() == "form_key": + continue + cookies.set(k, v, domain="wholesale.suma.coop", path="/") + + base_login = config()["base_login"] + base_url = config()["base_url"] + + async with httpx.AsyncClient( + follow_redirects=True, + timeout=httpx.Timeout(timeout, connect=15.0), + http2=True, + limits=limits, + cookies=cookies, + headers=_chrome_headers(), + trust_env=True, + ) as client: + # 1) GET login page for fresh form_key + import time + login_bust = base_login + ("&" if "?" in base_login else "?") + f"_={int(time.time()*1000)}" + login_bust = base_login + r_get = await client.get(login_bust, headers=_chrome_headers()) + print("Login GET failed. Status:", r_get.status_code) + print("Login GET URL:", r_get.url) + print("Response text:", r_get.text[:1000]) # trim if long + r_get.raise_for_status() + soup = BeautifulSoup(r_get.text, "lxml") + + form = soup.select_one("form.form.form-login#login-form") or soup.select_one("#login-form") + if not form: + raise LoginFailed( + "Login form not found (possible bot challenge or theme change).", + debug={"get_status": r_get.status_code, "final_url": str(r_get.url)}, + ) + action = urljoin(base_login, form.get("action") or base_login) + fk_el = form.find("input", attrs={"name": "form_key"}) + hidden_form_key = (fk_el.get("value") if fk_el else "") or "" + + # mirror Magento behavior: form_key also appears as a cookie + client.cookies.set("form_key", hidden_form_key, domain="wholesale.suma.coop", path="/") + + payload = { + "form_key": hidden_form_key, + "login[username]": username, + "login[password]": password, + "send": "Login", + } + + post_headers = _chrome_headers(referer=base_login, origin=base_url) + post_headers["Content-Type"] = "application/x-www-form-urlencoded" + post_headers["Cookie"] = _cookie_header_from_jar( + client.cookies, domain="wholesale.suma.coop", path="/customer/" + ) + + r_post = await client.post(action, data=payload, headers=post_headers) + + # 2) Primary check: sections API must say logged in + is_logged_in = False + sections_url = "https://wholesale.suma.coop/customer/section/load/?sections=customer&force_new_section_timestamp=1" + section_json: Dict[str, Any] = {} + try: + r_sec = await client.get(sections_url, headers=_chrome_headers(referer=base_login)) + if r_sec.status_code == 200: + section_json = r_sec.json() + cust = section_json.get("customer") or {} + is_logged_in = bool(cust.get("is_logged_in")) + except Exception: + pass + + # 3) Secondary check: account page should NOT be the login page + looks_like_login = False + final_account_url = "" + try: + r_acc = await client.get("https://wholesale.suma.coop/customer/account/", headers=_chrome_headers(referer=base_login)) + final_account_url = str(r_acc.url) + looks_like_login = ( + "/customer/account/login" in final_account_url + or _looks_like_login_page(r_acc.text) + ) + except Exception: + # ignore; we'll rely on section status + pass + + # Decide success/failure strictly + if not (is_logged_in or (final_account_url and not looks_like_login)): + errors = _extract_magento_errors(r_post.text) + # Clean up transient form_key cookie + try: + client.cookies.jar.clear("wholesale.suma.coop", "/", "form_key") + except Exception: + pass + raise LoginFailed( + errors[0] if errors else "Invalid username or password.", + debug={ + "get_status": r_get.status_code, + "post_status": r_post.status_code, + "post_final_url": str(r_post.url), + "sections_customer": section_json.get("customer"), + "account_final_url": final_account_url, + "looks_like_login_page": looks_like_login, + }, + ) + def clear_cookie_everywhere(cookies: httpx.Cookies, name: str) -> None: + to_delete = [] + for c in list(cookies.jar): # http.cookiejar.Cookie objects + if c.name == name: + # Note: CookieJar.clear requires exact (domain, path, name) + to_delete.append((c.domain, c.path, c.name)) + + for domain, path, nm in to_delete: + try: + cookies.jar.clear(domain, path, nm) + except KeyError: + # Mismatch can happen if domain has a leading dot vs not, etc. + # Try again with a normalized domain variant. + if domain and domain.startswith("."): + + cookies.jar.clear(domain.lstrip("."), path, nm) + else: + # or try with leading dot + cookies.jar.clear("." + domain, path, nm) + if name in cookies: + del cookies[name] + + clear_cookie_everywhere(client.cookies, "form_key") + #client.cookies.jar.clear(config()["base_host"] or "wholesale.suma.coop", "/", "form_key") + print('cookies', client.cookies) + return client.cookies diff --git a/market/scrape/html_utils.py b/market/scrape/html_utils.py new file mode 100644 index 0000000..9d9d3ef --- /dev/null +++ b/market/scrape/html_utils.py @@ -0,0 +1,44 @@ +# suma_browser/html_utils.py +from __future__ import annotations +from typing import Optional +from bs4 import BeautifulSoup +from urllib.parse import urljoin +from shared.config import config + + + +def to_fragment(html: Optional[str]) -> str: + """Return just the fragment contents (no / wrappers).""" + if not html: + return "" + soup = BeautifulSoup(html, "lxml") + + # unwrap document-level containers + for t in soup.find_all(["html", "body"]): + t.unwrap() + + return "".join(str(c) for c in soup.contents).strip() + +def absolutize_fragment(html: Optional[str]) -> str: + """Absolutize href/src against BASE_URL and return a fragment (no wrappers).""" + if not html: + return "" + frag = BeautifulSoup(html, "lxml") + + for tag in frag.find_all(True): + if tag.has_attr("href"): + raw = str(tag["href"]) + abs_href = urljoin(config()["base_url"], raw) if raw.startswith("/") else raw + #if rewrite_suma_href_to_local: + # local = rewrite_suma_href_to_local(abs_href) + # tag["href"] = local if local else abs_href + #else: + tag["href"] = abs_href + if tag.has_attr("src"): + raw = str(tag["src"]) + tag["src"] = urljoin(config()["base_url"], raw) if raw.startswith("/") else raw + + # unwrap wrappers and return only the inner HTML + for t in frag.find_all(["html", "body"]): + t.unwrap() + return "".join(str(c) for c in frag.contents).strip() diff --git a/market/scrape/http_client.py b/market/scrape/http_client.py new file mode 100644 index 0000000..3865605 --- /dev/null +++ b/market/scrape/http_client.py @@ -0,0 +1,220 @@ +# suma_browser/http_client.py +from __future__ import annotations + +import asyncio +import os +import secrets +from typing import Optional, Dict + +import httpx +from shared.config import config + +_CLIENT: httpx.AsyncClient | None = None + +# ----- optional decoders -> Accept-Encoding +BROTLI_OK = False +ZSTD_OK = False +try: + import brotli # noqa: F401 + BROTLI_OK = True +except Exception: + pass +try: + import zstandard as zstd # noqa: F401 + ZSTD_OK = True +except Exception: + pass + +def _accept_encoding() -> str: + enc = ["gzip", "deflate"] + if BROTLI_OK: + enc.append("br") + if ZSTD_OK: + enc.append("zstd") + return ", ".join(enc) + +FIREFOX_UA = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0" + +def _ff_headers(referer: Optional[str] = None) -> Dict[str, str]: + h = { + "User-Agent": FIREFOX_UA, + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-GB,en;q=0.5", + "Accept-Encoding": _accept_encoding(), + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none" if not referer else "same-origin", + "Sec-Fetch-User": "?1", + "DNT": "1", + "Sec-GPC": "1", + "Priority": "u=0, i", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + } + if referer: + h["Referer"] = referer + return h +def _chrome_headers(referer=None, origin=None): + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + if referer: + headers["Referer"] = referer + if origin: + headers["Origin"] = origin + return headers + +def _parse_cookie_header(cookie_header: str) -> Dict[str, str]: + jar: Dict[str, str] = {} + for part in cookie_header.split(";"): + part = part.strip() + if not part or "=" not in part: + continue + k, v = part.split("=", 1) + jar[k.strip()] = v.strip() + return jar + +def _looks_like_cloudflare(html: bytes) -> bool: + if not html: + return False + s = html[:40000].lower() + return ( + b"please wait while your request is being verified" in s + or b"/cdn-cgi/challenge-platform/scripts/jsd/main.js" in s + or b"rocket-loader.min.js" in s + or b"cf-ray" in s + or b"challenge-platform" in s + or b"cf-chl-" in s + ) + +# -------- runtime cookie configuration (preferred over env) -------------------- +_INITIAL_COOKIES: Dict[str, str] = {} +_INITIAL_COOKIE_HEADER: Optional[str] = None + +async def configure_cookies(cookies: Dict[str, str]) -> None: + """ + Configure initial cookies programmatically (preferred over env). + Call BEFORE the first request (i.e., before get_client()/fetch()). + If a client already exists, its jar is updated immediately. + """ + global _INITIAL_COOKIES, _INITIAL_COOKIE_HEADER + _INITIAL_COOKIE_HEADER = None + _INITIAL_COOKIES = dict(cookies or {}) + # If client already built, update it now + if _CLIENT is not None: + print('configuring cookies') + host = config()["base_host"] or "wholesale.suma.coop" + for k, v in _INITIAL_COOKIES.items(): + _CLIENT.cookies.set(k, v, domain=host, path="/") + +def configure_cookies_from_header(cookie_header: str) -> None: + """ + Configure initial cookies from a raw 'Cookie:' header string. + Preferred over env; call BEFORE the first request. + """ + global _INITIAL_COOKIES, _INITIAL_COOKIE_HEADER + _INITIAL_COOKIE_HEADER = cookie_header or "" + _INITIAL_COOKIES = _parse_cookie_header(_INITIAL_COOKIE_HEADER) + if _CLIENT is not None: + host = config()["base_host"] or "wholesale.suma.coop" + for k, v in _INITIAL_COOKIES.items(): + _CLIENT.cookies.set(k, v, domain=host, path="/") + +# ------------------------------------------------------------------------------ +async def get_client() -> httpx.AsyncClient: + """Public accessor (same as _get_client).""" + return await _get_client() + +async def _get_client() -> httpx.AsyncClient: + global _CLIENT + if _CLIENT is None: + timeout = httpx.Timeout(300.0, connect=150.0) + limits = httpx.Limits(max_keepalive_connections=8, max_connections=16) + _CLIENT = httpx.AsyncClient( + follow_redirects=True, + timeout=timeout, + http2=True, + limits=limits, + headers=_chrome_headers(), + trust_env=True, + ) + + # ---- Seed cookies (priority: runtime config > env var) --------------- + host = config()["base_host"] or "wholesale.suma.coop" + + if _INITIAL_COOKIES or _INITIAL_COOKIE_HEADER: + # From runtime config + if _INITIAL_COOKIE_HEADER: + _CLIENT.cookies.update(_parse_cookie_header(_INITIAL_COOKIE_HEADER)) + for k, v in _INITIAL_COOKIES.items(): + _CLIENT.cookies.set(k, v, domain=host, path="/") + else: + # Fallback to environment + cookie_str = os.environ.get("SUMA_COOKIES", "").strip() + if cookie_str: + _CLIENT.cookies.update(_parse_cookie_header(cookie_str)) + + # Ensure private_content_version is present + if "private_content_version" not in _CLIENT.cookies: + pcv = secrets.token_hex(16) + _CLIENT.cookies.set("private_content_version", pcv, domain=host, path="/") + # --------------------------------------------------------------------- + + return _CLIENT + +async def aclose_client() -> None: + global _CLIENT + if _CLIENT is not None: + await _CLIENT.aclose() + _CLIENT = None + +async def fetch(url: str, *, referer: Optional[str] = None, retries: int = 3) -> str: + client = await _get_client() + + # Warm-up visit to look like a real session + if len(client.cookies.jar) == 0: + try: + await client.get(config()["base_url"].rstrip("/") + "/", headers=_chrome_headers()) + await asyncio.sleep(0.25) + except Exception: + pass + + last_exc: Optional[Exception] = None + for attempt in range(1, retries + 1): + try: + h = _chrome_headers(referer=referer or (config()["base_url"].rstrip("/") + "/")) + r = await client.get(url, headers=h) + if _looks_like_cloudflare(r.content): + if attempt < retries: + await asyncio.sleep(0.9 if attempt == 1 else 1.3) + try: + await client.get(config()["base_url"].rstrip("/") + "/", headers=_chrome_headers()) + await asyncio.sleep(0.4) + except Exception: + pass + continue + try: + r.raise_for_status() + except httpx.HTTPStatusError as e: + print(f"Fetch failed for {url}") + print("Status:", r.status_code) + print("Body:", r.text[:1000]) # Trimmed + raise + return r.text + except Exception as e: + last_exc = e + if attempt >= retries: + raise + await asyncio.sleep(0.45 * attempt + 0.25) + + if last_exc: + raise last_exc + raise RuntimeError("fetch failed unexpectedly") diff --git a/market/scrape/listings.py b/market/scrape/listings.py new file mode 100644 index 0000000..0a7e197 --- /dev/null +++ b/market/scrape/listings.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import math +import re +from typing import Callable, Dict, List, Optional, Tuple +from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse + + +from .http_client import fetch +from bp.browse.services.slugs import product_slug_from_href +from bp.browse.services.state import ( + KNOWN_PRODUCT_SLUGS, + _listing_page_cache, + _listing_page_ttl, + _listing_variant_cache, + _listing_variant_ttl, + now, +) +from shared.utils import normalize_text, soup_of +from shared.config import config + + +def parse_total_pages_from_text(text: str) -> Optional[int]: + m = re.search(r"Showing\s+(\d+)\s+of\s+(\d+)", text, re.I) + if not m: + return None + shown = int(m.group(1)) + total = int(m.group(2)) + per_page = 36 if shown in (12, 24, 36) else shown + return max(1, math.ceil(total / per_page)) + + +def _first_from_srcset(val: str) -> Optional[str]: + if not val: + return None + first = val.split(",")[0].strip() + parts = first.split() + return parts[0] if parts else first + + +def _abs_url(u: Optional[str]) -> Optional[str]: + if not u: + return None + return urljoin(config()["base_url"], u) if isinstance(u, str) and u.startswith("/") else u + + +def _collect_img_candidates(el) -> List[str]: + urls: List[str] = [] + if not el: + return urls + attrs = ["src", "data-src", "data-original", "data-zoom-image", "data-thumb", "content", "href"] + for a in attrs: + v = el.get(a) + if v: + urls.append(v) + for a in ["srcset", "data-srcset"]: + v = el.get(a) + if v: + first = _first_from_srcset(v) + if first: + urls.append(first) + return urls + + +def _dedupe_preserve_order_by(seq: List[str], key: Callable[[str], str]) -> List[str]: + seen = set() + out: List[str] = [] + for s in seq: + if not s: + continue + k = key(s) + if k in seen: + continue + seen.add(k) + out.append(s) + return out + + +def _filename_key(u: str) -> str: + p = urlparse(u) + path = p.path or "" + if path.endswith("/"): + path = path[:-1] + last = path.split("/")[-1] + return f"{p.netloc}:{last}".lower() + + +def _parse_cards_from_soup(soup) -> List[Dict]: + """Extract product tiles (name, href, image, desc) from listing soup. + De-duplicate by slug to avoid doubles from overlapping selectors.""" + items: List[str] = [] + seen_slugs: set[str] = set() + + # Primary selectors (Magento 2 default) + card_wrappers = soup.select( + "li.product-item, .product-item, ol.products.list.items li, .products.list.items li, .product-item-info" + ) + for card in card_wrappers: + a = ( + card.select_one("a.product-item-link") + or card.select_one(".product-item-name a") + or card.select_one("a[href$='.html'], a[href$='.htm']") + ) + if not a: + continue + #name = normalize_text(a.get_text()) or normalize_text(a.get("title") or "") + href = a.get("href") + #if not name or not href: + # continue + if href.startswith("/"): + href = urljoin(config()["base_url"], href) + + + slug = product_slug_from_href(href) + KNOWN_PRODUCT_SLUGS.add(slug) + + if slug and slug not in seen_slugs: + seen_slugs.add(slug) + items.append(slug) + # Secondary: any product-looking anchors inside products container + if not items: + products_container = soup.select_one(".products") or soup + for a in products_container.select("a[href$='.html'], a[href$='.htm']"): + href = a.get("href") + if href.startswith("/"): + href = urljoin(config()["base_url"], href) + slug = product_slug_from_href(href) + KNOWN_PRODUCT_SLUGS.add(slug) + if slug not in seen_slugs: + seen_slugs.add(slug) + items.append(slug) + + # Tertiary: JSON-LD fallback (ItemList/Product) + if not items: + import json + + def add_product(name: Optional[str], url: Optional[str], image: Optional[str]): + if not url: + return + absu = urljoin(config()["base_url"], url) if url.startswith("/") else url + slug = product_slug_from_href(absu) + if not slug: + return + KNOWN_PRODUCT_SLUGS.add(slug) + if slug not in seen_slugs: + seen_slugs.add(slug) + items.append(slug) + + for script in soup.find_all("script", attrs={"type": "application/ld+json"}): + #try: + data = json.loads(script.get_text()) + #except Exception: + # continue + if isinstance(data, dict): + if data.get("@type") == "ItemList" and isinstance(data.get("itemListElement"), list): + for it in data["itemListElement"]: + if isinstance(it, dict): + ent = it.get("item") or it + if isinstance(ent, dict): + add_product( + ent.get("name"), + ent.get("url"), + (ent.get("image") if isinstance(ent.get("image"), str) else None), + ) + if data.get("@type") == "Product": + add_product( + data.get("name"), + data.get("url"), + (data.get("image") if isinstance(data.get("image"), str) else None), + ) + elif isinstance(data, list): + for ent in data: + if not isinstance(ent, dict): + continue + if ent.get("@type") == "Product": + add_product( + ent.get("name"), + ent.get("url"), + (ent.get("image") if isinstance(ent.get("image"), str) else None), + ) + if ent.get("@type") == "ItemList": + for it in ent.get("itemListElement", []): + if isinstance(it, dict): + obj = it.get("item") or it + if isinstance(obj, dict): + add_product( + obj.get("name"), + obj.get("url"), + (obj.get("image") if isinstance(obj.get("image"), str) else None), + ) + + return items + + +def _with_query(url: str, add: Dict[str, str]) -> str: + p = urlparse(url) + q = dict(parse_qsl(p.query, keep_blank_values=True)) + q.update(add) + new_q = urlencode(q) + return urlunparse((p.scheme, p.netloc, p.path, p.params, new_q, p.fragment)) + + +def _with_page(url: str, page: int) -> str: + if page and page > 1: + return _with_query(url, {"p": str(page)}) + return url + + +def _listing_base_key(url: str) -> str: + p = urlparse(url) + path = p.path.rstrip("/") + return f"{p.scheme}://{p.netloc}{path}".lower() + + +def _variant_cache_get(base_key: str) -> Optional[str]: + info = _listing_variant_cache.get(base_key) + if not info: + return None + url, ts = info + if (now() - ts) > _listing_variant_ttl: + _listing_variant_cache.pop(base_key, None) + return None + return url + + +def _variant_cache_set(base_key: str, working_url: str) -> None: + _listing_variant_cache[base_key] = (working_url, now()) + + +def _page_cache_get(working_url: str, page: int) -> Optional[Tuple[List[Dict], int]]: + key = f"{working_url}|p={page}" + info = _listing_page_cache.get(key) + if not info: + return None + (items, total_pages), ts = info + if (now() - ts) > _listing_page_ttl: + _listing_page_cache.pop(key, None) + return None + return items, total_pages + + +def _page_cache_set(working_url: str, page: int, items: List[Dict], total_pages: int) -> None: + key = f"{working_url}|p={page}" + _listing_page_cache[key] = ((items, total_pages), now()) + + +async def _fetch_parse(url: str, page: int): + html = await fetch(_with_page(url, page)) + soup = soup_of(html) + items = _parse_cards_from_soup(soup) + return items, soup + + + + +async def scrape_products(list_url: str, page: int = 1): + """Fast listing fetch with variant memoization + page cache.""" + _listing_base_key(list_url) + items, soup = await _fetch_parse(list_url, page) + + total_pages = _derive_total_pages(soup) + return items, total_pages + +def _derive_total_pages(soup) -> int: + total_pages = 1 + textdump = normalize_text(soup.get_text(" ")) + pages_from_text = parse_total_pages_from_text(textdump) + if pages_from_text: + total_pages = pages_from_text + else: + pages = {1} + for a in soup.find_all("a", href=True): + m = re.search(r"[?&]p=(\d+)", a["href"]) + if m: + pages.add(int(m.group(1))) + total_pages = max(pages) if pages else 1 + return total_pages + + +def _slugs_from_list_url(list_url: str) -> Tuple[str, Optional[str]]: + p = urlparse(list_url) + parts = [x for x in (p.path or "").split("/") if x] + top = parts[0].lower() if parts else "" + sub = None + if len(parts) >= 2: + sub = parts[1] + if sub.lower().endswith((".html", ".htm")): + sub = re.sub(r"\.(html?|HTML?)$", "", sub) + return top, sub diff --git a/market/scrape/nav.py b/market/scrape/nav.py new file mode 100644 index 0000000..7e187d6 --- /dev/null +++ b/market/scrape/nav.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import re +from typing import Dict, List, Tuple, Optional +from urllib.parse import urlparse, urljoin + +from bs4 import BeautifulSoup +from shared.config import config +from .http_client import fetch # only fetch; define soup_of locally +#from .. import cache_backend as cb +#from ..blacklist.category import is_category_blocked # Reverse map: slug -> label + + +# ------------------ Caches ------------------ + + + +def soup_of(html: str) -> BeautifulSoup: + return BeautifulSoup(html or "", "lxml") + + +def normalize_text(s: str) -> str: + return re.sub(r"\s+", " ", (s or "").strip()) + + +async def scrape_nav_raw() -> List[Tuple[str, str]]: + html = await fetch(config()["base_url"]) + soup = soup_of(html) + results: List[Tuple[str, str]] = [] + for a in soup.find_all("a", href=True): + text = normalize_text(a.get_text()) + if not text: + continue + href = a["href"].strip() + if href.startswith("/"): + href = urljoin(config()["base_url"], href) + if not href.startswith(config()["base_url"]): + continue + results.append((text, href)) + return results + + +def extract_sub_slug(href: str, top_slug: str) -> Optional[str]: + p = urlparse(href) + parts = [x for x in (p.path or "").split("/") if x] + if len(parts) >= 2 and parts[0].lower() == top_slug.lower(): + sub = parts[1] + if sub.lower().endswith((".html", ".htm")): + sub = re.sub(r"\.(html?|HTML?)$", "", sub) + return sub + return None + + +async def group_by_category(slug_to_links: Dict[str, List[Tuple[str, str]]]) -> Dict[str, Dict]: + nav = {"cats": {}} + for label, slug in config()["categories"]["allow"].items(): + top_href = urljoin(config()["base_url"], f"/{slug}") + subs = [] + for text, href in slug_to_links.get(slug, []): + sub_slug = extract_sub_slug(href, slug) + if sub_slug: + #list_url = _join(config()["base_url"], f"/{slug}/{sub_slug}") + #log(f"naving [{slug}/{sub_slug}] page 1…") + #items, total_pages = await scrape_products(list_url, page=1) + #for p in range(2, total_pages + 1): + # log(f"naving [{slug}/{sub_slug}] page {p}…") + # moreitems, _tp = await scrape_products(list_url, page=p) + # items.extend( + # moreitems, + # ) + subs.append({"name": text, "href": href, "slug": sub_slug}) + subs.sort(key=lambda x: x["name"].lower()) + #list_url = _join(config()["base_url"], f"/{slug}") + #log(f"naving [{slug}] page 1…") + #items, total_pages = await scrape_products(list_url, page=1) + #for p in range(2, total_pages + 1): + # log(f"naving [{slug}] page {p}…") + # moreitems, _tp = await scrape_products(list_url, page=p) + # items.extend( + # moreitems, + # ) + nav["cats"][label] = {"href": top_href, "slug": slug, "subs": subs} + return nav + + +async def scrape_nav_filtered() -> Dict[str, Dict]: + anchors = await scrape_nav_raw() + slug_to_links: Dict[str, List[Tuple[str, str]]] = {} + for text, href in anchors: + p = urlparse(href) + parts = [x for x in (p.path or "").split("/") if x] + if not parts: + continue + top = parts[0].lower() + if top in config()["slugs"]["skip"]: + continue + slug_to_links.setdefault(top, []).append((text, href)) + return await group_by_category(slug_to_links) + +async def nav_scrape() -> Dict[str, Dict]: + """Return navigation structure; use snapshot when offline.""" + + nav = await scrape_nav_filtered() + return nav diff --git a/market/scrape/persist_api/__init__.py b/market/scrape/persist_api/__init__.py new file mode 100644 index 0000000..d5273af --- /dev/null +++ b/market/scrape/persist_api/__init__.py @@ -0,0 +1,6 @@ +from .upsert_product import upsert_product +from .log_product_result import log_product_result +from .save_nav import save_nav +from .save_subcategory_redirects import save_subcategory_redirects +from .capture_listing import capture_listing + diff --git a/market/scrape/persist_api/capture_listing.py b/market/scrape/persist_api/capture_listing.py new file mode 100644 index 0000000..3943253 --- /dev/null +++ b/market/scrape/persist_api/capture_listing.py @@ -0,0 +1,27 @@ +# replace your existing upsert_product with this version + +import os +import httpx + +from typing import List + +async def capture_listing( + url: str, + items: List[str], + total_pages: int +): + + sync_url = os.getenv("CAPTURE_LISTING_URL", "http://localhost:8001/market/suma-market/api/products/listing/") + + async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client: + _d = { + "url": url, + "items": items, + "total_pages": total_pages + } + resp = await client.post(sync_url, json=_d) + # Raise for non-2xx + resp.raise_for_status() + data = resp.json() if resp.content else {} + return data + \ No newline at end of file diff --git a/market/scrape/persist_api/log_product_result.py b/market/scrape/persist_api/log_product_result.py new file mode 100644 index 0000000..bf285ed --- /dev/null +++ b/market/scrape/persist_api/log_product_result.py @@ -0,0 +1,24 @@ +# replace your existing upsert_product with this version + +import os +import httpx + + +async def log_product_result( + ok: bool, + payload +): + + sync_url = os.getenv("PRODUCT_LOG_URL", "http://localhost:8000/market/api/products/log/") + + async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client: + _d = { + "ok": ok, + "payload": payload + } + resp = await client.post(sync_url, json=_d) + # Raise for non-2xx + resp.raise_for_status() + data = resp.json() if resp.content else {} + return data + \ No newline at end of file diff --git a/market/scrape/persist_api/save_nav.py b/market/scrape/persist_api/save_nav.py new file mode 100644 index 0000000..3feeadb --- /dev/null +++ b/market/scrape/persist_api/save_nav.py @@ -0,0 +1,19 @@ +# replace your existing upsert_product with this version + +import os +import httpx + +from typing import Dict + +async def save_nav( + nav: Dict, +): + sync_url = os.getenv("SAVE_NAV_URL", "http://localhost:8001/market/suma-market/api/products/nav/") + + async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client: + resp = await client.post(sync_url, json=nav) + # Raise for non-2xx + resp.raise_for_status() + data = resp.json() if resp.content else {} + return data + \ No newline at end of file diff --git a/market/scrape/persist_api/save_subcategory_redirects.py b/market/scrape/persist_api/save_subcategory_redirects.py new file mode 100644 index 0000000..60eba97 --- /dev/null +++ b/market/scrape/persist_api/save_subcategory_redirects.py @@ -0,0 +1,15 @@ +import os +import httpx + +from typing import Dict + +async def save_subcategory_redirects(mapping: Dict[str, str]) -> None: + sync_url = os.getenv("SAVE_REDIRECTS", "http://localhost:8000/market/api/products/redirects/") + + async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client: + resp = await client.post(sync_url, json=mapping) + # Raise for non-2xx + resp.raise_for_status() + data = resp.json() if resp.content else {} + return data + \ No newline at end of file diff --git a/market/scrape/persist_api/upsert_product.py b/market/scrape/persist_api/upsert_product.py new file mode 100644 index 0000000..d65149a --- /dev/null +++ b/market/scrape/persist_api/upsert_product.py @@ -0,0 +1,256 @@ +# replace your existing upsert_product with this version + +import os +import httpx + +from typing import Dict, List, Any + +async def upsert_product( + slug, + href, + d, +): + """ + Posts the given product dict `d` to the /api/products/sync endpoint. + Keeps the same signature as before and preserves logging/commit behavior. + """ + + + # Ensure slug in payload matches the function arg if present + if not d.get("slug"): + d["slug"] = slug + + # Where to post; override via env if needed + sync_url = os.getenv("PRODUCT_SYNC_URL", "http://localhost:8001/market/suma-market/api/products/sync/") + + + + + payload = _massage_payload(d) + + async def _do_call() -> Dict[str, Any]: + async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client: + resp = await client.post(sync_url, json=payload) + resp.raise_for_status() + # tolerate empty body + if not resp.content: + return {} + # prefer JSON if possible, otherwise return text + try: + return resp.json() + except ValueError: + return {"raw": resp.text} + + async def _log_error(exc: BaseException) -> None: + # Optional: add your own logging here + print(f"[upsert_product] POST failed: {type(exc).__name__}: {exc}. Retrying in 5s... slug={slug} url={sync_url}") + + return await retry_until_success(_do_call, delay=5.0, on_error=_log_error) + + + + #async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client: + # _d=_massage_payload(d) + # resp = await client.post(sync_url, json=_d) + # Raise for non-2xx + #resp.raise_for_status() + #data = resp.json() if resp.content else {} + #return data + +import asyncio +from typing import Any, Awaitable, Callable, Dict, Optional + +async def retry_until_success( + fn: Callable[[], Awaitable[Any]], + *, + delay: float = 5.0, + on_error: Optional[Callable[[BaseException], Awaitable[None]]] = None, +) -> Any: + """ + Repeatedly call the async no-arg function `fn` until it succeeds (returns without raising). + Waits `delay` seconds between attempts. Never gives up. + If provided, `on_error(exc)` is awaited after each failure. + """ + attempt = 0 + while True: + try: + return await fn() + except asyncio.CancelledError: + # bubble up cancellations immediately + raise + except BaseException as exc: + attempt += 1 + if on_error is not None: + try: + await on_error(exc) + except Exception: + # don't let error handler failures prevent retrying + pass + # fallback stderr log if no on_error handler + if on_error is None: + print(f"[retry] attempt {attempt} failed: {type(exc).__name__}: {exc}") + await asyncio.sleep(delay) + + + +def _get(d, key, default=None): + v = d.get(key) + return default if v in (None, "", [], {}) else v + + +def _massage_payload(d: Dict[str, Any]) -> Dict[str, Any]: + """Mirror the DB-upsert massaging so the API sees the same structure/values.""" + slug = d.get("slug") + if not slug: + raise ValueError("product missing slug") + + # --- Top-level fields (use _get where DB upsert uses it) --- + out: Dict[str, Any] = { + "slug": slug, + "title": _get(d, "title"), + "image": _get(d, "image"), + "description_short": _get(d, "description_short"), + "description_html": _get(d, "description_html"), + "suma_href": _get(d, "suma_href"), + "brand": _get(d, "brand"), + "rrp": _get(d, "rrp"), + "rrp_currency": _get(d, "rrp_currency"), + "rrp_raw": _get(d, "rrp_raw"), + "price_per_unit": _get(d, "price_per_unit"), + "price_per_unit_currency": _get(d, "price_per_unit_currency"), + "price_per_unit_raw": _get(d, "price_per_unit_raw"), + "special_price": _get(d, "special_price"), + "special_price_currency": _get(d, "special_price_currency"), + "special_price_raw": _get(d, "special_price_raw"), + "regular_price": _get(d, "regular_price"), + "regular_price_currency": _get(d, "regular_price_currency"), + "regular_price_raw": _get(d, "regular_price_raw"), + "case_size_count": _get(d, "case_size_count"), + "case_size_item_qty": _get(d, "case_size_item_qty"), + "case_size_item_unit": _get(d, "case_size_item_unit"), + "case_size_raw": _get(d, "case_size_raw"), + "ean": d.get("ean") or d.get("barcode") or None, + "sku": d.get("sku"), + "unit_size": d.get("unit_size"), + "pack_size": d.get("pack_size"), + } + + # --- Sections: only dicts with title+html (like DB sync) --- + sections_in = d.get("sections") or [] + sections_out: List[Dict[str, Any]] = [] + for sec in sections_in: + if isinstance(sec, dict) and sec.get("title") and sec.get("html"): + sections_out.append({"title": sec["title"], "html": sec["html"]}) + out["sections"] = sections_out + + # --- Images: same 3 buckets used in DB sync --- + def _coerce_str_list(x): + if not x: + return [] + # accept list of strings or list of dicts with {"url": ...} + out_urls = [] + for item in x: + if isinstance(item, str): + if item: + out_urls.append(item) + elif isinstance(item, dict): + u = item.get("url") + if u: + out_urls.append(u) + return out_urls + + out["images"] = _coerce_str_list(d.get("images")) + out["embedded_image_urls"] = _coerce_str_list(d.get("embedded_image_urls")) + out["all_image_urls"] = _coerce_str_list(d.get("all_image_urls")) + + # --- Labels: strip (DB code trims) --- + labels_in = d.get("labels") or [] + out["labels"] = [str(x).strip() for x in labels_in if x] + + # --- Stickers: strip + lower (DB code lower-cases) --- + stickers_in = d.get("stickers") or [] + out["stickers"] = [str(x).strip().lower() for x in stickers_in if x] + + # --- Attributes: pass through the same dict sources the DB code reads --- + out["info_table"] = d.get("info_table") or {} + #out["oe_list_price"] = d.get("oe_list_price") or {} + + # --- Nutrition: allow dict or list of dicts, mirroring DB code --- + nutrition = d.get("nutrition") or [] + if isinstance(nutrition, dict): + out["nutrition"] = {str(k).strip(): (None if v is None else str(v)) for k, v in nutrition.items()} + elif isinstance(nutrition, list): + rows = [] + for row in nutrition: + if not isinstance(row, dict): + continue + key = str(row.get("key") or "").strip() + if not key: + continue + rows.append({ + "key": key, + "value": None if row.get("value") is None else str(row.get("value")), + "unit": None if row.get("unit") is None else str(row.get("unit")), + }) + out["nutrition"] = rows + else: + out["nutrition"] = [] + + # --- Allergens: accept str (→ contains=True) or dict --- + alls_in = d.get("allergens") or [] + alls_out = [] + for a in alls_in: + if isinstance(a, str): + nm, contains = a.strip(), True + elif isinstance(a, dict): + nm, contains = (a.get("name") or "").strip(), bool(a.get("contains", True)) + else: + continue + if nm: + alls_out.append({"name": nm, "contains": contains}) + out["allergens"] = alls_out + + out["images"]=[ + {"url": s.strip(), "kind": "gallery", "position": i} + for i, s in enumerate(out.get("images") or []) + if isinstance(s, str) and s.strip() + ] + [ + {"url": s.strip(), "kind": "embedded", "position": i} + for i, s in enumerate(out.get("embedded_image_urls") or []) + if isinstance(s, str) and s.strip() + ] + [ + {"url": s.strip(), "kind": "all", "position": i} + for i, s in enumerate(out.get("all_image_urls") or []) + if isinstance(s, str) and s.strip() + ] + out["labels"]= [{"name": s.strip()} for s in out["labels"] if isinstance(s, str) and s.strip()] + out["stickers"]= [{"name": s.strip()} for s in out["stickers"] if isinstance(s, str) and s.strip()] + out["attributes"] = build_attributes_list(d) + + + return out + + + + + +def build_attributes_list(d: Dict[str, Any]) -> List[Dict[str, Any]]: + attrs = [] + for src, prefix in [ + (d.get("info_table") or {}, "info_table"), + (d.get("oe_list_price") or {}, "oe_list_price"), + ]: + for k, v in src.items(): + key = f"{prefix}/{str(k).strip()}" + val = None if v is None else str(v) + attrs.append({"key": key, "value": val}) + # optional: dedupe by (key, value) + seen = set() + dedup = [] + for item in attrs: + t = (item["key"], item["value"]) + if t in seen: + continue + seen.add(t) + dedup.append(item) + return dedup diff --git a/market/scrape/persist_snapshot/__init__.py b/market/scrape/persist_snapshot/__init__.py new file mode 100644 index 0000000..43d7e24 --- /dev/null +++ b/market/scrape/persist_snapshot/__init__.py @@ -0,0 +1,7 @@ +from .log_product_result import log_product_result +from .upsert_product import upsert_product +from .save_nav import save_nav +from .capture_listing import capture_listing +from .save_link_reports import save_link_reports +from .save_subcategory_redirects import save_subcategory_redirects + diff --git a/market/scrape/persist_snapshot/_get.py b/market/scrape/persist_snapshot/_get.py new file mode 100644 index 0000000..dd316b6 --- /dev/null +++ b/market/scrape/persist_snapshot/_get.py @@ -0,0 +1,3 @@ +def _get(d, key, default=None): + v = d.get(key) + return default if v in (None, "", [], {}) else v diff --git a/market/scrape/persist_snapshot/capture_listing.py b/market/scrape/persist_snapshot/capture_listing.py new file mode 100644 index 0000000..c6948dc --- /dev/null +++ b/market/scrape/persist_snapshot/capture_listing.py @@ -0,0 +1,137 @@ +# at top of persist_snapshot.py: +from typing import Optional, List +from sqlalchemy.ext.asyncio import AsyncSession + +from typing import List, Optional, Tuple +from sqlalchemy.dialects.postgresql import insert as pg_insert +from datetime import datetime +from sqlalchemy import ( + select, update +) +from urllib.parse import urlparse +import re + +from models.market import ( + NavTop, + NavSub, + Listing, + ListingItem, +) +from shared.db.session import get_session + +# --- Models are unchanged, see original code --- + +# ---------------------- Helper fns called from scraper ------------------------ + + + +async def capture_listing( + #product_slugs: Set[str], + url: str, + items: List[str], + total_pages: int + ) -> None: + async with get_session() as session: + await _capture_listing( + session, + url, + items, + total_pages + ) + await session.commit() + + +async def _capture_listing( + session, + url: str, + items: List[str], + total_pages: int + ) -> None: + top_id, sub_id = await _nav_ids_from_list_url(session, url) + await _save_listing(session, top_id, sub_id, items, total_pages) + +async def _save_listing(session: AsyncSession, top_id: int, sub_id: Optional[int], + items: List[str], total_pages: Optional[int]) -> None: + res = await session.execute( + select(Listing).where(Listing.top_id == top_id, Listing.sub_id == sub_id, Listing.deleted_at.is_(None)) + ) + listing = res.scalar_one_or_none() + if not listing: + listing = Listing(top_id=top_id, sub_id=sub_id, total_pages=total_pages) + session.add(listing) + await session.flush() + else: + listing.total_pages = total_pages + + # Normalize and deduplicate incoming slugs + seen: set[str] = set() + deduped: list[str] = [] + for s in items or []: + if s and isinstance(s, str) and s not in seen: + seen.add(s) + deduped.append(s) + + if not deduped: + return + + # Fetch existing slugs from the database + res = await session.execute( + select(ListingItem.slug) + .where(ListingItem.listing_id == listing.id, ListingItem.deleted_at.is_(None)) + ) + existing_slugs = set(res.scalars().all()) + + now = datetime.utcnow() + + # Slugs to delete (present in DB but not in the new data) + to_delete = existing_slugs - seen + if to_delete: + await session.execute( + update(ListingItem) + .where( + ListingItem.listing_id == listing.id, + ListingItem.slug.in_(to_delete), + ListingItem.deleted_at.is_(None) + ) + .values(deleted_at=now) + ) + + # Slugs to insert (new ones not in DB) + to_insert = seen - existing_slugs + if to_insert: + stmt = pg_insert(ListingItem).values( + [{"listing_id": listing.id, "slug": s} for s in to_insert] + ) + #.on_conflict_do_nothing( + # constraint="uq_listing_items_listing_slug" + #) + await session.execute(stmt) + +async def _nav_ids_from_list_url(session: AsyncSession, list_url: str) -> Tuple[int, Optional[int]]: + parts = [x for x in (urlparse(list_url).path or "").split("/") if x] + top_slug = parts[0].lower() if parts else "" + sub_slug = None + if len(parts) >= 2: + sub_slug = parts[1] + if sub_slug.lower().endswith((".html", ".htm")): + sub_slug = re.sub(r"\\.(html?|HTML?)$", "", sub_slug) + return await _get_nav_ids(session, top_slug, sub_slug) + + + +async def _get_nav_ids(session: AsyncSession, top_slug: str, sub_slug: Optional[str]) -> Tuple[int, Optional[int]]: + res_top = await session.execute(select(NavTop.id).where(NavTop.slug == top_slug, NavTop.deleted_at.is_(None))) + top_id = res_top.scalar_one_or_none() + if not top_id: + raise ValueError(f"NavTop not found for slug: {top_slug}") + + sub_id = None + if sub_slug: + res_sub = await session.execute( + select(NavSub.id).where(NavSub.slug == sub_slug, NavSub.top_id == top_id, NavSub.deleted_at.is_(None)) + ) + sub_id = res_sub.scalar_one_or_none() + if sub_id is None: + raise ValueError(f"NavSub not found for slug: {sub_slug} under top_id={top_id}") + + return top_id, sub_id diff --git a/market/scrape/persist_snapshot/log_product_result.py b/market/scrape/persist_snapshot/log_product_result.py new file mode 100644 index 0000000..88eb27b --- /dev/null +++ b/market/scrape/persist_snapshot/log_product_result.py @@ -0,0 +1,35 @@ +# at top of persist_snapshot.py: +from sqlalchemy.ext.asyncio import AsyncSession + +from typing import Dict +from models.market import ( + ProductLog, +) +from shared.db.session import get_session + + +async def log_product_result(ok: bool, payload: Dict) -> None: + async with get_session() as session: + await _log_product_result(session, ok, payload) + await session.commit() + + +async def _log_product_result(session: AsyncSession, ok: bool, payload: Dict) -> None: + session.add(ProductLog( + ok=ok, + slug=payload.get("slug"), + href_tried=payload.get("href_tried"), + error_type=payload.get("error_type"), + error_message=payload.get("error_message"), + http_status=payload.get("http_status"), + final_url=payload.get("final_url"), + transport_error=payload.get("transport_error"), + title=payload.get("title"), + has_description_html=payload.get("has_description_html"), + has_description_short=payload.get("has_description_short"), + sections_count=payload.get("sections_count"), + images_count=payload.get("images_count"), + embedded_images_count=payload.get("embedded_images_count"), + all_images_count=payload.get("all_images_count"), + )) + diff --git a/market/scrape/persist_snapshot/save_link_reports.py b/market/scrape/persist_snapshot/save_link_reports.py new file mode 100644 index 0000000..932b61a --- /dev/null +++ b/market/scrape/persist_snapshot/save_link_reports.py @@ -0,0 +1,29 @@ +# at top of persist_snapshot.py: +from typing import List + +from typing import Dict, List + +from models.market import ( + LinkError, + LinkExternal, +) +from shared.db.session import get_session + +# --- Models are unchanged, see original code --- + +# ---------------------- Helper fns called from scraper ------------------------ + + + +async def save_link_reports(link_errors: List[Dict], link_externals: List[Dict]) -> None: + async with get_session() as session: + for e in link_errors: + session.add(LinkError( + product_slug=e.get("product"), href=e.get("href"), text=e.get("text"), + top=e.get("top"), sub=e.get("sub"), target_slug=e.get("target_slug"), type=e.get("type"), + )) + for e in link_externals: + session.add(LinkExternal( + product_slug=e.get("product"), href=e.get("href"), text=e.get("text"), host=e.get("host"), + )) + await session.commit() diff --git a/market/scrape/persist_snapshot/save_nav.py b/market/scrape/persist_snapshot/save_nav.py new file mode 100644 index 0000000..5f73626 --- /dev/null +++ b/market/scrape/persist_snapshot/save_nav.py @@ -0,0 +1,110 @@ +# at top of persist_snapshot.py: +from datetime import datetime +from sqlalchemy import ( + select, tuple_ +) +from typing import Dict + +from models.market import ( + NavTop, + NavSub, +) +from shared.db.session import get_session + + + + +async def save_nav(nav: Dict) -> None: + async with get_session() as session: + await _save_nav(session, nav) + await session.commit() + +async def _save_nav(session, nav: Dict, market_id=None) -> None: + print('===================SAVE NAV========================') + print(nav) + now = datetime.utcnow() + + incoming_top_slugs = set() + incoming_sub_keys = set() # (top_slug, sub_slug) + + # First pass: collect slugs + for label, data in (nav.get("cats") or {}).items(): + top_slug = (data or {}).get("slug") + if not top_slug: + continue + incoming_top_slugs.add(top_slug) + + for s in (data.get("subs") or []): + sub_slug = s.get("slug") + if sub_slug: + incoming_sub_keys.add((top_slug, sub_slug)) + + # Soft-delete stale NavSub entries + # This requires joining NavTop to access top_slug + subs_to_delete = await session.execute( + select(NavSub) + .join(NavTop, NavSub.top_id == NavTop.id) + .where( + NavSub.deleted_at.is_(None), + ~tuple_(NavTop.slug, NavSub.slug).in_(incoming_sub_keys) + ) + ) + for sub in subs_to_delete.scalars(): + sub.deleted_at = now + + # Soft-delete stale NavTop entries + tops_to_delete = await session.execute( + select(NavTop) + .where( + NavTop.deleted_at.is_(None), + ~NavTop.slug.in_(incoming_top_slugs) + ) + ) + for top in tops_to_delete.scalars(): + top.deleted_at = now + + await session.flush() + + # Upsert NavTop and NavSub + for label, data in (nav.get("cats") or {}).items(): + top_slug = (data or {}).get("slug") + if not top_slug: + continue + + res = await session.execute( + select(NavTop).where(NavTop.slug == top_slug) + ) + top = res.scalar_one_or_none() + + if top: + top.label = label + top.deleted_at = None + if market_id is not None and top.market_id is None: + top.market_id = market_id + else: + top = NavTop(label=label, slug=top_slug, market_id=market_id) + session.add(top) + + await session.flush() + + for s in (data.get("subs") or []): + sub_slug = s.get("slug") + if not sub_slug: + continue + sub_label = s.get("label") + sub_href = s.get("href") + + res_sub = await session.execute( + select(NavSub).where( + NavSub.slug == sub_slug, + NavSub.top_id == top.id + ) + ) + sub = res_sub.scalar_one_or_none() + if sub: + sub.label = sub_label + sub.href = sub_href + sub.deleted_at = None + else: + session.add(NavSub(top_id=top.id, label=sub_label, slug=sub_slug, href=sub_href)) + diff --git a/market/scrape/persist_snapshot/save_subcategory_redirects.py b/market/scrape/persist_snapshot/save_subcategory_redirects.py new file mode 100644 index 0000000..6ffdd7b --- /dev/null +++ b/market/scrape/persist_snapshot/save_subcategory_redirects.py @@ -0,0 +1,32 @@ +# at top of persist_snapshot.py: + +from typing import Dict +from datetime import datetime +from sqlalchemy import ( + update +) +from models.market import ( + SubcategoryRedirect, +) +from shared.db.session import get_session + +# --- Models are unchanged, see original code --- + +# ---------------------- Helper fns called from scraper ------------------------ + + +async def save_subcategory_redirects(mapping: Dict[str, str]) -> None: + async with get_session() as session: + await _save_subcategory_redirects(session, mapping) + await session.commit() + + +async def _save_subcategory_redirects(session, mapping: Dict[str, str]) -> None: + await session.execute(update(SubcategoryRedirect).where(SubcategoryRedirect.deleted_at.is_(None)).values(deleted_at=datetime.utcnow())) + for old, new in mapping.items(): + session.add(SubcategoryRedirect(old_path=old, new_path=new)) + + + + #for slug in items: + # product_slugs.add(slug) diff --git a/market/scrape/persist_snapshot/upsert_product.py b/market/scrape/persist_snapshot/upsert_product.py new file mode 100644 index 0000000..4ab1613 --- /dev/null +++ b/market/scrape/persist_snapshot/upsert_product.py @@ -0,0 +1,237 @@ +# at top of persist_snapshot.py: +from sqlalchemy.ext.asyncio import AsyncSession + +from typing import Dict +from datetime import datetime +from sqlalchemy import ( + func, select, update +) + +from models.market import ( + Product, + ProductImage, + ProductSection, + ProductLabel, + ProductSticker, + ProductAttribute, + ProductNutrition, + ProductAllergen +) +from shared.db.session import get_session + +from ._get import _get +from .log_product_result import _log_product_result + +# --- Models are unchanged, see original code --- + +# ---------------------- Helper fns called from scraper ------------------------ + + + + +async def _upsert_product(session: AsyncSession, d: Dict) -> Product: + slug = d.get("slug") + if not slug: + raise ValueError("product missing slug") + res = await session.execute(select(Product).where(Product.slug == slug, Product.deleted_at.is_(None))) + p = res.scalar_one_or_none() + if not p: + p = Product(slug=slug) + session.add(p) + + p.title = _get(d, "title") + p.image = _get(d, "image") + p.description_short = _get(d, "description_short") + p.description_html = _get(d, "description_html") + p.suma_href = _get(d, "suma_href") + p.brand = _get(d, "brand") + p.rrp = _get(d, "rrp") + p.rrp_currency = _get(d, "rrp_currency") + p.rrp_raw = _get(d, "rrp_raw") + p.price_per_unit = _get(d, "price_per_unit") + p.price_per_unit_currency = _get(d, "price_per_unit_currency") + p.price_per_unit_raw = _get(d, "price_per_unit_raw") + p.special_price = _get(d, "special_price") + p.special_price_currency = _get(d, "special_price_currency") + p.special_price_raw = _get(d, "special_price_raw") + p.regular_price = _get(d, "regular_price") + p.regular_price_currency = _get(d, "regular_price_currency") + p.regular_price_raw = _get(d, "regular_price_raw") + p.case_size_count = _get(d, "case_size_count") + p.case_size_item_qty = _get(d, "case_size_item_qty") + p.case_size_item_unit = _get(d, "case_size_item_unit") + p.case_size_raw = _get(d, "case_size_raw") + p.ean = d.get("ean") or d.get("barcode") or None + p.sku = d.get("sku") + p.unit_size = d.get("unit_size") + p.pack_size = d.get("pack_size") + p.updated_at = func.now() + + now = datetime.utcnow() + + + + # ProductSection sync + existing_sections = await session.execute(select(ProductSection).where(ProductSection.product_id == p.id, ProductSection.deleted_at.is_(None))) + existing_sections_set = {(s.title, s.html) for s in existing_sections.scalars()} + + new_sections_set = set() + for sec in d.get("sections") or []: + if isinstance(sec, dict) and sec.get("title") and sec.get("html"): + new_sections_set.add((sec["title"], sec["html"])) + if (sec["title"], sec["html"]) not in existing_sections_set: + session.add(ProductSection(product_id=p.id, title=sec["title"], html=sec["html"])) + + for s in existing_sections_set - new_sections_set: + await session.execute(update(ProductSection).where(ProductSection.product_id == p.id, ProductSection.title == s[0], ProductSection.html == s[1], ProductSection.deleted_at.is_(None)).values(deleted_at=now)) + + # ProductImage sync + existing_images = await session.execute(select(ProductImage).where(ProductImage.product_id == p.id, ProductImage.deleted_at.is_(None))) + existing_images_set = {(img.url, img.kind) for img in existing_images.scalars()} + + new_images_set = set() + for kind, urls in [ + ("gallery", d.get("images") or []), + ("embedded", d.get("embedded_image_urls") or []), + ("all", d.get("all_image_urls") or []), + ]: + for idx, url in enumerate(urls): + if url: + new_images_set.add((url, kind)) + if (url, kind) not in existing_images_set: + session.add(ProductImage(product_id=p.id, url=url, position=idx, kind=kind)) + + for img in existing_images_set - new_images_set: + await session.execute(update(ProductImage).where(ProductImage.product_id == p.id, ProductImage.url == img[0], ProductImage.kind == img[1], ProductImage.deleted_at.is_(None)).values(deleted_at=now)) + + # ProductLabel sync + existing_labels = await session.execute(select(ProductLabel).where(ProductLabel.product_id == p.id, ProductLabel.deleted_at.is_(None))) + existing_labels_set = {label.name.strip() for label in existing_labels.scalars()} + + new_labels = {str(name).strip() for name in (d.get("labels") or []) if name} + + for name in new_labels - existing_labels_set: + session.add(ProductLabel(product_id=p.id, name=name)) + + for name in existing_labels_set - new_labels: + await session.execute(update(ProductLabel).where(ProductLabel.product_id == p.id, ProductLabel.name == name, ProductLabel.deleted_at.is_(None)).values(deleted_at=now)) + + # ProductSticker sync + existing_stickers = await session.execute(select(ProductSticker).where(ProductSticker.product_id == p.id, ProductSticker.deleted_at.is_(None))) + existing_stickers_set = {sticker.name.strip() for sticker in existing_stickers.scalars()} + + new_stickers = {str(name).strip().lower() for name in (d.get("stickers") or []) if name} + + for name in new_stickers - existing_stickers_set: + session.add(ProductSticker(product_id=p.id, name=name)) + + for name in existing_stickers_set - new_stickers: + await session.execute(update(ProductSticker).where(ProductSticker.product_id == p.id, ProductSticker.name == name, ProductSticker.deleted_at.is_(None)).values(deleted_at=now)) + + # ProductAttribute sync + existing_attrs = await session.execute(select(ProductAttribute).where(ProductAttribute.product_id == p.id, ProductAttribute.deleted_at.is_(None))) + existing_attrs_set = {(a.key, a.value) for a in existing_attrs.scalars()} + + new_attrs_set = set() + for src, prefix in [(d.get("info_table") or {}, "info_table"), (d.get("oe_list_price") or {}, "oe_list_price")]: + for k, v in src.items(): + key = f"{prefix}/{str(k).strip()}" + val = None if v is None else str(v) + new_attrs_set.add((key, val)) + if (key, val) not in existing_attrs_set: + session.add(ProductAttribute(product_id=p.id, key=key, value=val)) + + for key, val in existing_attrs_set - new_attrs_set: + await session.execute(update(ProductAttribute).where(ProductAttribute.product_id == p.id, ProductAttribute.key == key, ProductAttribute.value == val, ProductAttribute.deleted_at.is_(None)).values(deleted_at=now)) + + # ProductNutrition sync + existing_nuts = await session.execute(select(ProductNutrition).where(ProductNutrition.product_id == p.id, ProductNutrition.deleted_at.is_(None))) + existing_nuts_set = {(n.key, n.value, n.unit) for n in existing_nuts.scalars()} + + new_nuts_set = set() + nutrition = d.get("nutrition") or [] + if isinstance(nutrition, dict): + for k, v in nutrition.items(): + key, val = str(k).strip(), str(v) if v is not None else None + new_nuts_set.add((key, val, None)) + if (key, val, None) not in existing_nuts_set: + session.add(ProductNutrition(product_id=p.id, key=key, value=val, unit=None)) + elif isinstance(nutrition, list): + for row in nutrition: + try: + key = str(row.get("key") or "").strip() + val = None if row.get("value") is None else str(row.get("value")) + unit = None if row.get("unit") is None else str(row.get("unit")) + if key: + new_nuts_set.add((key, val, unit)) + if (key, val, unit) not in existing_nuts_set: + session.add(ProductNutrition(product_id=p.id, key=key, value=val, unit=unit)) + except Exception: + continue + + for key, val, unit in existing_nuts_set - new_nuts_set: + await session.execute(update(ProductNutrition).where(ProductNutrition.product_id == p.id, ProductNutrition.key == key, ProductNutrition.value == val, ProductNutrition.unit == unit, ProductNutrition.deleted_at.is_(None)).values(deleted_at=now)) + + # ProductAllergen sync + existing_allergens = await session.execute(select(ProductAllergen).where(ProductAllergen.product_id == p.id, ProductAllergen.deleted_at.is_(None))) + existing_allergens_set = {(a.name, a.contains) for a in existing_allergens.scalars()} + + new_allergens_set = set() + for a in d.get("allergens") or []: + if isinstance(a, str): + nm, contains = a.strip(), True + elif isinstance(a, dict): + nm, contains = (a.get("name") or "").strip(), bool(a.get("contains", True)) + else: + continue + if nm: + new_allergens_set.add((nm, contains)) + if (nm, contains) not in existing_allergens_set: + session.add(ProductAllergen(product_id=p.id, name=nm, contains=contains)) + + for name, contains in existing_allergens_set - new_allergens_set: + await session.execute(update(ProductAllergen).where(ProductAllergen.product_id == p.id, ProductAllergen.name == name, ProductAllergen.contains == contains, ProductAllergen.deleted_at.is_(None)).values(deleted_at=now)) + + + + + await session.flush() + return p + +async def upsert_product( + slug, + href, + d, +): + async with get_session() as session: + try: + await _upsert_product(session, d) + await _log_product_result(session, ok=True, payload={ + "slug": slug, + "href_tried": href, + "title": d.get("title"), + "has_description_html": bool(d.get("description_html")), + "has_description_short": bool(d.get("description_short")), + "sections_count": len(d.get("sections") or []), + "images_count": len(d.get("images")), + "embedded_images_count": len(d.get("embedded_image_urls")), + "all_images_count": len(d.get("all_image_urls")), + }) + + except Exception as e: + print(f"[ERROR] Failed to upsert product '{d.get('slug')}'") + print(f" Title: {d}.get('title')") + print(f" URL: {d.get('suma_href')}") + print(f" Error type: {type(e).__name__}") + print(f" Error message: {str(e)}") + import traceback + traceback.print_exc() + await _log_product_result(session, ok=False, payload={ + "slug": d.get("slug"), + "href_tried": d.get("suma_href"), + "error_type": type(e).__name__, + "error_message": str(e), + "title": d.get("title"), + }) + raise + await session.commit() \ No newline at end of file diff --git a/market/scrape/product/__init__.py b/market/scrape/product/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/market/scrape/product/__init__.py @@ -0,0 +1 @@ + diff --git a/market/scrape/product/extractors/__init__.py b/market/scrape/product/extractors/__init__.py new file mode 100644 index 0000000..9e29637 --- /dev/null +++ b/market/scrape/product/extractors/__init__.py @@ -0,0 +1,13 @@ + +# Auto-import all extractor modules so they register themselves. +from .title import ex_title # noqa: F401 +from .images import ex_images # noqa: F401 +from .short_description import ex_short_description # noqa: F401 +from .description_sections import ex_description_sections # noqa: F401 +from .nutrition_ex import ex_nutrition # noqa: F401 +from .stickers import ex_stickers # noqa: F401 +from .labels import ex_labels # noqa: F401 +from .info_table import ex_info_table # noqa: F401 +from .oe_list_price import ex_oe_list_price # noqa: F401 +from .regular_price_fallback import ex_regular_price_fallback # noqa: F401 +from .breadcrumbs import ex_breadcrumbs # noqa: F401 diff --git a/market/scrape/product/extractors/breadcrumbs.py b/market/scrape/product/extractors/breadcrumbs.py new file mode 100644 index 0000000..6aadefa --- /dev/null +++ b/market/scrape/product/extractors/breadcrumbs.py @@ -0,0 +1,68 @@ + +from __future__ import annotations +from typing import Dict, List, Union +from urllib.parse import urlparse +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ..registry import extractor + +@extractor +def ex_breadcrumbs(soup: BeautifulSoup, url: str) -> Dict: + """ + Parse breadcrumbs to identify top and sub categories. + """ + bc_ul = (soup.select_one(".breadcrumbs ul.items") + or soup.select_one("nav.breadcrumbs ul.items") + or soup.select_one("ul.items")) + if not bc_ul: + return {} + + crumbs = [] + for li in bc_ul.select("li.item"): + a = li.find("a") + if a: + title = normalize_text(a.get("title") or a.get_text()) + href = a.get("href") + else: + title = normalize_text(li.get_text()) + href = None + slug = None + if href: + try: + p = urlparse(href) + path = (p.path or "").strip("/") + slug = path.split("/")[-1] if path else None + except Exception: + slug = None + if slug: + crumbs.append({"title": title or None, "href": href or None, "slug": slug}) + + category_links = [c for c in crumbs if c.get("href")] + top = None + sub = None + for c in category_links: + t = (c.get("title") or "").lower() + s = (c.get("slug") or "").lower() + if t == "home" or s in ("", "home"): + continue + if top is None: + top = c + continue + if sub is None: + sub = c + break + + out: Dict[str, Union[str, List[Dict[str, str]]]] = { + "category_breadcrumbs": crumbs + } + if top: + out["category_top_title"] = top.get("title") + out["category_top_href"] = top.get("href") + out["category_top_slug"] = top.get("slug") + if sub: + out["category_sub_title"] = sub.get("title") + out["category_sub_href"] = sub.get("href") + out["category_sub_slug"] = sub.get("slug") + if top and sub: + out["category_path"] = f"{(top.get('slug') or '').strip()}/{(sub.get('slug') or '').strip()}" + return out diff --git a/market/scrape/product/extractors/description_sections.py b/market/scrape/product/extractors/description_sections.py new file mode 100644 index 0000000..719ed06 --- /dev/null +++ b/market/scrape/product/extractors/description_sections.py @@ -0,0 +1,43 @@ + +from __future__ import annotations +from typing import Dict, List +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ...html_utils import absolutize_fragment +from ..registry import extractor +from ..helpers.desc import ( + split_description_container, find_description_container, + pair_title_content_from_magento_tabs, scan_headings_for_sections, + additional_attributes_table, +) +from ..helpers.text import clean_title, is_blacklisted_heading + +@extractor +def ex_description_sections(soup: BeautifulSoup, url: str) -> Dict: + description_html = None + sections: List[Dict] = [] + desc_el = find_description_container(soup) + if desc_el: + open_html, sections_from_desc = split_description_container(desc_el) + description_html = open_html or None + sections.extend(sections_from_desc) + + existing = {s["title"].lower() for s in sections} + for t, html_fragment in (pair_title_content_from_magento_tabs(soup) or scan_headings_for_sections(soup)): + low = t.lower() + if "product description" in low or low == "description" or "details" in low: + if not description_html and html_fragment: + description_html = absolutize_fragment(html_fragment) + continue + if t.lower() not in existing and normalize_text(BeautifulSoup(html_fragment, "lxml").get_text()): + if not is_blacklisted_heading(t): + sections.append({"title": clean_title(t), "html": absolutize_fragment(html_fragment)}) + existing.add(t.lower()) + addl = additional_attributes_table(soup) + if addl and "additional information" not in existing and not is_blacklisted_heading("additional information"): + sections.append({"title": "Additional Information", "html": addl}) + out = {"sections": sections} + if description_html: + out["description_html"] = description_html + return out + diff --git a/market/scrape/product/extractors/images.py b/market/scrape/product/extractors/images.py new file mode 100644 index 0000000..b3d519d --- /dev/null +++ b/market/scrape/product/extractors/images.py @@ -0,0 +1,89 @@ +from __future__ import annotations +import json, re +from typing import Dict, List +from bs4 import BeautifulSoup +from ..registry import extractor +from ..helpers.html import abs_url, collect_img_candidates, dedup_by_filename + +@extractor +def ex_images(soup: BeautifulSoup, url: str) -> Dict: + images: List[str] = [] + debug = False # set True while debugging + + # 1) Magento init script (gallery) + scripts = soup.find_all("script", attrs={"type": "text/x-magento-init"}) + if debug: print(f"[ex_images] x-magento-init scripts: {len(scripts)}") + + for script in scripts: + # Use raw string as-is; no stripping/collapsing + text = script.string or script.get_text() or "" + if "mage/gallery/gallery" not in text: + continue + + # Correct (not over-escaped) patterns: + m = re.search(r'"data"\s*:\s*(\[[\s\S]*?\])', text) + if not m: + if debug: print("[ex_images] 'data' array not found in gallery block") + continue + + arr_txt = m.group(1) + added = False + try: + data = json.loads(arr_txt) + for entry in data: + u = abs_url(entry.get("full")) or abs_url(entry.get("img")) + if u: + images.append(u); added = True + except Exception as e: + if debug: print(f"[ex_images] json.loads failed: {e!r}; trying regex fallback") + # Fallback to simple key extraction + fulls = re.findall(r'"full"\s*:\s*"([^"]+)"', arr_txt) + imgs = re.findall(r'"img"\s*:\s*"([^"]+)"', arr_txt) if not fulls else [] + for u in (fulls or imgs): + u = abs_url(u) + if u: + images.append(u); added = True + + if added: + break # got what we need from the gallery block + + # 2) JSON-LD fallback + if not images: + for script in soup.find_all("script", attrs={"type": "application/ld+json"}): + raw = script.string or script.get_text() or "" + try: + data = json.loads(raw) + except Exception: + continue + + def add_from(val): + if isinstance(val, str): + u = abs_url(val); u and images.append(u) + elif isinstance(val, list): + for v in val: + if isinstance(v, str): + u = abs_url(v); u and images.append(u) + elif isinstance(v, dict) and "url" in v: + u = abs_url(v["url"]); u and images.append(u) + elif isinstance(val, dict) and "url" in val: + u = abs_url(val["url"]); u and images.append(u) + + if isinstance(data, dict) and "image" in data: + add_from(data["image"]) + if isinstance(data, list): + for item in data: + if isinstance(item, dict) and "image" in item: + add_from(item["image"]) + + # 3) Generic DOM scan fallback + if not images: + # consider broadening selectors if needed, e.g. '.fotorama__img' + for el in soup.select(".product.media img, .gallery-placeholder img, .fotorama__stage img"): + for cand in collect_img_candidates(el): + u = abs_url(cand) + if u: + images.append(u) + + images = dedup_by_filename(images) + if debug: print(f"[ex_images] found images: {images}") + return {"images": images, "image": images[0] if images else None} diff --git a/market/scrape/product/extractors/info_table.py b/market/scrape/product/extractors/info_table.py new file mode 100644 index 0000000..e1a8ef0 --- /dev/null +++ b/market/scrape/product/extractors/info_table.py @@ -0,0 +1,76 @@ + +from __future__ import annotations +from typing import Dict, Union +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ..registry import extractor +from ..helpers.price import parse_price, parse_case_size + +@extractor +def ex_info_table(soup: BeautifulSoup, url: str) -> Dict: + """ + Extracts: +
    ... rows of label/content ...
    + Produces: + info_table (raw map), brand, rrp[_raw|_currency], price_per_unit[_raw|_currency], + case_size_* fields + """ + container = soup.select_one(".product-page-info-table") or None + if not container: + return {} + rows_parent = container.select_one(".product-page-info-table-rows") or container + rows = rows_parent.select(".product-page-info-table-row") or [] + if not rows: + return {} + + raw_map: Dict[str, str] = {} + for r in rows: + lab_el = r.select_one(".product-page-info-table__label") + val_el = r.select_one(".product-page-info-table__content") + if not lab_el or not val_el: + continue + label = normalize_text(lab_el.get_text()) + value = normalize_text(val_el.get_text()) + if label: + raw_map[label] = value + + out: Dict[str, Union[str, float, int, Dict]] = {"info_table": raw_map} + + # Brand + brand = raw_map.get("Brand") or raw_map.get("Brand Name") or None + if brand: + out["brand"] = brand + + # RRP + rrp_val, rrp_cur, rrp_raw = parse_price(raw_map.get("RRP", "")) + if rrp_raw and (rrp_val is not None or rrp_cur is not None): + out["rrp_raw"] = rrp_raw + if rrp_val is not None: + out["rrp"] = rrp_val + if rrp_cur: + out["rrp_currency"] = rrp_cur + + # Price Per Unit + ppu_val, ppu_cur, ppu_raw = parse_price( + raw_map.get("Price Per Unit", "") or raw_map.get("Unit Price", "") + ) + if ppu_raw and (ppu_val is not None or ppu_cur is not None): + out["price_per_unit_raw"] = ppu_raw + if ppu_val is not None: + out["price_per_unit"] = ppu_val + if ppu_cur: + out["price_per_unit_currency"] = ppu_cur + + # Case Size + cs_text = raw_map.get("Case Size", "") or raw_map.get("Pack Size", "") + cs_count, cs_item_qty, cs_item_unit, cs_raw = parse_case_size(cs_text) + if cs_raw: + out["case_size_raw"] = cs_raw + if cs_count is not None: + out["case_size_count"] = cs_count + if cs_item_qty is not None: + out["case_size_item_qty"] = cs_item_qty + if cs_item_unit: + out["case_size_item_unit"] = cs_item_unit + + return out diff --git a/market/scrape/product/extractors/labels.py b/market/scrape/product/extractors/labels.py new file mode 100644 index 0000000..b4e4bd1 --- /dev/null +++ b/market/scrape/product/extractors/labels.py @@ -0,0 +1,41 @@ + +from __future__ import annotations +from typing import Dict, List +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ..registry import extractor + +@extractor +def ex_labels(soup: BeautifulSoup, url: str) -> Dict: + """ + From: +
      +
    • NEW
    • +
    + Returns "labels": lower-cased union of class hints and visible text. + """ + root = soup.select_one("ul.cdz-product-labels") + if not root: + return {} + items: List[str] = [] + texts: List[str] = [] + + for li in root.select("li.label-item"): + for c in (li.get("class") or []): + c = (c or "").strip() + if c and c.lower() != "label-item" and c not in items: + items.append(c) + txt = normalize_text(li.get_text()) + if txt and txt not in texts: + texts.append(txt) + + if not items and not texts: + return {} + union = [] + seen = set() + for s in items + [t.lower() for t in texts]: + key = (s or "").strip().lower() + if key and key not in seen: + seen.add(key) + union.append(key) + return {"labels": union} diff --git a/market/scrape/product/extractors/nutrition_ex.py b/market/scrape/product/extractors/nutrition_ex.py new file mode 100644 index 0000000..d39253d --- /dev/null +++ b/market/scrape/product/extractors/nutrition_ex.py @@ -0,0 +1,129 @@ +from __future__ import annotations +from typing import Dict, List, Optional, Tuple +import re +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ..registry import extractor +from ..helpers.desc import ( + split_description_container, find_description_container, + pair_title_content_from_magento_tabs, scan_headings_for_sections, +) + +# ----- value/unit parser ------------------------------------------------------ + +_NUM_UNIT_RE = re.compile( + r""" + ^\s* + (?P[-+]?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d+)?|\d+(?:[.,]\d+)?) + \s* + (?P[a-zA-Z%µ/]+)? + \s*$ + """, + re.X, +) + +def _parse_value_unit(s: str) -> Tuple[Optional[str], Optional[str]]: + if not s: + return None, None + s = re.sub(r"\s+", " ", s.strip()) + m = _NUM_UNIT_RE.match(s) + if not m: + return None, None + num = (m.group("num") or "").replace(",", "") + unit = m.group("unit") or None + if unit: + u = unit.lower() + if u in {"kcal", "kcal.", "kcalories", "kcalorie"}: + unit = "kcal" + elif u in {"kj", "kj.", "kilojoule", "kilojoules"}: + unit = "kJ" + return (num or None, unit) + +# ----- section finder --------------------------------------------------------- + +def _find_nutrition_section_html(soup: BeautifulSoup) -> Optional[str]: + """ + Return the HTML for the section whose title matches 'Nutritional Information'. + We look in the same places your description extractor does. + """ + # 1) Magento tabs + for t, html in (pair_title_content_from_magento_tabs(soup) or []): + if not t or not html: + continue + title = normalize_text(t).rstrip(":").lower() + if "nutritional information" in title: + return html + + # 2) Description container split into sections + desc_el = find_description_container(soup) + if desc_el: + _open_html, sections = split_description_container(desc_el) + for sec in sections or []: + title = normalize_text((sec.get("title") or "")).rstrip(":").lower() + if "nutritional information" in title: + return sec.get("html") or "" + + # 3) Fallback: generic heading scan + for t, html in (scan_headings_for_sections(soup) or []): + if not t or not html: + continue + title = normalize_text(t).rstrip(":").lower() + if "nutritional information" in title: + return html + + return None + +# ----- table parser ----------------------------------------------------------- + +def _extract_rows_from_table(root: BeautifulSoup) -> List[Dict[str, str]]: + out: List[Dict[str, str]] = [] + table = root.select_one("table") + if not table: + return out + + for tr in table.select("tr"): + th = tr.find("th") + tds = tr.find_all("td") + if th and tds: + key = normalize_text(th.get_text(" ").strip()) + val_raw = normalize_text(tds[0].get_text(" ").strip()) + elif len(tds) >= 2: + key = normalize_text(tds[0].get_text(" ").strip()) + val_raw = normalize_text(tds[1].get_text(" ").strip()) + else: + continue + + if not key or not val_raw: + continue + + value, unit = _parse_value_unit(val_raw) + if value is None: # keep raw if not parseable + value, unit = val_raw, None + + out.append({"key": key, "value": value, "unit": unit}) + + # Deduplicate while preserving order + seen = set() + dedup: List[Dict[str, str]] = [] + for r in out: + t = (r["key"], r.get("value"), r.get("unit")) + if t in seen: + continue + seen.add(t) + dedup.append(r) + return dedup + +# ----- extractor -------------------------------------------------------------- + +@extractor +def ex_nutrition(soup: BeautifulSoup, url: str) -> Dict: + """ + Extract nutrition ONLY from the section titled 'Nutritional Information'. + Returns: {"nutrition": [{"key": "...", "value": "...", "unit": "..."}]} + """ + section_html = _find_nutrition_section_html(soup) + if not section_html: + return {"nutrition": []} + section_soup = BeautifulSoup(section_html, "lxml") + rows = _extract_rows_from_table(section_soup) + return {"nutrition": rows} diff --git a/market/scrape/product/extractors/oe_list_price.py b/market/scrape/product/extractors/oe_list_price.py new file mode 100644 index 0000000..7e790fa --- /dev/null +++ b/market/scrape/product/extractors/oe_list_price.py @@ -0,0 +1,56 @@ + +from __future__ import annotations +from typing import Dict, Union +from bs4 import BeautifulSoup +from ..registry import extractor +from ..helpers.price import parse_price + +@extractor +def ex_oe_list_price(soup: BeautifulSoup, url: str) -> Dict: + """ + Extract Magento "oe-list-price" block: +
    +
    £30.50
    +
    £23.63
    +
    + Produces: + oe_list_price: { rrp_raw, rrp, rrp_currency, special_raw, special, special_currency } + Also promotes special_* to top-level (special_price_*) if available. + """ + box = soup.select_one(".oe-list-price") + if not box: + return {} + out: Dict[str, Union[str, float, dict]] = {} + oe: Dict[str, Union[str, float]] = {} + + # RRP inside oe-list-price (if present) + rrp = box.select_one(".rrp-price") + if rrp: + txt = (rrp.select_one("span.price") or rrp.select_one("span") or rrp).get_text(strip=True) + val, cur, raw = parse_price(txt) + if raw: + oe["rrp_raw"] = raw + if val is not None: + oe["rrp"] = val + if cur: + oe["rrp_currency"] = cur + + # Special Price inside oe-list-price + sp = box.select_one(".oe-final-price, .special-price, .final-price") + if sp: + txt = (sp.select_one("span.price") or sp.select_one("span") or sp).get_text(strip=True) + val, cur, raw = parse_price(txt) + if raw: + oe["special_raw"] = raw + if val is not None: + oe["special"] = val + out["special_price"] = val + if cur: + oe["special_currency"] = cur + out["special_price_currency"] = cur + if raw: + out["special_price_raw"] = raw + + if oe: + out["oe_list_price"] = oe + return out diff --git a/market/scrape/product/extractors/regular_price_fallback.py b/market/scrape/product/extractors/regular_price_fallback.py new file mode 100644 index 0000000..2693a90 --- /dev/null +++ b/market/scrape/product/extractors/regular_price_fallback.py @@ -0,0 +1,33 @@ + +from __future__ import annotations +from typing import Dict, Union +from bs4 import BeautifulSoup +from ..registry import extractor +from ..helpers.price import parse_price + +@extractor +def ex_regular_price_fallback(soup: BeautifulSoup, url: str) -> Dict: + """ + Fallback extractor for legacy 'Regular Price' blocks outside oe-list-price: +
    £16.55
    + """ + rrp = soup.select_one("div.rrp-price") + if not rrp: + return {} + span = rrp.select_one("span.price") + price_text = span.get_text(strip=True) if span else rrp.get_text(" ", strip=True) + value, currency, raw = parse_price(price_text or "") + out: Dict[str, Union[str, float]] = {} + if raw: + out["regular_price_raw"] = raw + if value is not None: + out["regular_price"] = value + if currency: + out["regular_price_currency"] = currency + if value is not None: + out.setdefault("rrp", value) + if currency: + out.setdefault("rrp_currency", currency) + if raw: + out.setdefault("rrp_raw", raw) + return out diff --git a/market/scrape/product/extractors/short_description.py b/market/scrape/product/extractors/short_description.py new file mode 100644 index 0000000..fefa827 --- /dev/null +++ b/market/scrape/product/extractors/short_description.py @@ -0,0 +1,19 @@ + +from __future__ import annotations +from typing import Dict +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ..registry import extractor + +@extractor +def ex_short_description(soup: BeautifulSoup, url: str) -> Dict: + desc_short = None + for sel in [".product.attribute.description .value", ".product.attribute.overview .value", + "meta[name='description']", "meta[property='og:description']"]: + el = soup.select_one(sel) + if not el: + continue + desc_short = normalize_text(el.get_text() if el.name != "meta" else el.get("content")) + if desc_short: + break + return {"description_short": desc_short} diff --git a/market/scrape/product/extractors/stickers.py b/market/scrape/product/extractors/stickers.py new file mode 100644 index 0000000..6bd7444 --- /dev/null +++ b/market/scrape/product/extractors/stickers.py @@ -0,0 +1,30 @@ + +from __future__ import annotations +from typing import Dict, List +from bs4 import BeautifulSoup +from ..registry import extractor + +@extractor +def ex_stickers(soup: BeautifulSoup, url: str) -> Dict: + """ +
    + + ... +
    + """ + root = soup.select_one("div.stickers") + if not root: + return {"stickers": []} + stickers: List[str] = [] + seen = set() + for sp in root.select("span.sticker"): + classes = sp.get("class") or [] + extras = [c.strip() for c in classes if c and c.lower() != "sticker"] + data_name = (sp.get("data-sticker") or "").strip() + if data_name: + extras.append(data_name) + for x in extras: + if x and x not in seen: + seen.add(x) + stickers.append(x) + return {"stickers": stickers} diff --git a/market/scrape/product/extractors/title.py b/market/scrape/product/extractors/title.py new file mode 100644 index 0000000..f7677ab --- /dev/null +++ b/market/scrape/product/extractors/title.py @@ -0,0 +1,17 @@ + +from __future__ import annotations +from typing import Dict +from bs4 import BeautifulSoup +from shared.utils import normalize_text +from ..registry import extractor + +@extractor +def ex_title(soup: BeautifulSoup, url: str) -> Dict: + title = None + for sel in ["h1.page-title span", "h1.page-title", "h1.product-name", "meta[property='og:title']"]: + el = soup.select_one(sel) + if el: + title = normalize_text(el.get_text()) if el.name != "meta" else el.get("content") + if title: + break + return {"title": title or "Product"} diff --git a/market/scrape/product/helpers/desc.py b/market/scrape/product/helpers/desc.py new file mode 100644 index 0000000..c093362 --- /dev/null +++ b/market/scrape/product/helpers/desc.py @@ -0,0 +1,165 @@ + +from __future__ import annotations +from typing import Dict, List, Optional, Tuple +from bs4 import BeautifulSoup, NavigableString, Tag +from shared.utils import normalize_text +from ...html_utils import absolutize_fragment +from .text import clean_title, is_blacklisted_heading +from shared.config import config + + +def split_description_container(desc_el: Tag) -> Tuple[str, List[Dict]]: + """ + Extract sections from accordion blocks within the description container. + + Looks for headings with class 'accordion-title' and pairs each with its + next element-sibling having class 'accordion-details'. Returns: + - open_html: the remaining description HTML with those accordion blocks removed + - sections: [{"title": ..., "html": ...}, ...] + """ + # Work on an isolated copy to avoid mutating the original DOM + frag = BeautifulSoup(desc_el.decode_contents(), "lxml") + + # Collect candidate (heading, details) pairs without mutating during iteration + pairs: List[Tuple[Tag, Tag]] = [] + for h in frag.select("#accordion .accordion-title, .accordion .accordion-title, h5.accordion-title, .accordion-title"): + if not isinstance(h, Tag): + continue + title = clean_title((h.get_text() or "").strip()) + if not title: + continue + + # Walk forward siblings until we hit an element; accept the first with 'accordion-details' + sib = h.next_sibling + details: Optional[Tag] = None + while sib is not None: + if isinstance(sib, Tag): + classes = sib.get("class") or [] + if "accordion-details" in classes: + details = sib + break + sib = sib.next_sibling + + if details is not None: + pairs.append((h, details)) + + sections: List[Dict] = [] + + # Extract sections, then remove nodes from frag + for h, details in pairs: + # Pull details HTML + html = details.decode_contents() + # Only keep non-empty (textual) content + if normalize_text(BeautifulSoup(html, "lxml").get_text()): + sections.append({ + "title": clean_title(h.get_text() or ""), + "html": absolutize_fragment(html), + }) + # Remove the matched nodes from the fragment copy + details.decompose() + h.decompose() + + # Whatever remains is the open description html + open_html = absolutize_fragment(str(frag)) if frag else "" + + return open_html, sections + +def pair_title_content_from_magento_tabs(soup: BeautifulSoup): + out = [] + container = soup.select_one(".product.info.detailed .product.data.items") or soup.select_one(".product.data.items") + if not container: + return out + titles = container.select(".data.item.title") + for t in titles: + title = normalize_text(t.get_text()) + if not title: + continue + content_id = t.get("aria-controls") or t.get("data-target") + content = soup.select_one(f"#{content_id}") if content_id else None + if content is None: + sib = t.find_next_sibling( + lambda x: isinstance(x, Tag) and "data" in x.get("class", []) and "item" in x.get("class", []) and "content" in x.get("class", []) + ) + content = sib + if content: + html = content.decode_contents() + if not is_blacklisted_heading(title): + out.append((title, absolutize_fragment(html))) + return out + +def scan_headings_for_sections(soup: BeautifulSoup): + out = [] + container = ( + soup.select_one(".product.info.detailed") + or soup.select_one(".product-info-main") + or soup.select_one(".page-main") + or soup + ) + heads = container.select("h2, h3, h4, h5, h6") + section_titles = (config().get("section-titles") or []) + for h in heads: + title = clean_title(h.get_text() or "") + if not title: + continue + low = title.lower() + if not any(k in low for k in section_titles + ["product description", "description", "details"]): + continue + parts: List[str] = [] + for sib in h.next_siblings: + if isinstance(sib, NavigableString): + parts.append(str(sib)) + continue + if isinstance(sib, Tag) and sib.name in ("h2", "h3", "h4", "h5", "h6"): + break + if isinstance(sib, Tag): + parts.append(str(sib)) + html = absolutize_fragment("".join(parts).strip()) + if html and not is_blacklisted_heading(title): + out.append((title, html)) + return out + +def additional_attributes_table(soup: BeautifulSoup) -> Optional[str]: + table = soup.select_one(".additional-attributes, table.additional-attributes, .product.attribute.additional table") + if not table: + return None + try: + rows = [] + for tr in table.select("tr"): + th = tr.find("th") or tr.find("td") + tds = tr.find_all("td") + key = normalize_text(th.get_text()) if th else None + val = normalize_text(tds[-1].get_text()) if tds else None + if key and val: + rows.append((key, val)) + if not rows: + return None + items = "\n".join( + [ + f"""
    +
    {key}
    +
    {val}
    +
    """ + for key, val in rows + ] + ) + return f"
    {items}
    " + except Exception: + return None + +def find_description_container(soup: BeautifulSoup) -> Optional[Tag]: + for sel in ["#description", "#tab-description", ".product.attribute.description .value", + ".product.attribute.overview .value", ".product.info.detailed .value"]: + el = soup.select_one(sel) + if el and normalize_text(el.get_text()): + return el + for h in soup.select("h2, h3, h4, h5, h6"): + txt = normalize_text(h.get_text()).lower() + if txt.startswith("product description") or txt == "description": + wrapper = soup.new_tag("div") + for sib in h.next_siblings: + if isinstance(sib, Tag) and sib.name in ("h2", "h3", "h4", "h5", "h6"): + break + wrapper.append(sib if isinstance(sib, Tag) else NavigableString(str(sib))) + if normalize_text(wrapper.get_text()): + return wrapper + return None diff --git a/market/scrape/product/helpers/html.py b/market/scrape/product/helpers/html.py new file mode 100644 index 0000000..6f355c5 --- /dev/null +++ b/market/scrape/product/helpers/html.py @@ -0,0 +1,53 @@ + +from __future__ import annotations +from typing import List, Optional +from urllib.parse import urljoin, urlparse +from shared.config import config + +def first_from_srcset(val: str) -> Optional[str]: + if not val: + return None + first = val.split(",")[0].strip() + parts = first.split() + return parts[0] if parts else first + +def abs_url(u: Optional[str]) -> Optional[str]: + if not u: + return None + return urljoin(config()["base_url"], u) if isinstance(u, str) and u.startswith("/") else u + +def collect_img_candidates(el) -> List[str]: + urls: List[str] = [] + if not el: + return urls + attrs = ["src", "data-src", "data-original", "data-zoom-image", "data-thumb", "content", "href"] + for a in attrs: + v = el.get(a) + if v: + urls.append(v) + for a in ["srcset", "data-srcset"]: + v = el.get(a) + if v: + first = first_from_srcset(v) + if first: + urls.append(first) + return urls + +def _filename_key(u: str) -> str: + p = urlparse(u) + path = p.path or "" + if path.endswith("/"): + path = path[:-1] + last = path.split("/")[-1] + return f"{p.netloc}:{last}".lower() + +def dedup_by_filename(urls: List[str]) -> List[str]: + seen = set() + out: List[str] = [] + for u in urls: + k = _filename_key(u) + if k in seen: + continue + seen.add(k) + out.append(u) + return out diff --git a/market/scrape/product/helpers/price.py b/market/scrape/product/helpers/price.py new file mode 100644 index 0000000..68aad1b --- /dev/null +++ b/market/scrape/product/helpers/price.py @@ -0,0 +1,42 @@ + +from __future__ import annotations +import re +from typing import Optional, Tuple + +def parse_price(text: str) -> Tuple[Optional[float], Optional[str], str]: + """ + Return (value, currency, raw) from a price-like string. + Supports symbols £, €, $; strips thousands commas. + """ + raw = (text or "").strip() + m = re.search(r'([£€$])?\s*([0-9][0-9.,]*)', raw) + if not m: + return None, None, raw + sym = m.group(1) or "" + num = m.group(2).replace(",", "") + try: + value = float(num) + except ValueError: + return None, None, raw + currency = {"£": "GBP", "€": "EUR", "$": "USD"}.get(sym, None) + return value, currency, raw + +def parse_case_size(text: str) -> Tuple[Optional[int], Optional[float], Optional[str], str]: + """ + Parse strings like "6 x 500g", "12x1L", "24 × 330 ml" + Returns (count, item_qty, item_unit, raw) + """ + raw = (text or "").strip() + if not raw: + return None, None, None, raw + t = re.sub(r"[×Xx]\s*", " x ", raw) + m = re.search(r"(\d+)\s*x\s*([0-9]*\.?[0-9]+)\s*([a-zA-Z]+)", t) + if not m: + return None, None, None, raw + count = int(m.group(1)) + try: + item_qty = float(m.group(2)) + except ValueError: + item_qty = None + unit = m.group(3) + return count, item_qty, unit, raw diff --git a/market/scrape/product/helpers/text.py b/market/scrape/product/helpers/text.py new file mode 100644 index 0000000..c8d6190 --- /dev/null +++ b/market/scrape/product/helpers/text.py @@ -0,0 +1,16 @@ + +from __future__ import annotations +import re +from shared.utils import normalize_text +from shared.config import config + +def clean_title(t: str) -> str: + t = normalize_text(t) + t = re.sub(r":\s*$", "", t) + return t + +def is_blacklisted_heading(title: str) -> bool: + """Return True if heading should be skipped based on config blacklist.""" + bl = (config().get("blacklist") or {}).get("product-details") or [] + low = (title or "").strip().lower() + return any(low == (s or "").strip().lower() for s in bl) diff --git a/market/scrape/product/product_core.py b/market/scrape/product/product_core.py new file mode 100644 index 0000000..9fbf5f2 --- /dev/null +++ b/market/scrape/product/product_core.py @@ -0,0 +1,48 @@ + +from __future__ import annotations +from typing import Dict, Tuple, Union +from shared.utils import soup_of +from ..http_client import fetch +from ..html_utils import absolutize_fragment +from bp.browse.services.slugs import product_slug_from_href +from .registry import REGISTRY, merge_missing +from . import extractors as _auto_register # noqa: F401 (import-time side effects) + +async def scrape_product_detail(product_url: str, include_html: bool = False) -> Union[dict, Tuple[dict, str]]: + """ + Returns a dict with fields (subset): + title, images, image, description_short, description_html, sections, + slug, suma_href, stickers, labels, info_table fields, oe_list_price, prices, + breadcrumbs-derived category_* fields. + If include_html=True, returns (data, html). + """ + html = await fetch(product_url) + + + data: Dict[str, Union[str, float, int, list, dict, None]] = { + "suma_href": product_url, + "slug": product_slug_from_href(product_url), + } + + # Run all extractors + for fn in REGISTRY: + try: + soup = soup_of(html) + piece = fn(soup, product_url) or {} + except Exception: + # Tolerate site drift + continue + merge_missing(data, piece) + # If we found short description but not description_html, echo it + if not data.get("description_html") and data.get("description_short"): + data["description_html"] = absolutize_fragment(f"

    {data['description_short']}

    ") + + # Ensure "image" mirrors first of images if not set + if not data.get("image"): + imgs = data.get("images") or [] + if isinstance(imgs, list) and imgs: + data["image"] = imgs[0] + + if include_html: + return data, html + return data diff --git a/market/scrape/product/product_detail.py b/market/scrape/product/product_detail.py new file mode 100644 index 0000000..705d35b --- /dev/null +++ b/market/scrape/product/product_detail.py @@ -0,0 +1,4 @@ + +from __future__ import annotations +# Thin wrapper to keep import path stable +from .product_core import scrape_product_detail # re-export diff --git a/market/scrape/product/registry.py b/market/scrape/product/registry.py new file mode 100644 index 0000000..53cabc4 --- /dev/null +++ b/market/scrape/product/registry.py @@ -0,0 +1,20 @@ + +from __future__ import annotations +from typing import Callable, Dict, List, Union + +Extractor = Callable[[object, str], Dict[str, Union[str, float, int, list, dict, None]]] +REGISTRY: List[Extractor] = [] + +def extractor(fn: Extractor) -> Extractor: + """Decorator to register an extractor.""" + REGISTRY.append(fn) + return fn + +def merge_missing(dst: dict, src: dict) -> None: + """ + Merge src into dst. Only write keys that are missing or empty in dst. + "Empty" means None, "", [], {}. + """ + for k, v in (src or {}).items(): + if k not in dst or dst[k] in (None, "", [], {}): + dst[k] = v diff --git a/market/services/__init__.py b/market/services/__init__.py new file mode 100644 index 0000000..8453359 --- /dev/null +++ b/market/services/__init__.py @@ -0,0 +1,29 @@ +"""Market app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the market app. + + Market owns: Product, CartItem, MarketPlace, NavTop, NavSub, + Listing, ProductImage. + Standard deployment registers all 4 services as real DB impls + (shared DB). For composable deployments, swap non-owned services + with stubs from shared.services.stubs. + """ + from shared.services.registry import services + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + services.market = SqlMarketService() + if not services.has("blog"): + services.blog = SqlBlogService() + if not services.has("calendar"): + services.calendar = SqlCalendarService() + if not services.has("cart"): + services.cart = SqlCartService() + if not services.has("federation"): + from shared.services.federation_impl import SqlFederationService + services.federation = SqlFederationService() diff --git a/market/templates/_types/all_markets/_card.html b/market/templates/_types/all_markets/_card.html new file mode 100644 index 0000000..3680e60 --- /dev/null +++ b/market/templates/_types/all_markets/_card.html @@ -0,0 +1,33 @@ +{# Card for a single market in the global listing #} +{% set pi = page_info.get(market.container_id, {}) %} +{% set page_slug = pi.get('slug', '') %} +{% set page_title = pi.get('title') %} +{% if page_slug %} + {% set market_href = market_url('/' ~ page_slug ~ '/' ~ market.slug ~ '/') %} +{% else %} + {% set market_href = '' %} +{% endif %} +
    +
    + {% if market_href %} + +

    {{ market.name }}

    +
    + {% else %} +

    {{ market.name }}

    + {% endif %} + + {% if market.description %} +

    {{ market.description }}

    + {% endif %} +
    + +
    + {% if page_title %} + + {{ page_title }} + + {% endif %} +
    +
    diff --git a/market/templates/_types/all_markets/_cards.html b/market/templates/_types/all_markets/_cards.html new file mode 100644 index 0000000..f3545c5 --- /dev/null +++ b/market/templates/_types/all_markets/_cards.html @@ -0,0 +1,18 @@ +{% for market in markets %} + {% include "_types/all_markets/_card.html" %} +{% endfor %} +{% if has_more %} + {# Infinite scroll sentinel #} + {% set next_url = url_for('all_markets.markets_fragment', page=page + 1)|host %} + +{% endif %} diff --git a/market/templates/_types/all_markets/_main_panel.html b/market/templates/_types/all_markets/_main_panel.html new file mode 100644 index 0000000..3599065 --- /dev/null +++ b/market/templates/_types/all_markets/_main_panel.html @@ -0,0 +1,12 @@ +{# Markets grid #} +{% if markets %} +
    + {% include "_types/all_markets/_cards.html" %} +
    +{% else %} +
    + +

    No markets available

    +
    +{% endif %} +
    diff --git a/market/templates/_types/all_markets/index.html b/market/templates/_types/all_markets/index.html new file mode 100644 index 0000000..2e7990d --- /dev/null +++ b/market/templates/_types/all_markets/index.html @@ -0,0 +1,7 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block content %} + {% include '_types/all_markets/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/browse/_admin.html b/market/templates/_types/browse/_admin.html new file mode 100644 index 0000000..e3cf3a2 --- /dev/null +++ b/market/templates/_types/browse/_admin.html @@ -0,0 +1,7 @@ +{% import "macros/links.html" as links %} +{% if g.rights.admin %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{admin_nav_item( + url_for('market.browse.product.admin', product_slug=slug) + )}} +{% endif %} \ No newline at end of file diff --git a/market/templates/_types/browse/_main_panel.html b/market/templates/_types/browse/_main_panel.html new file mode 100644 index 0000000..8640ce8 --- /dev/null +++ b/market/templates/_types/browse/_main_panel.html @@ -0,0 +1,5 @@ + +
    + {% include "_types/browse/_product_cards.html" %} +
    +
    diff --git a/market/templates/_types/browse/_oob_elements.html b/market/templates/_types/browse/_oob_elements.html new file mode 100644 index 0000000..dac5626 --- /dev/null +++ b/market/templates/_types/browse/_oob_elements.html @@ -0,0 +1,37 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-header-child', 'market-header-child', '_types/market/header/_header.html')}} + + {% from '_types/post/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/market/mobile/_nav_panel.html' %} +{% endblock %} + +{# Filter container with child summary - from browse/index.html child_summary block #} +{% block filter %} + {% include "_types/browse/mobile/_filter/summary.html" %} +{% endblock %} + +{% block aside %} + {% include "_types/browse/desktop/menu.html" %} +{% endblock %} + + +{% block content %} + {% include "_types/browse/_main_panel.html" %} +{% endblock %} diff --git a/market/templates/_types/browse/_product_card.html b/market/templates/_types/browse/_product_card.html new file mode 100644 index 0000000..b923bc5 --- /dev/null +++ b/market/templates/_types/browse/_product_card.html @@ -0,0 +1,104 @@ +{% import 'macros/stickers.html' as stick %} +{% import '_types/product/prices.html' as prices %} +{% set prices_ns = namespace() %} +{{ prices.set_prices(p, prices_ns) }} +{% set item_href = url_for('market.browse.product.product_detail', product_slug=p.slug)|host %} + \ No newline at end of file diff --git a/market/templates/_types/browse/_product_cards.html b/market/templates/_types/browse/_product_cards.html new file mode 100644 index 0000000..cc8edb3 --- /dev/null +++ b/market/templates/_types/browse/_product_cards.html @@ -0,0 +1,107 @@ +{% for p in products %} + {% include "_types/browse/_product_card.html" %} +{% endfor %} +{% if page < total_pages|int %} + + + + + +{% else %} +
    End of results
    +{% endif %} + diff --git a/market/templates/_types/browse/desktop/_category_selector.html b/market/templates/_types/browse/desktop/_category_selector.html new file mode 100644 index 0000000..b3c68b6 --- /dev/null +++ b/market/templates/_types/browse/desktop/_category_selector.html @@ -0,0 +1,40 @@ +{# Categories #} + diff --git a/market/templates/_types/browse/desktop/_filter/brand.html b/market/templates/_types/browse/desktop/_filter/brand.html new file mode 100644 index 0000000..616e36e --- /dev/null +++ b/market/templates/_types/browse/desktop/_filter/brand.html @@ -0,0 +1,40 @@ +{# Brand filter (desktop, single-select) #} + +{# Brands #} + diff --git a/market/templates/_types/browse/desktop/_filter/labels.html b/market/templates/_types/browse/desktop/_filter/labels.html new file mode 100644 index 0000000..7a4a41e --- /dev/null +++ b/market/templates/_types/browse/desktop/_filter/labels.html @@ -0,0 +1,44 @@ + + + +{% import 'macros/stickers.html' as stick %} + + diff --git a/market/templates/_types/browse/desktop/_filter/like.html b/market/templates/_types/browse/desktop/_filter/like.html new file mode 100644 index 0000000..c830f98 --- /dev/null +++ b/market/templates/_types/browse/desktop/_filter/like.html @@ -0,0 +1,38 @@ +{% import 'macros/stickers.html' as stick %} + {% set qs = {"liked": None if liked else True, "page": None}|qs %} + {% set href = (current_local_href ~ qs)|host %} + + {% if liked %} + + {% else %} + + {% endif %} + + {{ liked_count }} + + diff --git a/market/templates/_types/browse/desktop/_filter/search.html b/market/templates/_types/browse/desktop/_filter/search.html new file mode 100644 index 0000000..2e0ea8e --- /dev/null +++ b/market/templates/_types/browse/desktop/_filter/search.html @@ -0,0 +1,44 @@ + +{% macro search(current_local_href,search, search_count, hx_select) -%} + + +
    + + +
    + {% if search %} + {{search_count}} + {% endif %} + {{zap_filter}} +
    +
    +{% endmacro %} \ No newline at end of file diff --git a/market/templates/_types/browse/desktop/_filter/sort.html b/market/templates/_types/browse/desktop/_filter/sort.html new file mode 100644 index 0000000..a4b5404 --- /dev/null +++ b/market/templates/_types/browse/desktop/_filter/sort.html @@ -0,0 +1,34 @@ + + + + +{% import 'macros/stickers.html' as stick %} +{% set sort_val = sort|default('az', true) %} + +
      + {% for key,label,icon in sort_options %} + {% set is_on = (sort_val == key) %} + {% set qs = {"sort": None, "page": None}|qs if is_on + else {"sort": key, "page": None}|qs %} + {% set href = (current_local_href ~ qs)|host %} + +
    • + + {{ stick.sticker(asset_url(icon), label, is_on) }} + +
    • + {% endfor %} +
    diff --git a/market/templates/_types/browse/desktop/_filter/stickers.html b/market/templates/_types/browse/desktop/_filter/stickers.html new file mode 100644 index 0000000..46fd22b --- /dev/null +++ b/market/templates/_types/browse/desktop/_filter/stickers.html @@ -0,0 +1,46 @@ + + + + +{% import 'macros/stickers.html' as stick %} + + diff --git a/market/templates/_types/browse/desktop/menu.html b/market/templates/_types/browse/desktop/menu.html new file mode 100644 index 0000000..893cf2d --- /dev/null +++ b/market/templates/_types/browse/desktop/menu.html @@ -0,0 +1,37 @@ + {% import '_types/browse/desktop/_filter/search.html' as s %} + {{ s.search(current_local_href, search, search_count, hx_select) }} + +
    +
    +
    {{ category_label }}
    +
    + {% include "_types/browse/desktop/_filter/sort.html" %} + + + {% if stickers %} + {% include "_types/browse/desktop/_filter/stickers.html" %} + {% endif %} + + + {% if subs_local and top_local_href %} + {% include "_types/browse/desktop/_category_selector.html" %} + {% endif %} + +
    + +
    + + {% include "_types/browse/desktop/_filter/brand.html" %} + +
    diff --git a/market/templates/_types/browse/index.html b/market/templates/_types/browse/index.html new file mode 100644 index 0000000..015e6b3 --- /dev/null +++ b/market/templates/_types/browse/index.html @@ -0,0 +1,13 @@ +{% extends '_types/market/index.html' %} + +{% block filter %} + {% include "_types/browse/mobile/_filter/summary.html" %} +{% endblock %} + +{% block aside %} + {% include "_types/browse/desktop/menu.html" %} +{% endblock %} + +{% block content %} + {% include "_types/browse/_main_panel.html" %} +{% endblock %} diff --git a/market/templates/_types/browse/like/button.html b/market/templates/_types/browse/like/button.html new file mode 100644 index 0000000..426bdc1 --- /dev/null +++ b/market/templates/_types/browse/like/button.html @@ -0,0 +1,20 @@ + diff --git a/market/templates/_types/browse/mobile/_filter/brand_ul.html b/market/templates/_types/browse/mobile/_filter/brand_ul.html new file mode 100644 index 0000000..ac15400 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/brand_ul.html @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/market/templates/_types/browse/mobile/_filter/index.html b/market/templates/_types/browse/mobile/_filter/index.html new file mode 100644 index 0000000..7c2a615 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/index.html @@ -0,0 +1,30 @@ + + {% include "_types/browse/mobile/_filter/sort_ul.html" %} + {% if search or selected_labels|length or selected_stickers|length or selected_brands|length %} + {% set href = (current_local_href ~ {"clear_filters": True}|qs)|host %} + + {% endif %} +
    + {% include "_types/browse/mobile/_filter/like.html" %} + {% include "_types/browse/mobile/_filter/labels.html" %} +
    + {% include "_types/browse/mobile/_filter/stickers.html" %} + {% include "_types/browse/mobile/_filter/brand_ul.html" %} + diff --git a/market/templates/_types/browse/mobile/_filter/labels.html b/market/templates/_types/browse/mobile/_filter/labels.html new file mode 100644 index 0000000..3868d42 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/labels.html @@ -0,0 +1,47 @@ +{% import 'macros/stickers.html' as stick %} + + +{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #} + diff --git a/market/templates/_types/browse/mobile/_filter/like.html b/market/templates/_types/browse/mobile/_filter/like.html new file mode 100644 index 0000000..509ea92 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/like.html @@ -0,0 +1,40 @@ +{% import 'macros/stickers.html' as stick %} + \ No newline at end of file diff --git a/market/templates/_types/browse/mobile/_filter/search.html b/market/templates/_types/browse/mobile/_filter/search.html new file mode 100644 index 0000000..0f39178 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/search.html @@ -0,0 +1,40 @@ +{% macro search(current_local_href, search, search_count, hx_select) -%} + +
    + + +
    + {% if search %} + {{search_count}} + {% endif %} +
    +
    +{% endmacro %} \ No newline at end of file diff --git a/market/templates/_types/browse/mobile/_filter/sort_ul.html b/market/templates/_types/browse/mobile/_filter/sort_ul.html new file mode 100644 index 0000000..c02de19 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/sort_ul.html @@ -0,0 +1,33 @@ + + + +{% import 'macros/stickers.html' as stick %} + + + \ No newline at end of file diff --git a/market/templates/_types/browse/mobile/_filter/stickers.html b/market/templates/_types/browse/mobile/_filter/stickers.html new file mode 100644 index 0000000..fed0927 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/stickers.html @@ -0,0 +1,50 @@ +{% import 'macros/stickers.html' as stick %} + + + +{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #} + diff --git a/market/templates/_types/browse/mobile/_filter/summary.html b/market/templates/_types/browse/mobile/_filter/summary.html new file mode 100644 index 0000000..07a86a1 --- /dev/null +++ b/market/templates/_types/browse/mobile/_filter/summary.html @@ -0,0 +1,120 @@ +{% import 'macros/stickers.html' as stick %} +{% import 'macros/layout.html' as layout %} + + + + +{% call layout.details('/filter', 'md:hidden') %} + {% call layout.filter_summary("filter-summary-mobile", current_local_href, search, search_count, hx_select) %} +
    + + +
    + {% if sort %} +
      + + {% for k,l,i in sort_options %} + {% if k == sort %} + {% set key = k %} + {% set label = l %} + {% set icon = i %} +
    • + {{ stick.sticker(asset_url(icon), label, True)}} +
    • + {% endif %} + {% endfor %} +
    + {% endif %} + {% if liked %} +
    + + {% if liked_count is not none %} +
    + {{ liked_count }} +
    + {% endif %} +
    + {% endif %} + {% if selected_labels and selected_labels|length %} +
      + {% for st in selected_labels %} + {% for s in labels %} + {% if st == s.name %} +
    • + {{ stick.sticker(asset_url('nav-labels/' + s.name + '.svg'), s.name, True)}} + {% if s.count is not none %} +
      + {{ s.count }} +
      + {% endif %} +
    • + {% endif %} + {% endfor %} + {% endfor %} +
    + {% endif %} + {% if selected_stickers and selected_stickers|length %} +
      + {% for st in selected_stickers %} + {% for s in stickers %} + {% if st == s.name %} +
    • + + {{ stick.sticker(asset_url('stickers/' + s.name + '.svg'), s.name, True)}} + {% if s.count is not none %} + + {{ s.count }} + + {% endif %} +
    • + {% endif %} + {% endfor %} + {% endfor %} +
    + {% endif %} +
    + + {% if selected_brands and selected_brands|length %} +
      + {% for b in selected_brands %} +
    • + {% set ns = namespace(count=0) %} + {% for brand in brands %} + {% if brand.name == b %} + {% set ns.count = brand.count %} + {% endif %} + {% endfor %} + {% if ns.count %} +
      {{ b }}
      +
      {{ ns.count }}
      + {% else %} +
      {{ b }}
      +
      0
      + {% endif %} +
    • + {% endfor %} + + +
    + {% endif %} +
    + {% endcall %} +
    + {% include "_types/browse/mobile/_filter/index.html" %} +
    +{% endcall %} diff --git a/market/templates/_types/market/_admin.html b/market/templates/_types/market/_admin.html new file mode 100644 index 0000000..0b09927 --- /dev/null +++ b/market/templates/_types/market/_admin.html @@ -0,0 +1,7 @@ +{% import "macros/links.html" as links %} +{% if g.rights.admin %} + {% from 'macros/admin_nav.html' import admin_nav_item %} + {{admin_nav_item( + url_for('market.admin.admin') + )}} +{% endif %} \ No newline at end of file diff --git a/market/templates/_types/market/_main_panel.html b/market/templates/_types/market/_main_panel.html new file mode 100644 index 0000000..87bb965 --- /dev/null +++ b/market/templates/_types/market/_main_panel.html @@ -0,0 +1,23 @@ +{# Main panel fragment for HTMX navigation - market landing page #} +
    + {% if post.custom_excerpt %} +
    + {{post.custom_excerpt|safe}} +
    + {% endif %} + {% if post.feature_image %} +
    + +
    + {% endif %} +
    + {% if post.html %} + {{post.html|safe}} + {% endif %} +
    +
    +
    diff --git a/market/templates/_types/market/_oob_elements.html b/market/templates/_types/market/_oob_elements.html new file mode 100644 index 0000000..075c166 --- /dev/null +++ b/market/templates/_types/market/_oob_elements.html @@ -0,0 +1,30 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('post-header-child', 'market-header-child', '_types/market/header/_header.html')}} + + {% from '_types/post/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/market/mobile/_nav_panel.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/market/_main_panel.html" %} +{% endblock %} + + diff --git a/market/templates/_types/market/_title.html b/market/templates/_types/market/_title.html new file mode 100644 index 0000000..6e8024b --- /dev/null +++ b/market/templates/_types/market/_title.html @@ -0,0 +1,17 @@ +
    +
    + + {{ market_title }} +
    +
    +
    + {{top_slug or ''}} +
    + {% if sub_slug %} +
    + {{sub_slug}} +
    + {% endif %} +
    +
    \ No newline at end of file diff --git a/market/templates/_types/market/admin/_main_panel.html b/market/templates/_types/market/admin/_main_panel.html new file mode 100644 index 0000000..a354325 --- /dev/null +++ b/market/templates/_types/market/admin/_main_panel.html @@ -0,0 +1 @@ +market admin \ No newline at end of file diff --git a/market/templates/_types/market/admin/_nav.html b/market/templates/_types/market/admin/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/market/templates/_types/market/admin/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/market/templates/_types/market/admin/_oob_elements.html b/market/templates/_types/market/admin/_oob_elements.html new file mode 100644 index 0000000..9b306fd --- /dev/null +++ b/market/templates/_types/market/admin/_oob_elements.html @@ -0,0 +1,29 @@ +{% extends 'oob_elements.html' %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{# Header with app title - includes cart-mini, navigation, and market-specific header #} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('market-header-child', 'market-admin-header-child', '_types/market/admin/header/_header.html')}} + + {% from '_types/market/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% block mobile_menu %} + {% include '_types/market/admin/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include "_types/market/admin/_main_panel.html" %} +{% endblock %} + + diff --git a/market/templates/_types/market/admin/header/_header.html b/market/templates/_types/market/admin/header/_header.html new file mode 100644 index 0000000..950eefc --- /dev/null +++ b/market/templates/_types/market/admin/header/_header.html @@ -0,0 +1,11 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='market-admin-row', oob=oob) %} + {% call links.link(url_for('market.admin.admin'), hx_select_search) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/market/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/market/templates/_types/market/admin/index.html b/market/templates/_types/market/admin/index.html new file mode 100644 index 0000000..4798c46 --- /dev/null +++ b/market/templates/_types/market/admin/index.html @@ -0,0 +1,19 @@ +{% extends '_types/market/index.html' %} + + +{% block market_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('market-admin-header-child', '_types/market/admin/header/_header.html') %} + {% block market_admin_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block _main_mobile_menu %} + {% include '_types/market/admin/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include '_types/market/admin/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/market/desktop/_nav.html b/market/templates/_types/market/desktop/_nav.html new file mode 100644 index 0000000..d4de6e6 --- /dev/null +++ b/market/templates/_types/market/desktop/_nav.html @@ -0,0 +1,38 @@ + + diff --git a/market/templates/_types/market/header/_header.html b/market/templates/_types/market/header/_header.html new file mode 100644 index 0000000..2d92286 --- /dev/null +++ b/market/templates/_types/market/header/_header.html @@ -0,0 +1,11 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='market-row', oob=oob) %} + {% call links.link(url_for('market.browse.home'), hx_select_search ) %} + {% include '_types/market/_title.html' %} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/market/desktop/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/market/templates/_types/market/index.html b/market/templates/_types/market/index.html new file mode 100644 index 0000000..4da7f68 --- /dev/null +++ b/market/templates/_types/market/index.html @@ -0,0 +1,27 @@ +{% extends '_types/root/_index.html' %} + + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% call index_row('market-header-child', '_types/market/header/_header.html') %} + {% block market_header_child %} + {% endblock %} + {% endcall %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {% include '_types/market/mobile/_nav_panel.html' %} +{% endblock %} + + + +{% block aside %} +{# No aside on landing page #} +{% endblock %} + +{% block content %} + {% include "_types/market/_main_panel.html" %} +{% endblock %} diff --git a/market/templates/_types/market/markets_listing.html b/market/templates/_types/market/markets_listing.html new file mode 100644 index 0000000..ab8d2b0 --- /dev/null +++ b/market/templates/_types/market/markets_listing.html @@ -0,0 +1,23 @@ +{% extends '_types/root/_index.html' %} + +{% block content %} +
    +

    Markets

    + + {% if markets %} + + {% else %} +

    No markets available.

    + {% endif %} +
    +{% endblock %} diff --git a/market/templates/_types/market/mobile/_nav_panel.html b/market/templates/_types/market/mobile/_nav_panel.html new file mode 100644 index 0000000..65a9685 --- /dev/null +++ b/market/templates/_types/market/mobile/_nav_panel.html @@ -0,0 +1,110 @@ +{% from 'macros/glyphs.html' import opener %} +
    +
    + {% set all_href = (url_for('market.browse.browse_all') ~ qs)|host %} + {% set all_active = (category_label == 'All Products') %} + +
    + All +
    +
    + {% for cat, data in categories.items() %} +
    + + + {% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host %} + + +
    {{ cat }}
    +
    {{ data.count }}
    +
    + {{ opener('cat')}} + +
    + +
    + {% if data.subs %} + +
    + +
    + {% for sub in data.subs %} + {% set href = (url_for('market.browse.browse_sub', top_slug=data.slug, sub_slug=sub.slug) ~qs)|host%} + {% if top_slug==(data.slug | lower) and sub_slug == sub.slug %} + +
    {{ sub.html_label or sub.name }}
    +
    {{ sub.count }}
    +
    + {% endif %} + {% endfor %} + {% for sub in data.subs %} + {% if not (top_slug==(data.slug | lower) and sub_slug == sub.slug) %} + {% set href = (url_for('market.browse.browse_sub', top_slug=data.slug, sub_slug=sub.slug) ~ qs)|host%} + +
    {{ sub.name }}
    +
    {{ sub.count }}
    +
    + {% endif %} + {% endfor %} +
    +
    + {% else %} + {% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%} + View all + {% endif %} +
    +
    + {% endfor %} + {% include '_types/market/_admin.html' %} +
    +
    diff --git a/market/templates/_types/market/mobile/menu.html b/market/templates/_types/market/mobile/menu.html new file mode 100644 index 0000000..145b551 --- /dev/null +++ b/market/templates/_types/market/mobile/menu.html @@ -0,0 +1,6 @@ +{% extends 'mobile/menu.html' %} +{% block menu %} + {% block mobile_menu %} + {% endblock %} + {% include '_types/market/mobile/_nav_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/page_markets/_card.html b/market/templates/_types/page_markets/_card.html new file mode 100644 index 0000000..19e31af --- /dev/null +++ b/market/templates/_types/page_markets/_card.html @@ -0,0 +1,13 @@ +{# Card for a single market in a page-scoped listing #} +{% set market_href = market_url('/' ~ post.slug ~ '/' ~ market.slug ~ '/') %} + diff --git a/market/templates/_types/page_markets/_cards.html b/market/templates/_types/page_markets/_cards.html new file mode 100644 index 0000000..bcce864 --- /dev/null +++ b/market/templates/_types/page_markets/_cards.html @@ -0,0 +1,18 @@ +{% for market in markets %} + {% include "_types/page_markets/_card.html" %} +{% endfor %} +{% if has_more %} + {# Infinite scroll sentinel #} + {% set next_url = url_for('page_markets.markets_fragment', page=page + 1)|host %} + +{% endif %} diff --git a/market/templates/_types/page_markets/_main_panel.html b/market/templates/_types/page_markets/_main_panel.html new file mode 100644 index 0000000..c01cfb2 --- /dev/null +++ b/market/templates/_types/page_markets/_main_panel.html @@ -0,0 +1,12 @@ +{# Markets grid for a single page #} +{% if markets %} +
    + {% include "_types/page_markets/_cards.html" %} +
    +{% else %} +
    + +

    No markets for this page

    +
    +{% endif %} +
    diff --git a/market/templates/_types/page_markets/index.html b/market/templates/_types/page_markets/index.html new file mode 100644 index 0000000..23f99a1 --- /dev/null +++ b/market/templates/_types/page_markets/index.html @@ -0,0 +1,15 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% block post_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/page_markets/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/post/_nav.html b/market/templates/_types/post/_nav.html new file mode 100644 index 0000000..037bdcd --- /dev/null +++ b/market/templates/_types/post/_nav.html @@ -0,0 +1,15 @@ +{% import 'macros/links.html' as links %} + {# Widget-driven container nav — entries, calendars, markets #} + {% if container_nav_widgets %} +
    + {% include '_types/post/admin/_nav_entries.html' %} +
    + {% endif %} + + {# Admin link #} + {% if post and has_access('blog.post.admin.admin') %} + {% call links.link(url_for('blog.post.admin.admin', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} + + {% endcall %} + {% endif %} diff --git a/market/templates/_types/post/admin/_nav_entries.html b/market/templates/_types/post/admin/_nav_entries.html new file mode 100644 index 0000000..47290d4 --- /dev/null +++ b/market/templates/_types/post/admin/_nav_entries.html @@ -0,0 +1,50 @@ + + {# Left scroll arrow - desktop only #} + + + {# Widget-driven nav items container #} +
    +
    + {% for wdata in container_nav_widgets %} + {% with ctx=wdata.ctx %} + {% include wdata.widget.template with context %} + {% endwith %} + {% endfor %} +
    +
    + + + + {# Right scroll arrow - desktop only #} + diff --git a/market/templates/_types/post/header/_header.html b/market/templates/_types/post/header/_header.html new file mode 100644 index 0000000..6655eb5 --- /dev/null +++ b/market/templates/_types/post/header/_header.html @@ -0,0 +1,28 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post-row', oob=oob) %} + {% call links.link(blog_url('/' + post.slug + '/'), hx_select_search ) %} + {% if post.feature_image %} + + {% endif %} + + {{ post.title | truncate(160, True, '…') }} + + {% endcall %} + {% call links.desktop_nav() %} + {% if page_cart_count is defined and page_cart_count > 0 %} + + + {{ page_cart_count }} + + {% endif %} + {% include '_types/post/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/market/templates/_types/product/_added.html b/market/templates/_types/product/_added.html new file mode 100644 index 0000000..251387a --- /dev/null +++ b/market/templates/_types/product/_added.html @@ -0,0 +1,17 @@ +{# HTMX response after add-to-cart: OOB-swap the mini cart + product buttons #} +{% import '_types/product/_cart.html' as _cart %} + +{# 1. Update mini cart directly — handler already has the cart data #} +{% from 'macros/cart_icon.html' import cart_icon %} +{{ cart_icon(count=cart | sum(attribute="quantity")) }} + +{# 2. Update add/remove buttons on the product #} +{{ _cart.add(d.slug, cart, oob='true') }} + +{# 3. Update cart item row if visible #} +{% from '_types/product/_cart.html' import cart_item with context %} +{% if item and item.quantity > 0 %} + {{ cart_item(oob='true') }} +{% elif item %} + {{ cart_item(oob='delete') }} +{% endif %} diff --git a/market/templates/_types/product/_cart.html b/market/templates/_types/product/_cart.html new file mode 100644 index 0000000..2c68284 --- /dev/null +++ b/market/templates/_types/product/_cart.html @@ -0,0 +1,250 @@ +{% macro add(slug, cart, oob='false') %} +{% set quantity = cart + | selectattr('product.slug', 'equalto', slug) + | sum(attribute='quantity') %} + +
    + + {% if not quantity %} +
    + + + + +
    + + {% else %} +
    + +
    + + + +
    + + + + + + + + + {{ quantity }} + + + + + + +
    + + + +
    +
    + {% endif %} +
    +{% endmacro %} + + + +{% macro cart_item(oob=False) %} + +{% set p = item.product %} +{% set unit_price = p.special_price or p.regular_price %} +
    +
    + {% if p.image %} + {{ p.title }} + {% else %} +
    + No image +
    'market', 'product', p.slug + {% endif %} +
    + + {# Details #} +
    +
    +
    +

    + {% set href=url_for('market.browse.product.product_detail', product_slug=p.slug) %} + + {{ p.title }} + +

    + + {% if p.brand %} +

    + {{ p.brand }} +

    + {% endif %} + + {% if item.is_deleted %} +

    + + This item is no longer available or price has changed +

    + {% endif %} +
    + + {# Unit price #} +
    + {% if unit_price %} + {% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %} +

    + {{ symbol }}{{ "%.2f"|format(unit_price) }} +

    + {% if p.special_price and p.special_price != p.regular_price %} +

    + {{ symbol }}{{ "%.2f"|format(p.regular_price) }} +

    + {% endif %} + {% else %} +

    No price

    + {% endif %} +
    +
    + +
    +
    + Quantity +
    + + + +
    + + {{ item.quantity }} + +
    + + + +
    +
    + +
    + {% if unit_price %} + {% set line_total = unit_price * item.quantity %} + {% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %} +

    + Line total: + {{ symbol }}{{ "%.2f"|format(line_total) }} +

    + {% endif %} +
    +
    +
    +
    + +{% endmacro %} diff --git a/market/templates/_types/product/_main_panel.html b/market/templates/_types/product/_main_panel.html new file mode 100644 index 0000000..cf8df31 --- /dev/null +++ b/market/templates/_types/product/_main_panel.html @@ -0,0 +1,131 @@ +{# Main panel fragment for HTMX navigation - product detail content #} +{% import 'macros/stickers.html' as stick %} +{% import '_types/product/prices.html' as prices %} +{% set prices_ns = namespace() %} +{{ prices.set_prices(d, prices_ns)}} + + {# Product detail grid from content block #} +
    +
    + {% if d.images and d.images|length > 0 %} +
    + {# --- like button overlay in top-right --- #} + {% if g.user %} +
    + {% set slug = d.slug %} + {% set liked = liked_by_current_user %} + {% include "_types/browse/like/button.html" %} +
    + {% endif %} + +
    +
    + {{ d.title }} + + {% for l in d.labels %} + + {% endfor %} +
    +
    + {{ d.brand }} +
    +
    + + {% if d.images|length > 1 %} + + + {% endif %} +
    + +
    +
    + {% for u in d.images %} + + + {% endfor %} +
    +
    + {% else %} +
    + {# Even if no image, still render the like button in the corner for consistency #} + {% if g.user %} +
    + {% set slug = d.slug %} + {% set liked = liked_by_current_user %} + {% include "_types/browse/like/button.html" %} +
    + {% endif %} + + No image +
    + {% endif %} + +
    + {% for s in d.stickers %} + {{ stick.sticker(asset_url('stickers/' + s + '.svg'), s, True, size=40) }} + {% endfor %} +
    +
    + +
    + {# Optional extras shown quietly #} +
    + {% if d.price_per_unit or d.price_per_unit_raw %} +
    Unit price: {{ prices.price_str(d.price_per_unit, d.price_per_unit_raw, d.price_per_unit_currency) }}
    + {% endif %} + {% if d.case_size_raw %} +
    Case size: {{ d.case_size_raw }}
    + {% endif %} + +
    + + {% if d.description_short or d.description_html %} +
    + {% if d.description_short %} +

    {{ d.description_short }}

    + {% endif %} + {% if d.description_html %} +
    + {{ d.description_html | safe }} +
    + {% endif %} +
    + {% endif %} + + {% if d.sections and d.sections|length %} +
    + {% for sec in d.sections %} +
    + + {{ sec.title }} + + +
    + {{ sec.html | safe }} +
    +
    + {% endfor %} +
    + {% endif %} +
    + +
    +
    diff --git a/market/templates/_types/product/_meta.html b/market/templates/_types/product/_meta.html new file mode 100644 index 0000000..aebb684 --- /dev/null +++ b/market/templates/_types/product/_meta.html @@ -0,0 +1,106 @@ +{# --- social/meta_product.html --- #} +{# Context expected: + site, d (Product), request +#} + +{# Visibility → robots: index unless soft-deleted #} +{% set robots_here = 'noindex,nofollow' if d.deleted_at else 'index,follow' %} + +{# Compute canonical #} +{% set _site_url = site().url.rstrip('/') if site and site().url else '' %} +{% set _product_path = request.path if request else ('/products/' ~ (d.slug or '')) %} +{% set canonical = _site_url ~ _product_path if _site_url else (request.url if request else None) %} + +{# Include common base (charset, viewport, robots default, RSS, Org/WebSite JSON-LD) #} +{% set robots_override = robots_here %} +{% include 'social/meta_base.html' %} + +{# ---- Titles / descriptions ---- #} +{% set base_product_title = d.title or base_title %} +{% set og_title = base_product_title %} +{% set tw_title = base_product_title %} + +{# Description: prefer short, then HTML stripped #} +{% set desc_source = d.description_short + or (d.description_html|striptags if d.description_html else '') %} +{% set description = (desc_source|trim|replace('\n',' ')|replace('\r',' ')|striptags)|truncate(160, True, '…') %} + +{# ---- Image priority: product image, then first gallery image, then site default ---- #} +{% set image_url = d.image + or ((d.images|first).url if d.images and (d.images|first).url else None) + or (site().default_image if site and site().default_image else None) %} + +{# ---- Price / offer helpers ---- #} +{% set price = d.special_price or d.regular_price or d.rrp %} +{% set price_currency = d.special_price_currency or d.regular_price_currency or d.rrp_currency %} + +{# ---- Basic meta ---- #} +{{ base_product_title }} + +{% if canonical %}{% endif %} + +{# ---- Open Graph ---- #} + + + + +{% if canonical %}{% endif %} +{% if image_url %}{% endif %} + +{# Optional product OG price tags #} +{% if price and price_currency %} + + +{% endif %} +{% if d.brand %} + +{% endif %} +{% if d.sku %} + +{% endif %} + +{# ---- Twitter ---- #} + +{% if site and site().twitter_site %}{% endif %} + + +{% if image_url %}{% endif %} + +{# ---- JSON-LD Product ---- #} +{% set jsonld = { + "@context": "https://schema.org", + "@type": "Product", + "name": d.title, + "image": image_url, + "description": description, + "sku": d.sku, + "brand": d.brand, + "url": canonical +} %} + +{# Brand as proper object if present #} +{% if d.brand %} + {% set jsonld = jsonld | combine({ + "brand": { + "@type": "Brand", + "name": d.brand + } + }) %} +{% endif %} + +{# Offers if price available #} +{% if price and price_currency %} + {% set jsonld = jsonld | combine({ + "offers": { + "@type": "Offer", + "price": price, + "priceCurrency": price_currency, + "url": canonical, + "availability": "https://schema.org/InStock" + } + }) %} +{% endif %} + + diff --git a/market/templates/_types/product/_oob_elements.html b/market/templates/_types/product/_oob_elements.html new file mode 100644 index 0000000..589d369 --- /dev/null +++ b/market/templates/_types/product/_oob_elements.html @@ -0,0 +1,49 @@ +{% extends 'oob_elements.html' %} +{# OOB elements for HTMX navigation - product extends browse so use similar structure #} +{% import 'macros/layout.html' as layout %} +{% import 'macros/stickers.html' as stick %} +{% import '_types/product/prices.html' as prices %} +{% set prices_ns = namespace() %} +{{ prices.set_prices(d, prices_ns)}} + +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + + +{% block oobs %} + {% from '_types/market/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('market-header-child', 'product-header-child', '_types/product/header/_header.html')}} + +{% endblock %} + + + +{% block mobile_menu %} + {% include '_types/market/mobile/_nav_panel.html' %} + {% include '_types/browse/_admin.html' %} +{% endblock %} + +{% block filter %} + {% call layout.details() %} + {% call layout.summary('blog-child-header') %} + {% endcall %} + {% call layout.menu('blog-child-menu') %} + {% endcall %} + {% endcall %} + + {% call layout.details() %} + {% call layout.summary('product-child-header') %} + {% endcall %} + {% call layout.menu('item-child-menu') %} + {% endcall %} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/product/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/product/_prices.html b/market/templates/_types/product/_prices.html new file mode 100644 index 0000000..e56339f --- /dev/null +++ b/market/templates/_types/product/_prices.html @@ -0,0 +1,33 @@ +{% import '_types/product/_cart.html' as _cart %} + {# ---- Price block ---- #} + {% import '_types/product/prices.html' as prices %} + {% set prices_ns = namespace() %} + {{ prices.set_prices(d, prices_ns)}} + +
    + {{ _cart.add(d.slug, cart)}} + + {% if prices_ns.sp_val %} +
    + Special price +
    +
    + {{ prices.price_str(prices_ns.sp_val, prices_ns.sp_raw, prices_ns.sp_cur) }} +
    + {% if prices_ns.sp_val and prices_ns.rp_val %} +
    + {{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }} +
    + {% endif %} + {% elif prices_ns.rp_val %} + +
    + {{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }} +
    + {% endif %} + {{ prices.rrp(prices_ns) }} + +
    + diff --git a/market/templates/_types/product/_title.html b/market/templates/_types/product/_title.html new file mode 100644 index 0000000..0b3be43 --- /dev/null +++ b/market/templates/_types/product/_title.html @@ -0,0 +1,2 @@ + +
    {{ d.title }}
    diff --git a/market/templates/_types/product/admin/_nav.html b/market/templates/_types/product/admin/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/market/templates/_types/product/admin/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/market/templates/_types/product/admin/_oob_elements.html b/market/templates/_types/product/admin/_oob_elements.html new file mode 100644 index 0000000..84acac6 --- /dev/null +++ b/market/templates/_types/product/admin/_oob_elements.html @@ -0,0 +1,40 @@ +{% extends 'oob_elements.html' %} + + +{# OOB elements for HTMX navigation - all elements that need updating #} +{# Import shared OOB macros #} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header('product-header-child', 'product-admin-header-child', '_types/product/admin/header/_header.html')}} + + {% from '_types/product/header/_header.html' import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{% from '_types/root/_n/macros.html' import header with context %} +{% call header(id='product-header-child', oob=True) %} + {% call header() %} + {% from '_types/product/admin/header/_header.html' import header_row with context %} + {{header_row()}} +
    + +
    + {% endcall %} +{% endcall %} + + +{% block mobile_menu %} + {% include '_types/product/admin/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include '_types/product/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/product/admin/header/_header.html b/market/templates/_types/product/admin/header/_header.html new file mode 100644 index 0000000..eacdf7d --- /dev/null +++ b/market/templates/_types/product/admin/header/_header.html @@ -0,0 +1,11 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='product-admin-row', oob=oob) %} + {% call links.link(url_for('market.browse.product.admin', product_slug=d.slug), hx_select_search ) %} + admin!! + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/product/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/market/templates/_types/product/admin/index.html b/market/templates/_types/product/admin/index.html new file mode 100644 index 0000000..d1cb714 --- /dev/null +++ b/market/templates/_types/product/admin/index.html @@ -0,0 +1,39 @@ +{% extends '_types/product/index.html' %} + +{% import 'macros/layout.html' as layout %} + +{% block product_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('market-header-child', '_types/product/admin/header/_header.html') %} + {% block product_admin_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + + +{% block ___app_title %} + {% import 'macros/links.html' as links %} + {% call links.menu_row() %} + {% call links.link(url_for('market.browse.product.admin', product_slug=slug), hx_select_search) %} + {{ links.admin() }} + {% endcall %} + {% call links.desktop_nav() %} + {% include '_types/product/admin/_nav.html' %} + {% endcall %} + {% endcall %} +{% endblock %} + + + +{% block _main_mobile_menu %} + {% include '_types/product/admin/_nav.html' %} +{% endblock %} + +{% block aside %} +{% endblock %} + + +{% block content %} +{% include '_types/product/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/product/header/_header.html b/market/templates/_types/product/header/_header.html new file mode 100644 index 0000000..6608fce --- /dev/null +++ b/market/templates/_types/product/header/_header.html @@ -0,0 +1,15 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='product-row', oob=oob) %} + {% call links.link(url_for('market.browse.product.product_detail', product_slug=d.slug), hx_select_search ) %} + {% include '_types/product/_title.html' %} + {% endcall %} + {% include '_types/product/_prices.html' %} + {% call links.desktop_nav() %} + {% include '_types/browse/_admin.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} + + + diff --git a/market/templates/_types/product/index.html b/market/templates/_types/product/index.html new file mode 100644 index 0000000..31ccd88 --- /dev/null +++ b/market/templates/_types/product/index.html @@ -0,0 +1,61 @@ +{% extends '_types/browse/index.html' %} + +{% block meta %} + {% include '_types/product/_meta.html' %} +{% endblock %} + + +{% import 'macros/stickers.html' as stick %} +{% import '_types/product/prices.html' as prices %} +{% set prices_ns = namespace() %} +{{ prices.set_prices(d, prices_ns)}} + + + +{% block market_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('market-header-child', '_types/product/header/_header.html') %} + {% block product_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + + +{% block _main_mobile_menu %} + {% include '_types/browse/_admin.html' %} +{% endblock %} + + + +{% block filter %} + +{% call layout.details() %} + {% call layout.summary('blog-child-header') %} + {% block blog_child_summary %} + {% endblock %} + {% endcall %} + {% call layout.menu('blog-child-menu') %} + {% block post_child_menu %} + {% endblock %} + {% endcall %} + {% endcall %} + + {% call layout.details() %} + {% call layout.summary('product-child-header') %} + {% block item_child_summary %} + {% endblock %} + {% endcall %} + {% call layout.menu('item-child-menu') %} + {% block item_child_menu %} + {% endblock %} + {% endcall %} + {% endcall %} + +{% endblock %} + +{% block aside %} +{% endblock %} + +{% block content %} + {% include '_types/product/_main_panel.html' %} +{% endblock %} diff --git a/market/templates/_types/product/prices.html b/market/templates/_types/product/prices.html new file mode 100644 index 0000000..be9cc4c --- /dev/null +++ b/market/templates/_types/product/prices.html @@ -0,0 +1,66 @@ +{# ---- Price formatting helpers ---- #} +{% set _sym = {'GBP':'£','EUR':'€','USD':'$'} %} +{% macro price_str(val, raw, cur) -%} + {%- if raw -%} + {{ raw }} + {%- elif val is number -%} + {{ (_sym.get(cur) or '') ~ ('%.2f'|format(val)) }} + {%- else -%} + {{ val or '' }} + {%- endif -%} +{%- endmacro %} + + +{% macro set_prices(item, ns) -%} + +{% set ns.sp_val = item.special_price or (item.oe_list_price and item.oe_list_price.special) %} +{% set ns.sp_raw = item.special_price_raw or (item.oe_list_price and item.oe_list_price.special_raw) %} +{% set ns.sp_cur = item.special_price_currency or (item.oe_list_price and item.oe_list_price.special_currency) %} + +{% set ns.rp_val = item.regular_price or item.rrp or (item.oe_list_price and item.oe_list_price.rrp) %} +{% set ns.rp_raw = item.regular_price_raw or item.rrp_raw or (item.oe_list_price and item.oe_list_price.rrp_raw) %} +{% set ns.rp_cur = item.regular_price_currency or item.rrp_currency or (item.oe_list_price and item.oe_list_price.rrp_currency) %} + +{% set ns.case_size_count = (item.case_size_count or 1) %} +{% set ns.rrp = item.rrp_raw[0] ~ "%.2f"|format(item.rrp * (ns.case_size_count)) %} +{% set ns.rrp_raw = item.rrp_raw %} + +{%- endmacro %} + + +{% macro rrp(ns) -%} + {% if ns.rrp %} +
    + rrp: + + {{ ns.rrp }} + +
    + {% endif %} +{%- endmacro %} + + +{% macro card_price(item) %} + + +{# price block unchanged #} + {% set _sym = {'GBP':'£','EUR':'€','USD':'$'} %} + {% set sp_val = item.special_price or (item.oe_list_price and item.oe_list_price.special) %} + {% set sp_raw = item.special_price_raw or (item.oe_list_price and item.oe_list_price.special_raw) %} + {% set sp_cur = item.special_price_currency or (item.oe_list_price and item.oe_list_price.special_currency) %} + {% set rp_val = item.regular_price or item.rrp or (item.oe_list_price and item.oe_list_price.rrp) %} + {% set rp_raw = item.regular_price_raw or item.rrp_raw or (item.oe_list_price and item.oe_list_price.rrp_raw) %} + {% set rp_cur = item.regular_price_currency or item.rrp_currency or (item.oe_list_price and item.oe_list_price.rrp_currency) %} + {% set sp_str = sp_raw if sp_raw else ( (_sym.get(sp_cur, '') ~ ('%.2f'|format(sp_val))) if sp_val is number else (sp_val or '')) %} + {% set rp_str = rp_raw if rp_raw else ( (_sym.get(rp_cur, '') ~ ('%.2f'|format(rp_val))) if rp_val is number else (rp_val or '')) %} +
    + {% if sp_val %} +
    {{ sp_str }}
    + {% if rp_val %} +
    {{ rp_str }}
    + {% endif %} + {% elif rp_val %} +
    {{ rp_str }}
    + {% endif %} +
    +{% endmacro %} diff --git a/market/templates/aside_clear.html b/market/templates/aside_clear.html new file mode 100644 index 0000000..e091ac2 --- /dev/null +++ b/market/templates/aside_clear.html @@ -0,0 +1,7 @@ + + diff --git a/market/templates/filter_clear.html b/market/templates/filter_clear.html new file mode 100644 index 0000000..fc3901e --- /dev/null +++ b/market/templates/filter_clear.html @@ -0,0 +1,5 @@ +
    +
    diff --git a/market/templates/fragments/container_nav_markets.html b/market/templates/fragments/container_nav_markets.html new file mode 100644 index 0000000..3c8814d --- /dev/null +++ b/market/templates/fragments/container_nav_markets.html @@ -0,0 +1,9 @@ +{# Market links nav — served as fragment from market app #} +{% for m in markets %} + + +
    {{m.name}}
    +
    +{% endfor %} diff --git a/market/templates/macros/filters.html b/market/templates/macros/filters.html new file mode 100644 index 0000000..8d13887 --- /dev/null +++ b/market/templates/macros/filters.html @@ -0,0 +1,117 @@ +{# + Unified filter macros for browse/shop pages + Consolidates duplicate mobile/desktop filter components +#} + +{% macro filter_item(href, is_on, title, icon_html, count=none, variant='desktop') %} + {# + Generic filter item (works for labels, stickers, etc.) + variant: 'desktop' or 'mobile' + #} + {% set base_class = "flex flex-col items-center justify-center" %} + {% if variant == 'mobile' %} + {% set item_class = base_class ~ " p-1 rounded hover:bg-stone-50" %} + {% set count_class = "text-[10px] text-stone-500 mt-1 leading-none tabular-nums" if count != 0 else "text-md text-red-500 font-bold mt-1 leading-none tabular-nums" %} + {% else %} + {% set item_class = base_class ~ " py-2 w-full h-full" %} + {% set count_class = "text-xs text-stone-500 leading-none justify-self-end tabular-nums" if count != 0 else "text-md text-red-500 font-bold leading-none justify-self-end tabular-nums" %} + {% endif %} + + + {{ icon_html | safe }} + {% if count is not none %} + {{ count }} + {% endif %} + +{% endmacro %} + + +{% macro labels_list(labels, selected_labels, current_local_href, variant='desktop') %} + {# + Unified labels filter list + variant: 'desktop' or 'mobile' + #} + {% import 'macros/stickers.html' as stick %} + + {% if variant == 'mobile' %} + + {% endif %} +{% endmacro %} + + +{% macro stickers_list(stickers, selected_stickers, current_local_href, variant='desktop') %} + {# + Unified stickers filter list + variant: 'desktop' or 'mobile' + #} + {% import 'macros/stickers.html' as stick %} + + {% if variant == 'mobile' %} + + + {% endif %} +{% endmacro %} + + diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..68992ac --- /dev/null +++ b/schema.sql @@ -0,0 +1,2741 @@ +-- +-- PostgreSQL database dump +-- + + +-- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) +-- Dumped by pg_dump version 16.10 (Ubuntu 16.10-1.pgdg22.04+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: alembic_version; Type: TABLE; Schema: public; Owner: postgres +-- + +-- +-- Name: authors; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.authors ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + slug character varying(191) NOT NULL, + name character varying(255) NOT NULL, + profile_image text, + cover_image text, + bio text, + website text, + location text, + facebook text, + twitter text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.authors OWNER TO postgres; + +-- +-- Name: authors_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.authors_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.authors_id_seq OWNER TO postgres; + +-- +-- Name: authors_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.authors_id_seq OWNED BY public.authors.id; + + +-- +-- Name: calendar_entries; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.calendar_entries ( + id integer NOT NULL, + calendar_id integer NOT NULL, + name character varying(255) NOT NULL, + start_at timestamp with time zone NOT NULL, + end_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + CONSTRAINT ck_calendar_entries_end_after_start CHECK (((end_at IS NULL) OR (end_at >= start_at))) +); + + +ALTER TABLE public.calendar_entries OWNER TO postgres; + +-- +-- Name: calendar_entries_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.calendar_entries_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.calendar_entries_id_seq OWNER TO postgres; + +-- +-- Name: calendar_entries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.calendar_entries_id_seq OWNED BY public.calendar_entries.id; + + +-- +-- Name: calendar_slots; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.calendar_slots ( + id integer NOT NULL, + calendar_id integer NOT NULL, + name character varying(255) NOT NULL, + description text, + mon boolean DEFAULT false NOT NULL, + tue boolean DEFAULT false NOT NULL, + wed boolean DEFAULT false NOT NULL, + thu boolean DEFAULT false NOT NULL, + fri boolean DEFAULT false NOT NULL, + sat boolean DEFAULT false NOT NULL, + sun boolean DEFAULT false NOT NULL, + time_start time without time zone NOT NULL, + time_end time without time zone NOT NULL, + cost numeric(10,2), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + CONSTRAINT ck_calendar_slots_time_end_after_start CHECK ((time_end > time_start)) +); + + +ALTER TABLE public.calendar_slots OWNER TO postgres; + +-- +-- Name: calendar_slots_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.calendar_slots_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.calendar_slots_id_seq OWNER TO postgres; + +-- +-- Name: calendar_slots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.calendar_slots_id_seq OWNED BY public.calendar_slots.id; + + +-- +-- Name: calendars; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.calendars ( + id integer NOT NULL, + post_id integer NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + description text +); + + +ALTER TABLE public.calendars OWNER TO postgres; + +-- +-- Name: calendars_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.calendars_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.calendars_id_seq OWNER TO postgres; + +-- +-- Name: calendars_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.calendars_id_seq OWNED BY public.calendars.id; + + +-- +-- Name: ghost_labels; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.ghost_labels ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.ghost_labels OWNER TO postgres; + +-- +-- Name: ghost_labels_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.ghost_labels_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.ghost_labels_id_seq OWNER TO postgres; + +-- +-- Name: ghost_labels_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.ghost_labels_id_seq OWNED BY public.ghost_labels.id; + + +-- +-- Name: ghost_newsletters; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.ghost_newsletters ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255), + description text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.ghost_newsletters OWNER TO postgres; + +-- +-- Name: ghost_newsletters_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.ghost_newsletters_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.ghost_newsletters_id_seq OWNER TO postgres; + +-- +-- Name: ghost_newsletters_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.ghost_newsletters_id_seq OWNED BY public.ghost_newsletters.id; + + +-- +-- Name: ghost_subscriptions; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.ghost_subscriptions ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + user_id integer NOT NULL, + status character varying(50), + tier_id integer, + cadence character varying(50), + price_amount integer, + price_currency character varying(10), + stripe_customer_id character varying(255), + stripe_subscription_id character varying(255), + raw jsonb +); + + +ALTER TABLE public.ghost_subscriptions OWNER TO postgres; + +-- +-- Name: ghost_subscriptions_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.ghost_subscriptions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.ghost_subscriptions_id_seq OWNER TO postgres; + +-- +-- Name: ghost_subscriptions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.ghost_subscriptions_id_seq OWNED BY public.ghost_subscriptions.id; + + +-- +-- Name: ghost_tiers; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.ghost_tiers ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255), + type character varying(50), + visibility character varying(50) +); + + +ALTER TABLE public.ghost_tiers OWNER TO postgres; + +-- +-- Name: ghost_tiers_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.ghost_tiers_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.ghost_tiers_id_seq OWNER TO postgres; + +-- +-- Name: ghost_tiers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.ghost_tiers_id_seq OWNED BY public.ghost_tiers.id; + + +-- +-- Name: kv; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.kv ( + key character varying(120) NOT NULL, + value text, + updated_at timestamp with time zone NOT NULL +); + + +ALTER TABLE public.kv OWNER TO postgres; + +-- +-- Name: link_errors; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.link_errors ( + id integer NOT NULL, + product_slug character varying(255), + href text, + text text, + top character varying(255), + sub character varying(255), + target_slug character varying(255), + type character varying(255), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.link_errors OWNER TO postgres; + +-- +-- Name: link_errors_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.link_errors_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.link_errors_id_seq OWNER TO postgres; + +-- +-- Name: link_errors_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.link_errors_id_seq OWNED BY public.link_errors.id; + + +-- +-- Name: link_externals; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.link_externals ( + id integer NOT NULL, + product_slug character varying(255), + href text, + text text, + host character varying(255), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone + +); + + +ALTER TABLE public.link_externals OWNER TO postgres; + +-- +-- Name: link_externals_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.link_externals_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.link_externals_id_seq OWNER TO postgres; + +-- +-- Name: link_externals_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.link_externals_id_seq OWNED BY public.link_externals.id; + + +-- +-- Name: listing_items; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.listing_items ( + id integer NOT NULL, + listing_id integer NOT NULL, + slug character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.listing_items OWNER TO postgres; + +-- +-- Name: listing_items_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.listing_items_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.listing_items_id_seq OWNER TO postgres; + +-- +-- Name: listing_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.listing_items_id_seq OWNED BY public.listing_items.id; + + +-- +-- Name: listings; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.listings ( + id integer NOT NULL, + total_pages integer, + top_id integer NOT NULL, + sub_id integer, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.listings OWNER TO postgres; + +-- +-- Name: listings_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.listings_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.listings_id_seq OWNER TO postgres; + +-- +-- Name: listings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.listings_id_seq OWNED BY public.listings.id; + + +-- +-- Name: magic_links; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.magic_links ( + id integer NOT NULL, + token character varying(128) NOT NULL, + user_id integer NOT NULL, + purpose character varying(32) NOT NULL, + expires_at timestamp with time zone NOT NULL, + used_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + ip character varying(64), + user_agent character varying(256) +); + + +ALTER TABLE public.magic_links OWNER TO postgres; + +-- +-- Name: magic_links_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.magic_links_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.magic_links_id_seq OWNER TO postgres; + +-- +-- Name: magic_links_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.magic_links_id_seq OWNED BY public.magic_links.id; + + +-- +-- Name: nav_subs; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.nav_subs ( + id integer NOT NULL, + top_id integer NOT NULL, + label character varying(255), + slug character varying(255) NOT NULL, + href text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.nav_subs OWNER TO postgres; + +-- +-- Name: nav_subs_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.nav_subs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.nav_subs_id_seq OWNER TO postgres; + +-- +-- Name: nav_subs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.nav_subs_id_seq OWNED BY public.nav_subs.id; + + +-- +-- Name: nav_tops; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.nav_tops ( + id integer NOT NULL, + label character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.nav_tops OWNER TO postgres; + +-- +-- Name: nav_tops_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.nav_tops_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.nav_tops_id_seq OWNER TO postgres; + +-- +-- Name: nav_tops_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.nav_tops_id_seq OWNED BY public.nav_tops.id; + + +-- +-- Name: post_authors; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.post_authors ( + post_id integer NOT NULL, + author_id integer NOT NULL, + sort_order integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.post_authors OWNER TO postgres; + +-- +-- Name: post_tags; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.post_tags ( + post_id integer NOT NULL, + tag_id integer NOT NULL, + sort_order integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.post_tags OWNER TO postgres; + +-- +-- Name: posts; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.posts ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + uuid character varying(64) NOT NULL, + slug character varying(191) NOT NULL, + title character varying(500) NOT NULL, + html text, + plaintext text, + mobiledoc text, + lexical text, + feature_image text, + feature_image_alt text, + feature_image_caption text, + excerpt text, + custom_excerpt text, + visibility character varying(32) DEFAULT 'public'::character varying NOT NULL, + status character varying(32) DEFAULT 'draft'::character varying NOT NULL, + featured boolean DEFAULT false NOT NULL, + is_page boolean DEFAULT false NOT NULL, + email_only boolean DEFAULT false NOT NULL, + canonical_url text, + meta_title character varying(500), + meta_description text, + og_image text, + og_title character varying(500), + og_description text, + twitter_image text, + twitter_title character varying(500), + twitter_description text, + custom_template character varying(191), + reading_time integer, + comment_id character varying(191), + published_at timestamp with time zone, + updated_at timestamp with time zone, + created_at timestamp with time zone, + deleted_at timestamp with time zone, + primary_author_id integer, + primary_tag_id integer +); + + +ALTER TABLE public.posts OWNER TO postgres; + +-- +-- Name: posts_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.posts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.posts_id_seq OWNER TO postgres; + +-- +-- Name: posts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.posts_id_seq OWNED BY public.posts.id; + + +-- +-- Name: product_allergens; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_allergens ( + id integer NOT NULL, + product_id integer NOT NULL, + name character varying(255) NOT NULL, + contains boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_allergens OWNER TO postgres; + +-- +-- Name: product_allergens_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_allergens_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_allergens_id_seq OWNER TO postgres; + +-- +-- Name: product_allergens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_allergens_id_seq OWNED BY public.product_allergens.id; + + +-- +-- Name: product_attributes; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_attributes ( + id integer NOT NULL, + product_id integer NOT NULL, + key character varying(255) NOT NULL, + value text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_attributes OWNER TO postgres; + +-- +-- Name: product_attributes_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_attributes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_attributes_id_seq OWNER TO postgres; + +-- +-- Name: product_attributes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_attributes_id_seq OWNED BY public.product_attributes.id; + + +-- +-- Name: product_images; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_images ( + id integer NOT NULL, + product_id integer NOT NULL, + url text NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + kind character varying(16) DEFAULT 'gallery'::character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + CONSTRAINT ck_product_images_position_nonneg CHECK (("position" >= 0)) +); + + +ALTER TABLE public.product_images OWNER TO postgres; + +-- +-- Name: product_images_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_images_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_images_id_seq OWNER TO postgres; + +-- +-- Name: product_images_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_images_id_seq OWNED BY public.product_images.id; + + +-- +-- Name: product_labels; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_labels ( + id integer NOT NULL, + product_id integer NOT NULL, + name character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_labels OWNER TO postgres; + +-- +-- Name: product_labels_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_labels_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_labels_id_seq OWNER TO postgres; + +-- +-- Name: product_labels_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_labels_id_seq OWNED BY public.product_labels.id; + + +-- +-- Name: product_likes; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_likes ( + user_id integer NOT NULL, + id integer NOT NULL, + product_slug character varying(255), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_likes OWNER TO postgres; + +-- +-- Name: product_likes_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_likes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_likes_id_seq OWNER TO postgres; + +-- +-- Name: product_likes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_likes_id_seq OWNED BY public.product_likes.id; + + +-- +-- Name: product_logs; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_logs ( + id integer NOT NULL, + slug character varying(255), + href_tried text, + ok boolean DEFAULT false NOT NULL, + error_type character varying(255), + error_message text, + http_status integer, + final_url text, + transport_error boolean, + title character varying(512), + has_description_html boolean, + has_description_short boolean, + sections_count integer, + images_count integer, + embedded_images_count integer, + all_images_count integer, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.product_logs OWNER TO postgres; + +-- +-- Name: product_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_logs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_logs_id_seq OWNER TO postgres; + +-- +-- Name: product_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_logs_id_seq OWNED BY public.product_logs.id; + + +-- +-- Name: product_nutrition; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_nutrition ( + id integer NOT NULL, + product_id integer NOT NULL, + key character varying(255) NOT NULL, + value character varying(255), + unit character varying(64), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_nutrition OWNER TO postgres; + +-- +-- Name: product_nutrition_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_nutrition_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_nutrition_id_seq OWNER TO postgres; + +-- +-- Name: product_nutrition_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_nutrition_id_seq OWNED BY public.product_nutrition.id; + + +-- +-- Name: product_sections; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_sections ( + id integer NOT NULL, + product_id integer NOT NULL, + title character varying(255) NOT NULL, + html text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_sections OWNER TO postgres; + +-- +-- Name: product_sections_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_sections_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_sections_id_seq OWNER TO postgres; + +-- +-- Name: product_sections_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_sections_id_seq OWNED BY public.product_sections.id; + + +-- +-- Name: product_stickers; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.product_stickers ( + id integer NOT NULL, + product_id integer NOT NULL, + name character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.product_stickers OWNER TO postgres; + +-- +-- Name: product_stickers_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.product_stickers_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.product_stickers_id_seq OWNER TO postgres; + +-- +-- Name: product_stickers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.product_stickers_id_seq OWNED BY public.product_stickers.id; + + +-- +-- Name: products; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.products ( + id integer NOT NULL, + slug character varying(255) NOT NULL, + title character varying(512), + image text, + description_short text, + description_html text, + suma_href text, + brand character varying(255), + rrp numeric(12,2), + rrp_currency character varying(16), + rrp_raw character varying(128), + price_per_unit numeric(12,4), + price_per_unit_currency character varying(16), + price_per_unit_raw character varying(128), + special_price numeric(12,2), + special_price_currency character varying(16), + special_price_raw character varying(128), + case_size_count integer, + case_size_item_qty numeric(12,3), + case_size_item_unit character varying(32), + case_size_raw character varying(128), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + ean character varying(64), + sku character varying(128), + unit_size character varying(128), + pack_size character varying(128), + regular_price numeric(12,2), + regular_price_currency character varying(16), + regular_price_raw character varying(128), + oe_list_price numeric(12,2) +); + + +ALTER TABLE public.products OWNER TO postgres; + +-- +-- Name: products_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.products_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.products_id_seq OWNER TO postgres; + +-- +-- Name: products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id; + + +-- +-- Name: subcategory_redirects; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.subcategory_redirects ( + id integer NOT NULL, + old_path character varying(512) NOT NULL, + new_path character varying(512) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.subcategory_redirects OWNER TO postgres; + +-- +-- Name: subcategory_redirects_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.subcategory_redirects_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.subcategory_redirects_id_seq OWNER TO postgres; + +-- +-- Name: subcategory_redirects_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.subcategory_redirects_id_seq OWNED BY public.subcategory_redirects.id; + + +-- +-- Name: tags; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.tags ( + id integer NOT NULL, + ghost_id character varying(64) NOT NULL, + slug character varying(191) NOT NULL, + name character varying(255) NOT NULL, + description text, + visibility character varying(32) DEFAULT 'public'::character varying NOT NULL, + feature_image text, + meta_title character varying(300), + meta_description text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.tags OWNER TO postgres; + +-- +-- Name: tags_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.tags_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.tags_id_seq OWNER TO postgres; + +-- +-- Name: tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.tags_id_seq OWNED BY public.tags.id; + + +-- +-- Name: user_labels; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.user_labels ( + id integer NOT NULL, + user_id integer NOT NULL, + label_id integer NOT NULL +); + + +ALTER TABLE public.user_labels OWNER TO postgres; + +-- +-- Name: user_labels_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.user_labels_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.user_labels_id_seq OWNER TO postgres; + +-- +-- Name: user_labels_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.user_labels_id_seq OWNED BY public.user_labels.id; + + +-- +-- Name: user_newsletters; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.user_newsletters ( + id integer NOT NULL, + user_id integer NOT NULL, + newsletter_id integer NOT NULL, + subscribed boolean DEFAULT true NOT NULL +); + + +ALTER TABLE public.user_newsletters OWNER TO postgres; + +-- +-- Name: user_newsletters_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.user_newsletters_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.user_newsletters_id_seq OWNER TO postgres; + +-- +-- Name: user_newsletters_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.user_newsletters_id_seq OWNED BY public.user_newsletters.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + email character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + last_login_at timestamp with time zone, + ghost_id character varying(64), + name character varying(255), + ghost_status character varying(50), + ghost_subscribed boolean DEFAULT true NOT NULL, + ghost_note text, + avatar_image text, + stripe_customer_id character varying(255), + ghost_raw jsonb +); + + +ALTER TABLE public.users OWNER TO postgres; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.users_id_seq OWNER TO postgres; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: authors id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.authors ALTER COLUMN id SET DEFAULT nextval('public.authors_id_seq'::regclass); + + +-- +-- Name: calendar_entries id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_entries ALTER COLUMN id SET DEFAULT nextval('public.calendar_entries_id_seq'::regclass); + + +-- +-- Name: calendar_slots id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_slots ALTER COLUMN id SET DEFAULT nextval('public.calendar_slots_id_seq'::regclass); + + +-- +-- Name: calendars id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendars ALTER COLUMN id SET DEFAULT nextval('public.calendars_id_seq'::regclass); + + +-- +-- Name: ghost_labels id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_labels ALTER COLUMN id SET DEFAULT nextval('public.ghost_labels_id_seq'::regclass); + + +-- +-- Name: ghost_newsletters id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_newsletters ALTER COLUMN id SET DEFAULT nextval('public.ghost_newsletters_id_seq'::regclass); + + +-- +-- Name: ghost_subscriptions id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_subscriptions ALTER COLUMN id SET DEFAULT nextval('public.ghost_subscriptions_id_seq'::regclass); + + +-- +-- Name: ghost_tiers id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_tiers ALTER COLUMN id SET DEFAULT nextval('public.ghost_tiers_id_seq'::regclass); + + +-- +-- Name: link_errors id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.link_errors ALTER COLUMN id SET DEFAULT nextval('public.link_errors_id_seq'::regclass); + + +-- +-- Name: link_externals id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.link_externals ALTER COLUMN id SET DEFAULT nextval('public.link_externals_id_seq'::regclass); + + +-- +-- Name: listing_items id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listing_items ALTER COLUMN id SET DEFAULT nextval('public.listing_items_id_seq'::regclass); + + +-- +-- Name: listings id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listings ALTER COLUMN id SET DEFAULT nextval('public.listings_id_seq'::regclass); + + +-- +-- Name: magic_links id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.magic_links ALTER COLUMN id SET DEFAULT nextval('public.magic_links_id_seq'::regclass); + + +-- +-- Name: nav_subs id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_subs ALTER COLUMN id SET DEFAULT nextval('public.nav_subs_id_seq'::regclass); + + +-- +-- Name: nav_tops id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_tops ALTER COLUMN id SET DEFAULT nextval('public.nav_tops_id_seq'::regclass); + + +-- +-- Name: posts id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.posts ALTER COLUMN id SET DEFAULT nextval('public.posts_id_seq'::regclass); + + +-- +-- Name: product_allergens id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_allergens ALTER COLUMN id SET DEFAULT nextval('public.product_allergens_id_seq'::regclass); + + +-- +-- Name: product_attributes id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_attributes ALTER COLUMN id SET DEFAULT nextval('public.product_attributes_id_seq'::regclass); + + +-- +-- Name: product_images id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_images ALTER COLUMN id SET DEFAULT nextval('public.product_images_id_seq'::regclass); + + +-- +-- Name: product_labels id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_labels ALTER COLUMN id SET DEFAULT nextval('public.product_labels_id_seq'::regclass); + + +-- +-- Name: product_likes id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_likes ALTER COLUMN id SET DEFAULT nextval('public.product_likes_id_seq'::regclass); + + +-- +-- Name: product_logs id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_logs ALTER COLUMN id SET DEFAULT nextval('public.product_logs_id_seq'::regclass); + + +-- +-- Name: product_nutrition id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_nutrition ALTER COLUMN id SET DEFAULT nextval('public.product_nutrition_id_seq'::regclass); + + +-- +-- Name: product_sections id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_sections ALTER COLUMN id SET DEFAULT nextval('public.product_sections_id_seq'::regclass); + + +-- +-- Name: product_stickers id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_stickers ALTER COLUMN id SET DEFAULT nextval('public.product_stickers_id_seq'::regclass); + + +-- +-- Name: products id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval('public.products_id_seq'::regclass); + + +-- +-- Name: subcategory_redirects id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.subcategory_redirects ALTER COLUMN id SET DEFAULT nextval('public.subcategory_redirects_id_seq'::regclass); + + +-- +-- Name: tags id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id_seq'::regclass); + + +-- +-- Name: user_labels id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_labels ALTER COLUMN id SET DEFAULT nextval('public.user_labels_id_seq'::regclass); + + +-- +-- Name: user_newsletters id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_newsletters ALTER COLUMN id SET DEFAULT nextval('public.user_newsletters_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- + + +-- +-- Name: authors authors_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.authors + ADD CONSTRAINT authors_pkey PRIMARY KEY (id); + + +-- +-- Name: calendar_entries calendar_entries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_entries + ADD CONSTRAINT calendar_entries_pkey PRIMARY KEY (id); + + +-- +-- Name: calendar_slots calendar_slots_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_slots + ADD CONSTRAINT calendar_slots_pkey PRIMARY KEY (id); + + +-- +-- Name: calendars calendars_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendars + ADD CONSTRAINT calendars_pkey PRIMARY KEY (id); + + +-- +-- Name: ghost_labels ghost_labels_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_labels + ADD CONSTRAINT ghost_labels_pkey PRIMARY KEY (id); + + +-- +-- Name: ghost_newsletters ghost_newsletters_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_newsletters + ADD CONSTRAINT ghost_newsletters_pkey PRIMARY KEY (id); + + +-- +-- Name: ghost_subscriptions ghost_subscriptions_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_subscriptions + ADD CONSTRAINT ghost_subscriptions_pkey PRIMARY KEY (id); + + +-- +-- Name: ghost_tiers ghost_tiers_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_tiers + ADD CONSTRAINT ghost_tiers_pkey PRIMARY KEY (id); + + +-- +-- Name: kv kv_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.kv + ADD CONSTRAINT kv_pkey PRIMARY KEY (key); + + +-- +-- Name: link_errors link_errors_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.link_errors + ADD CONSTRAINT link_errors_pkey PRIMARY KEY (id); + + +-- +-- Name: link_externals link_externals_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.link_externals + ADD CONSTRAINT link_externals_pkey PRIMARY KEY (id); + + +-- +-- Name: listing_items listing_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listing_items + ADD CONSTRAINT listing_items_pkey PRIMARY KEY (id); + + +-- +-- Name: listings listings_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listings + ADD CONSTRAINT listings_pkey PRIMARY KEY (id); + + +-- +-- Name: magic_links magic_links_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.magic_links + ADD CONSTRAINT magic_links_pkey PRIMARY KEY (id); + + +-- +-- Name: magic_links magic_links_token_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.magic_links + ADD CONSTRAINT magic_links_token_key UNIQUE (token); + + +-- +-- Name: nav_subs nav_subs_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_subs + ADD CONSTRAINT nav_subs_pkey PRIMARY KEY (id); + + +-- +-- Name: nav_tops nav_tops_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_tops + ADD CONSTRAINT nav_tops_pkey PRIMARY KEY (id); + + +-- +-- Name: post_authors post_authors_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.post_authors + ADD CONSTRAINT post_authors_pkey PRIMARY KEY (post_id, author_id); + + +-- +-- Name: post_tags post_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.post_tags + ADD CONSTRAINT post_tags_pkey PRIMARY KEY (post_id, tag_id); + + +-- +-- Name: posts posts_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT posts_pkey PRIMARY KEY (id); + + +-- +-- Name: product_allergens product_allergens_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_allergens + ADD CONSTRAINT product_allergens_pkey PRIMARY KEY (id); + + +-- +-- Name: product_attributes product_attributes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_attributes + ADD CONSTRAINT product_attributes_pkey PRIMARY KEY (id); + + +-- +-- Name: product_images product_images_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_images + ADD CONSTRAINT product_images_pkey PRIMARY KEY (id); + + +-- +-- Name: product_labels product_labels_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_labels + ADD CONSTRAINT product_labels_pkey PRIMARY KEY (id); + + +-- +-- Name: product_logs product_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_logs + ADD CONSTRAINT product_logs_pkey PRIMARY KEY (id); + + +-- +-- Name: product_nutrition product_nutrition_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_nutrition + ADD CONSTRAINT product_nutrition_pkey PRIMARY KEY (id); + + +-- +-- Name: product_sections product_sections_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_sections + ADD CONSTRAINT product_sections_pkey PRIMARY KEY (id); + + +-- +-- Name: product_stickers product_stickers_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_stickers + ADD CONSTRAINT product_stickers_pkey PRIMARY KEY (id); + + +-- +-- Name: products products_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_pkey PRIMARY KEY (id); + + +-- +-- Name: products products_slug_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.products + ADD CONSTRAINT products_slug_deleted_at UNIQUE (slug, deleted_at); + + +-- +-- Name: subcategory_redirects subcategory_redirects_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.subcategory_redirects + ADD CONSTRAINT subcategory_redirects_pkey PRIMARY KEY (id); + + +-- +-- Name: tags tags_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.tags + ADD CONSTRAINT tags_pkey PRIMARY KEY (id); + + +-- +-- Name: authors uq_authors_ghost_id; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.authors + ADD CONSTRAINT uq_authors_ghost_id UNIQUE (ghost_id); + + +-- +-- Name: calendar_slots uq_calendar_slots_unique_band; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_slots + ADD CONSTRAINT uq_calendar_slots_unique_band UNIQUE (calendar_id, name); + + +-- +-- Name: listing_items uq_listing_items; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listing_items + ADD CONSTRAINT uq_listing_items UNIQUE (listing_id, slug, deleted_at); + + +-- +-- Name: listing_items uq_listing_items_listing_slug; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listing_items + ADD CONSTRAINT uq_listing_items_listing_slug UNIQUE (listing_id, slug, deleted_at); + + +-- +-- Name: listings uq_listings_top_sub; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listings + ADD CONSTRAINT uq_listings_top_sub UNIQUE (top_id, sub_id, deleted_at); + + + +-- +-- Name: nav_subs uq_nav_subs_top_slug; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_subs + ADD CONSTRAINT uq_nav_subs_top_slug UNIQUE (top_id, slug, deleted_at); + + + + +-- +-- Name: nav_tops uq_nav_tops_label_slug; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_tops + ADD CONSTRAINT uq_nav_tops_label_slug UNIQUE (label, slug, deleted_at); + + +-- +-- Name: posts uq_posts_ghost_id; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT uq_posts_ghost_id UNIQUE (ghost_id); + + +-- +-- Name: posts uq_posts_uuid; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT uq_posts_uuid UNIQUE (uuid); + + +-- +-- Name: product_allergens uq_product_allergens_product_name; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_allergens + ADD CONSTRAINT uq_product_allergens_product_name UNIQUE (product_id, name, deleted_at); + + +-- +-- Name: product_attributes uq_product_attributes_product_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_attributes + ADD CONSTRAINT uq_product_attributes_product_key UNIQUE (product_id, key, deleted_at); + + +-- +-- Name: product_images uq_product_images_product_url_kind; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_images + ADD CONSTRAINT uq_product_images_product_url_kind UNIQUE (product_id, url, kind, deleted_at); + + +-- +-- Name: product_labels uq_product_labels_product_name; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_labels + ADD CONSTRAINT uq_product_labels_product_name UNIQUE (product_id, name, deleted_at); + + +-- +-- Name: product_likes uq_product_likes_product_user; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_likes + ADD CONSTRAINT uq_product_likes_product_user UNIQUE (product_slug, user_id, deleted_at); + + +-- +-- Name: product_nutrition uq_product_nutrition_product_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_nutrition + ADD CONSTRAINT uq_product_nutrition_product_key UNIQUE (product_id, key, deleted_at); + + +-- +-- Name: product_sections uq_product_sections_product_title; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_sections + ADD CONSTRAINT uq_product_sections_product_title UNIQUE (product_id, title, deleted_at); + + +-- +-- Name: product_stickers uq_product_stickers_product_name; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_stickers + ADD CONSTRAINT uq_product_stickers_product_name UNIQUE (product_id, name, deleted_at); + + +-- +-- Name: subcategory_redirects uq_subcategory_redirects_old_new; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.subcategory_redirects + ADD CONSTRAINT uq_subcategory_redirects_old_new UNIQUE (old_path, new_path, deleted_at); + + +-- +-- Name: tags uq_tags_ghost_id; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.tags + ADD CONSTRAINT uq_tags_ghost_id UNIQUE (ghost_id); + + +-- +-- Name: user_labels uq_user_label; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_labels + ADD CONSTRAINT uq_user_label UNIQUE (user_id, label_id); + + +-- +-- Name: user_newsletters uq_user_newsletter; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_newsletters + ADD CONSTRAINT uq_user_newsletter UNIQUE (user_id, newsletter_id); + + +-- +-- Name: user_labels user_labels_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_labels + ADD CONSTRAINT user_labels_pkey PRIMARY KEY (id); + + +-- +-- Name: user_newsletters user_newsletters_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_newsletters + ADD CONSTRAINT user_newsletters_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_email_key UNIQUE (email); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: ix_authors_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_authors_ghost_id ON public.authors USING btree (ghost_id); + + +-- +-- Name: ix_authors_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_authors_slug ON public.authors USING btree (slug); + + +-- +-- Name: ix_calendar_entries_calendar_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendar_entries_calendar_id ON public.calendar_entries USING btree (calendar_id); + + +-- +-- Name: ix_calendar_entries_name; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendar_entries_name ON public.calendar_entries USING btree (name); + + +-- +-- Name: ix_calendar_entries_start_at; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendar_entries_start_at ON public.calendar_entries USING btree (start_at); + + +-- +-- Name: ix_calendar_slots_calendar_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendar_slots_calendar_id ON public.calendar_slots USING btree (calendar_id); + + +-- +-- Name: ix_calendar_slots_time_start; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendar_slots_time_start ON public.calendar_slots USING btree (time_start); + + +-- +-- Name: ix_calendars_name; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendars_name ON public.calendars USING btree (name); + + +-- +-- Name: ix_calendars_post_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendars_post_id ON public.calendars USING btree (post_id); + + +-- +-- Name: ix_calendars_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_calendars_slug ON public.calendars USING btree (slug); + + +-- +-- Name: ix_ghost_labels_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_ghost_labels_ghost_id ON public.ghost_labels USING btree (ghost_id); + + +-- +-- Name: ix_ghost_newsletters_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_ghost_newsletters_ghost_id ON public.ghost_newsletters USING btree (ghost_id); + + +-- +-- Name: ix_ghost_subscriptions_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_ghost_subscriptions_ghost_id ON public.ghost_subscriptions USING btree (ghost_id); + + +-- +-- Name: ix_ghost_subscriptions_stripe_customer_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_ghost_subscriptions_stripe_customer_id ON public.ghost_subscriptions USING btree (stripe_customer_id); + + +-- +-- Name: ix_ghost_subscriptions_stripe_subscription_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_ghost_subscriptions_stripe_subscription_id ON public.ghost_subscriptions USING btree (stripe_subscription_id); + + +-- +-- Name: ix_ghost_subscriptions_tier_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_ghost_subscriptions_tier_id ON public.ghost_subscriptions USING btree (tier_id); + + +-- +-- Name: ix_ghost_subscriptions_user_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_ghost_subscriptions_user_id ON public.ghost_subscriptions USING btree (user_id); + + +-- +-- Name: ix_ghost_tiers_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_ghost_tiers_ghost_id ON public.ghost_tiers USING btree (ghost_id); + + +-- +-- Name: ix_link_errors_product_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_link_errors_product_slug ON public.link_errors USING btree (product_slug); + + +-- +-- Name: ix_link_externals_product_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_link_externals_product_slug ON public.link_externals USING btree (product_slug); + + +-- +-- Name: ix_listing_items_listing_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_listing_items_listing_id ON public.listing_items USING btree (listing_id); + + +-- +-- Name: ix_listing_items_listing_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_listing_items_listing_slug ON public.listing_items USING btree (listing_id, slug); + + +-- +-- Name: ix_listing_items_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_listing_items_slug ON public.listing_items USING btree (slug); + + +-- +-- Name: ix_listings_sub_slug; Type: INDEX; Schema: public; Owner: postgres +-- + + + +-- +-- Name: ix_listings_top_slug; Type: INDEX; Schema: public; Owner: postgres +-- + + + +-- +-- Name: ix_magic_links_token; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_magic_links_token ON public.magic_links USING btree (token); + + +-- +-- Name: ix_magic_links_user; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_magic_links_user ON public.magic_links USING btree (user_id); + + +-- +-- Name: ix_nav_subs_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_nav_subs_slug ON public.nav_subs USING btree (slug); + + +-- +-- Name: ix_nav_subs_top_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_nav_subs_top_id ON public.nav_subs USING btree (top_id); + + +-- +-- Name: ix_nav_tops_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_nav_tops_slug ON public.nav_tops USING btree (slug); + + +-- +-- Name: ix_posts_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_posts_ghost_id ON public.posts USING btree (ghost_id); + + +-- +-- Name: ix_posts_is_page; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_posts_is_page ON public.posts USING btree (is_page); + + +-- +-- Name: ix_posts_published_at; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_posts_published_at ON public.posts USING btree (published_at); + + +-- +-- Name: ix_posts_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_posts_slug ON public.posts USING btree (slug); + + +-- +-- Name: ix_posts_status; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_posts_status ON public.posts USING btree (status); + + +-- +-- Name: ix_posts_visibility; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_posts_visibility ON public.posts USING btree (visibility); + + +-- +-- Name: ix_product_allergens_name; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_allergens_name ON public.product_allergens USING btree (name); + + +-- +-- Name: ix_product_allergens_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_allergens_product_id ON public.product_allergens USING btree (product_id); + + +-- +-- Name: ix_product_attributes_key; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_attributes_key ON public.product_attributes USING btree (key); + + +-- +-- Name: ix_product_attributes_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_attributes_product_id ON public.product_attributes USING btree (product_id); + + +-- +-- Name: ix_product_images_position; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_images_position ON public.product_images USING btree ("position"); + + +-- +-- Name: ix_product_images_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_images_product_id ON public.product_images USING btree (product_id); + + +-- +-- Name: ix_product_labels_name; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_labels_name ON public.product_labels USING btree (name); + + +-- +-- Name: ix_product_labels_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_labels_product_id ON public.product_labels USING btree (product_id); + + +-- +-- Name: ix_product_likes_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_likes_product_id ON public.product_likes USING btree (product_slug); + + +-- +-- Name: ix_product_likes_user_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_likes_user_id ON public.product_likes USING btree (user_id); + + +-- +-- Name: ix_product_likes_user_product; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_product_likes_user_product ON public.product_likes USING btree (user_id, product_slug, deleted_at); + + +-- +-- Name: ix_product_logs_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_logs_slug ON public.product_logs USING btree (slug); + + +-- +-- Name: ix_product_nutrition_key; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_nutrition_key ON public.product_nutrition USING btree (key); + + +-- +-- Name: ix_product_nutrition_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_nutrition_product_id ON public.product_nutrition USING btree (product_id); + + +-- +-- Name: ix_product_sections_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_sections_product_id ON public.product_sections USING btree (product_id); + + +-- +-- Name: ix_product_stickers_name; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_stickers_name ON public.product_stickers USING btree (name); + + +-- +-- Name: ix_product_stickers_product_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_product_stickers_product_id ON public.product_stickers USING btree (product_id); + + +-- +-- Name: ix_products_brand; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_products_brand ON public.products USING btree (brand); + + +-- +-- Name: ix_products_ean; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_products_ean ON public.products USING btree (ean); + + +-- +-- Name: ix_products_regular_price; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_products_regular_price ON public.products USING btree (regular_price); + + +-- +-- Name: ix_products_sku; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_products_sku ON public.products USING btree (sku); + + +-- +-- Name: ix_products_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_products_slug ON public.products USING btree (slug); + + +-- +-- Name: ix_subcategory_redirects_old_path; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_subcategory_redirects_old_path ON public.subcategory_redirects USING btree (old_path); + + +-- +-- Name: ix_tags_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_tags_ghost_id ON public.tags USING btree (ghost_id); + + +-- +-- Name: ix_tags_slug; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_tags_slug ON public.tags USING btree (slug); + + +-- +-- Name: ix_users_email; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_users_email ON public.users USING btree (email); + + +-- +-- Name: ix_users_ghost_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ix_users_ghost_id ON public.users USING btree (ghost_id); + + +-- +-- Name: ix_users_stripe_customer_id; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX ix_users_stripe_customer_id ON public.users USING btree (stripe_customer_id); + + +-- +-- Name: ux_calendars_post_slug_active; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE UNIQUE INDEX ux_calendars_post_slug_active ON public.calendars USING btree (post_id, lower((slug)::text)) WHERE (deleted_at IS NULL); + + +-- +-- Name: calendar_entries calendar_entries_calendar_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_entries + ADD CONSTRAINT calendar_entries_calendar_id_fkey FOREIGN KEY (calendar_id) REFERENCES public.calendars(id) ON DELETE CASCADE; + + +-- +-- Name: calendar_slots calendar_slots_calendar_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendar_slots + ADD CONSTRAINT calendar_slots_calendar_id_fkey FOREIGN KEY (calendar_id) REFERENCES public.calendars(id) ON DELETE CASCADE; + + +-- +-- Name: calendars calendars_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.calendars + ADD CONSTRAINT calendars_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: product_likes fk_product_likes_product_id_products; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_likes + ADD CONSTRAINT fk_product_likes_product_id_products FOREIGN KEY (product_slug) REFERENCES public.products(slug) ON DELETE CASCADE; + + +-- +-- Name: ghost_subscriptions ghost_subscriptions_tier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_subscriptions + ADD CONSTRAINT ghost_subscriptions_tier_id_fkey FOREIGN KEY (tier_id) REFERENCES public.ghost_tiers(id) ON DELETE SET NULL; + + +-- +-- Name: ghost_subscriptions ghost_subscriptions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.ghost_subscriptions + ADD CONSTRAINT ghost_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: listing_items listing_items_listing_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listing_items + ADD CONSTRAINT listing_items_listing_id_fkey FOREIGN KEY (listing_id) REFERENCES public.listings(id) ON DELETE CASCADE; + + +-- +-- Name: listings listings_sub_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listings + ADD CONSTRAINT listings_sub_id_fkey FOREIGN KEY (sub_id) REFERENCES public.nav_subs(id); + + +-- +-- Name: listings listings_top_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.listings + ADD CONSTRAINT listings_top_id_fkey FOREIGN KEY (top_id) REFERENCES public.nav_tops(id); + + +-- +-- Name: magic_links magic_links_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.magic_links + ADD CONSTRAINT magic_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: nav_subs nav_subs_top_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.nav_subs + ADD CONSTRAINT nav_subs_top_id_fkey FOREIGN KEY (top_id) REFERENCES public.nav_tops(id) ON DELETE CASCADE; + + +-- +-- Name: post_authors post_authors_author_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.post_authors + ADD CONSTRAINT post_authors_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.authors(id) ON DELETE CASCADE; + + +-- +-- Name: post_authors post_authors_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.post_authors + ADD CONSTRAINT post_authors_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: post_tags post_tags_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.post_tags + ADD CONSTRAINT post_tags_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: post_tags post_tags_tag_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.post_tags + ADD CONSTRAINT post_tags_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES public.tags(id) ON DELETE CASCADE; + + +-- +-- Name: posts posts_primary_author_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT posts_primary_author_id_fkey FOREIGN KEY (primary_author_id) REFERENCES public.authors(id) ON DELETE SET NULL; + + +-- +-- Name: posts posts_primary_tag_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT posts_primary_tag_id_fkey FOREIGN KEY (primary_tag_id) REFERENCES public.tags(id) ON DELETE SET NULL; + + +-- +-- Name: product_allergens product_allergens_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_allergens + ADD CONSTRAINT product_allergens_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_attributes product_attributes_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_attributes + ADD CONSTRAINT product_attributes_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_images product_images_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_images + ADD CONSTRAINT product_images_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_labels product_labels_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_labels + ADD CONSTRAINT product_labels_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_likes product_likes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_likes + ADD CONSTRAINT product_likes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: product_nutrition product_nutrition_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_nutrition + ADD CONSTRAINT product_nutrition_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_sections product_sections_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_sections + ADD CONSTRAINT product_sections_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: product_stickers product_stickers_product_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.product_stickers + ADD CONSTRAINT product_stickers_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE; + + +-- +-- Name: user_labels user_labels_label_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_labels + ADD CONSTRAINT user_labels_label_id_fkey FOREIGN KEY (label_id) REFERENCES public.ghost_labels(id) ON DELETE CASCADE; + + +-- +-- Name: user_labels user_labels_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_labels + ADD CONSTRAINT user_labels_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: user_newsletters user_newsletters_newsletter_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_newsletters + ADD CONSTRAINT user_newsletters_newsletter_id_fkey FOREIGN KEY (newsletter_id) REFERENCES public.ghost_newsletters(id) ON DELETE CASCADE; + + +-- +-- Name: user_newsletters user_newsletters_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.user_newsletters + ADD CONSTRAINT user_newsletters_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + + diff --git a/shared/.gitignore b/shared/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/shared/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/shared/README.md b/shared/README.md new file mode 100644 index 0000000..8de7495 --- /dev/null +++ b/shared/README.md @@ -0,0 +1,91 @@ +# Shared + +Shared infrastructure, models, contracts, services, and templates used by all five Rose Ash microservices (blog, market, cart, events, federation). Included as a git submodule in each app. + +## Structure + +``` +shared/ + db/ + base.py # SQLAlchemy declarative Base + session.py # Async session factory (get_session, register_db) + models/ # Canonical domain models + user.py # User + magic_link.py # MagicLink (auth tokens) + (domain_event.py removed — table dropped, see migration n4l2i8j0k1) + kv.py # KeyValue (key-value store) + menu_item.py # MenuItem (deprecated — use MenuNode) + menu_node.py # MenuNode (navigation tree) + container_relation.py # ContainerRelation (parent-child content) + ghost_membership_entities.py # GhostNewsletter, UserNewsletter + federation.py # ActorProfile, APActivity, APFollower, APFollowing, + # RemoteActor, APRemotePost, APLocalPost, + # APInteraction, APNotification, APAnchor, IPFSPin + contracts/ + dtos.py # Frozen dataclasses for cross-domain data transfer + protocols.py # Service protocols (Blog, Calendar, Market, Cart, Federation) + widgets.py # Widget types (NavWidget, CardWidget, AccountPageWidget) + services/ + registry.py # Typed singleton: services.blog, .calendar, .market, .cart, .federation + blog_impl.py # SqlBlogService + calendar_impl.py # SqlCalendarService + market_impl.py # SqlMarketService + cart_impl.py # SqlCartService + federation_impl.py # SqlFederationService + federation_publish.py # try_publish() — inline AP publication helper + stubs.py # No-op stubs for absent domains + navigation.py # get_navigation_tree() + relationships.py # attach_child, get_children, detach_child + widget_registry.py # Widget registry singleton + widgets/ # Per-domain widget registration + infrastructure/ + factory.py # create_base_app() — Quart app factory + cart_identity.py # current_cart_identity() (user_id or session_id) + cart_loader.py # Cart data loader for context processors + context.py # Jinja2 context processors + jinja_setup.py # Jinja2 template environment setup + urls.py # URL helpers (blog_url, market_url, etc.) + user_loader.py # Load current user from session + http_utils.py # HTTP utility functions + events/ + bus.py # emit_activity(), register_activity_handler() + processor.py # EventProcessor (polls ap_activities, runs handlers) + handlers/ # Shared activity handlers + container_handlers.py # Navigation rebuild on attach/detach + login_handlers.py # Cart/entry adoption on login + order_handlers.py # Order lifecycle events + ap_delivery_handler.py # AP activity delivery to follower inboxes (wildcard) + utils/ + __init__.py + calendar_helpers.py # Calendar period/entry utilities + http_signatures.py # RSA keypair generation, HTTP signature signing/verification + ipfs_client.py # Async IPFS client (add_bytes, add_json, pin_cid) + anchoring.py # Merkle trees + OpenTimestamps Bitcoin anchoring + webfinger.py # WebFinger actor resolution + browser/ + app/ # Middleware, CSRF, errors, Redis caching, authz, filters + templates/ # ~300 Jinja2 templates shared across all apps + containers.py # ContainerType, container_filter, content_filter helpers + config.py # YAML config loader + log_config/setup.py # Logging configuration (JSON formatter) + static/ # Shared static assets (CSS, JS, images, FontAwesome) + editor/ # Koenig (Ghost) rich text editor build + alembic/ # Database migrations +``` + +## Key Patterns + +- **App factory:** All apps call `create_base_app()` which sets up DB sessions, CSRF, error handling, event processing, logging, widget registration, and domain service wiring. +- **Service contracts:** Cross-domain communication via typed Protocols + frozen DTO dataclasses. Apps call `services.calendar.method()`, never import models from other domains. +- **Service registry:** Typed singleton (`services.blog`, `.calendar`, `.market`, `.cart`, `.federation`). Apps wire their own domain + stubs for others via `register_domain_services()`. +- **Activity bus:** `emit_activity()` writes to `ap_activities` table in the caller's transaction. `EventProcessor` polls pending activities and dispatches to registered handlers. Internal events use `visibility="internal"`; federation activities use `visibility="public"` and are delivered to follower inboxes by the wildcard delivery handler. +- **Widget registry:** Domain services register widgets (nav, card, account); templates consume via `widgets.container_nav`, `widgets.container_cards`. +- **Cart identity:** `current_cart_identity()` returns `{"user_id": int|None, "session_id": str|None}` from the request session. + +## Alembic Migrations + +All apps share one PostgreSQL database. Migrations are managed here and run from the blog app's entrypoint (other apps skip migrations on startup). + +```bash +alembic -c shared/alembic.ini upgrade head +``` diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000..f01aafe --- /dev/null +++ b/shared/__init__.py @@ -0,0 +1 @@ +# shared package — infrastructure, models, contracts, and services diff --git a/shared/alembic.ini b/shared/alembic.ini new file mode 100644 index 0000000..a04e071 --- /dev/null +++ b/shared/alembic.ini @@ -0,0 +1,35 @@ +[alembic] +script_location = alembic +sqlalchemy.url = + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s diff --git a/shared/alembic/env.py b/shared/alembic/env.py new file mode 100644 index 0000000..f43a95d --- /dev/null +++ b/shared/alembic/env.py @@ -0,0 +1,69 @@ +from __future__ import annotations +import os, sys +from logging.config import fileConfig +from alembic import context +from sqlalchemy import engine_from_config, pool + +config = context.config + +if config.config_file_name is not None: + try: + fileConfig(config.config_file_name) + except Exception: + pass + +# Add project root so all app model packages are importable +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from shared.db.base import Base + +# Import ALL models so Base.metadata sees every table +import shared.models # noqa: F401 User, KV, MagicLink, MenuItem, Ghost* +for _mod in ("blog.models", "market.models", "cart.models", "events.models", "federation.models"): + try: + __import__(_mod) + except ImportError: + pass # OK in Docker — only needed for autogenerate + +target_metadata = Base.metadata + +def _get_url() -> str: + url = os.getenv( + "ALEMBIC_DATABASE_URL", + os.getenv("DATABASE_URL", config.get_main_option("sqlalchemy.url") or "") + ) + print(url) + return url + +def run_migrations_offline() -> None: + url = _get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + url = _get_url() + if url: + config.set_main_option("sqlalchemy.url", url) + + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/shared/alembic/script.py.mako b/shared/alembic/script.py.mako new file mode 100644 index 0000000..31bee0b --- /dev/null +++ b/shared/alembic/script.py.mako @@ -0,0 +1,24 @@ +<%text> +# Alembic migration script template + +"""empty message + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/shared/alembic/versions/0001_initial_schem.py b/shared/alembic/versions/0001_initial_schem.py new file mode 100644 index 0000000..b131310 --- /dev/null +++ b/shared/alembic/versions/0001_initial_schem.py @@ -0,0 +1,33 @@ +"""Initial database schema from schema.sql""" + +from alembic import op +import sqlalchemy as sa +import pathlib + +# revision identifiers, used by Alembic +revision = '0001_initial_schema' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + return + schema_path = pathlib.Path(__file__).parent.parent.parent / "schema.sql" + with open(schema_path, encoding="utf-8") as f: + sql = f.read() + conn = op.get_bind() + conn.execute(sa.text(sql)) + +def downgrade(): + return + # Drop all user-defined tables in the 'public' schema + conn = op.get_bind() + conn.execute(sa.text(""" + DO $$ DECLARE + r RECORD; + BEGIN + FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP + EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE'; + END LOOP; + END $$; + """)) \ No newline at end of file diff --git a/shared/alembic/versions/0002_add_cart_items.py b/shared/alembic/versions/0002_add_cart_items.py new file mode 100644 index 0000000..ecae098 --- /dev/null +++ b/shared/alembic/versions/0002_add_cart_items.py @@ -0,0 +1,78 @@ +"""Add cart_items table for shopping cart""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "0002_add_cart_items" +down_revision = "0001_initial_schema" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "cart_items", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + + # Either a logged-in user *or* an anonymous session_id + sa.Column( + "user_id", + sa.Integer(), + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=True, + ), + sa.Column("session_id", sa.String(length=128), nullable=True), + + # IMPORTANT: reference products.id (PK), not slug + sa.Column( + "product_id", + sa.Integer(), + sa.ForeignKey("products.id", ondelete="CASCADE"), + nullable=False, + ), + + sa.Column( + "quantity", + sa.Integer(), + nullable=False, + server_default="1", + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "deleted_at", + sa.DateTime(timezone=True), + nullable=True, + ), + ) + + # Indexes to speed up cart lookups + op.create_index( + "ix_cart_items_user_product", + "cart_items", + ["user_id", "product_id"], + unique=False, + ) + op.create_index( + "ix_cart_items_session_product", + "cart_items", + ["session_id", "product_id"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("ix_cart_items_session_product", table_name="cart_items") + op.drop_index("ix_cart_items_user_product", table_name="cart_items") + op.drop_table("cart_items") diff --git a/shared/alembic/versions/0003_add_orders.py b/shared/alembic/versions/0003_add_orders.py new file mode 100644 index 0000000..4387219 --- /dev/null +++ b/shared/alembic/versions/0003_add_orders.py @@ -0,0 +1,118 @@ +"""Add orders and order_items tables for checkout""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "0003_add_orders" +down_revision = "0002_add_cart_items" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "orders", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True), + sa.Column("session_id", sa.String(length=64), nullable=True), + + sa.Column( + "status", + sa.String(length=32), + nullable=False, + server_default="pending", + ), + sa.Column( + "currency", + sa.String(length=16), + nullable=False, + server_default="GBP", + ), + sa.Column( + "total_amount", + sa.Numeric(12, 2), + nullable=False, + ), + + # SumUp integration fields + sa.Column("sumup_checkout_id", sa.String(length=128), nullable=True), + sa.Column("sumup_status", sa.String(length=32), nullable=True), + sa.Column("sumup_hosted_url", sa.Text(), nullable=True), + + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + ) + + # Indexes to match model hints (session_id + sumup_checkout_id index=True) + op.create_index( + "ix_orders_session_id", + "orders", + ["session_id"], + unique=False, + ) + op.create_index( + "ix_orders_sumup_checkout_id", + "orders", + ["sumup_checkout_id"], + unique=False, + ) + + op.create_table( + "order_items", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column( + "order_id", + sa.Integer(), + sa.ForeignKey("orders.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "product_id", + sa.Integer(), + sa.ForeignKey("products.id"), + nullable=False, + ), + sa.Column("product_title", sa.String(length=512), nullable=True), + + sa.Column( + "quantity", + sa.Integer(), + nullable=False, + server_default="1", + ), + sa.Column( + "unit_price", + sa.Numeric(12, 2), + nullable=False, + ), + sa.Column( + "currency", + sa.String(length=16), + nullable=False, + server_default="GBP", + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + ) + + +def downgrade() -> None: + op.drop_table("order_items") + op.drop_index("ix_orders_sumup_checkout_id", table_name="orders") + op.drop_index("ix_orders_session_id", table_name="orders") + op.drop_table("orders") diff --git a/shared/alembic/versions/0004_add_sumup_reference.py b/shared/alembic/versions/0004_add_sumup_reference.py new file mode 100644 index 0000000..2738cd2 --- /dev/null +++ b/shared/alembic/versions/0004_add_sumup_reference.py @@ -0,0 +1,27 @@ +"""Add sumup_reference to orders""" + +from alembic import op +import sqlalchemy as sa + +revision = "0004_add_sumup_reference" +down_revision = "0003_add_orders" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "orders", + sa.Column("sumup_reference", sa.String(length=255), nullable=True), + ) + op.create_index( + "ix_orders_sumup_reference", + "orders", + ["sumup_reference"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("ix_orders_sumup_reference", table_name="orders") + op.drop_column("orders", "sumup_reference") diff --git a/shared/alembic/versions/0005_add_description.py b/shared/alembic/versions/0005_add_description.py new file mode 100644 index 0000000..37e84ed --- /dev/null +++ b/shared/alembic/versions/0005_add_description.py @@ -0,0 +1,27 @@ +"""Add description field to orders""" + +from alembic import op +import sqlalchemy as sa + +revision = "0005_add_description" +down_revision = "0004_add_sumup_reference" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "orders", + sa.Column("description", sa.Text(), nullable=True), + ) + op.create_index( + "ix_orders_description", + "orders", + ["description"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("ix_orders_description", table_name="orders") + op.drop_column("orders", "description") diff --git a/shared/alembic/versions/0006_update_calendar_entries.py b/shared/alembic/versions/0006_update_calendar_entries.py new file mode 100644 index 0000000..cd6f9bd --- /dev/null +++ b/shared/alembic/versions/0006_update_calendar_entries.py @@ -0,0 +1,28 @@ +from alembic import op +import sqlalchemy as sa + +revision = '0006_update_calendar_entries' +down_revision = '0005_add_description' # use the appropriate previous revision ID +branch_labels = None +depends_on = None + +def upgrade(): + # Add user_id and session_id columns + op.add_column('calendar_entries', sa.Column('user_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_calendar_entries_user_id', 'calendar_entries', 'users', ['user_id'], ['id']) + op.add_column('calendar_entries', sa.Column('session_id', sa.String(length=128), nullable=True)) + # Add state and cost columns + op.add_column('calendar_entries', sa.Column('state', sa.String(length=20), nullable=False, server_default='pending')) + op.add_column('calendar_entries', sa.Column('cost', sa.Numeric(10,2), nullable=False, server_default='10')) + # (Optional) Create indexes on the new columns + op.create_index('ix_calendar_entries_user_id', 'calendar_entries', ['user_id']) + op.create_index('ix_calendar_entries_session_id', 'calendar_entries', ['session_id']) + +def downgrade(): + op.drop_index('ix_calendar_entries_session_id', table_name='calendar_entries') + op.drop_index('ix_calendar_entries_user_id', table_name='calendar_entries') + op.drop_column('calendar_entries', 'cost') + op.drop_column('calendar_entries', 'state') + op.drop_column('calendar_entries', 'session_id') + op.drop_constraint('fk_calendar_entries_user_id', 'calendar_entries', type_='foreignkey') + op.drop_column('calendar_entries', 'user_id') diff --git a/shared/alembic/versions/0007_add_oid_entries.py b/shared/alembic/versions/0007_add_oid_entries.py new file mode 100644 index 0000000..be05343 --- /dev/null +++ b/shared/alembic/versions/0007_add_oid_entries.py @@ -0,0 +1,50 @@ +from alembic import op +import sqlalchemy as sa + +revision = "0007_add_oid_entries" +down_revision = "0006_update_calendar_entries" +branch_labels = None +depends_on = None + + +def upgrade(): + # Add order_id column + op.add_column( + "calendar_entries", + sa.Column("order_id", sa.Integer(), nullable=True), + ) + op.create_foreign_key( + "fk_calendar_entries_order_id", + "calendar_entries", + "orders", + ["order_id"], + ["id"], + ondelete="SET NULL", + ) + op.create_index( + "ix_calendar_entries_order_id", + "calendar_entries", + ["order_id"], + unique=False, + ) + + # Optional: add an index on state if you want faster queries by state + op.create_index( + "ix_calendar_entries_state", + "calendar_entries", + ["state"], + unique=False, + ) + + +def downgrade(): + # Drop indexes and FK in reverse order + op.drop_index("ix_calendar_entries_state", table_name="calendar_entries") + + op.drop_index("ix_calendar_entries_order_id", table_name="calendar_entries") + op.drop_constraint( + "fk_calendar_entries_order_id", + "calendar_entries", + type_="foreignkey", + ) + op.drop_column("calendar_entries", "order_id") diff --git a/shared/alembic/versions/0008_add_flexible_to_slots.py b/shared/alembic/versions/0008_add_flexible_to_slots.py new file mode 100644 index 0000000..0af0cfe --- /dev/null +++ b/shared/alembic/versions/0008_add_flexible_to_slots.py @@ -0,0 +1,33 @@ +"""add flexible flag to calendar_slots + +Revision ID: 0008_add_flexible_to_calendar_slots +Revises: 0007_add_order_id_to_calendar_entries +Create Date: 2025-12-06 12:34:56.000000 +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "0008_add_flexible_to_slots" +down_revision = "0007_add_oid_entries" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "calendar_slots", + sa.Column( + "flexible", + sa.Boolean(), + nullable=False, + server_default=sa.false(), # set existing rows to False + ), + ) + # Optional: drop server_default so future inserts must supply a value + op.alter_column("calendar_slots", "flexible", server_default=None) + + +def downgrade() -> None: + op.drop_column("calendar_slots", "flexible") diff --git a/shared/alembic/versions/0009_add_slot_id_to_entries.py b/shared/alembic/versions/0009_add_slot_id_to_entries.py new file mode 100644 index 0000000..32c0de4 --- /dev/null +++ b/shared/alembic/versions/0009_add_slot_id_to_entries.py @@ -0,0 +1,54 @@ +"""add slot_id to calendar_entries + +Revision ID: 0009_add_slot_id_to_entries +Revises: 0008_add_flexible_to_slots +Create Date: 2025-12-06 13:00:00.000000 +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "0009_add_slot_id_to_entries" +down_revision = "0008_add_flexible_to_slots" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add slot_id column as nullable initially + op.add_column( + "calendar_entries", + sa.Column( + "slot_id", + sa.Integer(), + nullable=True, + ), + ) + + # Add foreign key constraint + op.create_foreign_key( + "fk_calendar_entries_slot_id_calendar_slots", + "calendar_entries", + "calendar_slots", + ["slot_id"], + ["id"], + ondelete="SET NULL", + ) + + # Add index for better query performance + op.create_index( + "ix_calendar_entries_slot_id", + "calendar_entries", + ["slot_id"], + ) + + +def downgrade() -> None: + op.drop_index("ix_calendar_entries_slot_id", table_name="calendar_entries") + op.drop_constraint( + "fk_calendar_entries_slot_id_calendar_slots", + "calendar_entries", + type_="foreignkey", + ) + op.drop_column("calendar_entries", "slot_id") \ No newline at end of file diff --git a/shared/alembic/versions/0010_add_post_likes.py b/shared/alembic/versions/0010_add_post_likes.py new file mode 100644 index 0000000..17bc15b --- /dev/null +++ b/shared/alembic/versions/0010_add_post_likes.py @@ -0,0 +1,64 @@ +"""Add post_likes table for liking blog posts + +Revision ID: 0010_add_post_likes +Revises: 0009_add_slot_id_to_entries +Create Date: 2025-12-07 13:00:00.000000 +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "0010_add_post_likes" +down_revision = "0009_add_slot_id_to_entries" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "post_likes", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column( + "user_id", + sa.Integer(), + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "post_id", + sa.Integer(), + sa.ForeignKey("posts.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "deleted_at", + sa.DateTime(timezone=True), + nullable=True, + ), + ) + + # Index for fast user+post lookups + op.create_index( + "ix_post_likes_user_post", + "post_likes", + ["user_id", "post_id"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("ix_post_likes_user_post", table_name="post_likes") + op.drop_table("post_likes") diff --git a/shared/alembic/versions/0011_add_entry_tickets.py b/shared/alembic/versions/0011_add_entry_tickets.py new file mode 100644 index 0000000..4b5936f --- /dev/null +++ b/shared/alembic/versions/0011_add_entry_tickets.py @@ -0,0 +1,43 @@ +"""Add ticket_price and ticket_count to calendar_entries + +Revision ID: 0011_add_entry_tickets +Revises: 0010_add_post_likes +Create Date: 2025-12-07 14:00:00.000000 +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import NUMERIC + +# revision identifiers, used by Alembic. +revision = "0011_add_entry_tickets" +down_revision = "0010_add_post_likes" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add ticket_price column (nullable - NULL means no tickets) + op.add_column( + "calendar_entries", + sa.Column( + "ticket_price", + NUMERIC(10, 2), + nullable=True, + ), + ) + + # Add ticket_count column (nullable - NULL means unlimited) + op.add_column( + "calendar_entries", + sa.Column( + "ticket_count", + sa.Integer(), + nullable=True, + ), + ) + + +def downgrade() -> None: + op.drop_column("calendar_entries", "ticket_count") + op.drop_column("calendar_entries", "ticket_price") diff --git a/shared/alembic/versions/47fc53fc0d2b_add_ticket_types_table.py b/shared/alembic/versions/47fc53fc0d2b_add_ticket_types_table.py new file mode 100644 index 0000000..4c3cd5a --- /dev/null +++ b/shared/alembic/versions/47fc53fc0d2b_add_ticket_types_table.py @@ -0,0 +1,41 @@ + +# Alembic migration script template + +"""add ticket_types table + +Revision ID: 47fc53fc0d2b +Revises: a9f54e4eaf02 +Create Date: 2025-12-08 07:29:11.422435 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '47fc53fc0d2b' +down_revision = 'a9f54e4eaf02' +branch_labels = None +depends_on = None + +def upgrade() -> None: + op.create_table( + 'ticket_types', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('entry_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('cost', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('count', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['entry_id'], ['calendar_entries.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_ticket_types_entry_id', 'ticket_types', ['entry_id'], unique=False) + op.create_index('ix_ticket_types_name', 'ticket_types', ['name'], unique=False) + +def downgrade() -> None: + op.drop_index('ix_ticket_types_name', table_name='ticket_types') + op.drop_index('ix_ticket_types_entry_id', table_name='ticket_types') + op.drop_table('ticket_types') diff --git a/shared/alembic/versions/6cb124491c9d_entry_posts.py b/shared/alembic/versions/6cb124491c9d_entry_posts.py new file mode 100644 index 0000000..6062096 --- /dev/null +++ b/shared/alembic/versions/6cb124491c9d_entry_posts.py @@ -0,0 +1,36 @@ + +# Alembic migration script template + +"""Add calendar_entry_posts association table + +Revision ID: 6cb124491c9d +Revises: 0011_add_entry_tickets +Create Date: 2025-12-07 03:40:49.194068 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import TIMESTAMP + +# revision identifiers, used by Alembic. +revision = '6cb124491c9d' +down_revision = '0011_add_entry_tickets' +branch_labels = None +depends_on = None + +def upgrade() -> None: + op.create_table( + 'calendar_entry_posts', + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), + sa.Column('entry_id', sa.Integer(), sa.ForeignKey('calendar_entries.id', ondelete='CASCADE'), nullable=False), + sa.Column('post_id', sa.Integer(), sa.ForeignKey('posts.id', ondelete='CASCADE'), nullable=False), + sa.Column('created_at', TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column('deleted_at', TIMESTAMP(timezone=True), nullable=True), + ) + op.create_index('ix_entry_posts_entry_id', 'calendar_entry_posts', ['entry_id']) + op.create_index('ix_entry_posts_post_id', 'calendar_entry_posts', ['post_id']) + +def downgrade() -> None: + op.drop_index('ix_entry_posts_post_id', 'calendar_entry_posts') + op.drop_index('ix_entry_posts_entry_id', 'calendar_entry_posts') + op.drop_table('calendar_entry_posts') diff --git a/shared/alembic/versions/a1b2c3d4e5f6_add_page_configs_table.py b/shared/alembic/versions/a1b2c3d4e5f6_add_page_configs_table.py new file mode 100644 index 0000000..9cb858c --- /dev/null +++ b/shared/alembic/versions/a1b2c3d4e5f6_add_page_configs_table.py @@ -0,0 +1,74 @@ +"""add page_configs table + +Revision ID: a1b2c3d4e5f6 +Revises: f6d4a1b2c3e7 +Create Date: 2026-02-10 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text + +revision = 'a1b2c3d4e5f6' +down_revision = 'f6d4a1b2c3e7' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'page_configs', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('post_id', sa.Integer(), nullable=False), + sa.Column('features', sa.JSON(), server_default='{}', nullable=False), + sa.Column('sumup_merchant_code', sa.String(64), nullable=True), + sa.Column('sumup_api_key', sa.Text(), nullable=True), + sa.Column('sumup_checkout_prefix', sa.String(64), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('post_id'), + ) + + # Backfill: create PageConfig for every existing page + conn = op.get_bind() + + # 1. Pages with calendars -> features={"calendar": true} + conn.execute(text(""" + INSERT INTO page_configs (post_id, features, created_at, updated_at) + SELECT p.id, '{"calendar": true}'::jsonb, now(), now() + FROM posts p + WHERE p.is_page = true + AND p.deleted_at IS NULL + AND EXISTS ( + SELECT 1 FROM calendars c + WHERE c.post_id = p.id AND c.deleted_at IS NULL + ) + """)) + + # 2. Market page (slug='market', is_page=true) -> features={"market": true} + # Only if not already inserted above + conn.execute(text(""" + INSERT INTO page_configs (post_id, features, created_at, updated_at) + SELECT p.id, '{"market": true}'::jsonb, now(), now() + FROM posts p + WHERE p.slug = 'market' + AND p.is_page = true + AND p.deleted_at IS NULL + AND p.id NOT IN (SELECT post_id FROM page_configs) + """)) + + # 3. All other pages -> features={} + conn.execute(text(""" + INSERT INTO page_configs (post_id, features, created_at, updated_at) + SELECT p.id, '{}'::jsonb, now(), now() + FROM posts p + WHERE p.is_page = true + AND p.deleted_at IS NULL + AND p.id NOT IN (SELECT post_id FROM page_configs) + """)) + + +def downgrade() -> None: + op.drop_table('page_configs') diff --git a/shared/alembic/versions/a9f54e4eaf02_add_menu_items_table.py b/shared/alembic/versions/a9f54e4eaf02_add_menu_items_table.py new file mode 100644 index 0000000..960c10c --- /dev/null +++ b/shared/alembic/versions/a9f54e4eaf02_add_menu_items_table.py @@ -0,0 +1,37 @@ + +# Alembic migration script template + +"""add menu_items table + +Revision ID: a9f54e4eaf02 +Revises: 6cb124491c9d +Create Date: 2025-12-07 17:38:54.839296 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'a9f54e4eaf02' +down_revision = '6cb124491c9d' +branch_labels = None +depends_on = None + +def upgrade() -> None: + op.create_table('menu_items', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('post_id', sa.Integer(), nullable=False), + sa.Column('sort_order', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_menu_items_post_id'), 'menu_items', ['post_id'], unique=False) + op.create_index(op.f('ix_menu_items_sort_order'), 'menu_items', ['sort_order'], unique=False) + +def downgrade() -> None: + op.drop_index(op.f('ix_menu_items_sort_order'), table_name='menu_items') + op.drop_index(op.f('ix_menu_items_post_id'), table_name='menu_items') + op.drop_table('menu_items') diff --git a/shared/alembic/versions/b2c3d4e5f6a7_add_market_places_table.py b/shared/alembic/versions/b2c3d4e5f6a7_add_market_places_table.py new file mode 100644 index 0000000..4dbb124 --- /dev/null +++ b/shared/alembic/versions/b2c3d4e5f6a7_add_market_places_table.py @@ -0,0 +1,97 @@ +"""add market_places table and nav_tops.market_id + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f6 +Create Date: 2026-02-10 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text + +revision = 'b2c3d4e5f6a7' +down_revision = 'a1b2c3d4e5f6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. Create market_places table + op.create_table( + 'market_places', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('post_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(255), nullable=False), + sa.Column('slug', sa.String(255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_market_places_post_id', 'market_places', ['post_id']) + op.create_index( + 'ux_market_places_slug_active', + 'market_places', + [sa.text('lower(slug)')], + unique=True, + postgresql_where=sa.text('deleted_at IS NULL'), + ) + + # 2. Add market_id column to nav_tops + op.add_column( + 'nav_tops', + sa.Column('market_id', sa.Integer(), nullable=True), + ) + op.create_foreign_key( + 'fk_nav_tops_market_id', + 'nav_tops', + 'market_places', + ['market_id'], + ['id'], + ondelete='SET NULL', + ) + op.create_index('ix_nav_tops_market_id', 'nav_tops', ['market_id']) + + # 3. Backfill: create default MarketPlace for the 'market' page + conn = op.get_bind() + + # Find the market page + result = conn.execute(text(""" + SELECT id FROM posts + WHERE slug = 'market' AND is_page = true AND deleted_at IS NULL + LIMIT 1 + """)) + row = result.fetchone() + if row: + post_id = row[0] + + # Insert the default market + conn.execute(text(""" + INSERT INTO market_places (post_id, name, slug, created_at, updated_at) + VALUES (:post_id, 'Suma Market', 'suma-market', now(), now()) + """), {"post_id": post_id}) + + # Get the new market_places id + market_row = conn.execute(text(""" + SELECT id FROM market_places + WHERE slug = 'suma-market' AND deleted_at IS NULL + LIMIT 1 + """)).fetchone() + + if market_row: + market_id = market_row[0] + # Assign all active nav_tops to this market + conn.execute(text(""" + UPDATE nav_tops SET market_id = :market_id + WHERE deleted_at IS NULL + """), {"market_id": market_id}) + + +def downgrade() -> None: + op.drop_index('ix_nav_tops_market_id', table_name='nav_tops') + op.drop_constraint('fk_nav_tops_market_id', 'nav_tops', type_='foreignkey') + op.drop_column('nav_tops', 'market_id') + op.drop_index('ux_market_places_slug_active', table_name='market_places') + op.drop_index('ix_market_places_post_id', table_name='market_places') + op.drop_table('market_places') diff --git a/shared/alembic/versions/c3a1f7b9d4e5_add_snippets_table.py b/shared/alembic/versions/c3a1f7b9d4e5_add_snippets_table.py new file mode 100644 index 0000000..c17c08c --- /dev/null +++ b/shared/alembic/versions/c3a1f7b9d4e5_add_snippets_table.py @@ -0,0 +1,35 @@ +"""add snippets table + +Revision ID: c3a1f7b9d4e5 +Revises: 47fc53fc0d2b +Create Date: 2026-02-07 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'c3a1f7b9d4e5' +down_revision = '47fc53fc0d2b' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'snippets', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('value', sa.Text(), nullable=False), + sa.Column('visibility', sa.String(length=20), server_default='private', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'name', name='uq_snippets_user_name'), + ) + op.create_index('ix_snippets_visibility', 'snippets', ['visibility']) + + +def downgrade() -> None: + op.drop_index('ix_snippets_visibility', table_name='snippets') + op.drop_table('snippets') diff --git a/shared/alembic/versions/c3d4e5f6a7b8_add_page_tracking_to_orders.py b/shared/alembic/versions/c3d4e5f6a7b8_add_page_tracking_to_orders.py new file mode 100644 index 0000000..9547d38 --- /dev/null +++ b/shared/alembic/versions/c3d4e5f6a7b8_add_page_tracking_to_orders.py @@ -0,0 +1,55 @@ +"""add page_config_id to orders, market_place_id to cart_items + +Revision ID: c3d4e5f6a7b8 +Revises: b2c3d4e5f6a7 +Create Date: 2026-02-10 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'c3d4e5f6a7b8' +down_revision = 'b2c3d4e5f6a7' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. Add market_place_id to cart_items + op.add_column( + 'cart_items', + sa.Column('market_place_id', sa.Integer(), nullable=True), + ) + op.create_foreign_key( + 'fk_cart_items_market_place_id', + 'cart_items', + 'market_places', + ['market_place_id'], + ['id'], + ondelete='SET NULL', + ) + op.create_index('ix_cart_items_market_place_id', 'cart_items', ['market_place_id']) + + # 2. Add page_config_id to orders + op.add_column( + 'orders', + sa.Column('page_config_id', sa.Integer(), nullable=True), + ) + op.create_foreign_key( + 'fk_orders_page_config_id', + 'orders', + 'page_configs', + ['page_config_id'], + ['id'], + ondelete='SET NULL', + ) + op.create_index('ix_orders_page_config_id', 'orders', ['page_config_id']) + + +def downgrade() -> None: + op.drop_index('ix_orders_page_config_id', table_name='orders') + op.drop_constraint('fk_orders_page_config_id', 'orders', type_='foreignkey') + op.drop_column('orders', 'page_config_id') + + op.drop_index('ix_cart_items_market_place_id', table_name='cart_items') + op.drop_constraint('fk_cart_items_market_place_id', 'cart_items', type_='foreignkey') + op.drop_column('cart_items', 'market_place_id') diff --git a/shared/alembic/versions/d4b2e8f1a3c7_add_post_user_id_and_author_email.py b/shared/alembic/versions/d4b2e8f1a3c7_add_post_user_id_and_author_email.py new file mode 100644 index 0000000..8d6f122 --- /dev/null +++ b/shared/alembic/versions/d4b2e8f1a3c7_add_post_user_id_and_author_email.py @@ -0,0 +1,45 @@ +"""add post user_id, author email, publish_requested + +Revision ID: d4b2e8f1a3c7 +Revises: c3a1f7b9d4e5 +Create Date: 2026-02-08 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'd4b2e8f1a3c7' +down_revision = 'c3a1f7b9d4e5' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add author.email + op.add_column('authors', sa.Column('email', sa.String(255), nullable=True)) + + # Add post.user_id FK + op.add_column('posts', sa.Column('user_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_posts_user_id', 'posts', 'users', ['user_id'], ['id'], ondelete='SET NULL') + op.create_index('ix_posts_user_id', 'posts', ['user_id']) + + # Add post.publish_requested + op.add_column('posts', sa.Column('publish_requested', sa.Boolean(), server_default='false', nullable=False)) + + # Backfill: match posts to users via primary_author email + op.execute(""" + UPDATE posts + SET user_id = u.id + FROM authors a + JOIN users u ON lower(a.email) = lower(u.email) + WHERE posts.primary_author_id = a.id + AND posts.user_id IS NULL + AND a.email IS NOT NULL + """) + + +def downgrade() -> None: + op.drop_column('posts', 'publish_requested') + op.drop_index('ix_posts_user_id', table_name='posts') + op.drop_constraint('fk_posts_user_id', 'posts', type_='foreignkey') + op.drop_column('posts', 'user_id') + op.drop_column('authors', 'email') diff --git a/shared/alembic/versions/e5c3f9a2b1d6_add_tag_groups_and_tag_group_tags.py b/shared/alembic/versions/e5c3f9a2b1d6_add_tag_groups_and_tag_group_tags.py new file mode 100644 index 0000000..5e21e22 --- /dev/null +++ b/shared/alembic/versions/e5c3f9a2b1d6_add_tag_groups_and_tag_group_tags.py @@ -0,0 +1,45 @@ +"""add tag_groups and tag_group_tags + +Revision ID: e5c3f9a2b1d6 +Revises: d4b2e8f1a3c7 +Create Date: 2026-02-08 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'e5c3f9a2b1d6' +down_revision = 'd4b2e8f1a3c7' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'tag_groups', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('slug', sa.String(length=191), nullable=False), + sa.Column('feature_image', sa.Text(), nullable=True), + sa.Column('colour', sa.String(length=32), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=False, server_default='0'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('slug'), + ) + + op.create_table( + 'tag_group_tags', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('tag_group_id', sa.Integer(), nullable=False), + sa.Column('tag_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['tag_group_id'], ['tag_groups.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('tag_group_id', 'tag_id', name='uq_tag_group_tag'), + ) + + +def downgrade() -> None: + op.drop_table('tag_group_tags') + op.drop_table('tag_groups') diff --git a/shared/alembic/versions/f6d4a0b2c3e7_add_domain_events_table.py b/shared/alembic/versions/f6d4a0b2c3e7_add_domain_events_table.py new file mode 100644 index 0000000..edd0ffb --- /dev/null +++ b/shared/alembic/versions/f6d4a0b2c3e7_add_domain_events_table.py @@ -0,0 +1,40 @@ +"""add domain_events table + +Revision ID: f6d4a0b2c3e7 +Revises: e5c3f9a2b1d6 +Create Date: 2026-02-11 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = 'f6d4a0b2c3e7' +down_revision = 'e5c3f9a2b1d6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'domain_events', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('event_type', sa.String(128), nullable=False), + sa.Column('aggregate_type', sa.String(64), nullable=False), + sa.Column('aggregate_id', sa.Integer(), nullable=False), + sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('state', sa.String(20), server_default='pending', nullable=False), + sa.Column('attempts', sa.Integer(), server_default='0', nullable=False), + sa.Column('max_attempts', sa.Integer(), server_default='5', nullable=False), + sa.Column('last_error', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_domain_events_event_type', 'domain_events', ['event_type']) + op.create_index('ix_domain_events_state', 'domain_events', ['state']) + + +def downgrade() -> None: + op.drop_index('ix_domain_events_state', table_name='domain_events') + op.drop_index('ix_domain_events_event_type', table_name='domain_events') + op.drop_table('domain_events') diff --git a/shared/alembic/versions/f6d4a1b2c3e7_add_tickets_table.py b/shared/alembic/versions/f6d4a1b2c3e7_add_tickets_table.py new file mode 100644 index 0000000..06a0f76 --- /dev/null +++ b/shared/alembic/versions/f6d4a1b2c3e7_add_tickets_table.py @@ -0,0 +1,47 @@ +"""add tickets table + +Revision ID: f6d4a1b2c3e7 +Revises: e5c3f9a2b1d6 +Create Date: 2026-02-09 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'f6d4a1b2c3e7' +down_revision = 'e5c3f9a2b1d6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'tickets', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('entry_id', sa.Integer(), sa.ForeignKey('calendar_entries.id', ondelete='CASCADE'), nullable=False), + sa.Column('ticket_type_id', sa.Integer(), sa.ForeignKey('ticket_types.id', ondelete='SET NULL'), nullable=True), + sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), nullable=True), + sa.Column('session_id', sa.String(64), nullable=True), + sa.Column('order_id', sa.Integer(), sa.ForeignKey('orders.id', ondelete='SET NULL'), nullable=True), + sa.Column('code', sa.String(64), unique=True, nullable=False), + sa.Column('state', sa.String(20), nullable=False, server_default=sa.text("'reserved'")), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column('checked_in_at', sa.DateTime(timezone=True), nullable=True), + ) + op.create_index('ix_tickets_entry_id', 'tickets', ['entry_id']) + op.create_index('ix_tickets_ticket_type_id', 'tickets', ['ticket_type_id']) + op.create_index('ix_tickets_user_id', 'tickets', ['user_id']) + op.create_index('ix_tickets_session_id', 'tickets', ['session_id']) + op.create_index('ix_tickets_order_id', 'tickets', ['order_id']) + op.create_index('ix_tickets_code', 'tickets', ['code'], unique=True) + op.create_index('ix_tickets_state', 'tickets', ['state']) + + +def downgrade() -> None: + op.drop_index('ix_tickets_state', 'tickets') + op.drop_index('ix_tickets_code', 'tickets') + op.drop_index('ix_tickets_order_id', 'tickets') + op.drop_index('ix_tickets_session_id', 'tickets') + op.drop_index('ix_tickets_user_id', 'tickets') + op.drop_index('ix_tickets_ticket_type_id', 'tickets') + op.drop_index('ix_tickets_entry_id', 'tickets') + op.drop_table('tickets') diff --git a/shared/alembic/versions/g7e5b1c3d4f8_generic_containers.py b/shared/alembic/versions/g7e5b1c3d4f8_generic_containers.py new file mode 100644 index 0000000..7756957 --- /dev/null +++ b/shared/alembic/versions/g7e5b1c3d4f8_generic_containers.py @@ -0,0 +1,115 @@ +"""replace post_id FKs with container_type + container_id + +Revision ID: g7e5b1c3d4f8 +Revises: f6d4a0b2c3e7 +Create Date: 2026-02-11 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'g7e5b1c3d4f8' +down_revision = 'f6d4a0b2c3e7' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # --- calendars: post_id → container_type + container_id --- + op.add_column('calendars', sa.Column('container_type', sa.String(32), nullable=True)) + op.add_column('calendars', sa.Column('container_id', sa.Integer(), nullable=True)) + op.execute("UPDATE calendars SET container_type = 'page', container_id = post_id") + op.alter_column('calendars', 'container_type', nullable=False, server_default=sa.text("'page'")) + op.alter_column('calendars', 'container_id', nullable=False) + op.drop_index('ix_calendars_post_id', table_name='calendars') + op.drop_index('ux_calendars_post_slug_active', table_name='calendars') + op.drop_constraint('calendars_post_id_fkey', 'calendars', type_='foreignkey') + op.drop_column('calendars', 'post_id') + op.create_index('ix_calendars_container', 'calendars', ['container_type', 'container_id']) + op.create_index( + 'ux_calendars_container_slug_active', + 'calendars', + ['container_type', 'container_id', sa.text('lower(slug)')], + unique=True, + postgresql_where=sa.text('deleted_at IS NULL'), + ) + + # --- market_places: post_id → container_type + container_id --- + op.add_column('market_places', sa.Column('container_type', sa.String(32), nullable=True)) + op.add_column('market_places', sa.Column('container_id', sa.Integer(), nullable=True)) + op.execute("UPDATE market_places SET container_type = 'page', container_id = post_id") + op.alter_column('market_places', 'container_type', nullable=False, server_default=sa.text("'page'")) + op.alter_column('market_places', 'container_id', nullable=False) + op.drop_index('ix_market_places_post_id', table_name='market_places') + op.drop_constraint('market_places_post_id_fkey', 'market_places', type_='foreignkey') + op.drop_column('market_places', 'post_id') + op.create_index('ix_market_places_container', 'market_places', ['container_type', 'container_id']) + + # --- page_configs: post_id → container_type + container_id --- + op.add_column('page_configs', sa.Column('container_type', sa.String(32), nullable=True)) + op.add_column('page_configs', sa.Column('container_id', sa.Integer(), nullable=True)) + op.execute("UPDATE page_configs SET container_type = 'page', container_id = post_id") + op.alter_column('page_configs', 'container_type', nullable=False, server_default=sa.text("'page'")) + op.alter_column('page_configs', 'container_id', nullable=False) + op.drop_constraint('page_configs_post_id_fkey', 'page_configs', type_='foreignkey') + op.drop_column('page_configs', 'post_id') + op.create_index('ix_page_configs_container', 'page_configs', ['container_type', 'container_id']) + + # --- calendar_entry_posts: post_id → content_type + content_id --- + op.add_column('calendar_entry_posts', sa.Column('content_type', sa.String(32), nullable=True)) + op.add_column('calendar_entry_posts', sa.Column('content_id', sa.Integer(), nullable=True)) + op.execute("UPDATE calendar_entry_posts SET content_type = 'post', content_id = post_id") + op.alter_column('calendar_entry_posts', 'content_type', nullable=False, server_default=sa.text("'post'")) + op.alter_column('calendar_entry_posts', 'content_id', nullable=False) + op.drop_index('ix_entry_posts_post_id', table_name='calendar_entry_posts') + op.drop_constraint('calendar_entry_posts_post_id_fkey', 'calendar_entry_posts', type_='foreignkey') + op.drop_column('calendar_entry_posts', 'post_id') + op.create_index('ix_entry_posts_content', 'calendar_entry_posts', ['content_type', 'content_id']) + + +def downgrade() -> None: + # --- calendar_entry_posts: restore post_id --- + op.add_column('calendar_entry_posts', sa.Column('post_id', sa.Integer(), nullable=True)) + op.execute("UPDATE calendar_entry_posts SET post_id = content_id WHERE content_type = 'post'") + op.alter_column('calendar_entry_posts', 'post_id', nullable=False) + op.create_foreign_key('calendar_entry_posts_post_id_fkey', 'calendar_entry_posts', 'posts', ['post_id'], ['id'], ondelete='CASCADE') + op.create_index('ix_entry_posts_post_id', 'calendar_entry_posts', ['post_id']) + op.drop_index('ix_entry_posts_content', table_name='calendar_entry_posts') + op.drop_column('calendar_entry_posts', 'content_id') + op.drop_column('calendar_entry_posts', 'content_type') + + # --- page_configs: restore post_id --- + op.add_column('page_configs', sa.Column('post_id', sa.Integer(), nullable=True)) + op.execute("UPDATE page_configs SET post_id = container_id WHERE container_type = 'page'") + op.alter_column('page_configs', 'post_id', nullable=False) + op.create_foreign_key('page_configs_post_id_fkey', 'page_configs', 'posts', ['post_id'], ['id'], ondelete='CASCADE') + op.drop_index('ix_page_configs_container', table_name='page_configs') + op.drop_column('page_configs', 'container_id') + op.drop_column('page_configs', 'container_type') + + # --- market_places: restore post_id --- + op.add_column('market_places', sa.Column('post_id', sa.Integer(), nullable=True)) + op.execute("UPDATE market_places SET post_id = container_id WHERE container_type = 'page'") + op.alter_column('market_places', 'post_id', nullable=False) + op.create_foreign_key('market_places_post_id_fkey', 'market_places', 'posts', ['post_id'], ['id'], ondelete='CASCADE') + op.create_index('ix_market_places_post_id', 'market_places', ['post_id']) + op.drop_index('ix_market_places_container', table_name='market_places') + op.drop_column('market_places', 'container_id') + op.drop_column('market_places', 'container_type') + + # --- calendars: restore post_id --- + op.add_column('calendars', sa.Column('post_id', sa.Integer(), nullable=True)) + op.execute("UPDATE calendars SET post_id = container_id WHERE container_type = 'page'") + op.alter_column('calendars', 'post_id', nullable=False) + op.create_foreign_key('calendars_post_id_fkey', 'calendars', 'posts', ['post_id'], ['id'], ondelete='CASCADE') + op.create_index('ix_calendars_post_id', 'calendars', ['post_id']) + op.create_index( + 'ux_calendars_post_slug_active', + 'calendars', + ['post_id', sa.text('lower(slug)')], + unique=True, + postgresql_where=sa.text('deleted_at IS NULL'), + ) + op.drop_index('ux_calendars_container_slug_active', table_name='calendars') + op.drop_index('ix_calendars_container', table_name='calendars') + op.drop_column('calendars', 'container_id') + op.drop_column('calendars', 'container_type') diff --git a/shared/alembic/versions/h8f6c2d4e5a9_merge_heads.py b/shared/alembic/versions/h8f6c2d4e5a9_merge_heads.py new file mode 100644 index 0000000..769134d --- /dev/null +++ b/shared/alembic/versions/h8f6c2d4e5a9_merge_heads.py @@ -0,0 +1,23 @@ +"""merge heads + +Revision ID: h8f6c2d4e5a9 +Revises: c3d4e5f6a7b8, g7e5b1c3d4f8 +Create Date: 2026-02-11 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'h8f6c2d4e5a9' +down_revision = ('c3d4e5f6a7b8', 'g7e5b1c3d4f8') +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/shared/alembic/versions/i9g7d3e5f6_add_glue_layer_tables.py b/shared/alembic/versions/i9g7d3e5f6_add_glue_layer_tables.py new file mode 100644 index 0000000..781f613 --- /dev/null +++ b/shared/alembic/versions/i9g7d3e5f6_add_glue_layer_tables.py @@ -0,0 +1,98 @@ +"""add glue layer tables (container_relations + menu_nodes) + +Revision ID: i9g7d3e5f6 +Revises: h8f6c2d4e5a9 +Create Date: 2026-02-11 + +""" +from alembic import op +import sqlalchemy as sa + +revision = 'i9g7d3e5f6' +down_revision = 'h8f6c2d4e5a9' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # --- container_relations --- + op.create_table( + 'container_relations', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('parent_type', sa.String(32), nullable=False), + sa.Column('parent_id', sa.Integer(), nullable=False), + sa.Column('child_type', sa.String(32), nullable=False), + sa.Column('child_id', sa.Integer(), nullable=False), + sa.Column('sort_order', sa.Integer(), nullable=False, server_default='0'), + sa.Column('label', sa.String(255), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint( + 'parent_type', 'parent_id', 'child_type', 'child_id', + name='uq_container_relations_parent_child', + ), + ) + op.create_index('ix_container_relations_parent', 'container_relations', ['parent_type', 'parent_id']) + op.create_index('ix_container_relations_child', 'container_relations', ['child_type', 'child_id']) + + # --- menu_nodes --- + op.create_table( + 'menu_nodes', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('container_type', sa.String(32), nullable=False), + sa.Column('container_id', sa.Integer(), nullable=False), + sa.Column('parent_id', sa.Integer(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=False, server_default='0'), + sa.Column('depth', sa.Integer(), nullable=False, server_default='0'), + sa.Column('label', sa.String(255), nullable=False), + sa.Column('slug', sa.String(255), nullable=True), + sa.Column('href', sa.String(1024), nullable=True), + sa.Column('icon', sa.String(64), nullable=True), + sa.Column('feature_image', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['parent_id'], ['menu_nodes.id'], ondelete='SET NULL'), + ) + op.create_index('ix_menu_nodes_container', 'menu_nodes', ['container_type', 'container_id']) + op.create_index('ix_menu_nodes_parent_id', 'menu_nodes', ['parent_id']) + + # --- Backfill container_relations from existing container-pattern tables --- + op.execute(""" + INSERT INTO container_relations (parent_type, parent_id, child_type, child_id, sort_order) + SELECT 'page', container_id, 'calendar', id, 0 + FROM calendars + WHERE deleted_at IS NULL AND container_type = 'page' + """) + op.execute(""" + INSERT INTO container_relations (parent_type, parent_id, child_type, child_id, sort_order) + SELECT 'page', container_id, 'market', id, 0 + FROM market_places + WHERE deleted_at IS NULL AND container_type = 'page' + """) + op.execute(""" + INSERT INTO container_relations (parent_type, parent_id, child_type, child_id, sort_order) + SELECT 'page', container_id, 'page_config', id, 0 + FROM page_configs + WHERE deleted_at IS NULL AND container_type = 'page' + """) + + # --- Backfill menu_nodes from existing menu_items + posts --- + op.execute(""" + INSERT INTO menu_nodes (container_type, container_id, label, slug, feature_image, sort_order) + SELECT 'page', mi.post_id, p.title, p.slug, p.feature_image, mi.sort_order + FROM menu_items mi + JOIN posts p ON mi.post_id = p.id + WHERE mi.deleted_at IS NULL + """) + + +def downgrade() -> None: + op.drop_index('ix_menu_nodes_parent_id', table_name='menu_nodes') + op.drop_index('ix_menu_nodes_container', table_name='menu_nodes') + op.drop_table('menu_nodes') + op.drop_index('ix_container_relations_child', table_name='container_relations') + op.drop_index('ix_container_relations_parent', table_name='container_relations') + op.drop_table('container_relations') diff --git a/shared/alembic/versions/j0h8e4f6g7_drop_cross_domain_fks.py b/shared/alembic/versions/j0h8e4f6g7_drop_cross_domain_fks.py new file mode 100644 index 0000000..fcbd499 --- /dev/null +++ b/shared/alembic/versions/j0h8e4f6g7_drop_cross_domain_fks.py @@ -0,0 +1,51 @@ +"""drop cross-domain FK constraints (events → cart) + +Merge three existing heads and remove: +- calendar_entries.order_id FK → orders.id +- tickets.order_id FK → orders.id + +Columns are kept as plain integers. + +Revision ID: j0h8e4f6g7 +Revises: c3d4e5f6a7b8, i9g7d3e5f6, g7e5b1c3d4f8 +Create Date: 2026-02-14 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'j0h8e4f6g7' +down_revision = ('c3d4e5f6a7b8', 'i9g7d3e5f6', 'g7e5b1c3d4f8') +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.drop_constraint( + 'fk_calendar_entries_order_id', + 'calendar_entries', + type_='foreignkey', + ) + op.drop_constraint( + 'tickets_order_id_fkey', + 'tickets', + type_='foreignkey', + ) + + +def downgrade() -> None: + op.create_foreign_key( + 'fk_calendar_entries_order_id', + 'calendar_entries', + 'orders', + ['order_id'], + ['id'], + ondelete='SET NULL', + ) + op.create_foreign_key( + 'tickets_order_id_fkey', + 'tickets', + 'orders', + ['order_id'], + ['id'], + ondelete='SET NULL', + ) diff --git a/shared/alembic/versions/k1i9f5g7h8_add_federation_tables.py b/shared/alembic/versions/k1i9f5g7h8_add_federation_tables.py new file mode 100644 index 0000000..78af7f6 --- /dev/null +++ b/shared/alembic/versions/k1i9f5g7h8_add_federation_tables.py @@ -0,0 +1,142 @@ +"""add federation tables + +Revision ID: k1i9f5g7h8 +Revises: j0h8e4f6g7 +Create Date: 2026-02-21 + +Creates: +- ap_actor_profiles — AP identity per user +- ap_activities — local + remote AP activities +- ap_followers — remote followers +- ap_inbox_items — raw incoming AP activities +- ap_anchors — OpenTimestamps merkle batches +- ipfs_pins — IPFS content tracking (platform-wide) +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = "k1i9f5g7h8" +down_revision = "j0h8e4f6g7" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # -- ap_anchors (referenced by ap_activities) ---------------------------- + op.create_table( + "ap_anchors", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("merkle_root", sa.String(128), nullable=False), + sa.Column("tree_ipfs_cid", sa.String(128), nullable=True), + sa.Column("ots_proof_cid", sa.String(128), nullable=True), + sa.Column("activity_count", sa.Integer(), nullable=False, server_default="0"), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("bitcoin_txid", sa.String(128), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + # -- ap_actor_profiles --------------------------------------------------- + op.create_table( + "ap_actor_profiles", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("preferred_username", sa.String(64), nullable=False), + sa.Column("display_name", sa.String(255), nullable=True), + sa.Column("summary", sa.Text(), nullable=True), + sa.Column("public_key_pem", sa.Text(), nullable=False), + sa.Column("private_key_pem", sa.Text(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("preferred_username"), + sa.UniqueConstraint("user_id"), + ) + op.create_index("ix_ap_actor_user_id", "ap_actor_profiles", ["user_id"], unique=True) + op.create_index("ix_ap_actor_username", "ap_actor_profiles", ["preferred_username"], unique=True) + + # -- ap_activities ------------------------------------------------------- + op.create_table( + "ap_activities", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("activity_id", sa.String(512), nullable=False), + sa.Column("activity_type", sa.String(64), nullable=False), + sa.Column("actor_profile_id", sa.Integer(), nullable=False), + sa.Column("object_type", sa.String(64), nullable=True), + sa.Column("object_data", postgresql.JSONB(), nullable=True), + sa.Column("published", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("signature", postgresql.JSONB(), nullable=True), + sa.Column("is_local", sa.Boolean(), nullable=False, server_default="true"), + sa.Column("source_type", sa.String(64), nullable=True), + sa.Column("source_id", sa.Integer(), nullable=True), + sa.Column("ipfs_cid", sa.String(128), nullable=True), + sa.Column("anchor_id", sa.Integer(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.ForeignKeyConstraint(["actor_profile_id"], ["ap_actor_profiles.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["anchor_id"], ["ap_anchors.id"], ondelete="SET NULL"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("activity_id"), + ) + op.create_index("ix_ap_activity_actor", "ap_activities", ["actor_profile_id"]) + op.create_index("ix_ap_activity_source", "ap_activities", ["source_type", "source_id"]) + op.create_index("ix_ap_activity_published", "ap_activities", ["published"]) + + # -- ap_followers -------------------------------------------------------- + op.create_table( + "ap_followers", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("actor_profile_id", sa.Integer(), nullable=False), + sa.Column("follower_acct", sa.String(512), nullable=False), + sa.Column("follower_inbox", sa.String(512), nullable=False), + sa.Column("follower_actor_url", sa.String(512), nullable=False), + sa.Column("follower_public_key", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.ForeignKeyConstraint(["actor_profile_id"], ["ap_actor_profiles.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("actor_profile_id", "follower_acct", name="uq_follower_acct"), + ) + op.create_index("ix_ap_follower_actor", "ap_followers", ["actor_profile_id"]) + + # -- ap_inbox_items ------------------------------------------------------ + op.create_table( + "ap_inbox_items", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("actor_profile_id", sa.Integer(), nullable=False), + sa.Column("raw_json", postgresql.JSONB(), nullable=False), + sa.Column("activity_type", sa.String(64), nullable=True), + sa.Column("from_actor", sa.String(512), nullable=True), + sa.Column("state", sa.String(20), nullable=False, server_default="pending"), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("processed_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(["actor_profile_id"], ["ap_actor_profiles.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_ap_inbox_state", "ap_inbox_items", ["state"]) + op.create_index("ix_ap_inbox_actor", "ap_inbox_items", ["actor_profile_id"]) + + # -- ipfs_pins ----------------------------------------------------------- + op.create_table( + "ipfs_pins", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("content_hash", sa.String(128), nullable=False), + sa.Column("ipfs_cid", sa.String(128), nullable=False), + sa.Column("pin_type", sa.String(64), nullable=False), + sa.Column("source_type", sa.String(64), nullable=True), + sa.Column("source_id", sa.Integer(), nullable=True), + sa.Column("size_bytes", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("ipfs_cid"), + ) + op.create_index("ix_ipfs_pin_source", "ipfs_pins", ["source_type", "source_id"]) + op.create_index("ix_ipfs_pin_cid", "ipfs_pins", ["ipfs_cid"], unique=True) + + +def downgrade() -> None: + op.drop_table("ipfs_pins") + op.drop_table("ap_inbox_items") + op.drop_table("ap_followers") + op.drop_table("ap_activities") + op.drop_table("ap_actor_profiles") + op.drop_table("ap_anchors") diff --git a/shared/alembic/versions/l2j0g6h8i9_add_fediverse_tables.py b/shared/alembic/versions/l2j0g6h8i9_add_fediverse_tables.py new file mode 100644 index 0000000..c186bcc --- /dev/null +++ b/shared/alembic/versions/l2j0g6h8i9_add_fediverse_tables.py @@ -0,0 +1,138 @@ +"""add fediverse social tables + +Revision ID: l2j0g6h8i9 +Revises: k1i9f5g7h8 +Create Date: 2026-02-22 + +Creates: +- ap_remote_actors — cached profiles of remote actors +- ap_following — outbound follows (local → remote) +- ap_remote_posts — ingested posts from remote actors +- ap_local_posts — native posts composed in federation UI +- ap_interactions — likes and boosts +- ap_notifications — follow/like/boost/mention/reply notifications +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +revision = "l2j0g6h8i9" +down_revision = "k1i9f5g7h8" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # -- ap_remote_actors -- + op.create_table( + "ap_remote_actors", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("actor_url", sa.String(512), unique=True, nullable=False), + sa.Column("inbox_url", sa.String(512), nullable=False), + sa.Column("shared_inbox_url", sa.String(512), nullable=True), + sa.Column("preferred_username", sa.String(255), nullable=False), + sa.Column("display_name", sa.String(255), nullable=True), + sa.Column("summary", sa.Text, nullable=True), + sa.Column("icon_url", sa.String(512), nullable=True), + sa.Column("public_key_pem", sa.Text, nullable=True), + sa.Column("domain", sa.String(255), nullable=False), + sa.Column("fetched_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + ) + op.create_index("ix_ap_remote_actor_url", "ap_remote_actors", ["actor_url"], unique=True) + op.create_index("ix_ap_remote_actor_domain", "ap_remote_actors", ["domain"]) + + # -- ap_following -- + op.create_table( + "ap_following", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("actor_profile_id", sa.Integer, sa.ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False), + sa.Column("remote_actor_id", sa.Integer, sa.ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=False), + sa.Column("state", sa.String(20), nullable=False, server_default="pending"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("accepted_at", sa.DateTime(timezone=True), nullable=True), + sa.UniqueConstraint("actor_profile_id", "remote_actor_id", name="uq_following"), + ) + op.create_index("ix_ap_following_actor", "ap_following", ["actor_profile_id"]) + op.create_index("ix_ap_following_remote", "ap_following", ["remote_actor_id"]) + + # -- ap_remote_posts -- + op.create_table( + "ap_remote_posts", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("remote_actor_id", sa.Integer, sa.ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=False), + sa.Column("activity_id", sa.String(512), unique=True, nullable=False), + sa.Column("object_id", sa.String(512), unique=True, nullable=False), + sa.Column("object_type", sa.String(64), nullable=False, server_default="Note"), + sa.Column("content", sa.Text, nullable=True), + sa.Column("summary", sa.Text, nullable=True), + sa.Column("url", sa.String(512), nullable=True), + sa.Column("attachment_data", JSONB, nullable=True), + sa.Column("tag_data", JSONB, nullable=True), + sa.Column("in_reply_to", sa.String(512), nullable=True), + sa.Column("conversation", sa.String(512), nullable=True), + sa.Column("published", sa.DateTime(timezone=True), nullable=True), + sa.Column("fetched_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + ) + op.create_index("ix_ap_remote_post_actor", "ap_remote_posts", ["remote_actor_id"]) + op.create_index("ix_ap_remote_post_published", "ap_remote_posts", ["published"]) + op.create_index("ix_ap_remote_post_object", "ap_remote_posts", ["object_id"], unique=True) + + # -- ap_local_posts -- + op.create_table( + "ap_local_posts", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("actor_profile_id", sa.Integer, sa.ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False), + sa.Column("content", sa.Text, nullable=False), + sa.Column("visibility", sa.String(20), nullable=False, server_default="public"), + sa.Column("in_reply_to", sa.String(512), nullable=True), + sa.Column("published", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + ) + op.create_index("ix_ap_local_post_actor", "ap_local_posts", ["actor_profile_id"]) + op.create_index("ix_ap_local_post_published", "ap_local_posts", ["published"]) + + # -- ap_interactions -- + op.create_table( + "ap_interactions", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("actor_profile_id", sa.Integer, sa.ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=True), + sa.Column("remote_actor_id", sa.Integer, sa.ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=True), + sa.Column("post_type", sa.String(20), nullable=False), + sa.Column("post_id", sa.Integer, nullable=False), + sa.Column("interaction_type", sa.String(20), nullable=False), + sa.Column("activity_id", sa.String(512), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + ) + op.create_index("ix_ap_interaction_post", "ap_interactions", ["post_type", "post_id"]) + op.create_index("ix_ap_interaction_actor", "ap_interactions", ["actor_profile_id"]) + op.create_index("ix_ap_interaction_remote", "ap_interactions", ["remote_actor_id"]) + + # -- ap_notifications -- + op.create_table( + "ap_notifications", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("actor_profile_id", sa.Integer, sa.ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False), + sa.Column("notification_type", sa.String(20), nullable=False), + sa.Column("from_remote_actor_id", sa.Integer, sa.ForeignKey("ap_remote_actors.id", ondelete="SET NULL"), nullable=True), + sa.Column("from_actor_profile_id", sa.Integer, sa.ForeignKey("ap_actor_profiles.id", ondelete="SET NULL"), nullable=True), + sa.Column("target_activity_id", sa.Integer, sa.ForeignKey("ap_activities.id", ondelete="SET NULL"), nullable=True), + sa.Column("target_remote_post_id", sa.Integer, sa.ForeignKey("ap_remote_posts.id", ondelete="SET NULL"), nullable=True), + sa.Column("read", sa.Boolean, nullable=False, server_default="false"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + ) + op.create_index("ix_ap_notification_actor", "ap_notifications", ["actor_profile_id"]) + op.create_index("ix_ap_notification_read", "ap_notifications", ["actor_profile_id", "read"]) + op.create_index("ix_ap_notification_created", "ap_notifications", ["created_at"]) + + +def downgrade() -> None: + op.drop_table("ap_notifications") + op.drop_table("ap_interactions") + op.drop_table("ap_local_posts") + op.drop_table("ap_remote_posts") + op.drop_table("ap_following") + op.drop_table("ap_remote_actors") diff --git a/shared/alembic/versions/m3k1h7i9j0_add_activity_bus_columns.py b/shared/alembic/versions/m3k1h7i9j0_add_activity_bus_columns.py new file mode 100644 index 0000000..b61aa5e --- /dev/null +++ b/shared/alembic/versions/m3k1h7i9j0_add_activity_bus_columns.py @@ -0,0 +1,113 @@ +"""add unified event bus columns to ap_activities + +Revision ID: m3k1h7i9j0 +Revises: l2j0g6h8i9 +Create Date: 2026-02-22 + +Adds processing and visibility columns so ap_activities can serve as the +unified event bus for both internal domain events and federation delivery. +""" + +revision = "m3k1h7i9j0" +down_revision = "l2j0g6h8i9" +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade() -> None: + # Add new columns with defaults so existing rows stay valid + op.add_column( + "ap_activities", + sa.Column("actor_uri", sa.String(512), nullable=True), + ) + op.add_column( + "ap_activities", + sa.Column( + "visibility", sa.String(20), + nullable=False, server_default="public", + ), + ) + op.add_column( + "ap_activities", + sa.Column( + "process_state", sa.String(20), + nullable=False, server_default="completed", + ), + ) + op.add_column( + "ap_activities", + sa.Column( + "process_attempts", sa.Integer(), + nullable=False, server_default="0", + ), + ) + op.add_column( + "ap_activities", + sa.Column( + "process_max_attempts", sa.Integer(), + nullable=False, server_default="5", + ), + ) + op.add_column( + "ap_activities", + sa.Column("process_error", sa.Text(), nullable=True), + ) + op.add_column( + "ap_activities", + sa.Column( + "processed_at", sa.DateTime(timezone=True), nullable=True, + ), + ) + + # Backfill actor_uri from the related actor_profile + op.execute( + """ + UPDATE ap_activities a + SET actor_uri = CONCAT( + 'https://', + COALESCE(current_setting('app.ap_domain', true), 'rose-ash.com'), + '/users/', + p.preferred_username + ) + FROM ap_actor_profiles p + WHERE a.actor_profile_id = p.id + AND a.actor_uri IS NULL + """ + ) + + # Make actor_profile_id nullable (internal events have no actor profile) + op.alter_column( + "ap_activities", "actor_profile_id", + existing_type=sa.Integer(), + nullable=True, + ) + + # Index for processor polling + op.create_index( + "ix_ap_activity_process", "ap_activities", ["process_state"], + ) + + +def downgrade() -> None: + op.drop_index("ix_ap_activity_process", table_name="ap_activities") + + # Restore actor_profile_id NOT NULL (remove any rows without it first) + op.execute( + "DELETE FROM ap_activities WHERE actor_profile_id IS NULL" + ) + op.alter_column( + "ap_activities", "actor_profile_id", + existing_type=sa.Integer(), + nullable=False, + ) + + op.drop_column("ap_activities", "processed_at") + op.drop_column("ap_activities", "process_error") + op.drop_column("ap_activities", "process_max_attempts") + op.drop_column("ap_activities", "process_attempts") + op.drop_column("ap_activities", "process_state") + op.drop_column("ap_activities", "visibility") + op.drop_column("ap_activities", "actor_uri") diff --git a/shared/alembic/versions/n4l2i8j0k1_drop_domain_events_table.py b/shared/alembic/versions/n4l2i8j0k1_drop_domain_events_table.py new file mode 100644 index 0000000..3d11dab --- /dev/null +++ b/shared/alembic/versions/n4l2i8j0k1_drop_domain_events_table.py @@ -0,0 +1,46 @@ +"""drop domain_events table + +Revision ID: n4l2i8j0k1 +Revises: m3k1h7i9j0 +Create Date: 2026-02-22 + +The domain_events table is no longer used — all events now flow through +ap_activities with the unified activity bus. +""" + +revision = "n4l2i8j0k1" +down_revision = "m3k1h7i9j0" +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + + +def upgrade() -> None: + op.drop_index("ix_domain_events_state", table_name="domain_events") + op.drop_index("ix_domain_events_event_type", table_name="domain_events") + op.drop_table("domain_events") + + +def downgrade() -> None: + op.create_table( + "domain_events", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column("event_type", sa.String(128), nullable=False), + sa.Column("aggregate_type", sa.String(64), nullable=False), + sa.Column("aggregate_id", sa.Integer(), nullable=False), + sa.Column("payload", JSONB(), nullable=True), + sa.Column("state", sa.String(20), nullable=False, server_default="pending"), + sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"), + sa.Column("max_attempts", sa.Integer(), nullable=False, server_default="5"), + sa.Column("last_error", sa.Text(), nullable=True), + sa.Column( + "created_at", sa.DateTime(timezone=True), + nullable=False, server_default=sa.func.now(), + ), + sa.Column("processed_at", sa.DateTime(timezone=True), nullable=True), + ) + op.create_index("ix_domain_events_event_type", "domain_events", ["event_type"]) + op.create_index("ix_domain_events_state", "domain_events", ["state"]) diff --git a/shared/alembic/versions/o5m3j9k1l2_add_origin_app_column.py b/shared/alembic/versions/o5m3j9k1l2_add_origin_app_column.py new file mode 100644 index 0000000..61e56af --- /dev/null +++ b/shared/alembic/versions/o5m3j9k1l2_add_origin_app_column.py @@ -0,0 +1,35 @@ +"""Add origin_app column to ap_activities + +Revision ID: o5m3j9k1l2 +Revises: n4l2i8j0k1 +Create Date: 2026-02-22 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect as sa_inspect + +revision = "o5m3j9k1l2" +down_revision = "n4l2i8j0k1" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa_inspect(conn) + columns = [c["name"] for c in inspector.get_columns("ap_activities")] + if "origin_app" not in columns: + op.add_column( + "ap_activities", + sa.Column("origin_app", sa.String(64), nullable=True), + ) + # Index is idempotent with if_not_exists + op.create_index( + "ix_ap_activity_origin_app", "ap_activities", ["origin_app"], + if_not_exists=True, + ) + + +def downgrade() -> None: + op.drop_index("ix_ap_activity_origin_app", table_name="ap_activities") + op.drop_column("ap_activities", "origin_app") diff --git a/shared/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py b/shared/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py new file mode 100644 index 0000000..d74a687 --- /dev/null +++ b/shared/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py @@ -0,0 +1,37 @@ +"""Add oauth_codes table + +Revision ID: p6n4k0l2m3 +Revises: o5m3j9k1l2 +Create Date: 2026-02-23 +""" +from alembic import op +import sqlalchemy as sa + +revision = "p6n4k0l2m3" +down_revision = "o5m3j9k1l2" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "oauth_codes", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("code", sa.String(128), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("client_id", sa.String(64), nullable=False), + sa.Column("redirect_uri", sa.String(512), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("used_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + ) + op.create_index("ix_oauth_code_code", "oauth_codes", ["code"], unique=True) + op.create_index("ix_oauth_code_user", "oauth_codes", ["user_id"]) + + +def downgrade() -> None: + op.drop_index("ix_oauth_code_user", table_name="oauth_codes") + op.drop_index("ix_oauth_code_code", table_name="oauth_codes") + op.drop_table("oauth_codes") diff --git a/shared/alembic/versions/q7o5l1m3n4_add_oauth_grants_table.py b/shared/alembic/versions/q7o5l1m3n4_add_oauth_grants_table.py new file mode 100644 index 0000000..b973872 --- /dev/null +++ b/shared/alembic/versions/q7o5l1m3n4_add_oauth_grants_table.py @@ -0,0 +1,41 @@ +"""Add oauth_grants table + +Revision ID: q7o5l1m3n4 +Revises: p6n4k0l2m3 +""" +from alembic import op +import sqlalchemy as sa + +revision = "q7o5l1m3n4" +down_revision = "p6n4k0l2m3" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "oauth_grants", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("token", sa.String(128), unique=True, nullable=False), + sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), + sa.Column("client_id", sa.String(64), nullable=False), + sa.Column("issuer_session", sa.String(128), nullable=False), + sa.Column("device_id", sa.String(128), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True), + ) + op.create_index("ix_oauth_grant_token", "oauth_grants", ["token"], unique=True) + op.create_index("ix_oauth_grant_issuer", "oauth_grants", ["issuer_session"]) + op.create_index("ix_oauth_grant_user", "oauth_grants", ["user_id"]) + op.create_index("ix_oauth_grant_device", "oauth_grants", ["device_id", "client_id"]) + + # Add grant_token column to oauth_codes to link code → grant + op.add_column("oauth_codes", sa.Column("grant_token", sa.String(128), nullable=True)) + + +def downgrade(): + op.drop_column("oauth_codes", "grant_token") + op.drop_index("ix_oauth_grant_user", table_name="oauth_grants") + op.drop_index("ix_oauth_grant_issuer", table_name="oauth_grants") + op.drop_index("ix_oauth_grant_token", table_name="oauth_grants") + op.drop_table("oauth_grants") diff --git a/shared/alembic/versions/r8p6m2n4o5_add_device_id_to_oauth_grants.py b/shared/alembic/versions/r8p6m2n4o5_add_device_id_to_oauth_grants.py new file mode 100644 index 0000000..6394f17 --- /dev/null +++ b/shared/alembic/versions/r8p6m2n4o5_add_device_id_to_oauth_grants.py @@ -0,0 +1,29 @@ +"""Add device_id column to oauth_grants + +Revision ID: r8p6m2n4o5 +Revises: q7o5l1m3n4 +""" +from alembic import op +import sqlalchemy as sa + +revision = "r8p6m2n4o5" +down_revision = "q7o5l1m3n4" +branch_labels = None +depends_on = None + + +def upgrade(): + # device_id was added to the create_table migration after it had already + # run, so the column is missing from the live DB. Add it now. + op.add_column( + "oauth_grants", + sa.Column("device_id", sa.String(128), nullable=True), + ) + op.create_index( + "ix_oauth_grant_device", "oauth_grants", ["device_id", "client_id"] + ) + + +def downgrade(): + op.drop_index("ix_oauth_grant_device", table_name="oauth_grants") + op.drop_column("oauth_grants", "device_id") diff --git a/shared/alembic/versions/s9q7n3o5p6_add_ap_delivery_log_table.py b/shared/alembic/versions/s9q7n3o5p6_add_ap_delivery_log_table.py new file mode 100644 index 0000000..0635431 --- /dev/null +++ b/shared/alembic/versions/s9q7n3o5p6_add_ap_delivery_log_table.py @@ -0,0 +1,30 @@ +"""Add ap_delivery_log table for idempotent federation delivery + +Revision ID: s9q7n3o5p6 +Revises: r8p6m2n4o5 +""" +from alembic import op +import sqlalchemy as sa + +revision = "s9q7n3o5p6" +down_revision = "r8p6m2n4o5" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "ap_delivery_log", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("activity_id", sa.Integer, sa.ForeignKey("ap_activities.id", ondelete="CASCADE"), nullable=False), + sa.Column("inbox_url", sa.String(512), nullable=False), + sa.Column("status_code", sa.Integer, nullable=True), + sa.Column("delivered_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.UniqueConstraint("activity_id", "inbox_url", name="uq_delivery_activity_inbox"), + ) + op.create_index("ix_ap_delivery_activity", "ap_delivery_log", ["activity_id"]) + + +def downgrade(): + op.drop_index("ix_ap_delivery_activity", table_name="ap_delivery_log") + op.drop_table("ap_delivery_log") diff --git a/shared/alembic/versions/t0r8n4o6p7_add_app_domain_to_ap_followers.py b/shared/alembic/versions/t0r8n4o6p7_add_app_domain_to_ap_followers.py new file mode 100644 index 0000000..3f1c8d0 --- /dev/null +++ b/shared/alembic/versions/t0r8n4o6p7_add_app_domain_to_ap_followers.py @@ -0,0 +1,51 @@ +"""Add app_domain to ap_followers for per-app AP actors + +Revision ID: t0r8n4o6p7 +Revises: s9q7n3o5p6 +""" +from alembic import op +import sqlalchemy as sa + +revision = "t0r8n4o6p7" +down_revision = "s9q7n3o5p6" +branch_labels = None +depends_on = None + + +def upgrade(): + # Add column as nullable first so we can backfill + op.add_column( + "ap_followers", + sa.Column("app_domain", sa.String(64), nullable=True), + ) + # Backfill existing rows: all current followers are aggregate + op.execute("UPDATE ap_followers SET app_domain = 'federation' WHERE app_domain IS NULL") + # Now make it NOT NULL with a default + op.alter_column( + "ap_followers", "app_domain", + nullable=False, server_default="federation", + ) + # Replace old unique constraint with one that includes app_domain + op.drop_constraint("uq_follower_acct", "ap_followers", type_="unique") + op.create_unique_constraint( + "uq_follower_acct_app", + "ap_followers", + ["actor_profile_id", "follower_acct", "app_domain"], + ) + op.create_index( + "ix_ap_follower_app_domain", + "ap_followers", + ["actor_profile_id", "app_domain"], + ) + + +def downgrade(): + op.drop_index("ix_ap_follower_app_domain", table_name="ap_followers") + op.drop_constraint("uq_follower_acct_app", "ap_followers", type_="unique") + op.create_unique_constraint( + "uq_follower_acct", + "ap_followers", + ["actor_profile_id", "follower_acct"], + ) + op.alter_column("ap_followers", "app_domain", nullable=True, server_default=None) + op.drop_column("ap_followers", "app_domain") diff --git a/shared/alembic/versions/u1s9o5p7q8_add_app_domain_to_delivery_log.py b/shared/alembic/versions/u1s9o5p7q8_add_app_domain_to_delivery_log.py new file mode 100644 index 0000000..1306c9f --- /dev/null +++ b/shared/alembic/versions/u1s9o5p7q8_add_app_domain_to_delivery_log.py @@ -0,0 +1,33 @@ +"""Add app_domain to ap_delivery_log for per-domain idempotency + +Revision ID: u1s9o5p7q8 +Revises: t0r8n4o6p7 +""" +from alembic import op +import sqlalchemy as sa + +revision = "u1s9o5p7q8" +down_revision = "t0r8n4o6p7" + + +def upgrade() -> None: + op.add_column( + "ap_delivery_log", + sa.Column("app_domain", sa.String(128), nullable=False, server_default="federation"), + ) + op.drop_constraint("uq_delivery_activity_inbox", "ap_delivery_log", type_="unique") + op.create_unique_constraint( + "uq_delivery_activity_inbox_domain", + "ap_delivery_log", + ["activity_id", "inbox_url", "app_domain"], + ) + + +def downgrade() -> None: + op.drop_constraint("uq_delivery_activity_inbox_domain", "ap_delivery_log", type_="unique") + op.drop_column("ap_delivery_log", "app_domain") + op.create_unique_constraint( + "uq_delivery_activity_inbox", + "ap_delivery_log", + ["activity_id", "inbox_url"], + ) diff --git a/shared/browser/__init__.py b/shared/browser/__init__.py new file mode 100644 index 0000000..6ded1e5 --- /dev/null +++ b/shared/browser/__init__.py @@ -0,0 +1 @@ +# suma_browser package diff --git a/shared/browser/app/__init__.py b/shared/browser/app/__init__.py new file mode 100644 index 0000000..7d0bf9b --- /dev/null +++ b/shared/browser/app/__init__.py @@ -0,0 +1,12 @@ +# The monolith has been split into three apps (apps/blog, apps/market, apps/cart). +# This package remains for shared infrastructure modules (middleware, redis_cacher, +# csrf, errors, authz, filters, utils, bp/*). +# +# To run individual apps: +# hypercorn apps.blog.app:app --bind 0.0.0.0:8000 +# hypercorn apps.market.app:app --bind 0.0.0.0:8001 +# hypercorn apps.cart.app:app --bind 0.0.0.0:8002 +# +# Legacy single-process: +# hypercorn suma_browser.app.app:app --bind 0.0.0.0:8000 +# (runs the old monolith from app.py, which still works) diff --git a/shared/browser/app/authz.py b/shared/browser/app/authz.py new file mode 100644 index 0000000..864e4ff --- /dev/null +++ b/shared/browser/app/authz.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from functools import wraps +from typing import Any, Dict, Iterable, Optional +import inspect + +from quart import g, abort, redirect, request, current_app +from shared.infrastructure.urls import login_url + + +def require_rights(*rights: str, any_of: bool = True): + """ + Decorator for routes that require certain user rights. + """ + + if not rights: + raise ValueError("require_rights needs at least one right name") + + required_set = frozenset(rights) + + def decorator(view_func): + @wraps(view_func) + async def wrapper(*args: Any, **kwargs: Any): + # Not logged in → go to login, with ?next= + user = g.get("user") + if not user: + return redirect(login_url(request.url)) + + rights_dict = g.get("rights") or {} + + if any_of: + allowed = any(rights_dict.get(name) for name in required_set) + else: + allowed = all(rights_dict.get(name) for name in required_set) + + if not allowed: + abort(403) + + result = view_func(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result + + # ---- expose access requirements on the wrapper ---- + wrapper.__access_requires__ = { + "rights": required_set, + "any_of": any_of, + } + + return wrapper + + return decorator + + +def require_login(view_func): + """ + Decorator for routes that require any logged-in user. + """ + @wraps(view_func) + async def wrapper(*args: Any, **kwargs: Any): + user = g.get("user") + if not user: + return redirect(login_url(request.url)) + result = view_func(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result + return wrapper + + +def require_admin(view_func=None): + """ + Shortcut for routes that require the 'admin' right. + """ + if view_func is None: + return require_rights("admin") + + return require_rights("admin")(view_func) + +def require_post_author(view_func): + """Allow admin or post owner.""" + @wraps(view_func) + async def wrapper(*args, **kwargs): + user = g.get("user") + if not user: + return redirect(login_url(request.url)) + is_admin = bool((g.get("rights") or {}).get("admin")) + if is_admin: + result = view_func(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result + post = getattr(g, "post_data", {}).get("original_post") + if post and post.user_id == user.id: + result = view_func(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result + abort(403) + return wrapper + + +def _get_access_meta(view_func) -> Optional[Dict[str, Any]]: + """ + Walk the wrapper chain looking for __access_requires__ metadata. + """ + func = view_func + seen: set[int] = set() + + while func is not None and id(func) not in seen: + seen.add(id(func)) + meta = getattr(func, "__access_requires__", None) + if meta is not None: + return meta + func = getattr(func, "__wrapped__", None) + + return None + + +def has_access(endpoint: str) -> bool: + """ + Return True if the current user has access to the given endpoint. + + Example: + has_access("settings.home") + has_access("settings.clear_cache_view") + """ + view = current_app.view_functions.get(endpoint) + if view is None: + # Unknown endpoint: be conservative + return False + + meta = _get_access_meta(view) + + # If the route has no rights metadata, treat it as public: + if meta is None: + return True + + required: Iterable[str] = meta["rights"] + any_of: bool = meta["any_of"] + + # Must be in a request context; if no user, they don't have access + user = g.get("user") + if not user: + return False + + rights_dict = g.get("rights") or {} + + if any_of: + return any(rights_dict.get(name) for name in required) + else: + return all(rights_dict.get(name) for name in required) diff --git a/shared/browser/app/csrf.py b/shared/browser/app/csrf.py new file mode 100644 index 0000000..bfd898d --- /dev/null +++ b/shared/browser/app/csrf.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import secrets +from typing import Callable, Awaitable, Optional + +from quart import ( + abort, + current_app, + request, + session as qsession, +) + +SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"} + + +def generate_csrf_token() -> str: + """ + Per-session CSRF token. + + In Jinja: + + """ + token = qsession.get("csrf_token") + if not token: + token = secrets.token_urlsafe(32) + qsession["csrf_token"] = token + return token + + +def _is_exempt_endpoint() -> bool: + endpoint = request.endpoint + if not endpoint: + return False + view = current_app.view_functions.get(endpoint) + + # Walk decorator stack (__wrapped__) to find csrf_exempt + while view is not None: + if getattr(view, "_csrf_exempt", False): + return True + view = getattr(view, "__wrapped__", None) + + return False + + +async def protect() -> None: + """ + Enforce CSRF on unsafe methods. + + Supports: + * Forms: hidden input "csrf_token" + * JSON: "csrf_token" or "csrfToken" field + * HTMX/AJAX: "X-CSRFToken" or "X-CSRF-Token" header + """ + if request.method in SAFE_METHODS: + return + + if _is_exempt_endpoint(): + return + + session_token = qsession.get("csrf_token") + if not session_token: + abort(400, "Missing CSRF session token") + + supplied_token: Optional[str] = None + + # JSON body + if request.mimetype == "application/json": + data = await request.get_json(silent=True) or {} + supplied_token = data.get("csrf_token") or data.get("csrfToken") + + # Form body + if not supplied_token and request.mimetype != "application/json": + form = await request.form + supplied_token = form.get("csrf_token") + + # Headers (HTMX / fetch) + if not supplied_token: + supplied_token = ( + request.headers.get("X-CSRFToken") + or request.headers.get("X-CSRF-Token") + ) + + if not supplied_token or supplied_token != session_token: + abort(400, "Invalid CSRF token") + + +def csrf_exempt(view: Callable[..., Awaitable]) -> Callable[..., Awaitable]: + """ + Mark a view as CSRF-exempt. + + from suma_browser.app.csrf import csrf_exempt + + @csrf_exempt + @blueprint.post("/hook") + async def webhook(): + ... + """ + setattr(view, "_csrf_exempt", True) + return view diff --git a/shared/browser/app/errors.py b/shared/browser/app/errors.py new file mode 100644 index 0000000..bb8cdf2 --- /dev/null +++ b/shared/browser/app/errors.py @@ -0,0 +1,126 @@ +from werkzeug.exceptions import HTTPException +from shared.utils import hx_fragment_request + +from quart import ( + request, + render_template, + make_response, + current_app +) + +from markupsafe import escape + +class AppError(ValueError): + """ + Base class for app-level, client-safe errors. + Behaves like ValueError so existing except ValueError: still works. + """ + status_code: int = 400 + + def __init__(self, message, *, status_code: int | None = None): + # Support a single message or a list/tuple of messages + if isinstance(message, (list, tuple, set)): + self.messages = [str(m) for m in message] + msg = self.messages[0] if self.messages else "" + else: + self.messages = [str(message)] + msg = str(message) + + super().__init__(msg) + + if status_code is not None: + self.status_code = status_code + + +def errors(app): + def _info(e): + return { + "exception": e, + "method": request.method, + "url": str(request.url), + "base_url": str(request.base_url), + "root_path": request.root_path, + "path": request.path, + "full_path": request.full_path, + "endpoint": request.endpoint, + "url_rule": str(request.url_rule) if request.url_rule else None, + "headers": {k: v for k, v in request.headers.items() + if k.lower().startswith("x-forwarded") or k in ("Host",)}, + } + + @app.errorhandler(404) + async def not_found(e): + current_app.logger.warning("404 %s", _info(e)) + if hx_fragment_request(): + html = await render_template( + "_types/root/exceptions/hx/_.html", + errnum='404' + ) + else: + html = await render_template( + "_types/root/exceptions/_.html", + errnum='404', + ) + + return await make_response(html, 404) + + @app.errorhandler(403) + async def not_allowed(e): + current_app.logger.warning("403 %s", _info(e)) + if hx_fragment_request(): + html = await render_template( + "_types/root/exceptions/hx/_.html", + errnum='403' + ) + else: + html = await render_template( + "_types/root/exceptions/_.html", + errnum='403', + ) + + return await make_response(html, 403) + + @app.errorhandler(AppError) + async def app_error(e: AppError): + # App-level, client-safe errors + current_app.logger.info("AppError %s", _info(e)) + status = getattr(e, "status_code", 400) + messages = getattr(e, "messages", [str(e)]) + + if request.headers.get("HX-Request") == "true": + # Build a little styled
    • ...
    snippet + lis = "".join( + f"
  • {escape(m)}
  • " + for m in messages if m + ) + html = ( + "
      " + f"{lis}" + "
    " + ) + return await make_response(html, status) + + # Non-HTMX: show a nicer page with error messages + html = await render_template( + "_types/root/exceptions/app_error.html", + messages=messages, + ) + return await make_response(html, status) + + @app.errorhandler(Exception) + async def error(e): + current_app.logger.exception("Exception %s", _info(e)) + + status = 500 + if isinstance(e, HTTPException): + status = e.code or 500 + + if request.headers.get("HX-Request") == "true": + # Generic message for unexpected/untrusted errors + return await make_response( + "Something went wrong. Please try again.", + status, + ) + + html = await render_template("_types/root/exceptions/error.html") + return await make_response(html, status) diff --git a/shared/browser/app/filters/__init__.py b/shared/browser/app/filters/__init__.py new file mode 100644 index 0000000..4e34162 --- /dev/null +++ b/shared/browser/app/filters/__init__.py @@ -0,0 +1,17 @@ +def register(app): + from .highlight import highlight + app.jinja_env.filters["highlight"] = highlight + + from .qs import register as qs + from .url_join import register as url_join + from .combine import register as combine + from .currency import register as currency + from .truncate import register as truncate + from .getattr import register as getattr + + qs(app) + url_join(app) + combine(app) + currency(app) + getattr(app) + # truncate(app) \ No newline at end of file diff --git a/shared/browser/app/filters/combine.py b/shared/browser/app/filters/combine.py new file mode 100644 index 0000000..9edf07b --- /dev/null +++ b/shared/browser/app/filters/combine.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from typing import Any, Mapping + +def _deep_merge(dst: dict, src: Mapping) -> dict: + out = dict(dst) + for k, v in src.items(): + if isinstance(v, Mapping) and isinstance(out.get(k), Mapping): + out[k] = _deep_merge(out[k], v) # type: ignore[arg-type] + else: + out[k] = v + return out +def register(app): + @app.template_filter("combine") + def combine_filter(a: Any, b: Any, deep: bool = False, drop_none: bool = False) -> Any: + """ + Jinja filter: merge two dict-like objects. + + - Non-dict inputs: returns `a` unchanged. + - If drop_none=True, keys in `b` with value None are ignored. + - If deep=True, nested dicts are merged recursively. + """ + if not isinstance(a, Mapping) or not isinstance(b, Mapping): + return a + b2 = {k: v for k, v in b.items() if not (drop_none and v is None)} + return _deep_merge(a, b2) if deep else {**a, **b2} \ No newline at end of file diff --git a/shared/browser/app/filters/currency.py b/shared/browser/app/filters/currency.py new file mode 100644 index 0000000..0309b9b --- /dev/null +++ b/shared/browser/app/filters/currency.py @@ -0,0 +1,12 @@ +from decimal import Decimal + +def register(app): + @app.template_filter("currency") + def currency_filter(value, code="GBP"): + if value is None: + return "" + # ensure decimal-ish + if isinstance(value, float): + value = Decimal(str(value)) + symbol = "£" if code == "GBP" else code + return f"{symbol}{value:.2f}" diff --git a/shared/browser/app/filters/getattr.py b/shared/browser/app/filters/getattr.py new file mode 100644 index 0000000..7d98684 --- /dev/null +++ b/shared/browser/app/filters/getattr.py @@ -0,0 +1,6 @@ + +def register(app): + @app.template_filter("getattr") + def jinja_getattr(obj, name, default=None): + # Safe getattr: returns default if the attribute is missing + return getattr(obj, name, default) diff --git a/shared/browser/app/filters/highlight.py b/shared/browser/app/filters/highlight.py new file mode 100644 index 0000000..876a10b --- /dev/null +++ b/shared/browser/app/filters/highlight.py @@ -0,0 +1,21 @@ +# ---------- misc helpers / filters ---------- +from markupsafe import Markup, escape + +def highlight(text: str, needle: str, cls: str = "bg-yellow-200 rounded") -> Markup: + """ + Wraps case-insensitive matches of `needle` inside . + Escapes everything safely. + """ + import re + if not text or not needle: + return Markup(escape(text or "")) + + pattern = re.compile(re.escape(needle), re.IGNORECASE) + + def repl(m: re.Match) -> str: + return f'{escape(m.group(0))}' + + esc = escape(text) + result = pattern.sub(lambda m: Markup(repl(m)), esc) + return Markup(result) + diff --git a/shared/browser/app/filters/qs.py b/shared/browser/app/filters/qs.py new file mode 100644 index 0000000..49d3b5d --- /dev/null +++ b/shared/browser/app/filters/qs.py @@ -0,0 +1,13 @@ +from typing import Dict +from quart import g + +def register(app): + @app.template_filter("qs") + def qs_filter(dict: Dict): + if getattr(g, "makeqs_factory", False): + q= g.makeqs_factory()( + **dict, + ) + return q + else: + return "" diff --git a/shared/browser/app/filters/qs_base.py b/shared/browser/app/filters/qs_base.py new file mode 100644 index 0000000..6a8a8b5 --- /dev/null +++ b/shared/browser/app/filters/qs_base.py @@ -0,0 +1,78 @@ +""" +Shared query-string primitives used by blog, market, and order qs modules. +""" +from __future__ import annotations + +from urllib.parse import urlencode + +# Sentinel meaning "leave value as-is" (used as default arg in makeqs) +KEEP = object() + + +def _iterify(x): + """Normalize *x* to a list: None → [], scalar → [scalar], iterable → as-is.""" + if x is None: + return [] + if isinstance(x, (list, tuple, set)): + return x + return [x] + + +def _norm(s: str) -> str: + """Strip + lowercase — used for case-insensitive filter dedup.""" + return s.strip().lower() + + +def make_filter_set( + base: list[str], + add, + remove, + clear_filters: bool, + *, + single_select: bool = False, +) -> list[str]: + """ + Build a deduplicated, sorted filter list. + + Parameters + ---------- + base : list[str] + Current filter values. + add : str | list | None + Value(s) to add. + remove : str | list | None + Value(s) to remove. + clear_filters : bool + If True, start from empty instead of *base*. + single_select : bool + If True, *add* **replaces** the list (blog tags/authors). + If False, *add* is **appended** (market brands/stickers/labels). + """ + add_list = [s for s in _iterify(add) if s is not None] + + if single_select: + # Blog-style: adding replaces the entire set + if add_list: + table = {_norm(s): s for s in add_list} + else: + table = {_norm(s): s for s in base if not clear_filters} + else: + # Market-style: adding appends to the existing set + table = {_norm(s): s for s in base if not clear_filters} + for s in add_list: + k = _norm(s) + if k not in table: + table[k] = s + + for s in _iterify(remove): + if s is None: + continue + table.pop(_norm(s), None) + + return [table[k] for k in sorted(table)] + + +def build_qs(params: list[tuple[str, str]], *, leading_q: bool = True) -> str: + """URL-encode *params* and optionally prepend ``?``.""" + qs = urlencode(params, doseq=True) + return ("?" + qs) if (qs and leading_q) else qs diff --git a/shared/browser/app/filters/query_types.py b/shared/browser/app/filters/query_types.py new file mode 100644 index 0000000..3a7482c --- /dev/null +++ b/shared/browser/app/filters/query_types.py @@ -0,0 +1,33 @@ +""" +NamedTuple types returned by each blueprint's ``decode()`` function. +""" +from __future__ import annotations + +from typing import NamedTuple + + +class BlogQuery(NamedTuple): + page: int + search: str | None + sort: str | None + selected_tags: tuple[str, ...] + selected_authors: tuple[str, ...] + liked: str | None + view: str | None + drafts: str | None + selected_groups: tuple[str, ...] + + +class MarketQuery(NamedTuple): + page: int + search: str | None + sort: str | None + selected_brands: tuple[str, ...] + selected_stickers: tuple[str, ...] + selected_labels: tuple[str, ...] + liked: str | None + + +class OrderQuery(NamedTuple): + page: int + search: str | None diff --git a/shared/browser/app/filters/truncate.py b/shared/browser/app/filters/truncate.py new file mode 100644 index 0000000..754851a --- /dev/null +++ b/shared/browser/app/filters/truncate.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +def register(app): + @app.template_filter("truncate") + def truncate(text, max_length=100): + """ + Truncate text to max_length characters and add an ellipsis character (…) + if it was longer. + """ + if text is None: + return "" + + text = str(text) + + if len(text) <= max_length: + return text + + # Leave space for the ellipsis itself + if max_length <= 1: + return "…" + + return text[:max_length - 1] + "…" \ No newline at end of file diff --git a/shared/browser/app/filters/url_join.py b/shared/browser/app/filters/url_join.py new file mode 100644 index 0000000..120d7fe --- /dev/null +++ b/shared/browser/app/filters/url_join.py @@ -0,0 +1,19 @@ +from typing import Iterable, Union + +from shared.utils import join_url, host_url, _join_url_parts, route_prefix + + +# --- Register as a Jinja filter (Quart / Flask) --- +def register(app): + @app.template_filter("urljoin") + def urljoin_filter(value: Union[str, Iterable[str]]): + return join_url(value) + @app.template_filter("urlhost") + def urlhost_filter(value: Union[str, Iterable[str]]): + return host_url(value) + @app.template_filter("urlhost_no_slash") + def urlhost_no_slash_filter(value: Union[str, Iterable[str]]): + return host_url(value, True) + @app.template_filter("host") + def host_filter(value: str): + return _join_url_parts([route_prefix(), value]) diff --git a/shared/browser/app/middleware.py b/shared/browser/app/middleware.py new file mode 100644 index 0000000..bb156d4 --- /dev/null +++ b/shared/browser/app/middleware.py @@ -0,0 +1,58 @@ + +def register(app): + import json + from typing import Any + + def _decode_headers(scope) -> dict[str, str]: + out = {} + for k, v in scope.get("headers", []): + try: + out[k.decode("latin1")] = v.decode("latin1") + except Exception: + out[repr(k)] = repr(v) + return out + + def _safe(obj: Any): + # make scope json-serialisable; fall back to repr() + try: + json.dumps(obj) + return obj + except Exception: + return repr(obj) + + class ScopeDumpMiddleware: + def __init__(self, app, *, log_bodies: bool = False): + self.app = app + self.log_bodies = log_bodies # keep False; bodies aren't needed for routing + + async def __call__(self, scope, receive, send): + if scope["type"] in ("http", "websocket"): + # Build a compact view of keys relevant to routing + scope_view = { + "type": scope.get("type"), + "asgi": scope.get("asgi"), + "http_version": scope.get("http_version"), + "scheme": scope.get("scheme"), + "method": scope.get("method"), + "server": scope.get("server"), + "client": scope.get("client"), + "root_path": scope.get("root_path"), + "path": scope.get("path"), + "raw_path": scope.get("raw_path").decode("latin1") if scope.get("raw_path") else None, + "query_string": scope.get("query_string", b"").decode("latin1"), + "headers": _decode_headers(scope), + } + + print("\n=== ASGI SCOPE (routing) ===") + print(json.dumps({_safe(k): _safe(v) for k, v in scope_view.items()}, indent=2)) + print("=== END SCOPE ===\n", flush=True) + + return await self.app(scope, receive, send) + + # wrap LAST so you see what hits Quart + #app.asgi_app = ScopeDumpMiddleware(app.asgi_app) + + + from hypercorn.middleware import ProxyFixMiddleware + # trust a single proxy hop; use legacy X-Forwarded-* headers + app.asgi_app = ProxyFixMiddleware(app.asgi_app, mode="legacy", trusted_hops=1) diff --git a/shared/browser/app/payments/__init__.py b/shared/browser/app/payments/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/shared/browser/app/payments/__init__.py @@ -0,0 +1 @@ + diff --git a/shared/browser/app/payments/sumup.py b/shared/browser/app/payments/sumup.py new file mode 100644 index 0000000..7c15852 --- /dev/null +++ b/shared/browser/app/payments/sumup.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import os +from typing import Any, Dict, TYPE_CHECKING + +import httpx +from quart import current_app + +from shared.config import config + +if TYPE_CHECKING: + from shared.models.order import Order + +SUMUP_BASE_URL = "https://api.sumup.com/v0.1" + + +def _sumup_settings() -> Dict[str, str]: + cfg = config() + sumup_cfg = cfg.get("sumup", {}) or {} + api_key_env = sumup_cfg.get("api_key_env", "SUMUP_API_KEY") + api_key = os.getenv(api_key_env) + if not api_key: + raise RuntimeError(f"Missing SumUp API key in environment variable {api_key_env}") + + merchant_code = sumup_cfg.get("merchant_code") + prefix = sumup_cfg.get("checkout_prefix", "") + if not merchant_code: + raise RuntimeError("Missing 'sumup.merchant_code' in app-config.yaml") + + currency = sumup_cfg.get("currency", "GBP") + + return { + "api_key": api_key, + "merchant_code": merchant_code, + "currency": currency, + "checkout_reference_prefix": prefix, + } + + +async def create_checkout( + order: Order, + redirect_url: str, + webhook_url: str | None = None, + description: str | None = None, + page_config: Any | None = None, +) -> Dict[str, Any]: + settings = _sumup_settings() + + # Per-page SumUp credentials override globals + if page_config and getattr(page_config, "sumup_api_key", None): + settings["api_key"] = page_config.sumup_api_key + if page_config and getattr(page_config, "sumup_merchant_code", None): + settings["merchant_code"] = page_config.sumup_merchant_code + + # Use stored reference if present, otherwise build it + checkout_reference = order.sumup_reference or f"{settings['checkout_reference_prefix']}{order.id}" + + payload: Dict[str, Any] = { + "checkout_reference": checkout_reference, + "amount": float(order.total_amount), + "currency": settings["currency"], + "merchant_code": settings["merchant_code"], + "description": description or f"Order {order.id} at {current_app.config.get('APP_TITLE', 'Rose Ash')}", + "return_url": webhook_url or redirect_url, + "redirect_url": redirect_url, + "hosted_checkout": {"enabled": True}, + } + headers = { + "Authorization": f"Bearer {settings['api_key']}", + "Content-Type": "application/json", + } + + # Optional: log for debugging + current_app.logger.info( + "Creating SumUp checkout %s for Order %s amount %.2f", + checkout_reference, + order.id, + float(order.total_amount), + ) + + async with httpx.AsyncClient(timeout=15.0) as client: + resp = await client.post(f"{SUMUP_BASE_URL}/checkouts", json=payload, headers=headers) + + if resp.status_code == 409: + # Duplicate checkout — retrieve the existing one by reference + current_app.logger.warning( + "SumUp duplicate checkout for ref %s order %s, fetching existing", + checkout_reference, + order.id, + ) + list_resp = await client.get( + f"{SUMUP_BASE_URL}/checkouts", + params={"checkout_reference": checkout_reference}, + headers=headers, + ) + list_resp.raise_for_status() + items = list_resp.json() + if isinstance(items, list) and items: + return items[0] + if isinstance(items, dict) and items.get("items"): + return items["items"][0] + # Fallback: re-raise original error + resp.raise_for_status() + + if resp.status_code >= 400: + current_app.logger.error( + "SumUp checkout error for ref %s order %s: %s", + checkout_reference, + order.id, + resp.text, + ) + resp.raise_for_status() + data = resp.json() + + return data + + +async def get_checkout(checkout_id: str, page_config: Any | None = None) -> Dict[str, Any]: + """Fetch checkout status/details from SumUp.""" + settings = _sumup_settings() + + if page_config and getattr(page_config, "sumup_api_key", None): + settings["api_key"] = page_config.sumup_api_key + + headers = { + "Authorization": f"Bearer {settings['api_key']}", + "Content-Type": "application/json", + } + + async with httpx.AsyncClient(timeout=10.0) as client: + resp = await client.get(f"{SUMUP_BASE_URL}/checkouts/{checkout_id}", headers=headers) + resp.raise_for_status() + return resp.json() diff --git a/shared/browser/app/redis_cacher.py b/shared/browser/app/redis_cacher.py new file mode 100644 index 0000000..154d410 --- /dev/null +++ b/shared/browser/app/redis_cacher.py @@ -0,0 +1,346 @@ +from __future__ import annotations + +from functools import wraps +from typing import Optional, Literal + +import asyncio + +from quart import ( + Quart, + request, + Response, + g, + current_app, +) +from redis import asyncio as aioredis + +Scope = Literal["user", "global", "anon"] +TagScope = Literal["all", "user"] # for clear_cache + + +# --------------------------------------------------------------------------- +# Redis setup +# --------------------------------------------------------------------------- + +def register(app: Quart) -> None: + @app.before_serving + async def setup_redis() -> None: + if app.config["REDIS_URL"] and app.config["REDIS_URL"] != 'no': + app.redis = aioredis.Redis.from_url( + app.config["REDIS_URL"], + encoding="utf-8", + decode_responses=False, # store bytes + ) + else: + app.redis = False + + @app.after_serving + async def close_redis() -> None: + if app.redis: + await app.redis.close() + # optional: await app.redis.connection_pool.disconnect() + + +def get_redis(): + return current_app.redis + + +# --------------------------------------------------------------------------- +# Key helpers +# --------------------------------------------------------------------------- + +def get_user_id() -> str: + """ + Returns a string id or 'anon'. + Adjust based on your auth system. + """ + user = getattr(g, "user", None) + if user: + return str(user.id) + return "anon" + + +def make_cache_key(cache_user_id: str) -> str: + """ + Build a cache key for this (user/global/anon) + path + query + HTMX status. + + HTMX requests and normal requests get different cache keys because they + return different content (partials vs full pages). + + Keys are namespaced by app name (from CACHE_APP_PREFIX) to avoid + collisions between apps that may share the same paths. + """ + app_prefix = current_app.config.get("CACHE_APP_PREFIX", "app") + path = request.path + qs = request.query_string.decode() if request.query_string else "" + + # Check if this is an HTMX request + is_htmx = request.headers.get("HX-Request", "").lower() == "true" + htmx_suffix = ":htmx" if is_htmx else "" + + if qs: + return f"cache:{app_prefix}:page:{cache_user_id}:{path}?{qs}{htmx_suffix}" + else: + return f"cache:{app_prefix}:page:{cache_user_id}:{path}{htmx_suffix}" + + +def user_set_key(user_id: str) -> str: + """ + Redis set that tracks all cache keys for a given user id. + Only used when scope='user'. + """ + return f"cache:user:{user_id}" + + +def tag_set_key(tag: str) -> str: + """ + Redis set that tracks all cache keys associated with a tag + (across all scopes/users). + """ + return f"cache:tag:{tag}" + + +# --------------------------------------------------------------------------- +# Invalidation helpers +# --------------------------------------------------------------------------- + +async def invalidate_user_cache(user_id: str) -> None: + """ + Delete all cached pages for a specific user (scope='user' caches). + """ + r = get_redis() + if r: + s_key = user_set_key(user_id) + keys = await r.smembers(s_key) # set of bytes + if keys: + await r.delete(*keys) + await r.delete(s_key) + + +async def invalidate_tag_cache(tag: str) -> None: + """ + Delete all cached pages associated with this tag, for all users/scopes. + """ + r = get_redis() + if r: + t_key = tag_set_key(tag) + keys = await r.smembers(t_key) # set of bytes + if keys: + await r.delete(*keys) + await r.delete(t_key) + + +async def invalidate_tag_cache_for_user(tag: str, cache_uid: str) -> None: + r = get_redis() + if not r: + return + + t_key = tag_set_key(tag) + keys = await r.smembers(t_key) # set of bytes + if not keys: + return + + prefix = f"cache:page:{cache_uid}:".encode("utf-8") + + # Filter keys belonging to this cache_uid only + to_delete = [k for k in keys if k.startswith(prefix)] + if not to_delete: + return + + # Delete those page entries + await r.delete(*to_delete) + # Remove them from the tag set (leave other users' keys intact) + await r.srem(t_key, *to_delete) + +async def invalidate_tag_cache_for_current_user(tag: str) -> None: + """ + Convenience helper: delete tag cache for the current user_id (scope='user'). + """ + uid = get_user_id() + await invalidate_tag_cache_for_user(tag, uid) + + +# --------------------------------------------------------------------------- +# Cache decorator for GET +# --------------------------------------------------------------------------- + +def cache_page( + ttl: int = 0, + tag: Optional[str] = None, + scope: Scope = "user", +): + """ + Cache GET responses in Redis. + + ttl: + Seconds to keep the cache. 0 = no expiry. + tag: + Optional tag name used for bulk invalidation via invalidate_tag_cache(). + scope: + "user" → cache per-user (includes 'anon'), tracked in cache:user:{id} + "global" → single cache shared by everyone (no per-user tracking) + "anon" → cache only for anonymous users; logged-in users bypass cache + """ + + def decorator(view): + @wraps(view) + async def wrapper(*args, **kwargs): + r = get_redis() + + if not r or request.method != "GET": + return await view(*args, **kwargs) + uid = get_user_id() + + # Decide who the cache key is keyed on + if scope == "global": + cache_uid = "global" + elif scope == "anon": + # Only cache for anonymous users + if uid != "anon": + return await view(*args, **kwargs) + cache_uid = "anon" + else: # scope == "user" + cache_uid = uid + + key = make_cache_key(cache_uid) + + cached = await r.hgetall(key) + if cached: + body = cached[b"body"] + status = int(cached[b"status"].decode()) + content_type = cached.get(b"content_type", b"text/html").decode() + return Response(body, status=status, content_type=content_type) + + # Not cached, call the view + resp = await view(*args, **kwargs) + + # Normalise: if the view returned a string/bytes, wrap it + if not isinstance(resp, Response): + resp = Response(resp, content_type="text/html") + + # Only cache successful responses + if resp.status_code == 200: + body = await resp.get_data() # bytes + + pipe = r.pipeline() + pipe.hset( + key, + mapping={ + "body": body, + "status": str(resp.status_code), + "content_type": resp.content_type or "text/html", + }, + ) + if ttl: + pipe.expire(key, ttl) + + # Track per-user keys only when scope='user' + if scope == "user": + pipe.sadd(user_set_key(cache_uid), key) + + # Track per-tag keys (all scopes) + if tag: + pipe.sadd(tag_set_key(tag), key) + + await pipe.execute() + + resp.set_data(body) + + return resp + + return wrapper + + return decorator + + +# --------------------------------------------------------------------------- +# Clear cache decorator for POST (or any method) +# --------------------------------------------------------------------------- + +def clear_cache( + *, + tag: Optional[str] = None, + tag_scope: TagScope = "all", + clear_user: bool = False, +): + """ + Decorator for routes that should clear cache after they run. + + Use on POST/PUT/PATCH/DELETE handlers. + + Params: + tag: + If set, will clear caches for this tag. + tag_scope: + "all" → invalidate_tag_cache(tag) (all users/scopes) + "user" → invalidate_tag_cache_for_current_user(tag) + clear_user: + If True, also run invalidate_user_cache(current_user_id). + + Typical usage: + + @bp.post("/posts//edit") + @clear_cache(tag="post.post_detail", tag_scope="all") + async def edit_post(slug): + ... + + @bp.post("/prefs") + @clear_cache(tag="dashboard", tag_scope="user", clear_user=True) + async def update_prefs(): + ... + """ + + def decorator(view): + @wraps(view) + async def wrapper(*args, **kwargs): + # Run the view first + resp = await view(*args, **kwargs) + if get_redis(): + + # Only clear cache if the view succeeded (2xx) + status = getattr(resp, "status_code", None) + if status is None: + # Non-Response return (string, dict) -> treat as success + success = True + else: + success = 200 <= status < 300 + + if not success: + return resp + + # Perform invalidations + tasks = [] + + if clear_user: + uid = get_user_id() + tasks.append(invalidate_user_cache(uid)) + + if tag: + if tag_scope == "all": + tasks.append(invalidate_tag_cache(tag)) + else: # tag_scope == "user" + tasks.append(invalidate_tag_cache_for_current_user(tag)) + + if tasks: + # Run them concurrently + await asyncio.gather(*tasks) + + return resp + + return wrapper + + return decorator + +async def clear_all_cache(prefix: str = "cache:") -> None: + r = get_redis() + if not r: + return + + cursor = 0 + pattern = f"{prefix}*" + while True: + cursor, keys = await r.scan(cursor=cursor, match=pattern, count=500) + if keys: + await r.delete(*keys) + if cursor == 0: + break diff --git a/shared/browser/app/utils/__init__.py b/shared/browser/app/utils/__init__.py new file mode 100644 index 0000000..75b8279 --- /dev/null +++ b/shared/browser/app/utils/__init__.py @@ -0,0 +1,12 @@ +from .parse import ( + parse_time, + parse_cost, + parse_dt +) +from .utils import ( + current_route_relative_path, + current_url_without_page, + vary, +) + +from .utc import utcnow \ No newline at end of file diff --git a/shared/browser/app/utils/htmx.py b/shared/browser/app/utils/htmx.py new file mode 100644 index 0000000..17f80e6 --- /dev/null +++ b/shared/browser/app/utils/htmx.py @@ -0,0 +1,46 @@ +"""HTMX utilities for detecting and handling HTMX requests.""" + +from quart import request + + +def is_htmx_request() -> bool: + """ + Check if the current request is an HTMX request. + + Returns: + bool: True if HX-Request header is present and true + """ + return request.headers.get("HX-Request", "").lower() == "true" + + +def get_htmx_target() -> str | None: + """ + Get the target element ID from HTMX request headers. + + Returns: + str | None: Target element ID or None + """ + return request.headers.get("HX-Target") + + +def get_htmx_trigger() -> str | None: + """ + Get the trigger element ID from HTMX request headers. + + Returns: + str | None: Trigger element ID or None + """ + return request.headers.get("HX-Trigger") + + +def should_return_fragment() -> bool: + """ + Determine if we should return a fragment vs full page. + + For HTMX requests, return fragment. + For normal requests, return full page. + + Returns: + bool: True if fragment should be returned + """ + return is_htmx_request() diff --git a/shared/browser/app/utils/parse.py b/shared/browser/app/utils/parse.py new file mode 100644 index 0000000..ee6d8de --- /dev/null +++ b/shared/browser/app/utils/parse.py @@ -0,0 +1,36 @@ +from datetime import datetime, timezone + +def parse_time(val: str | None): + if not val: + return None + try: + h,m = val.split(':', 1) + from datetime import time + return time(int(h), int(m)) + except Exception: + return None + +def parse_cost(val: str | None): + if not val: + return None + try: + return float(val) + except Exception: + return None + + if not val: + return None + dt = datetime.fromisoformat(val) + # make TZ-aware (assume local if naive; convert to UTC) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt + +def parse_dt(val: str | None) -> datetime | None: + if not val: + return None + dt = datetime.fromisoformat(val) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt + diff --git a/shared/browser/app/utils/utc.py b/shared/browser/app/utils/utc.py new file mode 100644 index 0000000..084886c --- /dev/null +++ b/shared/browser/app/utils/utc.py @@ -0,0 +1,6 @@ +from datetime import datetime, timezone + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + diff --git a/shared/browser/app/utils/utils.py b/shared/browser/app/utils/utils.py new file mode 100644 index 0000000..71cd993 --- /dev/null +++ b/shared/browser/app/utils/utils.py @@ -0,0 +1,51 @@ +from quart import ( + Response, + request, + g, +) +from shared.utils import host_url +from urllib.parse import urlencode + +def current_route_relative_path() -> str: + """ + Returns the current request path relative to the app's mount point (script_root). + """ + + (request.script_root or "").rstrip("/") + path = request.path # excludes query string + + + + if g.root and path.startswith(f"/{g.root}"): + rel = path[len(g.root+1):] + return rel if rel.startswith("/") else "/" + rel + return path # app at / + + +def current_url_without_page() -> str: + """ + Build current URL (host+path+qs) but with ?page= removed. + Used for Hx-Push-Url. + """ + base = host_url(current_route_relative_path()) + + params = request.args.to_dict(flat=False) # keep multivals + params.pop("page", None) + qs = urlencode(params, doseq=True) + + return f"{base}?{qs}" if qs else base + +def vary(resp: Response) -> Response: + """ + Ensure caches/CDNs vary on HX headers so htmx/non-htmx versions don't get mixed. + """ + v = resp.headers.get("Vary", "") + parts = [p.strip() for p in v.split(",") if p.strip()] + for h in ("HX-Request", "X-Origin"): + if h not in parts: + parts.append(h) + if parts: + resp.headers["Vary"] = ", ".join(parts) + return resp + + diff --git a/shared/browser/templates/_oob_elements.html b/shared/browser/templates/_oob_elements.html new file mode 100644 index 0000000..da748da --- /dev/null +++ b/shared/browser/templates/_oob_elements.html @@ -0,0 +1,33 @@ +{% extends oob.oob_extends %} + +{# OOB elements for HTMX navigation - all elements that need updating #} + +{# Import shared OOB macros #} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + +{% block oobs %} + + {% from '_types/root/_n/macros.html' import oob_header with context %} + {{oob_header( + oob.parent_id, + oob.child_id, + oob.header, + )}} + + {% from oob.parent_header import header_row with context %} + {{ header_row(oob=True) }} +{% endblock %} + + +{# Mobile menu - from market/index.html _main_mobile_menu block #} +{% set mobile_nav %} + {% include oob.nav %} +{% endset %} +{{ mobile_menu(mobile_nav) }} + + +{% block content %} + {% include oob.main %} +{% endblock %} + + diff --git a/shared/browser/templates/_types/root/_full_user.html b/shared/browser/templates/_types/root/_full_user.html new file mode 100644 index 0000000..b5f46cc --- /dev/null +++ b/shared/browser/templates/_types/root/_full_user.html @@ -0,0 +1,11 @@ + +{% set href=account_url('/') %} + + + {{g.user.email}} + + \ No newline at end of file diff --git a/shared/browser/templates/_types/root/_hamburger.html b/shared/browser/templates/_types/root/_hamburger.html new file mode 100644 index 0000000..9a30a19 --- /dev/null +++ b/shared/browser/templates/_types/root/_hamburger.html @@ -0,0 +1,13 @@ + +
    + + + + +
    + + diff --git a/shared/browser/templates/_types/root/_head.html b/shared/browser/templates/_types/root/_head.html new file mode 100644 index 0000000..26a487b --- /dev/null +++ b/shared/browser/templates/_types/root/_head.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shared/browser/templates/_types/root/_index.html b/shared/browser/templates/_types/root/_index.html new file mode 100644 index 0000000..5d3e313 --- /dev/null +++ b/shared/browser/templates/_types/root/_index.html @@ -0,0 +1,13 @@ +{% extends '_types/root/index.html' %} +{% from 'macros/glyphs.html' import opener %} + {% from 'macros/title.html' import title with context %} +{% block main_mobile_menu %} +
    + {% block _main_mobile_menu %} + {% include '_types/root/_nav.html' %} + {% include '_types/root/_nav_panel.html' %} + {% endblock %} +
    +{% endblock %} + + diff --git a/shared/browser/templates/_types/root/_n/macros.html b/shared/browser/templates/_types/root/_n/macros.html new file mode 100644 index 0000000..26e6128 --- /dev/null +++ b/shared/browser/templates/_types/root/_n/macros.html @@ -0,0 +1,35 @@ +{% macro header(id=False, oob=False) %} +
    + {{ caller() }} +
    +{% endmacro %} + + +{% macro oob_header(id, child_id, row_macro) %} + {% call header(id=id, oob=True) %} + {% call header() %} + {% from row_macro import header_row with context %} + {{header_row()}} +
    +
    + {% endcall %} + {% endcall %} +{% endmacro %} + + +{% macro index_row(id, row_macro) %} + {% from '_types/root/_n/macros.html' import header with context %} + {% set _caller = caller %} + {% call header() %} + {% from row_macro import header_row with context %} + {{ header_row() }} +
    + {{_caller()}} +
    + {% endcall %} + +{% endmacro %} \ No newline at end of file diff --git a/shared/browser/templates/_types/root/_nav.html b/shared/browser/templates/_types/root/_nav.html new file mode 100644 index 0000000..c220d05 --- /dev/null +++ b/shared/browser/templates/_types/root/_nav.html @@ -0,0 +1,29 @@ +{% set _app_slugs = { + 'cart': cart_url('/'), + 'market': market_url('/'), + 'events': events_url('/'), + 'federation': federation_url('/'), + 'account': account_url('/'), +} %} +{% set _first_seg = request.path.strip('/').split('/')[0] %} + diff --git a/shared/browser/templates/_types/root/_nav_panel.html b/shared/browser/templates/_types/root/_nav_panel.html new file mode 100644 index 0000000..e804082 --- /dev/null +++ b/shared/browser/templates/_types/root/_nav_panel.html @@ -0,0 +1,7 @@ + {% import 'macros/links.html' as links %} + {% if g.rights.admin %} + + + + {% endif %} + \ No newline at end of file diff --git a/shared/browser/templates/_types/root/_oob_menu.html b/shared/browser/templates/_types/root/_oob_menu.html new file mode 100644 index 0000000..b20c124 --- /dev/null +++ b/shared/browser/templates/_types/root/_oob_menu.html @@ -0,0 +1,46 @@ +{# + Shared mobile menu for both base templates and OOB updates + + This macro can be used in two modes: + - oob=true: Outputs full wrapper with hx-swap-oob attribute (for OOB updates) + - oob=false: Outputs just content, assumes wrapper exists (for base templates) + + The caller can pass section-specific nav items via section_nav parameter. +#} + +{% macro mobile_menu(section_nav='', oob=true) %} +{% if oob %} +
    +{% endif %} + +{% if oob %} +
    +{% endif %} +{% endmacro %} + + + + + +{% macro oob_mobile_menu() %} +
    + +
    +{% endmacro %} + + + diff --git a/shared/browser/templates/_types/root/_sign_in.html b/shared/browser/templates/_types/root/_sign_in.html new file mode 100644 index 0000000..d8777ae --- /dev/null +++ b/shared/browser/templates/_types/root/_sign_in.html @@ -0,0 +1,10 @@ + + + + sign in or register + diff --git a/shared/browser/templates/_types/root/exceptions/403/img.html b/shared/browser/templates/_types/root/exceptions/403/img.html new file mode 100644 index 0000000..c171b80 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/403/img.html @@ -0,0 +1 @@ +{{asset_url('errors/403.gif')}} \ No newline at end of file diff --git a/shared/browser/templates/_types/root/exceptions/403/message.html b/shared/browser/templates/_types/root/exceptions/403/message.html new file mode 100644 index 0000000..d8d39d5 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/403/message.html @@ -0,0 +1 @@ +YOU CAN'T DO THAT \ No newline at end of file diff --git a/shared/browser/templates/_types/root/exceptions/404/img.html b/shared/browser/templates/_types/root/exceptions/404/img.html new file mode 100644 index 0000000..fbfefa5 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/404/img.html @@ -0,0 +1 @@ +{{asset_url('errors/404.gif')}} \ No newline at end of file diff --git a/shared/browser/templates/_types/root/exceptions/404/message.html b/shared/browser/templates/_types/root/exceptions/404/message.html new file mode 100644 index 0000000..6647958 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/404/message.html @@ -0,0 +1 @@ +NOT FOUND \ No newline at end of file diff --git a/shared/browser/templates/_types/root/exceptions/_.html b/shared/browser/templates/_types/root/exceptions/_.html new file mode 100644 index 0000000..3e54b0d --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/_.html @@ -0,0 +1,12 @@ +{% extends '_types/root/exceptions/base.html' %} + +{% block error_summary %} +
    + {% include '_types/root/exceptions/' + errnum + '/message.html' %} +
    +{% endblock %} + +{% block error_content %} + +{% endblock %} + diff --git a/shared/browser/templates/_types/root/exceptions/app_error.html b/shared/browser/templates/_types/root/exceptions/app_error.html new file mode 100644 index 0000000..7d062e8 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/app_error.html @@ -0,0 +1,42 @@ +{% extends '_types/root/_index.html' %} + +{% block content %} +
    +
    +
    + + + +
    + +

    + Something went wrong +

    + + {% if messages %} +
    + {% for message in messages %} +
    + {{ message }} +
    + {% endfor %} +
    + {% endif %} + +
    + + + Home + +
    +
    +
    +{% endblock %} diff --git a/shared/browser/templates/_types/root/exceptions/base.html b/shared/browser/templates/_types/root/exceptions/base.html new file mode 100644 index 0000000..7d20283 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/base.html @@ -0,0 +1,17 @@ +{% extends '_types/root/index.html' %} + +{% block content %} +
    + {% block error_summary %} + {% endblock %} +
    +
    + {% block error_content %} + {% endblock %} +
    +{% endblock %} + diff --git a/shared/browser/templates/_types/root/exceptions/error.html b/shared/browser/templates/_types/root/exceptions/error.html new file mode 100644 index 0000000..70a8164 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/error.html @@ -0,0 +1,12 @@ +{% extends '_types/root/exceptions/base.html' %} + +{% block error_summary %} +
    + WELL THIS IS EMBARASSING... +
    +{% endblock %} + +{% block error_content %} + +{% endblock %} + diff --git a/shared/browser/templates/_types/root/exceptions/hx/_.html b/shared/browser/templates/_types/root/exceptions/hx/_.html new file mode 100644 index 0000000..6a916f1 --- /dev/null +++ b/shared/browser/templates/_types/root/exceptions/hx/_.html @@ -0,0 +1,8 @@ +
    +
    + {% include '_types/root/exceptions/' + errnum + '/message.html' %} +
    + + + +
    \ No newline at end of file diff --git a/shared/browser/templates/_types/root/header/_header.html b/shared/browser/templates/_types/root/header/_header.html new file mode 100644 index 0000000..348bba3 --- /dev/null +++ b/shared/browser/templates/_types/root/header/_header.html @@ -0,0 +1,41 @@ +{% set select_colours = " + [.hover-capable_&]:hover:bg-yellow-300 + aria-selected:bg-stone-500 aria-selected:text-white + [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 +"%} +{% import 'macros/links.html' as links %} + +{% macro header_row(oob=False) %} + {% call links.menu_row(id='root-row', oob=oob) %} +
    + {# Cart mini — fetched from cart app as fragment #} + {% if cart_mini_html %} + {{ cart_mini_html | safe }} + {% endif %} + + {# Site title #} +
    + {% from 'macros/title.html' import title with context %} + {{ title('flex justify-center md:justify-start')}} +
    + + {# Desktop nav #} + + {% include '_types/root/_hamburger.html' %} +
    + {% endcall %} + {# Mobile user info #} +
    + {% if auth_menu_html %} + {{ auth_menu_html | safe }} + {% endif %} +
    +{% endmacro %} \ No newline at end of file diff --git a/shared/browser/templates/_types/root/header/_oob.html b/shared/browser/templates/_types/root/header/_oob.html new file mode 100644 index 0000000..45b7240 --- /dev/null +++ b/shared/browser/templates/_types/root/header/_oob.html @@ -0,0 +1,67 @@ +{# + Shared root header for both base templates and OOB updates + + This macro can be used in two modes: + - oob=true: Outputs full div with hx-swap-oob attribute (for OOB updates) + - oob=false: Outputs just content, assumes wrapper div exists (for base templates) + + Usage: + 1. Call root_header_start(oob=true/false) + 2. Add any section-specific headers + 3. Call root_header_end(oob=true/false) +#} + +{% macro root_header_start(oob=true) %} +{% set select_colours = " + [.hover-capable_&]:hover:bg-yellow-300 + aria-selected:bg-stone-500 aria-selected:text-white + [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 +"%} + +{% if oob %} + +{% endif %} +{% endmacro %} diff --git a/shared/browser/templates/_types/root/header/_oob_.html b/shared/browser/templates/_types/root/header/_oob_.html new file mode 100644 index 0000000..772f2ab --- /dev/null +++ b/shared/browser/templates/_types/root/header/_oob_.html @@ -0,0 +1,38 @@ +{# + Shared root header for both base templates and OOB updates + + This macro can be used in two modes: + - oob=true: Outputs full div with hx-swap-oob attribute (for OOB updates) + - oob=false: Outputs just content, assumes wrapper div exists (for base templates) + + Usage: + 1. Call root_header_start(oob=true/false) + 2. Add any section-specific headers + 3. Call root_header_end(oob=true/false) +#} + +{% macro root_header(oob=true) %} +{% set select_colours = " + [.hover-capable_&]:hover:bg-yellow-300 + aria-selected:bg-stone-500 aria-selected:text-white + [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 +"%} + +{% if oob %} + +{% endif %} + +{% endmacro %} + diff --git a/shared/browser/templates/_types/root/index.html b/shared/browser/templates/_types/root/index.html new file mode 100644 index 0000000..06094f3 --- /dev/null +++ b/shared/browser/templates/_types/root/index.html @@ -0,0 +1,84 @@ +{% import 'macros/layout.html' as layout %} +{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %} +{% from '_types/root/_oob_menu.html' import mobile_menu with context %} + + + + + + {% block meta %} + {% include 'social/meta_site.html' %} + {% endblock %} + + {% include '_types/root/_head.html' %} + + +
    + {% block header %} + {% from '_types/root/_n/macros.html' import header with context %} + {% call header() %} + {% call layout.details('/root-header') %} + {% call layout.summary( + 'root-header-summary', + _class='flex items-start gap-2 p-1 + bg-' + menu_colour + '-' + (500-(level()*100))|string, + ) + %} +
    + + {% from '_types/root/header/_header.html' import header_row with context %} + {{ header_row() }} +
    + {% block root_header_child %} + {% endblock %} +
    +
    + + {% endcall %} + {% call layout.menu('root-menu', 'md:hidden bg-yellow-100') %} + {% block main_mobile_menu %} + {% endblock %} + {% endcall %} + {% endcall %} + + + + {% endcall %} + {% endblock %} + + +
    + {% block filter %} + {% endblock %} +
    +
    +
    +
    + + +
    + {% block content %} + {% endblock %} +
    +
    + +
    +
    +
    + +
    + + + diff --git a/shared/browser/templates/_types/root/mobile/_full_user.html b/shared/browser/templates/_types/root/mobile/_full_user.html new file mode 100644 index 0000000..1282e31 --- /dev/null +++ b/shared/browser/templates/_types/root/mobile/_full_user.html @@ -0,0 +1,10 @@ + +{% set href=account_url('/') %} + + + {{g.user.email}} + + \ No newline at end of file diff --git a/shared/browser/templates/_types/root/mobile/_sign_in.html b/shared/browser/templates/_types/root/mobile/_sign_in.html new file mode 100644 index 0000000..3c92646 --- /dev/null +++ b/shared/browser/templates/_types/root/mobile/_sign_in.html @@ -0,0 +1,8 @@ + + + + sign in or register + diff --git a/shared/browser/templates/macros/admin_nav.html b/shared/browser/templates/macros/admin_nav.html new file mode 100644 index 0000000..738a319 --- /dev/null +++ b/shared/browser/templates/macros/admin_nav.html @@ -0,0 +1,21 @@ +{# + Shared admin navigation macro + Use this instead of duplicate _nav.html files +#} + +{% macro admin_nav_item(href, icon='cog', label='', select_colours='', aclass=styles.nav_button) %} + {% import 'macros/links.html' as links %} + {% call links.link(href, hx_select_search, select_colours, True, aclass=aclass) %} + + {{ label }} + {% endcall %} +{% endmacro %} + +{% macro placeholder_nav() %} +{# Placeholder for admin sections without specific nav items #} + +{% endmacro %} diff --git a/shared/browser/templates/macros/cart_icon.html b/shared/browser/templates/macros/cart_icon.html new file mode 100644 index 0000000..7b8a958 --- /dev/null +++ b/shared/browser/templates/macros/cart_icon.html @@ -0,0 +1,31 @@ +{# Cart icon/badge — shows logo when empty, cart icon with count when items present #} + +{% macro cart_icon(count=0, oob=False) %} +
    + {% if count == 0 %} +
    + + + +
    + {% else %} + + + + {{ count }} + + + {% endif %} +
    +{% endmacro %} diff --git a/shared/browser/templates/macros/glyphs.html b/shared/browser/templates/macros/glyphs.html new file mode 100644 index 0000000..0e7e225 --- /dev/null +++ b/shared/browser/templates/macros/glyphs.html @@ -0,0 +1,17 @@ +{% macro opener(group=False) %} + + + + +{% endmacro %} \ No newline at end of file diff --git a/shared/browser/templates/macros/layout.html b/shared/browser/templates/macros/layout.html new file mode 100644 index 0000000..fc648e8 --- /dev/null +++ b/shared/browser/templates/macros/layout.html @@ -0,0 +1,61 @@ +{# templates/macros/layout.html #} + +{% macro details(group = '', _class='') %} +
    + {{ caller() }} +
    +{%- endmacro %} + +{% macro summary(id, _class=None, oob=False) %} + +
    +
    + {{ caller() }} +
    +
    +
    +{%- endmacro %} + +{% macro filter_summary(id, current_local_href, search, search_count, hx_select, oob=True) %} + +
    +
    +
    + + + + + + +
    +
    +
    +
    + {{ caller() }} + +
    +
    + {% from 'macros/search.html' import search_mobile %} + {{ search_mobile(current_local_href, search, search_count, hx_select) }} +
    +
    +{%- endmacro %} + + +{% macro menu(id, _class="") %} +
    + {{ caller() }} +
    +{%- endmacro %} diff --git a/shared/browser/templates/macros/links.html b/shared/browser/templates/macros/links.html new file mode 100644 index 0000000..d80a51d --- /dev/null +++ b/shared/browser/templates/macros/links.html @@ -0,0 +1,59 @@ + + +{% macro link(url, select, select_colours='', highlight=True, _class='', aclass='') %} + {% set href=url|host%} + +{% endmacro %} + + +{% macro menu_row(id=False, oob=False) %} +
    + {{ caller() }} +
    + {{level_up()}} +{% endmacro %} + +{% macro desktop_nav() %} + +{% endmacro %} + +{% macro admin() %} + +
    + settings +
    + +{% endmacro %} \ No newline at end of file diff --git a/shared/browser/templates/macros/scrolling_menu.html b/shared/browser/templates/macros/scrolling_menu.html new file mode 100644 index 0000000..d1a823a --- /dev/null +++ b/shared/browser/templates/macros/scrolling_menu.html @@ -0,0 +1,68 @@ +{# + Scrolling menu macro with arrow navigation + + Creates a horizontally scrollable menu (desktop) or vertically scrollable (mobile) + with arrow buttons that appear/hide based on content overflow. + + Parameters: + - container_id: Unique ID for the scroll container + - items: List of items to iterate over + - item_content: Caller block that renders each item (receives 'item' variable) + - wrapper_class: Optional additional classes for outer wrapper + - container_class: Optional additional classes for scroll container + - item_class: Optional additional classes for each item wrapper +#} + +{% macro scrolling_menu(container_id, items, wrapper_class='', container_class='', item_class='') %} + {% if items %} + {# Left scroll arrow - desktop only #} + + + {# Scrollable container #} +
    +
    + {% for item in items %} +
    + {{ caller(item) }} +
    + {% endfor %} +
    +
    + + + + {# Right scroll arrow - desktop only #} + + {% endif %} +{% endmacro %} diff --git a/shared/browser/templates/macros/search.html b/shared/browser/templates/macros/search.html new file mode 100644 index 0000000..98c0cde --- /dev/null +++ b/shared/browser/templates/macros/search.html @@ -0,0 +1,83 @@ +{# Shared search input macros for filter UIs #} + +{% macro search_mobile(current_local_href, search, search_count, hx_select) -%} +
    + + +
    + {% if search %} + {{search_count}} + {% endif %} +
    +
    +{%- endmacro %} + +{% macro search_desktop(current_local_href, search, search_count, hx_select) -%} +
    + + +
    + {% if search %} + {{search_count}} + {% endif %} + {{zap_filter}} +
    +
    +{%- endmacro %} diff --git a/shared/browser/templates/macros/stickers.html b/shared/browser/templates/macros/stickers.html new file mode 100644 index 0000000..2be5b9f --- /dev/null +++ b/shared/browser/templates/macros/stickers.html @@ -0,0 +1,24 @@ +{% macro sticker(src, title, enabled, size=40, found=false) -%} + + + + {{ title|capitalize }} + + + + + +{%- endmacro -%} + diff --git a/shared/browser/templates/macros/title.html b/shared/browser/templates/macros/title.html new file mode 100644 index 0000000..4477fc2 --- /dev/null +++ b/shared/browser/templates/macros/title.html @@ -0,0 +1,10 @@ +{% macro title(_class='') %} + +

    + {{ site().title }} +

    +
    +{% endmacro %} diff --git a/shared/browser/templates/mobile/menu.html b/shared/browser/templates/mobile/menu.html new file mode 100644 index 0000000..729c141 --- /dev/null +++ b/shared/browser/templates/mobile/menu.html @@ -0,0 +1,5 @@ + +
    +{% block menu %} +{% endblock %} +
    \ No newline at end of file diff --git a/shared/browser/templates/oob_elements.html b/shared/browser/templates/oob_elements.html new file mode 100644 index 0000000..7a6b88a --- /dev/null +++ b/shared/browser/templates/oob_elements.html @@ -0,0 +1,38 @@ + +{% block oobs %} +{% endblock %} + +
    +{% block filter %} +{% endblock %} +
    + + + + + +
    + {% block mobile_menu %} + {% endblock %} +
    + + +
    + {% block content %} + + {% endblock %} + +
    diff --git a/shared/browser/templates/sentinel/desktop_content.html b/shared/browser/templates/sentinel/desktop_content.html new file mode 100644 index 0000000..1bb6127 --- /dev/null +++ b/shared/browser/templates/sentinel/desktop_content.html @@ -0,0 +1,9 @@ +
    + loading… {{ page }} / {{ total_pages }} +
    + + \ No newline at end of file diff --git a/shared/browser/templates/sentinel/mobile_content.html b/shared/browser/templates/sentinel/mobile_content.html new file mode 100644 index 0000000..f4ca68e --- /dev/null +++ b/shared/browser/templates/sentinel/mobile_content.html @@ -0,0 +1,11 @@ + +
    + loading… {{ page }} / {{ total_pages }} +
    + + + \ No newline at end of file diff --git a/shared/browser/templates/sentinel/wireless_error.svg b/shared/browser/templates/sentinel/wireless_error.svg new file mode 100644 index 0000000..7df8fac --- /dev/null +++ b/shared/browser/templates/sentinel/wireless_error.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/shared/browser/templates/social/meta_base.html b/shared/browser/templates/social/meta_base.html new file mode 100644 index 0000000..215768e --- /dev/null +++ b/shared/browser/templates/social/meta_base.html @@ -0,0 +1,54 @@ +{# social/meta_base.html — common, non-conflicting head tags #} +{# Expected context: + site: { title, url, logo, default_image, twitter_site, fb_app_id, description? } + request: Quart request (for canonical derivation) + robots_override: optional string ("index,follow" / "noindex,nofollow") +#} + + + + +{# Canonical #} +{% set _site_url = site().url.rstrip('/') if site and site().url else '' %} +{% set canonical = ( + request.url if request and request.url + else (_site_url ~ request.path if request and _site_url else _site_url or None) +) %} + +{# Robots: allow override; default to index,follow #} + + +{# Theme & RSS #} + +{% if _site_url %} + +{% endif %} + +{# JSON-LD: Organization & WebSite are safe on all pages (don't conflict with BlogPosting) #} +{% set org_jsonld = { + "@context": "https://schema.org", + "@type": "Organization", + "name": site().title if site and site().title else "", + "url": _site_url if _site_url else None, + "logo": site().logo if site and site().logo else None +} %} + + +{% set website_jsonld = { + "@context": "https://schema.org", + "@type": "WebSite", + "name": site().title if site and site().title else "", + "url": _site_url if _site_url else canonical, + "potentialAction": { + "@type": "SearchAction", + "target": (_site_url ~ "/search?q={query}") if _site_url else None, + "query-input": "required name=query" + } +} %} + diff --git a/shared/browser/templates/social/meta_site.html b/shared/browser/templates/social/meta_site.html new file mode 100644 index 0000000..6ccebb7 --- /dev/null +++ b/shared/browser/templates/social/meta_site.html @@ -0,0 +1,25 @@ +{# social/meta_site.html — generic site/page meta #} +{% include 'social/meta_base.html' %} + +{# Title/description (site-level) #} +{% set description = site().description or '' %} + +{{ base_title }} +{% if description %}{% endif %} +{% if canonical %}{% endif %} + +{# Open Graph (website) #} + + + +{% if description %}{% endif %} +{% if canonical %}{% endif %} +{% if site and site().default_image %}{% endif %} +{% if site and site().fb_app_id %}{% endif %} + +{# Twitter (website) #} + +{% if site and site().twitter_site %}{% endif %} + +{% if description %}{% endif %} +{% if site and site().default_image %}{% endif %} diff --git a/shared/config.py b/shared/config.py new file mode 100644 index 0000000..edee631 --- /dev/null +++ b/shared/config.py @@ -0,0 +1,84 @@ +# suma_browser/config.py +from __future__ import annotations + +import asyncio +import os +from types import MappingProxyType +from typing import Any, Optional +import copy +import yaml + +# Default config path (override with APP_CONFIG_FILE) +_DEFAULT_CONFIG_PATH = os.environ.get( + "APP_CONFIG_FILE", + os.path.join(os.getcwd(), "config/app-config.yaml"), +) + +# Module state +_init_lock = asyncio.Lock() +_data_frozen: Any = None # read-only view (mappingproxy / tuples / frozensets) +_data_plain: Any = None # plain builtins for pretty-print / logging + +# ---------------- utils ---------------- +def _freeze(obj: Any) -> Any: + """Deep-freeze containers to read-only equivalents.""" + if isinstance(obj, dict): + # freeze children first, then wrap dict in mappingproxy + return MappingProxyType({k: _freeze(v) for k, v in obj.items()}) + if isinstance(obj, list): + return tuple(_freeze(v) for v in obj) + if isinstance(obj, set): + return frozenset(_freeze(v) for v in obj) + if isinstance(obj, tuple): + return tuple(_freeze(v) for v in obj) + return obj + +# ---------------- API ---------------- +async def init_config(path: Optional[str] = None, *, force: bool = False) -> None: + """ + Load YAML exactly as-is and cache both a frozen (read-only) and a plain copy. + Idempotent; pass force=True to reload. + """ + global _data_frozen, _data_plain + + if _data_frozen is not None and not force: + return + + async with _init_lock: + if _data_frozen is not None and not force: + return + + cfg_path = path or _DEFAULT_CONFIG_PATH + if not os.path.exists(cfg_path): + raise FileNotFoundError(f"Config file not found: {cfg_path}") + + with open(cfg_path, "r", encoding="utf-8") as f: + raw = yaml.safe_load(f) # whatever the YAML root is + + # store plain as loaded; store frozen for normal use + _data_plain = raw + _data_frozen = _freeze(raw) + +def config() -> Any: + """ + Return the read-only (frozen) config. Call init_config() first. + """ + if _data_frozen is None: + raise RuntimeError("init_config() has not been awaited yet.") + return _data_frozen + +def as_plain() -> Any: + """ + Return a deep copy of the plain config for safe external use/pretty printing. + """ + if _data_plain is None: + raise RuntimeError("init_config() has not been awaited yet.") + return copy.deepcopy(_data_plain) + +def pretty() -> str: + """ + YAML pretty string without mappingproxy noise. + """ + if _data_plain is None: + raise RuntimeError("init_config() has not been awaited yet.") + return yaml.safe_dump(_data_plain, sort_keys=False, allow_unicode=True) diff --git a/shared/containers.py b/shared/containers.py new file mode 100644 index 0000000..4e2fff7 --- /dev/null +++ b/shared/containers.py @@ -0,0 +1,20 @@ +""" +Generic container concept — replaces hard-wired post_id FKs +with container_type + container_id soft references. +""" +from __future__ import annotations + + +class ContainerType: + PAGE = "page" + # Future: GROUP = "group", MARKET = "market", etc. + + +def container_filter(model, container_type: str, container_id: int): + """Return SQLAlchemy filter clauses for a container reference.""" + return [model.container_type == container_type, model.container_id == container_id] + + +def content_filter(model, content_type: str, content_id: int): + """Return SQLAlchemy filter clauses for a content reference (e.g. CalendarEntryContent).""" + return [model.content_type == content_type, model.content_id == content_id] diff --git a/shared/contracts/__init__.py b/shared/contracts/__init__.py new file mode 100644 index 0000000..d8cf7bc --- /dev/null +++ b/shared/contracts/__init__.py @@ -0,0 +1,31 @@ +"""Typed contracts (DTOs + Protocols) for cross-domain service interfaces.""" + +from .dtos import ( + PostDTO, + CalendarDTO, + CalendarEntryDTO, + MarketPlaceDTO, + ProductDTO, + CartItemDTO, + CartSummaryDTO, +) +from .protocols import ( + BlogService, + CalendarService, + MarketService, + CartService, +) + +__all__ = [ + "PostDTO", + "CalendarDTO", + "CalendarEntryDTO", + "MarketPlaceDTO", + "ProductDTO", + "CartItemDTO", + "CartSummaryDTO", + "BlogService", + "CalendarService", + "MarketService", + "CartService", +] diff --git a/shared/contracts/dtos.py b/shared/contracts/dtos.py new file mode 100644 index 0000000..cd5c50d --- /dev/null +++ b/shared/contracts/dtos.py @@ -0,0 +1,255 @@ +"""Frozen dataclasses for cross-domain data transfer. + +These are the *only* shapes that cross domain boundaries. Consumers never +see ORM model instances from another domain — only these DTOs. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from decimal import Decimal + + +# --------------------------------------------------------------------------- +# Blog domain +# --------------------------------------------------------------------------- + +@dataclass(frozen=True, slots=True) +class PostDTO: + id: int + slug: str + title: str + status: str + visibility: str + is_page: bool = False + feature_image: str | None = None + html: str | None = None + excerpt: str | None = None + custom_excerpt: str | None = None + published_at: datetime | None = None + + +# --------------------------------------------------------------------------- +# Calendar / Events domain +# --------------------------------------------------------------------------- + +@dataclass(frozen=True, slots=True) +class CalendarDTO: + id: int + container_type: str + container_id: int + name: str + slug: str + description: str | None = None + + +@dataclass(frozen=True, slots=True) +class TicketDTO: + id: int + code: str + state: str + entry_name: str + entry_start_at: datetime + entry_end_at: datetime | None = None + ticket_type_name: str | None = None + calendar_name: str | None = None + created_at: datetime | None = None + checked_in_at: datetime | None = None + entry_id: int | None = None + ticket_type_id: int | None = None + price: Decimal | None = None + order_id: int | None = None + calendar_container_id: int | None = None + + +@dataclass(frozen=True, slots=True) +class CalendarEntryDTO: + id: int + calendar_id: int + name: str + start_at: datetime + state: str + cost: Decimal + end_at: datetime | None = None + user_id: int | None = None + session_id: str | None = None + order_id: int | None = None + slot_id: int | None = None + ticket_price: Decimal | None = None + ticket_count: int | None = None + calendar_name: str | None = None + calendar_slug: str | None = None + calendar_container_id: int | None = None + calendar_container_type: str | None = None + + +# --------------------------------------------------------------------------- +# Market domain +# --------------------------------------------------------------------------- + +@dataclass(frozen=True, slots=True) +class MarketPlaceDTO: + id: int + container_type: str + container_id: int + name: str + slug: str + description: str | None = None + + +@dataclass(frozen=True, slots=True) +class ProductDTO: + id: int + slug: str + title: str | None = None + image: str | None = None + description_short: str | None = None + rrp: Decimal | None = None + regular_price: Decimal | None = None + special_price: Decimal | None = None + + +# --------------------------------------------------------------------------- +# Cart domain +# --------------------------------------------------------------------------- + +@dataclass(frozen=True, slots=True) +class CartItemDTO: + id: int + product_id: int + quantity: int + product_title: str | None = None + product_slug: str | None = None + product_image: str | None = None + unit_price: Decimal | None = None + market_place_id: int | None = None + + +@dataclass(frozen=True, slots=True) +class CartSummaryDTO: + count: int = 0 + total: Decimal = Decimal("0") + calendar_count: int = 0 + calendar_total: Decimal = Decimal("0") + items: list[CartItemDTO] = field(default_factory=list) + ticket_count: int = 0 + ticket_total: Decimal = Decimal("0") + + +# --------------------------------------------------------------------------- +# Federation / ActivityPub domain +# --------------------------------------------------------------------------- + +@dataclass(frozen=True, slots=True) +class ActorProfileDTO: + id: int + user_id: int + preferred_username: str + public_key_pem: str + display_name: str | None = None + summary: str | None = None + inbox_url: str | None = None + outbox_url: str | None = None + created_at: datetime | None = None + + +@dataclass(frozen=True, slots=True) +class APActivityDTO: + id: int + activity_id: str + activity_type: str + actor_profile_id: int + object_type: str | None = None + object_data: dict | None = None + published: datetime | None = None + is_local: bool = True + source_type: str | None = None + source_id: int | None = None + ipfs_cid: str | None = None + + +@dataclass(frozen=True, slots=True) +class APFollowerDTO: + id: int + actor_profile_id: int + follower_acct: str + follower_inbox: str + follower_actor_url: str + created_at: datetime | None = None + app_domain: str = "federation" + + +@dataclass(frozen=True, slots=True) +class APAnchorDTO: + id: int + merkle_root: str + activity_count: int = 0 + tree_ipfs_cid: str | None = None + ots_proof_cid: str | None = None + confirmed_at: datetime | None = None + bitcoin_txid: str | None = None + + +@dataclass(frozen=True, slots=True) +class RemoteActorDTO: + id: int + actor_url: str + inbox_url: str + preferred_username: str + domain: str + display_name: str | None = None + summary: str | None = None + icon_url: str | None = None + shared_inbox_url: str | None = None + public_key_pem: str | None = None + + +@dataclass(frozen=True, slots=True) +class RemotePostDTO: + id: int + remote_actor_id: int + object_id: str + content: str + summary: str | None = None + url: str | None = None + attachments: list[dict] = field(default_factory=list) + tags: list[dict] = field(default_factory=list) + published: datetime | None = None + actor: RemoteActorDTO | None = None + + +@dataclass(frozen=True, slots=True) +class TimelineItemDTO: + id: str # composite key for cursor pagination + post_type: str # "local" | "remote" | "boost" + content: str # HTML + published: datetime + actor_name: str + actor_username: str + object_id: str | None = None + summary: str | None = None + url: str | None = None + attachments: list[dict] = field(default_factory=list) + tags: list[dict] = field(default_factory=list) + actor_domain: str | None = None # None = local + actor_icon: str | None = None + actor_url: str | None = None + boosted_by: str | None = None + like_count: int = 0 + boost_count: int = 0 + liked_by_me: bool = False + boosted_by_me: bool = False + author_inbox: str | None = None + + +@dataclass(frozen=True, slots=True) +class NotificationDTO: + id: int + notification_type: str # follow/like/boost/mention/reply + from_actor_name: str + from_actor_username: str + created_at: datetime + read: bool + from_actor_domain: str | None = None + from_actor_icon: str | None = None + target_content_preview: str | None = None diff --git a/shared/contracts/protocols.py b/shared/contracts/protocols.py new file mode 100644 index 0000000..e806b8a --- /dev/null +++ b/shared/contracts/protocols.py @@ -0,0 +1,368 @@ +"""Protocol classes defining each domain's service interface. + +All cross-domain callers program against these Protocols. Concrete +implementations (Sql*Service) and no-op stubs both satisfy them. +""" +from __future__ import annotations + +from datetime import datetime +from typing import Protocol, runtime_checkable + +from sqlalchemy.ext.asyncio import AsyncSession + +from .dtos import ( + PostDTO, + CalendarDTO, + CalendarEntryDTO, + TicketDTO, + MarketPlaceDTO, + ProductDTO, + CartItemDTO, + CartSummaryDTO, + ActorProfileDTO, + APActivityDTO, + APFollowerDTO, + RemoteActorDTO, + RemotePostDTO, + TimelineItemDTO, + NotificationDTO, +) + + +@runtime_checkable +class BlogService(Protocol): + async def get_post_by_slug(self, session: AsyncSession, slug: str) -> PostDTO | None: ... + async def get_post_by_id(self, session: AsyncSession, id: int) -> PostDTO | None: ... + async def get_posts_by_ids(self, session: AsyncSession, ids: list[int]) -> list[PostDTO]: ... + + async def search_posts( + self, session: AsyncSession, query: str, page: int = 1, per_page: int = 10, + ) -> tuple[list[PostDTO], int]: ... + + +@runtime_checkable +class CalendarService(Protocol): + async def calendars_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + ) -> list[CalendarDTO]: ... + + async def pending_entries( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[CalendarEntryDTO]: ... + + async def entries_for_page( + self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None, + ) -> list[CalendarEntryDTO]: ... + + async def entry_by_id(self, session: AsyncSession, entry_id: int) -> CalendarEntryDTO | None: ... + + async def associated_entries( + self, session: AsyncSession, content_type: str, content_id: int, page: int, + ) -> tuple[list[CalendarEntryDTO], bool]: ... + + async def toggle_entry_post( + self, session: AsyncSession, entry_id: int, content_type: str, content_id: int, + ) -> bool: ... + + async def adopt_entries_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: ... + + async def claim_entries_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, page_post_id: int | None, + ) -> None: ... + + async def confirm_entries_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, + ) -> None: ... + + async def get_entries_for_order( + self, session: AsyncSession, order_id: int, + ) -> list[CalendarEntryDTO]: ... + + async def user_tickets( + self, session: AsyncSession, *, user_id: int, + ) -> list[TicketDTO]: ... + + async def user_bookings( + self, session: AsyncSession, *, user_id: int, + ) -> list[CalendarEntryDTO]: ... + + async def confirmed_entries_for_posts( + self, session: AsyncSession, post_ids: list[int], + ) -> dict[int, list[CalendarEntryDTO]]: ... + + async def pending_tickets( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[TicketDTO]: ... + + async def tickets_for_page( + self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None, + ) -> list[TicketDTO]: ... + + async def claim_tickets_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, page_post_id: int | None, + ) -> None: ... + + async def confirm_tickets_for_order( + self, session: AsyncSession, order_id: int, + ) -> None: ... + + async def get_tickets_for_order( + self, session: AsyncSession, order_id: int, + ) -> list[TicketDTO]: ... + + async def adopt_tickets_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: ... + + async def adjust_ticket_quantity( + self, session: AsyncSession, entry_id: int, count: int, *, + user_id: int | None, session_id: str | None, + ticket_type_id: int | None = None, + ) -> int: ... + + async def entry_ids_for_content( + self, session: AsyncSession, content_type: str, content_id: int, + ) -> set[int]: ... + + async def upcoming_entries_for_container( + self, session: AsyncSession, + container_type: str | None = None, container_id: int | None = None, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[CalendarEntryDTO], bool]: ... + + async def visible_entries_for_period( + self, session: AsyncSession, calendar_id: int, + period_start: datetime, period_end: datetime, + *, user_id: int | None, is_admin: bool, session_id: str | None, + ) -> list[CalendarEntryDTO]: ... + + +@runtime_checkable +class MarketService(Protocol): + async def marketplaces_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + ) -> list[MarketPlaceDTO]: ... + + async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None: ... + + async def create_marketplace( + self, session: AsyncSession, container_type: str, container_id: int, + name: str, slug: str, + ) -> MarketPlaceDTO: ... + + async def list_marketplaces( + self, session: AsyncSession, + container_type: str | None = None, container_id: int | None = None, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[MarketPlaceDTO], bool]: ... + + async def soft_delete_marketplace( + self, session: AsyncSession, container_type: str, container_id: int, + slug: str, + ) -> bool: ... + + +@runtime_checkable +class CartService(Protocol): + async def cart_summary( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + page_slug: str | None = None, + ) -> CartSummaryDTO: ... + + async def cart_items( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[CartItemDTO]: ... + + async def adopt_cart_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: ... + + +@runtime_checkable +class FederationService(Protocol): + # -- Actor management ----------------------------------------------------- + async def get_actor_by_username( + self, session: AsyncSession, username: str, + ) -> ActorProfileDTO | None: ... + + async def get_actor_by_user_id( + self, session: AsyncSession, user_id: int, + ) -> ActorProfileDTO | None: ... + + async def create_actor( + self, session: AsyncSession, user_id: int, preferred_username: str, + display_name: str | None = None, summary: str | None = None, + ) -> ActorProfileDTO: ... + + async def username_available( + self, session: AsyncSession, username: str, + ) -> bool: ... + + # -- Publishing (core cross-domain API) ----------------------------------- + async def publish_activity( + self, session: AsyncSession, *, + actor_user_id: int, + activity_type: str, + object_type: str, + object_data: dict, + source_type: str | None = None, + source_id: int | None = None, + ) -> APActivityDTO: ... + + # -- Queries -------------------------------------------------------------- + async def get_activity( + self, session: AsyncSession, activity_id: str, + ) -> APActivityDTO | None: ... + + async def get_outbox( + self, session: AsyncSession, username: str, + page: int = 1, per_page: int = 20, + origin_app: str | None = None, + ) -> tuple[list[APActivityDTO], int]: ... + + async def get_activity_for_source( + self, session: AsyncSession, source_type: str, source_id: int, + ) -> APActivityDTO | None: ... + + async def count_activities_for_source( + self, session: AsyncSession, source_type: str, source_id: int, + *, activity_type: str, + ) -> int: ... + + # -- Followers ------------------------------------------------------------ + async def get_followers( + self, session: AsyncSession, username: str, + app_domain: str | None = None, + ) -> list[APFollowerDTO]: ... + + async def get_followers_paginated( + self, session: AsyncSession, username: str, + page: int = 1, per_page: int = 20, + ) -> tuple[list[RemoteActorDTO], int]: ... + + async def add_follower( + self, session: AsyncSession, username: str, + follower_acct: str, follower_inbox: str, follower_actor_url: str, + follower_public_key: str | None = None, + app_domain: str = "federation", + ) -> APFollowerDTO: ... + + async def remove_follower( + self, session: AsyncSession, username: str, follower_acct: str, + app_domain: str = "federation", + ) -> bool: ... + + # -- Remote actors -------------------------------------------------------- + async def get_or_fetch_remote_actor( + self, session: AsyncSession, actor_url: str, + ) -> RemoteActorDTO | None: ... + + async def search_remote_actor( + self, session: AsyncSession, acct: str, + ) -> RemoteActorDTO | None: ... + + async def search_actors( + self, session: AsyncSession, query: str, page: int = 1, limit: int = 20, + ) -> tuple[list[RemoteActorDTO], int]: ... + + # -- Following (outbound) ------------------------------------------------- + async def send_follow( + self, session: AsyncSession, local_username: str, remote_actor_url: str, + ) -> None: ... + + async def get_following( + self, session: AsyncSession, username: str, + page: int = 1, per_page: int = 20, + ) -> tuple[list[RemoteActorDTO], int]: ... + + async def accept_follow_response( + self, session: AsyncSession, local_username: str, remote_actor_url: str, + ) -> None: ... + + async def unfollow( + self, session: AsyncSession, local_username: str, remote_actor_url: str, + ) -> None: ... + + # -- Remote posts --------------------------------------------------------- + async def ingest_remote_post( + self, session: AsyncSession, remote_actor_id: int, + activity_json: dict, object_json: dict, + ) -> None: ... + + async def delete_remote_post( + self, session: AsyncSession, object_id: str, + ) -> None: ... + + async def get_remote_post( + self, session: AsyncSession, object_id: str, + ) -> RemotePostDTO | None: ... + + # -- Timelines ------------------------------------------------------------ + async def get_home_timeline( + self, session: AsyncSession, actor_profile_id: int, + before: datetime | None = None, limit: int = 20, + ) -> list[TimelineItemDTO]: ... + + async def get_public_timeline( + self, session: AsyncSession, + before: datetime | None = None, limit: int = 20, + ) -> list[TimelineItemDTO]: ... + + async def get_actor_timeline( + self, session: AsyncSession, remote_actor_id: int, + before: datetime | None = None, limit: int = 20, + ) -> list[TimelineItemDTO]: ... + + # -- Local posts ---------------------------------------------------------- + async def create_local_post( + self, session: AsyncSession, actor_profile_id: int, + content: str, visibility: str = "public", + in_reply_to: str | None = None, + ) -> int: ... + + async def delete_local_post( + self, session: AsyncSession, actor_profile_id: int, post_id: int, + ) -> None: ... + + # -- Interactions --------------------------------------------------------- + async def like_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: ... + + async def unlike_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: ... + + async def boost_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: ... + + async def unboost_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: ... + + # -- Notifications -------------------------------------------------------- + async def get_notifications( + self, session: AsyncSession, actor_profile_id: int, + before: datetime | None = None, limit: int = 20, + ) -> list[NotificationDTO]: ... + + async def unread_notification_count( + self, session: AsyncSession, actor_profile_id: int, + ) -> int: ... + + async def mark_notifications_read( + self, session: AsyncSession, actor_profile_id: int, + ) -> None: ... + + # -- Stats ---------------------------------------------------------------- + async def get_stats(self, session: AsyncSession) -> dict: ... diff --git a/shared/contracts/widgets.py b/shared/contracts/widgets.py new file mode 100644 index 0000000..b5aef0f --- /dev/null +++ b/shared/contracts/widgets.py @@ -0,0 +1,49 @@ +"""Widget descriptors for cross-domain UI composition. + +Each widget type describes a UI fragment that one domain contributes to +another domain's page. Host apps iterate widgets generically — they never +name the contributing domain. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + + +@dataclass(frozen=True, slots=True) +class NavWidget: + """Renders nav items on a container page (entries, calendars, markets).""" + domain: str + order: int + context_fn: Callable # async (session, *, container_type, container_id, **kw) -> dict + template: str + + +@dataclass(frozen=True, slots=True) +class CardWidget: + """Decorates content cards in listings with domain data.""" + domain: str + order: int + batch_fn: Callable # async (session, post_ids) -> dict[int, list] + context_key: str # key injected into each post dict + template: str + + +@dataclass(frozen=True, slots=True) +class AccountPageWidget: + """Sub-page under /auth//.""" + domain: str + slug: str + label: str + order: int + context_fn: Callable # async (session, *, user_id, **kw) -> dict + template: str + + +@dataclass(frozen=True, slots=True) +class AccountNavLink: + """Nav link on account page (internal or external).""" + label: str + order: int + href_fn: Callable # () -> str + external: bool = False diff --git a/shared/db/__init__.py b/shared/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/db/base.py b/shared/db/base.py new file mode 100644 index 0000000..e070835 --- /dev/null +++ b/shared/db/base.py @@ -0,0 +1,4 @@ +from __future__ import annotations +from sqlalchemy.orm import declarative_base + +Base = declarative_base() diff --git a/shared/db/session.py b/shared/db/session.py new file mode 100644 index 0000000..bff449c --- /dev/null +++ b/shared/db/session.py @@ -0,0 +1,82 @@ +from __future__ import annotations +import os +from contextlib import asynccontextmanager +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from quart import Quart, g + +DATABASE_URL = ( + os.getenv("DATABASE_URL_ASYNC") + or os.getenv("DATABASE_URL") + or "postgresql+asyncpg://localhost/blog" +) + +_engine = create_async_engine( + DATABASE_URL, + future=True, + echo=False, + pool_pre_ping=True, + pool_size=5, + max_overflow=10, +) + +_Session = async_sessionmaker( + bind=_engine, + class_=AsyncSession, + expire_on_commit=False, +) + +@asynccontextmanager +async def get_session(): + """Always create a fresh AsyncSession for this block.""" + sess = _Session() + try: + yield sess + finally: + await sess.close() + + +def register_db(app: Quart): + + @app.before_request + async def open_session(): + g.s = _Session() + g.tx = await g.s.begin() + g.had_error = False + + @app.after_request + async def maybe_commit(response): + # Runs BEFORE bytes are sent. + if not g.had_error and 200 <= response.status_code < 400: + try: + if hasattr(g, "tx"): + await g.tx.commit() + except Exception as e: + print(f'commit failed {e}') + if hasattr(g, "tx"): + await g.tx.rollback() + from quart import make_response + return await make_response("Commit failed", 500) + return response + + @app.teardown_request + async def finish(exc): + try: + # If an exception occurred OR we didn't commit (still in txn), roll back. + if hasattr(g, "s"): + if exc is not None or g.s.in_transaction(): + if hasattr(g, "tx") and g.tx.is_active: + try: + await g.tx.rollback() + except Exception: + pass + finally: + if hasattr(g, "s"): + try: + await g.s.close() + except Exception: + pass + + @app.errorhandler(Exception) + async def mark_error(e): + g.had_error = True + raise diff --git a/shared/editor/build.mjs b/shared/editor/build.mjs new file mode 100644 index 0000000..13f4cb3 --- /dev/null +++ b/shared/editor/build.mjs @@ -0,0 +1,45 @@ +import * as esbuild from "esbuild"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const isProduction = process.env.NODE_ENV === "production"; +const isWatch = process.argv.includes("--watch"); + +/** @type {import('esbuild').BuildOptions} */ +const opts = { + alias: { + "koenig-styles": path.resolve( + __dirname, + "node_modules/@tryghost/koenig-lexical/dist/index.css" + ), + }, + entryPoints: ["src/index.jsx"], + bundle: true, + outdir: "../static/scripts", + entryNames: "editor", + format: "iife", + target: "es2020", + jsx: "automatic", + minify: isProduction, + define: { + "process.env.NODE_ENV": JSON.stringify( + isProduction ? "production" : "development" + ), + }, + loader: { + ".svg": "dataurl", + ".woff": "file", + ".woff2": "file", + ".ttf": "file", + }, + logLevel: "info", +}; + +if (isWatch) { + const ctx = await esbuild.context(opts); + await ctx.watch(); + console.log("Watching for changes..."); +} else { + await esbuild.build(opts); +} diff --git a/shared/editor/package-lock.json b/shared/editor/package-lock.json new file mode 100644 index 0000000..e102c57 --- /dev/null +++ b/shared/editor/package-lock.json @@ -0,0 +1,512 @@ +{ + "name": "coop-lexical-editor", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "coop-lexical-editor", + "version": "2.0.0", + "dependencies": { + "@tryghost/koenig-lexical": "^1.7.10", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "esbuild": "^0.24.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@tryghost/koenig-lexical": { + "version": "1.7.10", + "resolved": "https://registry.npmjs.org/@tryghost/koenig-lexical/-/koenig-lexical-1.7.10.tgz", + "integrity": "sha512-6tI2kbSzZ669hQ5GxpENB8n2aDLugZDmpR/nO0GriduOZJLLN8AdDDa/S3Y8dpF5/cOGKsOxFRj3oLGRDOi6tw==" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + } + } +} diff --git a/shared/editor/package.json b/shared/editor/package.json new file mode 100644 index 0000000..4d556f1 --- /dev/null +++ b/shared/editor/package.json @@ -0,0 +1,18 @@ +{ + "name": "coop-lexical-editor", + "version": "2.0.0", + "private": true, + "scripts": { + "build": "node build.mjs", + "build:prod": "NODE_ENV=production node build.mjs", + "dev": "node build.mjs --watch" + }, + "dependencies": { + "@tryghost/koenig-lexical": "^1.7.10", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "esbuild": "^0.24.0" + } +} diff --git a/shared/editor/src/Editor.jsx b/shared/editor/src/Editor.jsx new file mode 100644 index 0000000..9c01093 --- /dev/null +++ b/shared/editor/src/Editor.jsx @@ -0,0 +1,81 @@ +import { useMemo, useState, useEffect, useCallback } from "react"; +import { KoenigComposer, KoenigEditor, CardMenuPlugin } from "@tryghost/koenig-lexical"; +import "koenig-styles"; +import makeFileUploader from "./useFileUpload"; + +export default function Editor({ initialState, onChange, csrfToken, uploadUrls, oembedUrl, unsplashApiKey, snippetsUrl }) { + const fileUploader = useMemo(() => makeFileUploader(csrfToken, uploadUrls), [csrfToken, uploadUrls]); + + const [snippets, setSnippets] = useState([]); + + useEffect(() => { + if (!snippetsUrl) return; + fetch(snippetsUrl, { headers: { "X-CSRFToken": csrfToken || "" } }) + .then((r) => r.ok ? r.json() : []) + .then(setSnippets) + .catch(() => {}); + }, [snippetsUrl, csrfToken]); + + const createSnippet = useCallback(async ({ name, value }) => { + if (!snippetsUrl) return; + const resp = await fetch(snippetsUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": csrfToken || "", + }, + body: JSON.stringify({ name, value: JSON.stringify(value) }), + }); + if (!resp.ok) return; + const created = await resp.json(); + setSnippets((prev) => { + const idx = prev.findIndex((s) => s.name === created.name); + if (idx >= 0) { + const next = [...prev]; + next[idx] = created; + return next; + } + return [...prev, created].sort((a, b) => a.name.localeCompare(b.name)); + }); + }, [snippetsUrl, csrfToken]); + + const cardConfig = useMemo(() => ({ + fetchEmbed: async (url, { type } = {}) => { + const params = new URLSearchParams({ url }); + if (type) params.set("type", type); + const resp = await fetch(`${oembedUrl}?${params}`, { + headers: { "X-CSRFToken": csrfToken || "" }, + }); + if (!resp.ok) return {}; + return resp.json(); + }, + unsplash: unsplashApiKey + ? { defaultHeaders: { Authorization: `Client-ID ${unsplashApiKey}` } } + : false, + membersEnabled: true, + snippets: snippets.map((s) => ({ + id: s.id, + name: s.name, + value: typeof s.value === "string" ? JSON.parse(s.value) : s.value, + })), + createSnippet, + }), [oembedUrl, csrfToken, unsplashApiKey, snippets, createSnippet]); + + return ( + + { + if (onChange) { + onChange(JSON.stringify(serializedState)); + } + }} + > + + + + ); +} diff --git a/shared/editor/src/index.jsx b/shared/editor/src/index.jsx new file mode 100644 index 0000000..ec1e950 --- /dev/null +++ b/shared/editor/src/index.jsx @@ -0,0 +1,49 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import Editor from "./Editor"; + +/** + * Mount the Koenig editor into the given DOM element. + * + * @param {string} elementId - ID of the container element + * @param {object} opts + * @param {string} [opts.initialJson] - Serialised Lexical JSON (from Ghost) + * @param {string} [opts.csrfToken] - CSRF token for API calls + * @param {object} [opts.uploadUrls] - { image, media, file } upload endpoint URLs + * @param {string} [opts.oembedUrl] - oEmbed proxy endpoint URL + * @param {string} [opts.unsplashApiKey] - Unsplash API key for image search + */ +window.mountEditor = function mountEditor(elementId, opts = {}) { + const container = document.getElementById(elementId); + if (!container) { + console.error(`[editor] Element #${elementId} not found`); + return; + } + + let currentJson = opts.initialJson || null; + + function handleChange(json) { + currentJson = json; + // Stash the latest JSON in a hidden input for form submission + const hidden = document.getElementById("lexical-json-input"); + if (hidden) hidden.value = json; + } + + const root = createRoot(container); + root.render( + + ); + + // Return handle for programmatic access + return { + getJson: () => currentJson, + }; +}; diff --git a/shared/editor/src/useFileUpload.js b/shared/editor/src/useFileUpload.js new file mode 100644 index 0000000..014b8b5 --- /dev/null +++ b/shared/editor/src/useFileUpload.js @@ -0,0 +1,99 @@ +import { useState, useCallback, useRef } from "react"; + +/** + * Koenig expects `fileUploader.useFileUpload(type)` — a React hook it + * calls internally for each card type ("image", "audio", "file", etc.). + * + * `makeFileUploader(csrfToken, uploadUrls)` returns the object Koenig wants: + * { useFileUpload: (type) => { upload, progress, isLoading, errors, filesNumber } } + * + * `uploadUrls` is an object: { image, media, file } + * For backwards compat, a plain string is treated as the image URL. + */ + +const URL_KEY_MAP = { + image: { urlKey: "image", responseKey: "images" }, + audio: { urlKey: "media", responseKey: "media" }, + video: { urlKey: "media", responseKey: "media" }, + mediaThumbnail: { urlKey: "image", responseKey: "images" }, + file: { urlKey: "file", responseKey: "files" }, +}; + +export default function makeFileUploader(csrfToken, uploadUrls) { + // Normalise: string → object with all keys pointing to same URL + const urls = + typeof uploadUrls === "string" + ? { image: uploadUrls, media: uploadUrls, file: uploadUrls } + : uploadUrls || {}; + + return { + fileTypes: { + image: { mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'] }, + audio: { mimeTypes: ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/mp4', 'audio/aac'] }, + video: { mimeTypes: ['video/mp4', 'video/webm', 'video/ogg'] }, + mediaThumbnail: { mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] }, + file: { mimeTypes: [] }, + }, + useFileUpload(type) { + const mapping = URL_KEY_MAP[type] || URL_KEY_MAP.image; + const [progress, setProgress] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState([]); + const [filesNumber, setFilesNumber] = useState(0); + const csrfRef = useRef(csrfToken); + const urlRef = useRef(urls[mapping.urlKey] || urls.image || "/editor-api/images/upload/"); + const responseKeyRef = useRef(mapping.responseKey); + + const upload = useCallback(async (files) => { + const fileList = Array.from(files); + setFilesNumber(fileList.length); + setIsLoading(true); + setErrors([]); + setProgress(0); + + const results = []; + for (let i = 0; i < fileList.length; i++) { + const file = fileList[i]; + const formData = new FormData(); + formData.append("file", file); + + try { + const resp = await fetch(urlRef.current, { + method: "POST", + body: formData, + headers: { + "X-CSRFToken": csrfRef.current || "", + }, + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + const msg = + err.errors?.[0]?.message || `Upload failed (${resp.status})`; + setErrors((prev) => [ + ...prev, + { message: msg, fileName: file.name }, + ]); + continue; + } + const data = await resp.json(); + const fileUrl = data[responseKeyRef.current]?.[0]?.url; + if (fileUrl) { + results.push({ url: fileUrl, fileName: file.name }); + } + } catch (e) { + setErrors((prev) => [ + ...prev, + { message: e.message, fileName: file.name }, + ]); + } + setProgress(Math.round(((i + 1) / fileList.length) * 100)); + } + + setIsLoading(false); + return results; + }, []); + + return { upload, progress, isLoading, errors, filesNumber }; + }, + }; +} diff --git a/shared/events/__init__.py b/shared/events/__init__.py new file mode 100644 index 0000000..522cb5f --- /dev/null +++ b/shared/events/__init__.py @@ -0,0 +1,9 @@ +from .bus import emit_activity, register_activity_handler, get_activity_handlers +from .processor import EventProcessor + +__all__ = [ + "emit_activity", + "register_activity_handler", + "get_activity_handlers", + "EventProcessor", +] diff --git a/shared/events/bus.py b/shared/events/bus.py new file mode 100644 index 0000000..215194e --- /dev/null +++ b/shared/events/bus.py @@ -0,0 +1,126 @@ +""" +Unified activity bus. + +emit_activity() writes an APActivity row with process_state='pending' within +the caller's existing DB transaction — atomic with the domain change. + +register_activity_handler() registers async handler functions that the +EventProcessor dispatches when processing pending activities. +""" +from __future__ import annotations + +import logging +import uuid +from collections import defaultdict +from typing import Awaitable, Callable, Dict, List, Tuple + +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.federation import APActivity + +log = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Activity-handler registry +# --------------------------------------------------------------------------- +# Handler signature: async def handler(activity: APActivity, session: AsyncSession) -> None +ActivityHandlerFn = Callable[[APActivity, AsyncSession], Awaitable[None]] + +# Keyed by (activity_type, object_type). object_type="*" is wildcard. +_activity_handlers: Dict[Tuple[str, str], List[ActivityHandlerFn]] = defaultdict(list) + + +def register_activity_handler( + activity_type: str, + fn: ActivityHandlerFn, + *, + object_type: str | None = None, +) -> None: + """Register an async handler for an activity type + optional object type. + + Use ``activity_type="*"`` as a wildcard that fires for every activity + (e.g. federation delivery handler). + """ + key = (activity_type, object_type or "*") + _activity_handlers[key].append(fn) + log.info("Registered activity handler %s.%s for key %s", fn.__module__, fn.__qualname__, key) + + +def get_activity_handlers( + activity_type: str, + object_type: str | None = None, +) -> List[ActivityHandlerFn]: + """Return all matching handlers for an activity. + + Matches in order: + 1. Exact (activity_type, object_type) + 2. (activity_type, "*") — type-level wildcard + 3. ("*", "*") — global wildcard (e.g. delivery) + """ + handlers: List[ActivityHandlerFn] = [] + ot = object_type or "*" + + # Exact match + if ot != "*": + handlers.extend(_activity_handlers.get((activity_type, ot), [])) + # Type-level wildcard + handlers.extend(_activity_handlers.get((activity_type, "*"), [])) + # Global wildcard + if activity_type != "*": + handlers.extend(_activity_handlers.get(("*", "*"), [])) + + return handlers + + +# --------------------------------------------------------------------------- +# emit_activity — the primary way to emit events +# --------------------------------------------------------------------------- +async def emit_activity( + session: AsyncSession, + *, + activity_type: str, + actor_uri: str, + object_type: str, + object_data: dict | None = None, + source_type: str | None = None, + source_id: int | None = None, + visibility: str = "internal", + actor_profile_id: int | None = None, + origin_app: str | None = None, +) -> APActivity: + """ + Write an AP-shaped activity to ap_activities with process_state='pending'. + + Called inside a service function using the same session that performs the + domain change. The activity and the change commit together. + """ + if not origin_app: + try: + from quart import current_app + origin_app = current_app.name + except (ImportError, RuntimeError): + pass + + activity_uri = f"internal:{uuid.uuid4()}" if visibility == "internal" else f"urn:uuid:{uuid.uuid4()}" + + activity = APActivity( + activity_id=activity_uri, + activity_type=activity_type, + actor_profile_id=actor_profile_id, + actor_uri=actor_uri, + object_type=object_type, + object_data=object_data or {}, + is_local=True, + source_type=source_type, + source_id=source_id, + visibility=visibility, + process_state="pending", + origin_app=origin_app, + ) + session.add(activity) + await session.flush() + # Wake any listening EventProcessor as soon as this transaction commits. + # NOTIFY is transactional — delivered only after commit. + await session.execute(text("NOTIFY ap_activity_pending")) + return activity diff --git a/shared/events/handlers/__init__.py b/shared/events/handlers/__init__.py new file mode 100644 index 0000000..9f6a845 --- /dev/null +++ b/shared/events/handlers/__init__.py @@ -0,0 +1,10 @@ +"""Shared event handlers.""" + + +def register_shared_handlers(): + """Import handler modules to trigger registration. Call at app startup.""" + import shared.events.handlers.container_handlers # noqa: F401 + import shared.events.handlers.login_handlers # noqa: F401 + import shared.events.handlers.order_handlers # noqa: F401 + import shared.events.handlers.ap_delivery_handler # noqa: F401 + import shared.events.handlers.external_delivery_handler # noqa: F401 diff --git a/shared/events/handlers/ap_delivery_handler.py b/shared/events/handlers/ap_delivery_handler.py new file mode 100644 index 0000000..7a175bf --- /dev/null +++ b/shared/events/handlers/ap_delivery_handler.py @@ -0,0 +1,250 @@ +"""Deliver AP activities to remote followers. + +Registered as a wildcard handler — fires for every activity. Skips +non-public activities and those without an actor profile. + +Per-app delivery: activities are delivered using the domain that matches +the follower's subscription. A follower of ``@alice@blog.rose-ash.com`` +receives activities with ``actor: https://blog.rose-ash.com/users/alice`` +and signatures using that domain's key_id. Aggregate followers +(``app_domain='federation'``) receive the federation domain identity. + +Idempotent: successful deliveries are recorded in ap_delivery_log. +On retry (at-least-once reaper), already-delivered inboxes are skipped. +""" +from __future__ import annotations + +import logging +import os +from collections import defaultdict + +import httpx +from sqlalchemy import select, or_ +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.events.bus import register_activity_handler +from shared.models.federation import ActorProfile, APActivity, APFollower, APDeliveryLog +from shared.services.registry import services + +log = logging.getLogger(__name__) + +AP_CONTENT_TYPE = "application/activity+json" +DELIVERY_TIMEOUT = 15 # seconds per request + + +def _domain_for_app(app_name: str) -> str: + """Resolve the public AP domain for an app name.""" + from shared.infrastructure.activitypub import _ap_domain + return _ap_domain(app_name) + + +def _build_activity_json(activity: APActivity, actor: ActorProfile, domain: str) -> dict: + """Build the full AP activity JSON-LD for delivery.""" + username = actor.preferred_username + actor_url = f"https://{domain}/users/{username}" + + obj = dict(activity.object_data or {}) + + # Rewrite all URLs from the federation domain to the delivery domain + # so Mastodon's origin check passes (all IDs must match actor host). + import re + fed_domain = os.getenv("AP_DOMAIN", "federation.rose-ash.com") + + def _rewrite(url: str) -> str: + if isinstance(url, str) and fed_domain in url: + return url.replace(f"https://{fed_domain}", f"https://{domain}") + return url + + activity_id = _rewrite(activity.activity_id) + object_id = activity_id + "/object" + + # Rewrite any federation-domain URLs in object_data + if "id" in obj: + obj["id"] = _rewrite(obj["id"]) + if "attributedTo" in obj: + obj["attributedTo"] = _rewrite(obj["attributedTo"]) + + if activity.activity_type == "Delete": + obj.setdefault("id", object_id) + obj.setdefault("type", "Tombstone") + else: + obj.setdefault("id", object_id) + obj.setdefault("type", activity.object_type) + obj.setdefault("attributedTo", actor_url) + obj.setdefault("published", activity.published.isoformat() if activity.published else None) + obj.setdefault("to", ["https://www.w3.org/ns/activitystreams#Public"]) + obj.setdefault("cc", [f"{actor_url}/followers"]) + if activity.activity_type == "Update": + from datetime import datetime, timezone + obj["updated"] = datetime.now(timezone.utc).isoformat() + + return { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + "id": activity_id, + "type": activity.activity_type, + "actor": actor_url, + "published": activity.published.isoformat() if activity.published else None, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": [f"{actor_url}/followers"], + "object": obj, + } + + +async def _deliver_to_inbox( + client: httpx.AsyncClient, + inbox_url: str, + body: dict, + actor: ActorProfile, + domain: str, +) -> int | None: + """POST signed activity to a single inbox. Returns status code or None on error.""" + from shared.utils.http_signatures import sign_request + from urllib.parse import urlparse + import json + + body_bytes = json.dumps(body).encode() + key_id = f"https://{domain}/users/{actor.preferred_username}#main-key" + + parsed = urlparse(inbox_url) + headers = sign_request( + private_key_pem=actor.private_key_pem, + key_id=key_id, + method="POST", + path=parsed.path, + host=parsed.netloc, + body=body_bytes, + ) + headers["Content-Type"] = AP_CONTENT_TYPE + + try: + resp = await client.post( + inbox_url, + content=body_bytes, + headers=headers, + timeout=DELIVERY_TIMEOUT, + ) + if resp.status_code < 300: + log.info("Delivered to %s → %d", inbox_url, resp.status_code) + else: + log.warning("Delivery to %s → %d: %s", inbox_url, resp.status_code, resp.text[:200]) + return resp.status_code + except Exception: + log.exception("Delivery failed for %s", inbox_url) + return None + + +async def on_any_activity(activity: APActivity, session: AsyncSession) -> None: + """Deliver a public activity to all matching followers of its actor.""" + + # Only deliver public activities that have an actor profile + if activity.visibility != "public": + return + if activity.actor_profile_id is None: + return + if not services.has("federation"): + return + + # Load actor with private key + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == activity.actor_profile_id) + ) + ).scalar_one_or_none() + if not actor or not actor.private_key_pem: + log.warning("Actor not found or missing key for activity %s", activity.activity_id) + return + + # Load matching followers. + # Aggregate followers (app_domain='federation') always get everything. + # Per-app followers only get activities from their app. + origin_app = activity.origin_app + follower_filters = [APFollower.actor_profile_id == actor.id] + + if origin_app and origin_app != "federation": + follower_filters.append( + or_( + APFollower.app_domain == "federation", + APFollower.app_domain == origin_app, + ) + ) + + followers = ( + await session.execute( + select(APFollower).where(*follower_filters) + ) + ).scalars().all() + + if not followers: + log.debug("No followers to deliver to for %s", activity.activity_id) + return + + # Check delivery log — skip (inbox, domain) pairs already delivered (idempotency) + existing = ( + await session.execute( + select(APDeliveryLog.inbox_url, APDeliveryLog.app_domain).where( + APDeliveryLog.activity_id == activity.id, + APDeliveryLog.status_code < 300, + ) + ) + ).all() + already_delivered: set[tuple[str, str]] = {(r[0], r[1]) for r in existing} + + # Collect all (inbox, app_domain) pairs to deliver to. + # Each follower subscription gets its own delivery with the correct + # actor identity, so followers of @user@blog and @user@federation + # both see posts on their respective actor profiles. + delivery_pairs: set[tuple[str, str]] = set() + for f in followers: + if not f.follower_inbox: + continue + app_dom = f.app_domain or "federation" + pair = (f.follower_inbox, app_dom) + if pair not in already_delivered: + delivery_pairs.add(pair) + + if not delivery_pairs: + if already_delivered: + log.info("All deliveries already done for %s", activity.activity_id) + return + + if already_delivered: + log.info( + "Skipping %d already-delivered, delivering to %d remaining", + len(already_delivered), len(delivery_pairs), + ) + + # Group by domain to reuse activity JSON per domain + domain_inboxes: dict[str, list[str]] = defaultdict(list) + for inbox_url, app_dom in delivery_pairs: + domain_inboxes[app_dom].append(inbox_url) + + log.info( + "Delivering %s to %d target(s) for @%s across %d domain(s)", + activity.activity_type, len(delivery_pairs), + actor.preferred_username, len(domain_inboxes), + ) + + async with httpx.AsyncClient() as client: + for app_dom, inboxes in domain_inboxes.items(): + domain = _domain_for_app(app_dom) + activity_json = _build_activity_json(activity, actor, domain) + + for inbox_url in inboxes: + status_code = await _deliver_to_inbox( + client, inbox_url, activity_json, actor, domain + ) + if status_code is not None and status_code < 300: + session.add(APDeliveryLog( + activity_id=activity.id, + inbox_url=inbox_url, + app_domain=app_dom, + status_code=status_code, + )) + await session.flush() + + +# Wildcard: fires for every activity +register_activity_handler("*", on_any_activity) diff --git a/shared/events/handlers/container_handlers.py b/shared/events/handlers/container_handlers.py new file mode 100644 index 0000000..c405002 --- /dev/null +++ b/shared/events/handlers/container_handlers.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.events import register_activity_handler +from shared.models.federation import APActivity +from shared.services.navigation import rebuild_navigation + + +async def on_child_attached(activity: APActivity, session: AsyncSession) -> None: + await rebuild_navigation(session) + + +async def on_child_detached(activity: APActivity, session: AsyncSession) -> None: + await rebuild_navigation(session) + + +register_activity_handler("Add", on_child_attached, object_type="rose:ContainerRelation") +register_activity_handler("Remove", on_child_detached, object_type="rose:ContainerRelation") diff --git a/shared/events/handlers/external_delivery_handler.py b/shared/events/handlers/external_delivery_handler.py new file mode 100644 index 0000000..d40852a --- /dev/null +++ b/shared/events/handlers/external_delivery_handler.py @@ -0,0 +1,101 @@ +"""Deliver activities to external service inboxes via signed HTTP POST. + +External services (like artdag) that don't share the coop database receive +activities via HTTP, authenticated with the same HTTP Signatures used for +ActivityPub federation. + +Config via env: EXTERNAL_INBOXES=name|url,name2|url2,... +""" +from __future__ import annotations + +import json +import logging +import os +from urllib.parse import urlparse + +import httpx +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.events.bus import register_activity_handler +from shared.models.federation import ActorProfile, APActivity +from shared.utils.http_signatures import sign_request + +log = logging.getLogger(__name__) + +# Activity types to deliver externally +_DELIVERABLE_TYPES = {"rose:DeviceAuth"} + + +def _get_external_inboxes() -> list[tuple[str, str]]: + """Parse EXTERNAL_INBOXES env var into [(name, url), ...].""" + raw = os.environ.get("EXTERNAL_INBOXES", "") + if not raw: + return [] + result = [] + for entry in raw.split(","): + entry = entry.strip() + if "|" in entry: + name, url = entry.split("|", 1) + result.append((name.strip(), url.strip())) + return result + + +def _get_ap_domain() -> str: + return os.environ.get("AP_DOMAIN", "federation.rose-ash.com") + + +async def on_external_activity(activity: APActivity, session: AsyncSession) -> None: + """Deliver matching activities to configured external inboxes.""" + if activity.activity_type not in _DELIVERABLE_TYPES: + return + + inboxes = _get_external_inboxes() + if not inboxes: + return + + # Get the first actor profile for signing + actor = await session.scalar(select(ActorProfile).limit(1)) + if not actor: + log.warning("No ActorProfile available for signing external deliveries") + return + + domain = _get_ap_domain() + key_id = f"https://{domain}/users/{actor.preferred_username}#main-key" + + payload = { + "@context": "https://www.w3.org/ns/activitystreams", + "type": activity.activity_type, + "actor": activity.actor_uri, + "object": activity.object_data, + } + if activity.published: + payload["published"] = activity.published.isoformat() + + body_bytes = json.dumps(payload).encode() + + for name, inbox_url in inboxes: + parsed = urlparse(inbox_url) + headers = sign_request( + private_key_pem=actor.private_key_pem, + key_id=key_id, + method="POST", + path=parsed.path, + host=parsed.netloc, + body=body_bytes, + ) + headers["Content-Type"] = "application/activity+json" + try: + async with httpx.AsyncClient(timeout=3) as client: + resp = await client.post(inbox_url, content=body_bytes, headers=headers) + log.info( + "External delivery to %s: %d", + name, resp.status_code, + ) + except Exception: + log.warning("External delivery to %s failed", name, exc_info=True) + + +# Register for all deliverable types +for _t in _DELIVERABLE_TYPES: + register_activity_handler(_t, on_external_activity) diff --git a/shared/events/handlers/login_handlers.py b/shared/events/handlers/login_handlers.py new file mode 100644 index 0000000..d09ce23 --- /dev/null +++ b/shared/events/handlers/login_handlers.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.events import register_activity_handler +from shared.models.federation import APActivity +from shared.services.registry import services + + +async def on_user_logged_in(activity: APActivity, session: AsyncSession) -> None: + data = activity.object_data + user_id = data["user_id"] + session_id = data["session_id"] + + if services.has("cart"): + await services.cart.adopt_cart_for_user(session, user_id, session_id) + + if services.has("calendar"): + await services.calendar.adopt_entries_for_user(session, user_id, session_id) + await services.calendar.adopt_tickets_for_user(session, user_id, session_id) + + +register_activity_handler("rose:Login", on_user_logged_in) diff --git a/shared/events/handlers/order_handlers.py b/shared/events/handlers/order_handlers.py new file mode 100644 index 0000000..c608ae7 --- /dev/null +++ b/shared/events/handlers/order_handlers.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import logging + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.events import register_activity_handler +from shared.models.federation import APActivity + +log = logging.getLogger(__name__) + + +async def on_order_created(activity: APActivity, session: AsyncSession) -> None: + log.info("order.created: order_id=%s", activity.object_data.get("order_id")) + + +async def on_order_paid(activity: APActivity, session: AsyncSession) -> None: + log.info("order.paid: order_id=%s", activity.object_data.get("order_id")) + + +register_activity_handler("Create", on_order_created, object_type="rose:Order") +register_activity_handler("rose:OrderPaid", on_order_paid) diff --git a/shared/events/processor.py b/shared/events/processor.py new file mode 100644 index 0000000..935309b --- /dev/null +++ b/shared/events/processor.py @@ -0,0 +1,243 @@ +""" +Event processor — polls the ap_activities table and dispatches to registered +activity handlers. + +Runs as an asyncio background task within each app process. +Uses SELECT ... FOR UPDATE SKIP LOCKED for safe concurrent processing. + +A dedicated asyncpg LISTEN connection wakes the poll loop immediately when +emit_activity() fires NOTIFY ap_activity_pending, so latency drops from +~2 seconds (poll interval) to sub-100 ms. The fixed-interval poll remains +as a safety-net fallback. +""" +from __future__ import annotations + +import asyncio +import logging +import traceback +from datetime import datetime, timedelta, timezone + +import asyncpg +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.db.session import get_session, DATABASE_URL +from shared.models.federation import APActivity +from .bus import get_activity_handlers + +log = logging.getLogger(__name__) + + +class EventProcessor: + """Background event processor that polls the ap_activities table.""" + + def __init__( + self, + *, + app_name: str | None = None, + poll_interval: float = 2.0, + batch_size: int = 10, + stuck_timeout: float = 300.0, + ): + self._app_name = app_name + self._poll_interval = poll_interval + self._batch_size = batch_size + self._stuck_timeout = stuck_timeout # seconds before "processing" → "pending" + self._task: asyncio.Task | None = None + self._listen_task: asyncio.Task | None = None + self._listen_conn: asyncpg.Connection | None = None + self._wake = asyncio.Event() + self._running = False + self._reap_counter = 0 + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + async def start(self) -> None: + """Start the background polling loop.""" + if self._task is not None: + return + self._running = True + self._listen_task = asyncio.create_task(self._listen_for_notify()) + self._task = asyncio.create_task(self._poll_loop()) + + async def stop(self) -> None: + """Stop the background polling loop gracefully.""" + self._running = False + if self._listen_task is not None: + self._listen_task.cancel() + try: + await self._listen_task + except asyncio.CancelledError: + pass + self._listen_task = None + if self._listen_conn is not None and not self._listen_conn.is_closed(): + await self._listen_conn.close() + self._listen_conn = None + if self._task is not None: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + self._task = None + + # ------------------------------------------------------------------ + # LISTEN — wake poll loop on NOTIFY + # ------------------------------------------------------------------ + + async def _listen_for_notify(self) -> None: + """Maintain a LISTEN connection and wake the poll loop on NOTIFY.""" + dsn = DATABASE_URL.replace("+asyncpg", "") + while self._running: + try: + self._listen_conn = await asyncpg.connect(dsn) + await self._listen_conn.add_listener( + "ap_activity_pending", self._on_notify + ) + log.info("LISTEN ap_activity_pending active") + # Keep alive with periodic health check + while self._running: + await asyncio.sleep(30) + await self._listen_conn.execute("SELECT 1") + except asyncio.CancelledError: + break + except Exception: + log.warning("LISTEN connection lost, reconnecting…", exc_info=True) + await asyncio.sleep(2) + finally: + if self._listen_conn is not None and not self._listen_conn.is_closed(): + await self._listen_conn.close() + self._listen_conn = None + + def _on_notify(self, conn, pid, channel, payload) -> None: + """Called by asyncpg when a NOTIFY arrives.""" + self._wake.set() + + # ------------------------------------------------------------------ + # Poll loop + # ------------------------------------------------------------------ + + async def _poll_loop(self) -> None: + while self._running: + try: + # Periodically recover stuck activities (~every 30 cycles) + self._reap_counter += 1 + if self._reap_counter >= 30: + self._reap_counter = 0 + await self._recover_stuck() + + # Clear before processing so any NOTIFY that arrives during + # _process_batch sets the event and we loop immediately. + self._wake.clear() + processed = await self._process_batch() + if processed == 0: + try: + await asyncio.wait_for( + self._wake.wait(), timeout=self._poll_interval + ) + except asyncio.TimeoutError: + pass + # processed > 0 → loop immediately to drain the queue + except asyncio.CancelledError: + break + except Exception: + traceback.print_exc() + await asyncio.sleep(self._poll_interval) + + async def _recover_stuck(self) -> None: + """Reset activities stuck in 'processing' back to 'pending'. + + This handles the case where a process crashed mid-handler. + Combined with idempotent handlers, this gives at-least-once delivery. + """ + cutoff = datetime.now(timezone.utc) - timedelta(seconds=self._stuck_timeout) + try: + async with get_session() as session: + filters = [ + APActivity.process_state == "processing", + APActivity.created_at < cutoff, + ] + if self._app_name: + filters.append(APActivity.origin_app == self._app_name) + result = await session.execute( + update(APActivity) + .where(*filters) + .values(process_state="pending") + .returning(APActivity.id) + ) + recovered = result.scalars().all() + await session.commit() + if recovered: + log.warning( + "Recovered %d stuck activities: %s", + len(recovered), recovered, + ) + except Exception: + log.exception("Failed to recover stuck activities") + + async def _process_batch(self) -> int: + """Fetch and process a batch of pending activities. Returns count processed.""" + processed = 0 + async with get_session() as session: + filters = [ + APActivity.process_state == "pending", + APActivity.process_attempts < APActivity.process_max_attempts, + ] + if self._app_name: + filters.append(APActivity.origin_app == self._app_name) + stmt = ( + select(APActivity) + .where(*filters) + .order_by(APActivity.created_at) + .limit(self._batch_size) + .with_for_update(skip_locked=True) + ) + result = await session.execute(stmt) + activities = result.scalars().all() + + for activity in activities: + await self._process_one(session, activity) + processed += 1 + + await session.commit() + return processed + + async def _process_one(self, session: AsyncSession, activity: APActivity) -> None: + """Run all handlers for a single activity.""" + handlers = get_activity_handlers(activity.activity_type, activity.object_type) + now = datetime.now(timezone.utc) + + log.info( + "Processing activity %s: type=%s object_type=%s visibility=%s actor_profile_id=%s — %d handler(s) found", + activity.id, activity.activity_type, activity.object_type, + activity.visibility, activity.actor_profile_id, len(handlers), + ) + for h in handlers: + log.info(" handler: %s.%s", h.__module__, h.__qualname__) + + activity.process_state = "processing" + activity.process_attempts += 1 + await session.flush() + + if not handlers: + activity.process_state = "completed" + activity.processed_at = now + return + + try: + for handler in handlers: + log.info(" calling %s.%s …", handler.__module__, handler.__qualname__) + await handler(activity, session) + log.info(" done %s.%s", handler.__module__, handler.__qualname__) + activity.process_state = "completed" + activity.processed_at = now + except Exception as exc: + log.exception("Handler failed for activity %s", activity.id) + activity.process_error = f"{exc.__class__.__name__}: {exc}" + if activity.process_attempts >= activity.process_max_attempts: + activity.process_state = "failed" + activity.processed_at = now + else: + activity.process_state = "pending" # retry diff --git a/shared/infrastructure/__init__.py b/shared/infrastructure/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/shared/infrastructure/__init__.py @@ -0,0 +1 @@ + diff --git a/shared/infrastructure/activitypub.py b/shared/infrastructure/activitypub.py new file mode 100644 index 0000000..b7d6d7e --- /dev/null +++ b/shared/infrastructure/activitypub.py @@ -0,0 +1,454 @@ +"""Per-app ActivityPub blueprint. + +Factory function ``create_activitypub_blueprint(app_name)`` returns a +Blueprint with WebFinger, host-meta, nodeinfo, actor profile, inbox, +outbox, and followers endpoints. + +Per-app actors are *virtual projections* of the same ``ActorProfile``. +Same keypair, same ``preferred_username`` — the only differences are: +- the domain in URLs (e.g. blog.rose-ash.com vs federation.rose-ash.com) +- which activities are served in the outbox (filtered by ``origin_app``) +- which followers are returned (filtered by ``app_domain``) +- Follow requests create ``APFollower(app_domain=app_name)`` + +Federation app acts as the aggregate: no origin_app filter, app_domain=NULL. +""" +from __future__ import annotations + +import json +import logging +import os +from datetime import datetime, timezone + +from quart import Blueprint, request, abort, Response, g +from sqlalchemy import select + +from shared.services.registry import services +from shared.models.federation import ActorProfile, APInboxItem +from shared.browser.app.csrf import csrf_exempt + +log = logging.getLogger(__name__) + +AP_CONTENT_TYPE = "application/activity+json" + +# Apps that serve per-app AP actors +AP_APPS = {"blog", "market", "events", "federation"} + + +def _ap_domain(app_name: str) -> str: + """Return the public domain for this app's AP identity.""" + env_key = f"AP_DOMAIN_{app_name.upper()}" + env_val = os.getenv(env_key) + if env_val: + return env_val + # Default: {app}.rose-ash.com, except federation uses AP_DOMAIN + if app_name == "federation": + return os.getenv("AP_DOMAIN", "federation.rose-ash.com") + return f"{app_name}.rose-ash.com" + + +def _federation_domain() -> str: + """The aggregate federation domain (for alsoKnownAs links).""" + return os.getenv("AP_DOMAIN", "federation.rose-ash.com") + + +def _is_aggregate(app_name: str) -> bool: + """Federation serves the aggregate actor (no per-app filter).""" + return app_name == "federation" + + +def create_activitypub_blueprint(app_name: str) -> Blueprint: + """Return a Blueprint with AP endpoints for *app_name*.""" + bp = Blueprint("activitypub", __name__) + + domain = _ap_domain(app_name) + fed_domain = _federation_domain() + aggregate = _is_aggregate(app_name) + # For per-app follows, store app_domain; for federation, "federation" + follower_app_domain: str = app_name + # For per-app outboxes, filter by origin_app; for federation, show all + outbox_origin_app: str | None = None if aggregate else app_name + + # ------------------------------------------------------------------ + # Well-known endpoints + # ------------------------------------------------------------------ + + @bp.get("/.well-known/webfinger") + async def webfinger(): + resource = request.args.get("resource", "") + if not resource.startswith("acct:"): + abort(400, "Invalid resource format") + + parts = resource[5:].split("@") + if len(parts) != 2: + abort(400, "Invalid resource format") + + username, res_domain = parts + if res_domain != domain: + abort(404, "User not on this server") + + actor = await services.federation.get_actor_by_username(g.s, username) + if not actor: + abort(404, "User not found") + + actor_url = f"https://{domain}/users/{username}" + return Response( + response=json.dumps({ + "subject": resource, + "aliases": [actor_url], + "links": [ + { + "rel": "self", + "type": AP_CONTENT_TYPE, + "href": actor_url, + }, + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": actor_url, + }, + ], + }), + content_type="application/jrd+json", + ) + + @bp.get("/.well-known/nodeinfo") + async def nodeinfo_index(): + return Response( + response=json.dumps({ + "links": [ + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", + "href": f"https://{domain}/nodeinfo/2.0", + } + ] + }), + content_type="application/json", + ) + + @bp.get("/nodeinfo/2.0") + async def nodeinfo(): + stats = await services.federation.get_stats(g.s) + return Response( + response=json.dumps({ + "version": "2.0", + "software": { + "name": "rose-ash", + "version": "1.0.0", + }, + "protocols": ["activitypub"], + "usage": { + "users": { + "total": stats.get("actors", 0), + "activeMonth": stats.get("actors", 0), + }, + "localPosts": stats.get("activities", 0), + }, + "openRegistrations": False, + "metadata": { + "nodeName": f"Rose Ash ({app_name})", + "nodeDescription": f"Rose Ash {app_name} — ActivityPub federation", + }, + }), + content_type="application/json", + ) + + @bp.get("/.well-known/host-meta") + async def host_meta(): + xml = ( + '\n' + '\n' + f' \n' + '' + ) + return Response(response=xml, content_type="application/xrd+xml") + + # ------------------------------------------------------------------ + # Actor profile + # ------------------------------------------------------------------ + + @bp.get("/users/") + async def actor_profile(username: str): + actor = await services.federation.get_actor_by_username(g.s, username) + if not actor: + abort(404) + + accept_header = request.headers.get("accept", "") + + if "application/activity+json" in accept_header or "application/ld+json" in accept_header: + actor_url = f"https://{domain}/users/{username}" + actor_json = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + "type": "Person", + "id": actor_url, + "name": actor.display_name or username, + "preferredUsername": username, + "summary": actor.summary or "", + "manuallyApprovesFollowers": False, + "inbox": f"{actor_url}/inbox", + "outbox": f"{actor_url}/outbox", + "followers": f"{actor_url}/followers", + "following": f"{actor_url}/following", + "publicKey": { + "id": f"{actor_url}#main-key", + "owner": actor_url, + "publicKeyPem": actor.public_key_pem, + }, + "url": actor_url, + } + + if aggregate: + # Aggregate actor advertises all per-app actors + also_known = [ + f"https://{_ap_domain(a)}/users/{username}" + for a in AP_APPS if a != "federation" + ] + if also_known: + actor_json["alsoKnownAs"] = also_known + else: + # Per-app actors link back to the aggregate federation actor + actor_json["alsoKnownAs"] = [ + f"https://{fed_domain}/users/{username}", + ] + + return Response( + response=json.dumps(actor_json), + content_type=AP_CONTENT_TYPE, + ) + + # HTML: federation renders its own profile; other apps redirect there + if aggregate: + from quart import render_template + activities, total = await services.federation.get_outbox( + g.s, username, page=1, per_page=20, + ) + return await render_template( + "federation/profile.html", + actor=actor, + activities=activities, + total=total, + ) + from quart import redirect + return redirect(f"https://{fed_domain}/users/{username}") + + # ------------------------------------------------------------------ + # Inbox + # ------------------------------------------------------------------ + + @csrf_exempt + @bp.post("/users//inbox") + async def inbox(username: str): + actor = await services.federation.get_actor_by_username(g.s, username) + if not actor: + abort(404) + + body = await request.get_json() + if not body: + abort(400, "Invalid JSON") + + activity_type = body.get("type", "") + from_actor_url = body.get("actor", "") + + # Verify HTTP signature (best-effort) + sig_valid = False + try: + from shared.utils.http_signatures import verify_request_signature + from shared.infrastructure.ap_inbox_handlers import fetch_remote_actor + + req_headers = dict(request.headers) + sig_header = req_headers.get("Signature", "") + + remote_actor = await fetch_remote_actor(from_actor_url) + if remote_actor and sig_header: + pub_key_pem = (remote_actor.get("publicKey") or {}).get("publicKeyPem") + if pub_key_pem: + sig_valid = verify_request_signature( + public_key_pem=pub_key_pem, + signature_header=sig_header, + method="POST", + path=f"/users/{username}/inbox", + headers=req_headers, + ) + except Exception: + log.debug("Signature verification failed for %s", from_actor_url, exc_info=True) + + if not sig_valid: + log.warning( + "Unverified inbox POST from %s (%s) on %s — accepting anyway for now", + from_actor_url, activity_type, domain, + ) + + # Load actor row for DB operations + actor_row = ( + await g.s.execute( + select(ActorProfile).where( + ActorProfile.preferred_username == username + ) + ) + ).scalar_one() + + # Store raw inbox item + item = APInboxItem( + actor_profile_id=actor_row.id, + raw_json=body, + activity_type=activity_type, + from_actor=from_actor_url, + ) + g.s.add(item) + await g.s.flush() + + # Dispatch to shared handlers + from shared.infrastructure.ap_inbox_handlers import dispatch_inbox_activity + await dispatch_inbox_activity( + g.s, actor_row, body, from_actor_url, + domain=domain, + app_domain=follower_app_domain, + ) + + # Mark as processed + item.state = "processed" + item.processed_at = datetime.now(timezone.utc) + await g.s.flush() + + return Response(status=202) + + # ------------------------------------------------------------------ + # Outbox + # ------------------------------------------------------------------ + + @bp.get("/users//outbox") + async def outbox(username: str): + actor = await services.federation.get_actor_by_username(g.s, username) + if not actor: + abort(404) + + actor_url = f"https://{domain}/users/{username}" + page_param = request.args.get("page") + + if not page_param: + _, total = await services.federation.get_outbox( + g.s, username, page=1, per_page=1, + origin_app=outbox_origin_app, + ) + return Response( + response=json.dumps({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollection", + "id": f"{actor_url}/outbox", + "totalItems": total, + "first": f"{actor_url}/outbox?page=1", + }), + content_type=AP_CONTENT_TYPE, + ) + + page_num = int(page_param) + activities, total = await services.federation.get_outbox( + g.s, username, page=page_num, per_page=20, + origin_app=outbox_origin_app, + ) + + items = [] + for a in activities: + items.append({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": a.activity_type, + "id": a.activity_id, + "actor": actor_url, + "published": a.published.isoformat() if a.published else None, + "object": { + "type": a.object_type, + **(a.object_data or {}), + }, + }) + + return Response( + response=json.dumps({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollectionPage", + "id": f"{actor_url}/outbox?page={page_num}", + "partOf": f"{actor_url}/outbox", + "totalItems": total, + "orderedItems": items, + }), + content_type=AP_CONTENT_TYPE, + ) + + # ------------------------------------------------------------------ + # Followers / following collections + # ------------------------------------------------------------------ + + @bp.get("/users//followers") + async def followers(username: str): + actor = await services.federation.get_actor_by_username(g.s, username) + if not actor: + abort(404) + + collection_id = f"https://{domain}/users/{username}/followers" + follower_list = await services.federation.get_followers( + g.s, username, app_domain=follower_app_domain, + ) + page_param = request.args.get("page") + + if not page_param: + return Response( + response=json.dumps({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollection", + "id": collection_id, + "totalItems": len(follower_list), + "first": f"{collection_id}?page=1", + }), + content_type=AP_CONTENT_TYPE, + ) + + return Response( + response=json.dumps({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollectionPage", + "id": f"{collection_id}?page=1", + "partOf": collection_id, + "totalItems": len(follower_list), + "orderedItems": [f.follower_actor_url for f in follower_list], + }), + content_type=AP_CONTENT_TYPE, + ) + + @bp.get("/users//following") + async def following(username: str): + actor = await services.federation.get_actor_by_username(g.s, username) + if not actor: + abort(404) + + collection_id = f"https://{domain}/users/{username}/following" + following_list, total = await services.federation.get_following(g.s, username) + page_param = request.args.get("page") + + if not page_param: + return Response( + response=json.dumps({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollection", + "id": collection_id, + "totalItems": total, + "first": f"{collection_id}?page=1", + }), + content_type=AP_CONTENT_TYPE, + ) + + return Response( + response=json.dumps({ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "OrderedCollectionPage", + "id": f"{collection_id}?page=1", + "partOf": collection_id, + "totalItems": total, + "orderedItems": [f.actor_url for f in following_list], + }), + content_type=AP_CONTENT_TYPE, + ) + + return bp diff --git a/shared/infrastructure/ap_inbox_handlers.py b/shared/infrastructure/ap_inbox_handlers.py new file mode 100644 index 0000000..d972631 --- /dev/null +++ b/shared/infrastructure/ap_inbox_handlers.py @@ -0,0 +1,564 @@ +"""Reusable AP inbox handlers for all apps. + +Extracted from federation/bp/actors/routes.py so that every app's +shared AP blueprint can process Follow, Undo, Accept, Create, etc. +""" +from __future__ import annotations + +import json +import logging +import uuid +from datetime import datetime, timezone + +import httpx +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.federation import ( + ActorProfile, APInboxItem, APInteraction, APNotification, + APRemotePost, APActivity, RemoteActor, +) +from shared.services.registry import services + +log = logging.getLogger(__name__) + +AP_CONTENT_TYPE = "application/activity+json" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +async def fetch_remote_actor(actor_url: str) -> dict | None: + """Fetch a remote actor's JSON-LD profile.""" + try: + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.get( + actor_url, + headers={"Accept": AP_CONTENT_TYPE}, + ) + if resp.status_code == 200: + return resp.json() + except Exception: + log.exception("Failed to fetch remote actor: %s", actor_url) + return None + + +async def send_accept( + actor: ActorProfile, + follow_activity: dict, + follower_inbox: str, + domain: str, +) -> None: + """Send an Accept activity back to the follower.""" + from shared.utils.http_signatures import sign_request + from urllib.parse import urlparse + + username = actor.preferred_username + actor_url = f"https://{domain}/users/{username}" + + accept_id = f"{actor_url}/activities/{uuid.uuid4()}" + accept = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": accept_id, + "type": "Accept", + "actor": actor_url, + "object": follow_activity, + } + + body_bytes = json.dumps(accept).encode() + key_id = f"{actor_url}#main-key" + + parsed = urlparse(follower_inbox) + headers = sign_request( + private_key_pem=actor.private_key_pem, + key_id=key_id, + method="POST", + path=parsed.path, + host=parsed.netloc, + body=body_bytes, + ) + headers["Content-Type"] = AP_CONTENT_TYPE + + log.info("Accept payload → %s: %s", follower_inbox, json.dumps(accept)[:500]) + + try: + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.post( + follower_inbox, + content=body_bytes, + headers=headers, + ) + log.info("Accept → %s: %d %s", follower_inbox, resp.status_code, resp.text[:200]) + except Exception: + log.exception("Failed to send Accept to %s", follower_inbox) + + +async def backfill_follower( + session: AsyncSession, + actor: ActorProfile, + follower_inbox: str, + domain: str, + origin_app: str | None = None, +) -> None: + """Deliver recent *current* Create activities to a new follower's inbox. + + Skips Creates whose source was later Deleted, and uses the latest + Update data when available (so the follower sees the current version). + """ + from shared.events.handlers.ap_delivery_handler import ( + _build_activity_json, _deliver_to_inbox, + ) + + filters = [ + APActivity.actor_profile_id == actor.id, + APActivity.is_local == True, # noqa: E712 + APActivity.activity_type == "Create", + APActivity.source_type.isnot(None), + APActivity.source_id.isnot(None), + ] + if origin_app is not None: + filters.append(APActivity.origin_app == origin_app) + + creates = ( + await session.execute( + select(APActivity).where(*filters) + .order_by(APActivity.published.desc()) + .limit(40) + ) + ).scalars().all() + + if not creates: + return + + # Collect source keys that have been Deleted + source_keys = {(c.source_type, c.source_id) for c in creates} + deleted_keys: set[tuple[str | None, int | None]] = set() + if source_keys: + deletes = ( + await session.execute( + select(APActivity.source_type, APActivity.source_id).where( + APActivity.actor_profile_id == actor.id, + APActivity.activity_type == "Delete", + APActivity.is_local == True, # noqa: E712 + ) + ) + ).all() + deleted_keys = {(d[0], d[1]) for d in deletes} + + # For sources with Updates, grab the latest Update's object_data + updated_data: dict[tuple[str | None, int | None], dict] = {} + if source_keys: + updates = ( + await session.execute( + select(APActivity).where( + APActivity.actor_profile_id == actor.id, + APActivity.activity_type == "Update", + APActivity.is_local == True, # noqa: E712 + ).order_by(APActivity.published.desc()) + ) + ).scalars().all() + for u in updates: + key = (u.source_type, u.source_id) + if key not in updated_data and key in source_keys: + updated_data[key] = u.object_data or {} + + # Filter to current, non-deleted Creates (limit 20) + activities = [] + for c in creates: + key = (c.source_type, c.source_id) + if key in deleted_keys: + continue + # Apply latest Update data if available + if key in updated_data: + c.object_data = updated_data[key] + activities.append(c) + if len(activities) >= 20: + break + + if not activities: + return + + log.info( + "Backfilling %d posts to %s for @%s", + len(activities), follower_inbox, actor.preferred_username, + ) + + async with httpx.AsyncClient() as client: + for activity in reversed(activities): # oldest first + activity_json = _build_activity_json(activity, actor, domain) + await _deliver_to_inbox(client, follower_inbox, activity_json, actor, domain) + + +# --------------------------------------------------------------------------- +# Inbox activity handlers +# --------------------------------------------------------------------------- + +async def handle_follow( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, + domain: str, + app_domain: str = "federation", +) -> None: + """Process a Follow activity: add follower, send Accept, backfill.""" + remote_actor = await fetch_remote_actor(from_actor_url) + if not remote_actor: + log.warning("Could not fetch remote actor for Follow: %s", from_actor_url) + return + + follower_inbox = remote_actor.get("inbox") + if not follower_inbox: + log.warning("Remote actor has no inbox: %s", from_actor_url) + return + + remote_username = remote_actor.get("preferredUsername", "") + from urllib.parse import urlparse + remote_domain = urlparse(from_actor_url).netloc + follower_acct = f"{remote_username}@{remote_domain}" if remote_username else from_actor_url + + pub_key = (remote_actor.get("publicKey") or {}).get("publicKeyPem") + + await services.federation.add_follower( + session, + actor_row.preferred_username, + follower_acct=follower_acct, + follower_inbox=follower_inbox, + follower_actor_url=from_actor_url, + follower_public_key=pub_key, + app_domain=app_domain, + ) + + log.info( + "New follower: %s → @%s (app_domain=%s)", + follower_acct, actor_row.preferred_username, app_domain, + ) + + # Notification + ra = ( + await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == from_actor_url) + ) + ).scalar_one_or_none() + if not ra: + ra_dto = await services.federation.get_or_fetch_remote_actor(session, from_actor_url) + if ra_dto: + ra = (await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == from_actor_url) + )).scalar_one_or_none() + + if ra: + notif = APNotification( + actor_profile_id=actor_row.id, + notification_type="follow", + from_remote_actor_id=ra.id, + ) + session.add(notif) + + # Send Accept + await send_accept(actor_row, body, follower_inbox, domain) + + # Backfill: deliver recent posts (filtered by origin_app for per-app follows) + backfill_origin = app_domain if app_domain != "federation" else None + await backfill_follower(session, actor_row, follower_inbox, domain, origin_app=backfill_origin) + + +async def handle_undo( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, + app_domain: str = "federation", +) -> None: + """Process an Undo activity (typically Undo Follow).""" + inner = body.get("object") + if not inner: + return + + inner_type = inner.get("type") if isinstance(inner, dict) else None + if inner_type == "Follow": + from urllib.parse import urlparse + remote_domain = urlparse(from_actor_url).netloc + remote_actor = await fetch_remote_actor(from_actor_url) + remote_username = "" + if remote_actor: + remote_username = remote_actor.get("preferredUsername", "") + follower_acct = f"{remote_username}@{remote_domain}" if remote_username else from_actor_url + + removed = await services.federation.remove_follower( + session, actor_row.preferred_username, follower_acct, + app_domain=app_domain, + ) + if removed: + log.info("Unfollowed: %s → @%s (app_domain=%s)", follower_acct, actor_row.preferred_username, app_domain) + else: + log.debug("Undo Follow: follower not found: %s", follower_acct) + else: + log.debug("Undo for %s — not handled", inner_type) + + +async def handle_accept( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, +) -> None: + """Process Accept activity — update outbound follow state.""" + inner = body.get("object") + if not inner: + return + + inner_type = inner.get("type") if isinstance(inner, dict) else None + if inner_type == "Follow": + await services.federation.accept_follow_response( + session, actor_row.preferred_username, from_actor_url, + ) + log.info("Follow accepted by %s for @%s", from_actor_url, actor_row.preferred_username) + + +async def handle_create( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, + federation_domain: str, +) -> None: + """Process Create(Note/Article) — ingest remote post.""" + obj = body.get("object") + if not obj or not isinstance(obj, dict): + return + + obj_type = obj.get("type", "") + if obj_type not in ("Note", "Article"): + log.debug("Create with type %s — skipping", obj_type) + return + + remote = await services.federation.get_or_fetch_remote_actor(session, from_actor_url) + if not remote: + log.warning("Could not resolve remote actor for Create: %s", from_actor_url) + return + + await services.federation.ingest_remote_post(session, remote.id, body, obj) + log.info("Ingested %s from %s", obj_type, from_actor_url) + + # Mention notification + tags = obj.get("tag", []) + if isinstance(tags, list): + for tag in tags: + if not isinstance(tag, dict): + continue + if tag.get("type") != "Mention": + continue + href = tag.get("href", "") + if f"https://{federation_domain}/users/" in href: + mentioned_username = href.rsplit("/", 1)[-1] + mentioned = await services.federation.get_actor_by_username( + session, mentioned_username, + ) + if mentioned: + rp = (await session.execute( + select(APRemotePost).where( + APRemotePost.object_id == obj.get("id") + ) + )).scalar_one_or_none() + + ra = (await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == from_actor_url) + )).scalar_one_or_none() + + notif = APNotification( + actor_profile_id=mentioned.id, + notification_type="mention", + from_remote_actor_id=ra.id if ra else None, + target_remote_post_id=rp.id if rp else None, + ) + session.add(notif) + + # Reply notification + in_reply_to = obj.get("inReplyTo") + if in_reply_to and f"https://{federation_domain}/users/" in str(in_reply_to): + local_activity = (await session.execute( + select(APActivity).where( + APActivity.activity_id == in_reply_to, + ) + )).scalar_one_or_none() + if local_activity: + ra = (await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == from_actor_url) + )).scalar_one_or_none() + rp = (await session.execute( + select(APRemotePost).where( + APRemotePost.object_id == obj.get("id") + ) + )).scalar_one_or_none() + + notif = APNotification( + actor_profile_id=local_activity.actor_profile_id, + notification_type="reply", + from_remote_actor_id=ra.id if ra else None, + target_remote_post_id=rp.id if rp else None, + ) + session.add(notif) + + +async def handle_update( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, +) -> None: + """Process Update — re-ingest remote post.""" + obj = body.get("object") + if not obj or not isinstance(obj, dict): + return + obj_type = obj.get("type", "") + if obj_type in ("Note", "Article"): + remote = await services.federation.get_or_fetch_remote_actor(session, from_actor_url) + if remote: + await services.federation.ingest_remote_post(session, remote.id, body, obj) + log.info("Updated %s from %s", obj_type, from_actor_url) + + +async def handle_delete( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, +) -> None: + """Process Delete — remove remote post.""" + obj = body.get("object") + if isinstance(obj, str): + object_id = obj + elif isinstance(obj, dict): + object_id = obj.get("id", "") + else: + return + if object_id: + await services.federation.delete_remote_post(session, object_id) + log.info("Deleted remote post %s from %s", object_id, from_actor_url) + + +async def handle_like( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, +) -> None: + """Process incoming Like — record interaction + notify.""" + object_id = body.get("object", "") + if isinstance(object_id, dict): + object_id = object_id.get("id", "") + if not object_id: + return + + remote = await services.federation.get_or_fetch_remote_actor(session, from_actor_url) + if not remote: + return + + ra = (await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == from_actor_url) + )).scalar_one_or_none() + + target = (await session.execute( + select(APActivity).where(APActivity.activity_id == object_id) + )).scalar_one_or_none() + + if not target: + log.info("Like from %s for %s (target not found locally)", from_actor_url, object_id) + return + + interaction = APInteraction( + remote_actor_id=ra.id if ra else None, + post_type="local", + post_id=target.id, + interaction_type="like", + activity_id=body.get("id"), + ) + session.add(interaction) + + notif = APNotification( + actor_profile_id=target.actor_profile_id, + notification_type="like", + from_remote_actor_id=ra.id if ra else None, + target_activity_id=target.id, + ) + session.add(notif) + log.info("Like from %s on activity %s", from_actor_url, object_id) + + +async def handle_announce( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, +) -> None: + """Process incoming Announce (boost) — record interaction + notify.""" + object_id = body.get("object", "") + if isinstance(object_id, dict): + object_id = object_id.get("id", "") + if not object_id: + return + + remote = await services.federation.get_or_fetch_remote_actor(session, from_actor_url) + if not remote: + return + + ra = (await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == from_actor_url) + )).scalar_one_or_none() + + target = (await session.execute( + select(APActivity).where(APActivity.activity_id == object_id) + )).scalar_one_or_none() + + if not target: + log.info("Announce from %s for %s (target not found locally)", from_actor_url, object_id) + return + + interaction = APInteraction( + remote_actor_id=ra.id if ra else None, + post_type="local", + post_id=target.id, + interaction_type="boost", + activity_id=body.get("id"), + ) + session.add(interaction) + + notif = APNotification( + actor_profile_id=target.actor_profile_id, + notification_type="boost", + from_remote_actor_id=ra.id if ra else None, + target_activity_id=target.id, + ) + session.add(notif) + log.info("Announce from %s on activity %s", from_actor_url, object_id) + + +async def dispatch_inbox_activity( + session: AsyncSession, + actor_row: ActorProfile, + body: dict, + from_actor_url: str, + domain: str, + app_domain: str = "federation", +) -> None: + """Route an inbox activity to the correct handler.""" + activity_type = body.get("type", "") + + if activity_type == "Follow": + await handle_follow(session, actor_row, body, from_actor_url, domain, app_domain=app_domain) + elif activity_type == "Undo": + await handle_undo(session, actor_row, body, from_actor_url, app_domain=app_domain) + elif activity_type == "Accept": + await handle_accept(session, actor_row, body, from_actor_url) + elif activity_type == "Create": + await handle_create(session, actor_row, body, from_actor_url, domain) + elif activity_type == "Update": + await handle_update(session, actor_row, body, from_actor_url) + elif activity_type == "Delete": + await handle_delete(session, actor_row, body, from_actor_url) + elif activity_type == "Like": + await handle_like(session, actor_row, body, from_actor_url) + elif activity_type == "Announce": + await handle_announce(session, actor_row, body, from_actor_url) diff --git a/shared/infrastructure/cart_identity.py b/shared/infrastructure/cart_identity.py new file mode 100644 index 0000000..f16c674 --- /dev/null +++ b/shared/infrastructure/cart_identity.py @@ -0,0 +1,34 @@ +""" +Cart identity resolution — shared across all apps that need to know +who the current cart owner is (user_id or anonymous session_id). +""" +from __future__ import annotations + +import secrets +from typing import TypedDict, Optional + +from quart import g, session as qsession + + +class CartIdentity(TypedDict): + user_id: Optional[int] + session_id: Optional[str] + + +def current_cart_identity() -> CartIdentity: + """ + Decide how to identify the cart: + + - If user is logged in -> use user_id (and ignore session_id) + - Else -> generate / reuse an anonymous session_id stored in Quart's session + """ + user = getattr(g, "user", None) + if user is not None and getattr(user, "id", None) is not None: + return {"user_id": user.id, "session_id": None} + + sid = qsession.get("cart_sid") + if not sid: + sid = secrets.token_hex(16) + qsession["cart_sid"] = sid + + return {"user_id": None, "session_id": sid} diff --git a/shared/infrastructure/context.py b/shared/infrastructure/context.py new file mode 100644 index 0000000..a98227c --- /dev/null +++ b/shared/infrastructure/context.py @@ -0,0 +1,58 @@ +""" +Base template context shared by all apps. + +This module no longer imports cart or menu_items services directly. +Each app provides its own context_fn that calls this base and adds +app-specific variables (cart data, menu_items, etc.). +""" +from __future__ import annotations + +from datetime import datetime + +from quart import request, g, current_app + +from shared.config import config +from shared.utils import host_url +from shared.browser.app.utils import current_route_relative_path + + +async def base_context() -> dict: + """ + Common template variables available in every app. + + Does NOT include cart, calendar_cart_entries, total, calendar_total, + or menu_items — those are added by each app's context_fn. + """ + is_htmx = request.headers.get("HX-Request") == "true" + search = request.headers.get("X-Search", "") + zap_filter = is_htmx and search == "" + + def base_url(): + return host_url() + + hx_select = "#main-panel" + hx_select_search = ( + hx_select + + ", #search-mobile, #search-count-mobile, #search-desktop, #search-count-desktop, #menu-items-nav-wrapper" + ) + + return { + "is_htmx": is_htmx, + "request": request, + "now": datetime.now(), + "current_local_href": current_route_relative_path(), + "config": config(), + "asset_url": current_app.jinja_env.globals.get("asset_url", lambda p: ""), + "sort_options": [ + ("az", "A\u2013Z", "order/a-z.svg"), + ("za", "Z\u2013A", "order/z-a.svg"), + ("price-asc", "\u00a3 low\u2192high", "order/l-h.svg"), + ("price-desc", "\u00a3 high\u2192low", "order/h-l.svg"), + ], + "zap_filter": zap_filter, + "print": print, + "base_url": base_url, + "base_title": config()["title"], + "hx_select": hx_select, + "hx_select_search": hx_select_search, + } diff --git a/shared/infrastructure/factory.py b/shared/infrastructure/factory.py new file mode 100644 index 0000000..4f3869b --- /dev/null +++ b/shared/infrastructure/factory.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import asyncio +import os +import secrets +from pathlib import Path +from typing import Callable, Awaitable, Sequence + +from quart import Quart, request, g, redirect, send_from_directory + +from shared.config import init_config, config, pretty +from shared.models import KV # ensure shared models imported +# Register all app model classes with SQLAlchemy so cross-domain +# relationship() string references resolve correctly. +for _mod in ("blog.models", "market.models", "cart.models", "events.models", "federation.models", "account.models"): + try: + __import__(_mod) + except ImportError: + pass +from shared.log_config import configure_logging +from shared.events import EventProcessor + +from shared.db.session import register_db +from shared.browser.app.middleware import register as register_middleware +from shared.browser.app.redis_cacher import register as register_redis +from shared.browser.app.csrf import protect +from shared.browser.app.errors import errors + +from .jinja_setup import setup_jinja +from .user_loader import load_current_user + + +# Async init of config (runs once at import) +asyncio.run(init_config()) + +BASE_DIR = Path(__file__).resolve().parent.parent +STATIC_DIR = str(BASE_DIR / "static") +TEMPLATE_DIR = str(BASE_DIR / "browser" / "templates") + + +def create_base_app( + name: str, + *, + context_fn: Callable[[], Awaitable[dict]] | None = None, + before_request_fns: Sequence[Callable[[], Awaitable[None]]] | None = None, + domain_services_fn: Callable[[], None] | None = None, +) -> Quart: + """ + Create a Quart app with shared infrastructure. + + Parameters + ---------- + name: + Application name (also used as CACHE_APP_PREFIX). + context_fn: + Async function returning a dict for template context. + Each app provides its own — the cart app queries locally, + while blog/market apps fetch via internal API. + If not provided, a minimal default context is used. + before_request_fns: + Extra before-request hooks (e.g. cart_loader for the cart app). + domain_services_fn: + Callable that registers domain services on the shared registry. + Each app provides its own — registering real impls for owned + domains and stubs (or real impls) for others. + """ + if domain_services_fn is not None: + domain_services_fn() + + from shared.services.widgets import register_all_widgets + register_all_widgets() + + app = Quart( + name, + static_folder=STATIC_DIR, + static_url_path="/static", + template_folder=TEMPLATE_DIR, + ) + + configure_logging(name) + + app.secret_key = os.getenv("SECRET_KEY", "dev-secret-key-change-me-777") + + # Per-app first-party session cookie (no shared domain — avoids Safari ITP) + app.config["SESSION_COOKIE_NAME"] = f"{name}_session" + app.config["SESSION_COOKIE_SAMESITE"] = "Lax" + app.config["SESSION_COOKIE_SECURE"] = True + + # Ghost / Redis config + app.config["GHOST_API_URL"] = os.getenv("GHOST_API_URL") + app.config["GHOST_PUBLIC_URL"] = os.getenv("GHOST_PUBLIC_URL") + app.config["GHOST_CONTENT_KEY"] = os.getenv("GHOST_CONTENT_API_KEY") + app.config["REDIS_URL"] = os.getenv("REDIS_URL") + + # Cache app prefix for key namespacing + app.config["CACHE_APP_PREFIX"] = name + + # --- infrastructure --- + register_middleware(app) + register_db(app) + register_redis(app) + setup_jinja(app) + errors(app) + + # Auto-register OAuth client blueprint for non-account apps + # (account is the OAuth authorization server) + if name != "account": + from shared.infrastructure.oauth import create_oauth_blueprint + app.register_blueprint(create_oauth_blueprint(name)) + + # Auto-register ActivityPub blueprint for AP-enabled apps + from shared.infrastructure.activitypub import AP_APPS + if name in AP_APPS: + from shared.infrastructure.activitypub import create_activitypub_blueprint + app.register_blueprint(create_activitypub_blueprint(name)) + + # --- device id (all apps, including account) --- + _did_cookie = f"{name}_did" + + @app.before_request + async def _init_device_id(): + did = request.cookies.get(_did_cookie) + if did: + g.device_id = did + g._new_device_id = False + else: + g.device_id = secrets.token_urlsafe(32) + g._new_device_id = True + + @app.after_request + async def _set_device_cookie(response): + if getattr(g, "_new_device_id", False): + response.set_cookie( + _did_cookie, g.device_id, + max_age=30 * 24 * 3600, + secure=True, samesite="Lax", httponly=True, + ) + return response + + # --- before-request hooks --- + @app.before_request + async def _route_log(): + g.root = request.headers.get("x-forwarded-prefix", "/") + g.scheme = request.scheme + g.host = request.host + + @app.before_request + async def _load_user(): + await load_current_user() + + # Register any app-specific before-request hooks (e.g. cart loader) + if before_request_fns: + for fn in before_request_fns: + app.before_request(fn) + + # Auth state check via grant verification + silent OAuth handshake + if name != "account": + @app.before_request + async def _check_auth_state(): + from quart import session as qs + from urllib.parse import quote as _quote + if request.path.startswith(("/auth/", "/static/", "/.well-known/", "/users/", "/nodeinfo/", "/internal/")): + return + + uid = qs.get("uid") + grant_token = qs.get("grant_token") + + from shared.browser.app.redis_cacher import get_redis + redis = get_redis() + + # Case 1: logged in — verify grant still valid (direct DB, cached) + if uid and grant_token: + cache_key = f"grant:{grant_token}" + if redis: + # Quick check: if did_auth was cleared (logout), skip cache + device_id = g.device_id + did_auth_present = await redis.get(f"did_auth:{device_id}") if device_id else True + cached = await redis.get(cache_key) + if cached == b"ok" and did_auth_present: + return + if cached == b"revoked": + qs.pop("uid", None) + qs.pop("grant_token", None) + qs.pop("cart_sid", None) + return + + from sqlalchemy import select + from shared.db.session import get_session + from shared.models.oauth_grant import OAuthGrant + try: + async with get_session() as s: + grant = await s.scalar( + select(OAuthGrant).where(OAuthGrant.token == grant_token) + ) + valid = grant is not None and grant.revoked_at is None + except Exception: + return # DB error — don't log user out + + if redis: + await redis.set(cache_key, b"ok" if valid else b"revoked", ex=60) + if not valid: + qs.pop("uid", None) + qs.pop("grant_token", None) + qs.pop("cart_sid", None) + return + + # Case 2: not logged in — prompt=none OAuth (GET, non-HTMX only) + if not uid and request.method == "GET": + if request.headers.get("HX-Request"): + return + import time as _time + now = _time.time() + pnone_at = qs.get("_pnone_at") + device_id = g.device_id + + # Check if account signalled a login after we cached "not logged in" + # (blog_did == account_did — same value set during OAuth callback) + if device_id and redis and pnone_at: + auth_ts = await redis.get(f"did_auth:{device_id}") + if auth_ts: + try: + if float(auth_ts) > pnone_at: + qs.pop("_pnone_at", None) + return redirect(f"/auth/login?prompt=none&next={_quote(request.url, safe='')}") + except (ValueError, TypeError): + pass + + if pnone_at and (now - pnone_at) < 300: + return + if device_id and redis: + cached = await redis.get(f"prompt:{name}:{device_id}") + if cached == b"none": + return + return redirect(f"/auth/login?prompt=none&next={_quote(request.url, safe='')}") + + @app.before_request + async def _csrf_protect(): + await protect() + + # --- after-request hooks --- + # Clear old shared-domain session cookie (migration from .rose-ash.com) + @app.after_request + async def _clear_old_shared_cookie(response): + if request.cookies.get("blog_session"): + response.delete_cookie("blog_session", domain=".rose-ash.com", path="/") + return response + + @app.after_request + async def _add_hx_preserve_search_header(response): + value = request.headers.get("X-Search") + if value is not None: + response.headers["HX-Preserve-Search"] = value + return response + + # --- context processor --- + if context_fn is not None: + @app.context_processor + async def _inject_base(): + return await context_fn() + else: + # Minimal fallback (no cart, no menu_items) + from .context import base_context + + @app.context_processor + async def _inject_base(): + return await base_context() + + # --- event processor --- + _event_processor = EventProcessor(app_name=name) + + # --- startup --- + @app.before_serving + async def _startup(): + from shared.events.handlers import register_shared_handlers + register_shared_handlers() + await init_config() + print(pretty()) + await _event_processor.start() + + @app.after_serving + async def _stop_event_processor(): + await _event_processor.stop() + + # --- favicon --- + @app.get("/favicon.ico") + async def favicon(): + return await send_from_directory("static", "favicon.ico") + + return app diff --git a/shared/infrastructure/fragments.py b/shared/infrastructure/fragments.py new file mode 100644 index 0000000..699a539 --- /dev/null +++ b/shared/infrastructure/fragments.py @@ -0,0 +1,193 @@ +""" +Server-side fragment composition client. + +Each coop app exposes HTML fragments at ``/internal/fragments/{type}``. +This module provides helpers to fetch and cache those fragments so that +consuming apps can compose cross-app UI without shared templates. + +Failures raise ``FragmentError`` by default so broken fragments are +immediately visible rather than silently missing from the page. +""" + +from __future__ import annotations + +import asyncio +import logging +import os +from typing import Sequence + +import httpx + +log = logging.getLogger(__name__) + +# Re-usable async client (created lazily, one per process) +_client: httpx.AsyncClient | None = None + +# Default request timeout (seconds) +_DEFAULT_TIMEOUT = 2.0 + +# Header sent on every fragment request so providers can distinguish +# fragment fetches from normal browser traffic. +FRAGMENT_HEADER = "X-Fragment-Request" + + +class FragmentError(Exception): + """Raised when a fragment fetch fails.""" + + +def _get_client() -> httpx.AsyncClient: + global _client + if _client is None or _client.is_closed: + _client = httpx.AsyncClient( + timeout=httpx.Timeout(_DEFAULT_TIMEOUT), + follow_redirects=False, + ) + return _client + + +def _internal_url(app_name: str) -> str: + """Resolve the Docker-internal base URL for *app_name*. + + Looks up ``INTERNAL_URL_{APP}`` first, falls back to + ``http://{app}:8000``. + """ + env_key = f"INTERNAL_URL_{app_name.upper()}" + return os.getenv(env_key, f"http://{app_name}:8000").rstrip("/") + + +# ------------------------------------------------------------------ +# Public API +# ------------------------------------------------------------------ + +def _is_fragment_request() -> bool: + """True when the current request is itself a fragment fetch.""" + try: + from quart import request as _req + return bool(_req.headers.get(FRAGMENT_HEADER)) + except Exception: + return False + + +async def fetch_fragment( + app_name: str, + fragment_type: str, + *, + params: dict | None = None, + timeout: float = _DEFAULT_TIMEOUT, + required: bool = True, +) -> str: + """Fetch an HTML fragment from another app. + + Returns the raw HTML string. When *required* is True (default), + raises ``FragmentError`` on network errors or non-200 responses. + When *required* is False, returns ``""`` on failure. + + Automatically returns ``""`` when called inside a fragment request + to prevent circular dependencies between apps. + """ + if _is_fragment_request(): + return "" + + base = _internal_url(app_name) + url = f"{base}/internal/fragments/{fragment_type}" + try: + resp = await _get_client().get( + url, + params=params, + headers={FRAGMENT_HEADER: "1"}, + timeout=timeout, + ) + if resp.status_code == 200: + return resp.text + msg = f"Fragment {app_name}/{fragment_type} returned {resp.status_code}" + if required: + log.error(msg) + raise FragmentError(msg) + log.warning(msg) + return "" + except FragmentError: + raise + except Exception as exc: + msg = f"Fragment {app_name}/{fragment_type} failed: {exc}" + if required: + log.error(msg) + raise FragmentError(msg) from exc + log.warning(msg) + return "" + + +async def fetch_fragments( + requests: Sequence[tuple[str, str, dict | None]], + *, + timeout: float = _DEFAULT_TIMEOUT, + required: bool = True, +) -> list[str]: + """Fetch multiple fragments concurrently. + + *requests* is a sequence of ``(app_name, fragment_type, params)`` tuples. + Returns a list of HTML strings in the same order. When *required* + is True, any single failure raises ``FragmentError``. + """ + return list(await asyncio.gather(*( + fetch_fragment(app, ftype, params=params, timeout=timeout, required=required) + for app, ftype, params in requests + ))) + + +async def fetch_fragment_cached( + app_name: str, + fragment_type: str, + *, + params: dict | None = None, + ttl: int = 30, + timeout: float = _DEFAULT_TIMEOUT, + required: bool = True, +) -> str: + """Fetch a fragment with a Redis cache layer. + + Cache key: ``frag:{app}:{type}:{sorted_params}``. + """ + # Build a stable cache key + suffix = "" + if params: + sorted_items = sorted(params.items()) + suffix = ":" + "&".join(f"{k}={v}" for k, v in sorted_items) + cache_key = f"frag:{app_name}:{fragment_type}{suffix}" + + # Try Redis cache + redis = _get_redis() + if redis: + try: + cached = await redis.get(cache_key) + if cached is not None: + return cached.decode() if isinstance(cached, bytes) else cached + except Exception: + pass + + # Cache miss — fetch from provider + html = await fetch_fragment( + app_name, fragment_type, params=params, timeout=timeout, required=required, + ) + + # Store in cache (even empty string — avoids hammering a down service) + if redis and ttl > 0: + try: + await redis.set(cache_key, html.encode(), ex=ttl) + except Exception: + pass + + return html + + +# ------------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------------ + +def _get_redis(): + """Return the current app's Redis connection, or None.""" + try: + from quart import current_app + r = current_app.redis + return r if r else None + except Exception: + return None diff --git a/shared/infrastructure/http_utils.py b/shared/infrastructure/http_utils.py new file mode 100644 index 0000000..4b39779 --- /dev/null +++ b/shared/infrastructure/http_utils.py @@ -0,0 +1,49 @@ +""" +HTTP utility helpers shared across apps. + +Extracted from browse/services/services.py so order/orders blueprints +(which live in the cart app) don't need to import from the browse blueprint. +""" +from __future__ import annotations + +from urllib.parse import urlencode + +from quart import g, request +from shared.utils import host_url + + +def vary(resp): + """ + Ensure HX-Request and X-Origin are part of the Vary header + so caches distinguish HTMX from full-page requests. + """ + v = resp.headers.get("Vary", "") + parts = [p.strip() for p in v.split(",") if p.strip()] + for h in ("HX-Request", "X-Origin"): + if h not in parts: + parts.append(h) + if parts: + resp.headers["Vary"] = ", ".join(parts) + return resp + + +def current_url_without_page(): + """ + Return the current URL with the ``page`` query-string parameter removed. + Used for Hx-Push-Url headers on paginated routes. + """ + (request.script_root or "").rstrip("/") + root2 = "/" + g.root + path_only = request.path + + if root2 and path_only.startswith(root2): + rel = path_only[len(root2):] + rel = rel if rel.startswith("/") else "/" + rel + else: + rel = path_only + base = host_url(rel) + + params = request.args.to_dict(flat=False) + params.pop("page", None) + qs = urlencode(params, doseq=True) + return f"{base}?{qs}" if qs else base diff --git a/shared/infrastructure/jinja_setup.py b/shared/infrastructure/jinja_setup.py new file mode 100644 index 0000000..01c80e7 --- /dev/null +++ b/shared/infrastructure/jinja_setup.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import hashlib +import re +from pathlib import Path + +from quart import Quart, g, url_for + +from shared.config import config +from shared.utils import host_url + +from shared.browser.app.csrf import generate_csrf_token +from shared.browser.app.authz import has_access +from shared.browser.app.filters import register as register_filters + +from .urls import blog_url, market_url, cart_url, events_url, federation_url, account_url, login_url, page_cart_url, market_product_url + + +def setup_jinja(app: Quart) -> None: + app.jinja_env.add_extension("jinja2.ext.do") + + # --- template globals --- + app.add_template_global(generate_csrf_token, "csrf_token") + app.add_template_global(has_access, "has_access") + + def level(): + if not hasattr(g, "_level_counter"): + g._level_counter = 0 + return g._level_counter + + def level_up(): + if not hasattr(g, "_level_counter"): + g._level_counter = 0 + g._level_counter += 1 + return "" + + app.jinja_env.globals["level"] = level + app.jinja_env.globals["level_up"] = level_up + app.jinja_env.globals["menu_colour"] = "sky" + app.jinja_env.globals["app_name"] = app.name + + select_colours = """ + [.hover-capable_&]:hover:bg-yellow-300 + aria-selected:bg-stone-500 aria-selected:text-white + [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500""" + app.jinja_env.globals["select_colours"] = select_colours + + nav_button = f"""justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black + {select_colours}""" + + styles = { + "pill": """ + inline-flex items-center px-3 py-1 rounded-full bg-stone-200 text-stone-700 text-sm + hover:bg-stone-300 hover:text-stone-900 + focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 + """, + "tr": "odd:bg-slate-50 even:bg-white hover:bg-slate-100", + "action_button": "px-2 py-1 border rounded text-sm bg-sky-300 hover:bg-sky-400 flex gap-1 items-center", + "pre_action_button": "px-2 py-1 border rounded text-sm bg-green-200 hover:bg-green-300", + "cancel_button": "px-3 py-1.5 rounded-full text-sm border border-stone-300 text-stone-700 hover:bg-stone-100", + "list_container": "border border-stone-200 rounded-lg p-3 mb-3 bg-white space-y-3 bg-yellow-200", + "nav_button": f"{nav_button} p-3", + "nav_button_less_pad": f"{nav_button} p-2", + } + app.jinja_env.globals["styles"] = styles + + def _asset_url(path: str) -> str: + def squash_double_slashes(url: str) -> str: + m = re.match(r"(?:[A-Za-z][\w+.-]*:)?//", url) + prefix = m.group(0) if m else "" + rest = re.sub(r"/+", "/", url[len(prefix):]) + return prefix + rest + + file_path = Path("static") / path + try: + digest = hashlib.md5(file_path.read_bytes()).hexdigest()[:8] + except Exception: + digest = "dev" + return squash_double_slashes( + f"{g.scheme}://{g.host}{g.root}/{url_for('static', filename=path, v=digest)}" + ) + + app.jinja_env.globals["asset_url"] = _asset_url + + def site(): + return { + "url": host_url(), + "logo": _asset_url("img/logo.jpg"), + "default_image": _asset_url("img/logo.jpg"), + "title": config()["title"], + } + + app.jinja_env.globals["site"] = site + + # cross-app URL helpers available in all templates + app.jinja_env.globals["blog_url"] = blog_url + app.jinja_env.globals["market_url"] = market_url + app.jinja_env.globals["cart_url"] = cart_url + app.jinja_env.globals["events_url"] = events_url + app.jinja_env.globals["federation_url"] = federation_url + app.jinja_env.globals["account_url"] = account_url + app.jinja_env.globals["login_url"] = login_url + app.jinja_env.globals["page_cart_url"] = page_cart_url + app.jinja_env.globals["market_product_url"] = market_product_url + + # widget registry available in all templates + from shared.services.widget_registry import widgets as _widget_registry + app.jinja_env.globals["widgets"] = _widget_registry + + # fragment composition helper — fetch HTML from another app's fragment API + from shared.infrastructure.fragments import fetch_fragment_cached + + async def _fragment(app_name: str, fragment_type: str, ttl: int = 30, **params) -> str: + p = params if params else None + return await fetch_fragment_cached(app_name, fragment_type, params=p, ttl=ttl) + + app.jinja_env.globals["fragment"] = _fragment + + # register jinja filters + register_filters(app) diff --git a/shared/infrastructure/oauth.py b/shared/infrastructure/oauth.py new file mode 100644 index 0000000..85f51f3 --- /dev/null +++ b/shared/infrastructure/oauth.py @@ -0,0 +1,183 @@ +"""OAuth2 client blueprint for non-account apps. + +Each client app gets /auth/login, /auth/callback, /auth/logout. +Account is the OAuth authorization server. + +Device cookie ({app}_did) ties the browser to its auth state so +client apps can detect login/logout without cross-domain cookies. +""" +from __future__ import annotations + +import secrets +from datetime import datetime, timezone + +from quart import ( + Blueprint, + redirect, + request, + session as qsession, + g, + current_app, + make_response, +) +from sqlalchemy import select + +from shared.db.session import get_session +from shared.models.oauth_code import OAuthCode +from shared.infrastructure.urls import account_url, app_url +from shared.infrastructure.cart_identity import current_cart_identity +from shared.events import emit_activity + +SESSION_USER_KEY = "uid" +GRANT_TOKEN_KEY = "grant_token" + + +def create_oauth_blueprint(app_name: str) -> Blueprint: + """Return an OAuth client blueprint for *app_name*.""" + bp = Blueprint("oauth_auth", __name__, url_prefix="/auth") + + @bp.get("/login") + @bp.get("/login/") + async def login(): + next_url = request.args.get("next", "/") + prompt = request.args.get("prompt", "") + state = secrets.token_urlsafe(32) + qsession["oauth_state"] = state + qsession["oauth_next"] = next_url + + device_id = g.device_id + redirect_uri = app_url(app_name, "/auth/callback") + params = ( + f"?client_id={app_name}" + f"&redirect_uri={redirect_uri}" + f"&device_id={device_id}" + f"&state={state}" + ) + if prompt: + params += f"&prompt={prompt}" + authorize_url = account_url(f"/auth/oauth/authorize{params}") + return redirect(authorize_url) + + @bp.get("/callback") + @bp.get("/callback/") + async def callback(): + # Adopt account's device id as our own — one identity across all apps + account_did = request.args.get("account_did", "") + if account_did: + qsession["_account_did"] = account_did + # Overwrite this app's device cookie with account's device id + g.device_id = account_did + g._new_device_id = True # factory after_request will set the cookie + + # Handle prompt=none error (user not logged in on account) + error = request.args.get("error") + if error == "login_required": + next_url = qsession.pop("oauth_next", "/") + qsession.pop("oauth_state", None) + import time as _time + qsession["_pnone_at"] = _time.time() + device_id = g.device_id + if device_id: + from shared.browser.app.redis_cacher import get_redis + _redis = get_redis() + if _redis: + await _redis.set( + f"prompt:{app_name}:{device_id}", b"none", ex=300 + ) + return redirect(next_url) + + code = request.args.get("code") + state = request.args.get("state") + expected_state = qsession.pop("oauth_state", None) + next_url = qsession.pop("oauth_next", "/") + + if not code or not state or state != expected_state: + current_app.logger.warning("OAuth callback: bad state or missing code") + return redirect("/") + + expected_redirect = app_url(app_name, "/auth/callback") + now = datetime.now(timezone.utc) + + async with get_session() as s: + async with s.begin(): + result = await s.execute( + select(OAuthCode) + .where(OAuthCode.code == code) + .with_for_update() + ) + oauth_code = result.scalar_one_or_none() + + if not oauth_code: + current_app.logger.warning("OAuth callback: code not found") + return redirect("/") + + if oauth_code.used_at is not None: + current_app.logger.warning("OAuth callback: code already used") + return redirect("/") + + if oauth_code.expires_at < now: + current_app.logger.warning("OAuth callback: code expired") + return redirect("/") + + if oauth_code.client_id != app_name: + current_app.logger.warning("OAuth callback: client_id mismatch") + return redirect("/") + + if oauth_code.redirect_uri != expected_redirect: + current_app.logger.warning("OAuth callback: redirect_uri mismatch") + return redirect("/") + + oauth_code.used_at = now + user_id = oauth_code.user_id + grant_token = oauth_code.grant_token + + # Set local session with grant token for revocation checking + qsession[SESSION_USER_KEY] = user_id + if grant_token: + qsession[GRANT_TOKEN_KEY] = grant_token + qsession.pop("_pnone_at", None) + + # Emit login activity for cart adoption + ident = current_cart_identity() + anon_session_id = ident.get("session_id") + if anon_session_id: + try: + async with get_session() as s: + async with s.begin(): + await emit_activity( + s, + activity_type="rose:Login", + actor_uri="internal:system", + object_type="Person", + object_data={ + "user_id": user_id, + "session_id": anon_session_id, + }, + ) + except Exception: + current_app.logger.exception("OAuth: failed to emit login activity") + + return redirect(next_url, 303) + + @bp.get("/clear") + @bp.get("/clear/") + async def clear(): + """One-time migration helper: clear all session cookies.""" + qsession.clear() + resp = await make_response(redirect("/")) + resp.delete_cookie("blog_session", domain=".rose-ash.com", path="/") + resp.delete_cookie(f"{app_name}_did", path="/") + return resp + + @bp.post("/logout") + @bp.post("/logout/") + async def logout(): + qsession.pop(SESSION_USER_KEY, None) + qsession.pop(GRANT_TOKEN_KEY, None) + qsession.pop("cart_sid", None) + qsession.pop("_pnone_at", None) + qsession.pop("_account_did", None) + # Redirect through account to revoke grants + clear account session + return redirect(account_url("/auth/sso-logout/")) + + return bp diff --git a/shared/infrastructure/urls.py b/shared/infrastructure/urls.py new file mode 100644 index 0000000..28bcb45 --- /dev/null +++ b/shared/infrastructure/urls.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import os +from urllib.parse import quote + +from shared.config import config + + +def _get_app_url(app_name: str) -> str: + env_key = f"APP_URL_{app_name.upper()}" + env_val = os.getenv(env_key) + if env_val: + return env_val.rstrip("/") + return config()["app_urls"][app_name].rstrip("/") + + +def app_url(app_name: str, path: str = "/") -> str: + base = _get_app_url(app_name) + if not path.startswith("/"): + path = "/" + path + return base + path + + +def blog_url(path: str = "/") -> str: + return app_url("blog", path) + + +def market_url(path: str = "/") -> str: + return app_url("market", path) + + +def cart_url(path: str = "/") -> str: + return app_url("cart", path) + + +def events_url(path: str = "/") -> str: + return app_url("events", path) + + +def federation_url(path: str = "/") -> str: + return app_url("federation", path) + + +def account_url(path: str = "/") -> str: + return app_url("account", path) + + +def artdag_url(path: str = "/") -> str: + return app_url("artdag", path) + + +def page_cart_url(page_slug: str, path: str = "/") -> str: + if not path.startswith("/"): + path = "/" + path + return cart_url(f"/{page_slug}{path}") + + +def market_product_url(product_slug: str, suffix: str = "", market_place=None) -> str: + """Build a market product URL with the correct page/market prefix. + + Resolves the prefix from: + - market app context: g.post_slug + g.market_slug + - cart app context: g.page_slug + market_place.slug + """ + from quart import g + + page_slug = getattr(g, "post_slug", None) or getattr(g, "page_slug", None) + ms = getattr(g, "market_slug", None) or ( + getattr(market_place, "slug", None) if market_place else None + ) + prefix = f"/{page_slug}/{ms}" if page_slug and ms else "" + tail = f"/{suffix}" if suffix else "/" + return market_url(f"{prefix}/product/{product_slug}{tail}") + + +def login_url(next_url: str = "") -> str: + from quart import current_app + + # Account handles login directly (magic link flow — it's the OAuth server) + if current_app.name == "account": + base = "/auth/login/" + params: list[str] = [] + if next_url: + params.append(f"next={quote(next_url, safe='')}") + from quart import session as qsession + cart_sid = qsession.get("cart_sid") + if cart_sid: + params.append(f"cart_sid={quote(cart_sid, safe='')}") + if params: + return f"{base}?{'&'.join(params)}" + return base + + # Client apps: local /auth/login triggers OAuth redirect to account + base = "/auth/login/" + if next_url: + return f"{base}?next={quote(next_url, safe='')}" + return base diff --git a/shared/infrastructure/user_loader.py b/shared/infrastructure/user_loader.py new file mode 100644 index 0000000..aa488d4 --- /dev/null +++ b/shared/infrastructure/user_loader.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from quart import session as qsession, g +from sqlalchemy import select +from sqlalchemy.orm import selectinload + +from shared.models.user import User +from shared.models.ghost_membership_entities import UserNewsletter + + +async def load_user_by_id(session, user_id: int): + """Load a user by ID with labels and newsletters eagerly loaded.""" + stmt = ( + select(User) + .options( + selectinload(User.labels), + selectinload(User.user_newsletters).selectinload( + UserNewsletter.newsletter + ), + ) + .where(User.id == user_id) + ) + result = await session.execute(stmt) + return result.scalar_one_or_none() + + +async def load_current_user(): + uid = qsession.get("uid") + if not uid: + g.user = None + g.rights = {"admin": False} + return + + g.user = await load_user_by_id(g.s, uid) + g.rights = {l.name: True for l in g.user.labels} if g.user else {} diff --git a/shared/log_config/__init__.py b/shared/log_config/__init__.py new file mode 100644 index 0000000..359f540 --- /dev/null +++ b/shared/log_config/__init__.py @@ -0,0 +1,3 @@ +from .setup import configure_logging, get_logger + +__all__ = ["configure_logging", "get_logger"] diff --git a/shared/log_config/setup.py b/shared/log_config/setup.py new file mode 100644 index 0000000..50621c7 --- /dev/null +++ b/shared/log_config/setup.py @@ -0,0 +1,66 @@ +""" +Structured JSON logging for all Rose Ash apps. + +Call configure_logging(app_name) once at app startup. +Use get_logger(name) anywhere to get a logger that outputs JSON to stdout. +""" +from __future__ import annotations + +import json +import logging +import sys +from datetime import datetime, timezone + + +class JSONFormatter(logging.Formatter): + """Format log records as single-line JSON objects.""" + + def __init__(self, app_name: str = ""): + super().__init__() + self.app_name = app_name + + def format(self, record: logging.LogRecord) -> str: + entry = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "level": record.levelname, + "app": self.app_name, + "logger": record.name, + "message": record.getMessage(), + } + # Include extra fields if set on the record + for key in ("event_type", "user_id", "request_id", "duration_ms"): + val = getattr(record, key, None) + if val is not None: + entry[key] = val + + if record.exc_info and record.exc_info[0] is not None: + entry["exception"] = self.formatException(record.exc_info) + + return json.dumps(entry, default=str) + + +_configured = False + + +def configure_logging(app_name: str, level: int = logging.INFO) -> None: + """Set up structured JSON logging to stdout. Safe to call multiple times.""" + global _configured + if _configured: + return + _configured = True + + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(JSONFormatter(app_name=app_name)) + + root = logging.getLogger() + root.setLevel(level) + root.addHandler(handler) + + # Quiet down noisy libraries + for name in ("httpx", "httpcore", "asyncio", "sqlalchemy.engine"): + logging.getLogger(name).setLevel(logging.WARNING) + + +def get_logger(name: str) -> logging.Logger: + """Get a named logger. Uses the structured JSON format once configure_logging is called.""" + return logging.getLogger(name) diff --git a/shared/models/__init__.py b/shared/models/__init__.py new file mode 100644 index 0000000..c7303ee --- /dev/null +++ b/shared/models/__init__.py @@ -0,0 +1,33 @@ +from .user import User +from .kv import KV +from .magic_link import MagicLink +from .oauth_code import OAuthCode +from .oauth_grant import OAuthGrant +from .menu_item import MenuItem + +from .ghost_membership_entities import ( + GhostLabel, UserLabel, + GhostNewsletter, UserNewsletter, + GhostTier, GhostSubscription, +) +from .ghost_content import Tag, Post, Author, PostAuthor, PostTag, PostLike +from .page_config import PageConfig +from .order import Order, OrderItem +from .market import ( + Product, ProductLike, ProductImage, ProductSection, + NavTop, NavSub, Listing, ListingItem, + LinkError, LinkExternal, SubcategoryRedirect, ProductLog, + ProductLabel, ProductSticker, ProductAttribute, ProductNutrition, ProductAllergen, + CartItem, +) +from .market_place import MarketPlace +from .calendars import ( + Calendar, CalendarEntry, CalendarSlot, + TicketType, Ticket, CalendarEntryPost, +) +from .container_relation import ContainerRelation +from .menu_node import MenuNode +from .federation import ( + ActorProfile, APActivity, APFollower, APInboxItem, APAnchor, IPFSPin, + RemoteActor, APFollowing, APRemotePost, APLocalPost, APInteraction, APNotification, +) diff --git a/shared/models/calendars.py b/shared/models/calendars.py new file mode 100644 index 0000000..d4e8721 --- /dev/null +++ b/shared/models/calendars.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +from sqlalchemy import ( + Column, Integer, String, DateTime, ForeignKey, CheckConstraint, + Index, text, Text, Boolean, Time, Numeric +) +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +# Adjust this import to match where your Base lives +from shared.db.base import Base + +from datetime import datetime, timezone + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + + +class Calendar(Base): + __tablename__ = "calendars" + + id = Column(Integer, primary_key=True) + container_type = Column(String(32), nullable=False, server_default=text("'page'")) + container_id = Column(Integer, nullable=False) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + slug = Column(String(255), nullable=False) + + created_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + updated_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + # relationships + entries = relationship( + "CalendarEntry", + back_populates="calendar", + cascade="all, delete-orphan", + passive_deletes=True, + order_by="CalendarEntry.start_at", + ) + + slots = relationship( + "CalendarSlot", + back_populates="calendar", + cascade="all, delete-orphan", + passive_deletes=True, + order_by="CalendarSlot.time_start", + ) + + # Indexes / constraints + __table_args__ = ( + Index("ix_calendars_container", "container_type", "container_id"), + Index("ix_calendars_name", "name"), + Index("ix_calendars_slug", "slug"), + # Soft-delete-aware uniqueness: one active calendar per container/slug + Index( + "ux_calendars_container_slug_active", + "container_type", + "container_id", + func.lower(slug), + unique=True, + postgresql_where=text("deleted_at IS NULL"), + ), + ) + + +class CalendarEntry(Base): + __tablename__ = "calendar_entries" + + id = Column(Integer, primary_key=True) + calendar_id = Column( + Integer, + ForeignKey("calendars.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + + # NEW: ownership + order link + user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) + session_id = Column(String(64), nullable=True, index=True) + order_id = Column(Integer, nullable=True, index=True) + + # NEW: slot link + slot_id = Column(Integer, ForeignKey("calendar_slots.id", ondelete="SET NULL"), nullable=True, index=True) + + # details + name = Column(String(255), nullable=False) + start_at = Column(DateTime(timezone=True), nullable=False, index=True) + end_at = Column(DateTime(timezone=True), nullable=True) + + # NEW: booking state + cost + state = Column( + String(20), + nullable=False, + server_default=text("'pending'"), + ) + cost = Column(Numeric(10, 2), nullable=False, server_default=text("10")) + + # Ticket configuration + ticket_price = Column(Numeric(10, 2), nullable=True) # Price per ticket (NULL = no tickets) + ticket_count = Column(Integer, nullable=True) # Total available tickets (NULL = unlimited) + + created_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + updated_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + __table_args__ = ( + CheckConstraint( + "(end_at IS NULL) OR (end_at >= start_at)", + name="ck_calendar_entries_end_after_start", + ), + Index("ix_calendar_entries_name", "name"), + Index("ix_calendar_entries_start_at", "start_at"), + Index("ix_calendar_entries_user_id", "user_id"), + Index("ix_calendar_entries_session_id", "session_id"), + Index("ix_calendar_entries_state", "state"), + Index("ix_calendar_entries_order_id", "order_id"), + Index("ix_calendar_entries_slot_id", "slot_id"), + ) + + calendar = relationship("Calendar", back_populates="entries") + slot = relationship("CalendarSlot", back_populates="entries", lazy="selectin") + posts = relationship("CalendarEntryPost", back_populates="entry", cascade="all, delete-orphan") + ticket_types = relationship( + "TicketType", + back_populates="entry", + cascade="all, delete-orphan", + passive_deletes=True, + order_by="TicketType.name", + lazy="selectin", + ) + +DAY_LABELS = [ + ("mon", "Mon"), + ("tue", "Tue"), + ("wed", "Wed"), + ("thu", "Thu"), + ("fri", "Fri"), + ("sat", "Sat"), + ("sun", "Sun"), +] + + +class CalendarSlot(Base): + __tablename__ = "calendar_slots" + + id = Column(Integer, primary_key=True) + calendar_id = Column( + Integer, + ForeignKey("calendars.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + + mon = Column(Boolean, nullable=False, default=False) + tue = Column(Boolean, nullable=False, default=False) + wed = Column(Boolean, nullable=False, default=False) + thu = Column(Boolean, nullable=False, default=False) + fri = Column(Boolean, nullable=False, default=False) + sat = Column(Boolean, nullable=False, default=False) + sun = Column(Boolean, nullable=False, default=False) + + # NEW: whether bookings can be made at flexible times within this band + flexible = Column( + Boolean, + nullable=False, + server_default=text("false"), + default=False, + ) + + @property + def days_display(self) -> str: + days = [label for attr, label in DAY_LABELS if getattr(self, attr)] + if len(days) == len(DAY_LABELS): + # all days selected + return "All" # or "All days" if you prefer + return ", ".join(days) if days else "—" + + time_start = Column(Time(timezone=False), nullable=False) + time_end = Column(Time(timezone=False), nullable=False) + + cost = Column(Numeric(10, 2), nullable=True) + + created_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + updated_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + __table_args__ = ( + CheckConstraint( + "(time_end > time_start)", + name="ck_calendar_slots_time_end_after_start", + ), + Index("ix_calendar_slots_calendar_id", "calendar_id"), + Index("ix_calendar_slots_time_start", "time_start"), + ) + + calendar = relationship("Calendar", back_populates="slots") + entries = relationship("CalendarEntry", back_populates="slot") + + +class TicketType(Base): + __tablename__ = "ticket_types" + + id = Column(Integer, primary_key=True) + entry_id = Column( + Integer, + ForeignKey("calendar_entries.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + + name = Column(String(255), nullable=False) + cost = Column(Numeric(10, 2), nullable=False) + count = Column(Integer, nullable=False) + + created_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + updated_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + __table_args__ = ( + Index("ix_ticket_types_entry_id", "entry_id"), + Index("ix_ticket_types_name", "name"), + ) + + entry = relationship("CalendarEntry", back_populates="ticket_types") + + +class Ticket(Base): + __tablename__ = "tickets" + + id = Column(Integer, primary_key=True) + entry_id = Column( + Integer, + ForeignKey("calendar_entries.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + ticket_type_id = Column( + Integer, + ForeignKey("ticket_types.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) + session_id = Column(String(64), nullable=True, index=True) + order_id = Column(Integer, nullable=True, index=True) + + code = Column(String(64), unique=True, nullable=False) # QR/barcode value + state = Column( + String(20), + nullable=False, + server_default=text("'reserved'"), + ) # reserved, confirmed, checked_in, cancelled + + created_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + checked_in_at = Column(DateTime(timezone=True), nullable=True) + + __table_args__ = ( + Index("ix_tickets_entry_id", "entry_id"), + Index("ix_tickets_ticket_type_id", "ticket_type_id"), + Index("ix_tickets_user_id", "user_id"), + Index("ix_tickets_session_id", "session_id"), + Index("ix_tickets_order_id", "order_id"), + Index("ix_tickets_code", "code", unique=True), + Index("ix_tickets_state", "state"), + ) + + entry = relationship("CalendarEntry", backref="tickets") + ticket_type = relationship("TicketType", backref="tickets") + + +class CalendarEntryPost(Base): + """Junction between calendar entries and content (posts, etc.).""" + __tablename__ = "calendar_entry_posts" + + id = Column(Integer, primary_key=True, autoincrement=True) + entry_id = Column(Integer, ForeignKey("calendar_entries.id", ondelete="CASCADE"), nullable=False) + content_type = Column(String(32), nullable=False, server_default=text("'post'")) + content_id = Column(Integer, nullable=False) + + created_at = Column(DateTime(timezone=True), nullable=False, default=utcnow) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + __table_args__ = ( + Index("ix_entry_posts_entry_id", "entry_id"), + Index("ix_entry_posts_content", "content_type", "content_id"), + ) + + entry = relationship("CalendarEntry", back_populates="posts") + + +__all__ = ["Calendar", "CalendarEntry", "CalendarSlot", "TicketType", "Ticket", "CalendarEntryPost"] diff --git a/shared/models/container_relation.py b/shared/models/container_relation.py new file mode 100644 index 0000000..ecafaba --- /dev/null +++ b/shared/models/container_relation.py @@ -0,0 +1,38 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Integer, String, DateTime, Index, UniqueConstraint, func +from shared.db.base import Base + + +class ContainerRelation(Base): + __tablename__ = "container_relations" + + __table_args__ = ( + UniqueConstraint( + "parent_type", "parent_id", "child_type", "child_id", + name="uq_container_relations_parent_child", + ), + Index("ix_container_relations_parent", "parent_type", "parent_id"), + Index("ix_container_relations_child", "child_type", "child_id"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + parent_type: Mapped[str] = mapped_column(String(32), nullable=False) + parent_id: Mapped[int] = mapped_column(Integer, nullable=False) + child_type: Mapped[str] = mapped_column(String(32), nullable=False) + child_id: Mapped[int] = mapped_column(Integer, nullable=False) + + sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + label: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) diff --git a/shared/models/federation.py b/shared/models/federation.py new file mode 100644 index 0000000..daef64a --- /dev/null +++ b/shared/models/federation.py @@ -0,0 +1,466 @@ +"""Federation / ActivityPub ORM models. + +These models support AP identity, activities, followers, inbox processing, +IPFS content addressing, and OpenTimestamps anchoring. +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import ( + String, Integer, DateTime, Text, Boolean, BigInteger, + ForeignKey, UniqueConstraint, Index, func, +) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from shared.db.base import Base + + +class ActorProfile(Base): + """AP identity for a user. Created when user chooses a username.""" + __tablename__ = "ap_actor_profiles" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), + unique=True, nullable=False, + ) + preferred_username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) + display_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + summary: Mapped[str | None] = mapped_column(Text, nullable=True) + public_key_pem: Mapped[str] = mapped_column(Text, nullable=False) + private_key_pem: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + # Relationships + user = relationship("User", backref="actor_profile", uselist=False, lazy="selectin") + activities = relationship("APActivity", back_populates="actor_profile", lazy="dynamic") + followers = relationship("APFollower", back_populates="actor_profile", lazy="dynamic") + + __table_args__ = ( + Index("ix_ap_actor_user_id", "user_id", unique=True), + Index("ix_ap_actor_username", "preferred_username", unique=True), + ) + + def __repr__(self) -> str: + return f"" + + +class APActivity(Base): + """An ActivityPub activity (local or remote). + + Also serves as the unified event bus: internal domain events and public + federation activities both live here, distinguished by ``visibility``. + The ``EventProcessor`` polls rows with ``process_state='pending'``. + """ + __tablename__ = "ap_activities" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + activity_id: Mapped[str] = mapped_column(String(512), unique=True, nullable=False) + activity_type: Mapped[str] = mapped_column(String(64), nullable=False) + actor_profile_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=True, + ) + object_type: Mapped[str | None] = mapped_column(String(64), nullable=True) + object_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + published: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + signature: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + is_local: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true") + + # Link back to originating domain object (e.g. source_type='post', source_id=42) + source_type: Mapped[str | None] = mapped_column(String(64), nullable=True) + source_id: Mapped[int | None] = mapped_column(Integer, nullable=True) + + # IPFS content-addressed copy of the activity + ipfs_cid: Mapped[str | None] = mapped_column(String(128), nullable=True) + + # Anchoring (filled later when batched into a merkle tree) + anchor_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_anchors.id", ondelete="SET NULL"), nullable=True, + ) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + # --- Unified event-bus columns --- + actor_uri: Mapped[str | None] = mapped_column( + String(512), nullable=True, + ) + visibility: Mapped[str] = mapped_column( + String(20), nullable=False, default="public", server_default="public", + ) + process_state: Mapped[str] = mapped_column( + String(20), nullable=False, default="completed", server_default="completed", + ) + process_attempts: Mapped[int] = mapped_column( + Integer, nullable=False, default=0, server_default="0", + ) + process_max_attempts: Mapped[int] = mapped_column( + Integer, nullable=False, default=5, server_default="5", + ) + process_error: Mapped[str | None] = mapped_column(Text, nullable=True) + processed_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True, + ) + origin_app: Mapped[str | None] = mapped_column( + String(64), nullable=True, + ) + + # Relationships + actor_profile = relationship("ActorProfile", back_populates="activities") + + __table_args__ = ( + Index("ix_ap_activity_actor", "actor_profile_id"), + Index("ix_ap_activity_source", "source_type", "source_id"), + Index("ix_ap_activity_published", "published"), + Index("ix_ap_activity_process", "process_state"), + ) + + def __repr__(self) -> str: + return f"" + + +class APFollower(Base): + """A remote follower of a local actor. + + ``app_domain`` scopes the follow to a specific app (e.g. "blog", + "market", "events"). "federation" means the aggregate — the + follower subscribes to all activities. + """ + __tablename__ = "ap_followers" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_profile_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False, + ) + follower_acct: Mapped[str] = mapped_column(String(512), nullable=False) + follower_inbox: Mapped[str] = mapped_column(String(512), nullable=False) + follower_actor_url: Mapped[str] = mapped_column(String(512), nullable=False) + follower_public_key: Mapped[str | None] = mapped_column(Text, nullable=True) + app_domain: Mapped[str] = mapped_column( + String(64), nullable=False, default="federation", server_default="federation", + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + # Relationships + actor_profile = relationship("ActorProfile", back_populates="followers") + + __table_args__ = ( + UniqueConstraint( + "actor_profile_id", "follower_acct", "app_domain", + name="uq_follower_acct_app", + ), + Index("ix_ap_follower_actor", "actor_profile_id"), + Index("ix_ap_follower_app_domain", "actor_profile_id", "app_domain"), + ) + + def __repr__(self) -> str: + return f"" + + +class APInboxItem(Base): + """Raw incoming AP activity, stored for async processing.""" + __tablename__ = "ap_inbox_items" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_profile_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False, + ) + raw_json: Mapped[dict] = mapped_column(JSONB, nullable=False) + activity_type: Mapped[str | None] = mapped_column(String(64), nullable=True) + from_actor: Mapped[str | None] = mapped_column(String(512), nullable=True) + state: Mapped[str] = mapped_column( + String(20), nullable=False, default="pending", server_default="pending", + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + __table_args__ = ( + Index("ix_ap_inbox_state", "state"), + Index("ix_ap_inbox_actor", "actor_profile_id"), + ) + + def __repr__(self) -> str: + return f"" + + +class APAnchor(Base): + """OpenTimestamps anchoring batch — merkle tree of activities.""" + __tablename__ = "ap_anchors" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + merkle_root: Mapped[str] = mapped_column(String(128), nullable=False) + tree_ipfs_cid: Mapped[str | None] = mapped_column(String(128), nullable=True) + ots_proof_cid: Mapped[str | None] = mapped_column(String(128), nullable=True) + activity_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + confirmed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + bitcoin_txid: Mapped[str | None] = mapped_column(String(128), nullable=True) + + def __repr__(self) -> str: + return f"" + + +class IPFSPin(Base): + """Tracks content stored on IPFS — used by all domains.""" + __tablename__ = "ipfs_pins" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + content_hash: Mapped[str] = mapped_column(String(128), nullable=False) + ipfs_cid: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) + pin_type: Mapped[str] = mapped_column(String(64), nullable=False) + source_type: Mapped[str | None] = mapped_column(String(64), nullable=True) + source_id: Mapped[int | None] = mapped_column(Integer, nullable=True) + size_bytes: Mapped[int | None] = mapped_column(BigInteger, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + __table_args__ = ( + Index("ix_ipfs_pin_source", "source_type", "source_id"), + Index("ix_ipfs_pin_cid", "ipfs_cid", unique=True), + ) + + def __repr__(self) -> str: + return f"" + + +class RemoteActor(Base): + """Cached profile of a remote actor we interact with.""" + __tablename__ = "ap_remote_actors" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_url: Mapped[str] = mapped_column(String(512), unique=True, nullable=False) + inbox_url: Mapped[str] = mapped_column(String(512), nullable=False) + shared_inbox_url: Mapped[str | None] = mapped_column(String(512), nullable=True) + preferred_username: Mapped[str] = mapped_column(String(255), nullable=False) + display_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + summary: Mapped[str | None] = mapped_column(Text, nullable=True) + icon_url: Mapped[str | None] = mapped_column(String(512), nullable=True) + public_key_pem: Mapped[str | None] = mapped_column(Text, nullable=True) + domain: Mapped[str] = mapped_column(String(255), nullable=False) + fetched_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + __table_args__ = ( + Index("ix_ap_remote_actor_url", "actor_url", unique=True), + Index("ix_ap_remote_actor_domain", "domain"), + ) + + def __repr__(self) -> str: + return f"" + + +class APFollowing(Base): + """Outbound follow: local actor → remote actor.""" + __tablename__ = "ap_following" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_profile_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False, + ) + remote_actor_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=False, + ) + state: Mapped[str] = mapped_column( + String(20), nullable=False, default="pending", server_default="pending", + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + accepted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # Relationships + actor_profile = relationship("ActorProfile") + remote_actor = relationship("RemoteActor") + + __table_args__ = ( + UniqueConstraint("actor_profile_id", "remote_actor_id", name="uq_following"), + Index("ix_ap_following_actor", "actor_profile_id"), + Index("ix_ap_following_remote", "remote_actor_id"), + ) + + def __repr__(self) -> str: + return f"" + + +class APRemotePost(Base): + """A federated post ingested from a remote actor.""" + __tablename__ = "ap_remote_posts" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + remote_actor_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=False, + ) + activity_id: Mapped[str] = mapped_column(String(512), unique=True, nullable=False) + object_id: Mapped[str] = mapped_column(String(512), unique=True, nullable=False) + object_type: Mapped[str] = mapped_column(String(64), nullable=False, default="Note") + content: Mapped[str | None] = mapped_column(Text, nullable=True) + summary: Mapped[str | None] = mapped_column(Text, nullable=True) + url: Mapped[str | None] = mapped_column(String(512), nullable=True) + attachment_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + tag_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + in_reply_to: Mapped[str | None] = mapped_column(String(512), nullable=True) + conversation: Mapped[str | None] = mapped_column(String(512), nullable=True) + published: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + fetched_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + # Relationships + remote_actor = relationship("RemoteActor") + + __table_args__ = ( + Index("ix_ap_remote_post_actor", "remote_actor_id"), + Index("ix_ap_remote_post_published", "published"), + Index("ix_ap_remote_post_object", "object_id", unique=True), + ) + + def __repr__(self) -> str: + return f"" + + +class APLocalPost(Base): + """A native post composed in the federation UI.""" + __tablename__ = "ap_local_posts" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_profile_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False, + ) + content: Mapped[str] = mapped_column(Text, nullable=False) + visibility: Mapped[str] = mapped_column( + String(20), nullable=False, default="public", server_default="public", + ) + in_reply_to: Mapped[str | None] = mapped_column(String(512), nullable=True) + published: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), + ) + + # Relationships + actor_profile = relationship("ActorProfile") + + __table_args__ = ( + Index("ix_ap_local_post_actor", "actor_profile_id"), + Index("ix_ap_local_post_published", "published"), + ) + + def __repr__(self) -> str: + return f"" + + +class APInteraction(Base): + """Like or boost (local or remote).""" + __tablename__ = "ap_interactions" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_profile_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=True, + ) + remote_actor_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=True, + ) + post_type: Mapped[str] = mapped_column(String(20), nullable=False) # local/remote + post_id: Mapped[int] = mapped_column(Integer, nullable=False) + interaction_type: Mapped[str] = mapped_column(String(20), nullable=False) # like/boost + activity_id: Mapped[str | None] = mapped_column(String(512), nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + __table_args__ = ( + Index("ix_ap_interaction_post", "post_type", "post_id"), + Index("ix_ap_interaction_actor", "actor_profile_id"), + Index("ix_ap_interaction_remote", "remote_actor_id"), + ) + + def __repr__(self) -> str: + return f"" + + +class APNotification(Base): + """Notification for a local actor.""" + __tablename__ = "ap_notifications" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + actor_profile_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False, + ) + notification_type: Mapped[str] = mapped_column(String(20), nullable=False) + from_remote_actor_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_remote_actors.id", ondelete="SET NULL"), nullable=True, + ) + from_actor_profile_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_actor_profiles.id", ondelete="SET NULL"), nullable=True, + ) + target_activity_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_activities.id", ondelete="SET NULL"), nullable=True, + ) + target_remote_post_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("ap_remote_posts.id", ondelete="SET NULL"), nullable=True, + ) + read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false") + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + # Relationships + actor_profile = relationship("ActorProfile", foreign_keys=[actor_profile_id]) + from_remote_actor = relationship("RemoteActor") + from_actor_profile = relationship("ActorProfile", foreign_keys=[from_actor_profile_id]) + + __table_args__ = ( + Index("ix_ap_notification_actor", "actor_profile_id"), + Index("ix_ap_notification_read", "actor_profile_id", "read"), + Index("ix_ap_notification_created", "created_at"), + ) + + +class APDeliveryLog(Base): + """Tracks successful deliveries of activities to remote inboxes. + + Used for idempotency: the delivery handler skips inboxes that already + have a success row, so retries after a crash never send duplicates. + """ + __tablename__ = "ap_delivery_log" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + activity_id: Mapped[int] = mapped_column( + Integer, ForeignKey("ap_activities.id", ondelete="CASCADE"), nullable=False, + ) + inbox_url: Mapped[str] = mapped_column(String(512), nullable=False) + app_domain: Mapped[str] = mapped_column(String(128), nullable=False, server_default="federation") + status_code: Mapped[int | None] = mapped_column(Integer, nullable=True) + delivered_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + + __table_args__ = ( + UniqueConstraint("activity_id", "inbox_url", "app_domain", name="uq_delivery_activity_inbox_domain"), + Index("ix_ap_delivery_activity", "activity_id"), + ) diff --git a/shared/models/ghost_content.py b/shared/models/ghost_content.py new file mode 100644 index 0000000..197f651 --- /dev/null +++ b/shared/models/ghost_content.py @@ -0,0 +1,216 @@ +from datetime import datetime +from typing import List, Optional +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import ( + Integer, + String, + Text, + Boolean, + DateTime, + ForeignKey, + Column, + func, +) +from shared.db.base import Base # whatever your Base is +# from .author import Author # make sure imports resolve +# from ..app.blog.calendars.model import Calendar + +class Tag(Base): + __tablename__ = "tags" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ghost_id: Mapped[str] = mapped_column(String(64), index=True, unique=True, nullable=False) + + slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + + description: Mapped[Optional[str]] = mapped_column(Text()) + visibility: Mapped[str] = mapped_column(String(32), default="public", nullable=False) + feature_image: Mapped[Optional[str]] = mapped_column(Text()) + + meta_title: Mapped[Optional[str]] = mapped_column(String(300)) + meta_description: Mapped[Optional[str]] = mapped_column(Text()) + + created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + # NEW: posts relationship is now direct Post objects via PostTag + posts: Mapped[List["Post"]] = relationship( + "Post", + secondary="post_tags", + primaryjoin="Tag.id==post_tags.c.tag_id", + secondaryjoin="Post.id==post_tags.c.post_id", + back_populates="tags", + order_by="PostTag.sort_order", + ) + + +class Post(Base): + __tablename__ = "posts" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ghost_id: Mapped[str] = mapped_column(String(64), index=True, unique=True, nullable=False) + uuid: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) + slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False) + + title: Mapped[str] = mapped_column(String(500), nullable=False) + + html: Mapped[Optional[str]] = mapped_column(Text()) + plaintext: Mapped[Optional[str]] = mapped_column(Text()) + mobiledoc: Mapped[Optional[str]] = mapped_column(Text()) + lexical: Mapped[Optional[str]] = mapped_column(Text()) + + feature_image: Mapped[Optional[str]] = mapped_column(Text()) + feature_image_alt: Mapped[Optional[str]] = mapped_column(Text()) + feature_image_caption: Mapped[Optional[str]] = mapped_column(Text()) + + excerpt: Mapped[Optional[str]] = mapped_column(Text()) + custom_excerpt: Mapped[Optional[str]] = mapped_column(Text()) + + visibility: Mapped[str] = mapped_column(String(32), default="public", nullable=False) + status: Mapped[str] = mapped_column(String(32), default="draft", nullable=False) + featured: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False) + is_page: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False) + email_only: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False) + + canonical_url: Mapped[Optional[str]] = mapped_column(Text()) + meta_title: Mapped[Optional[str]] = mapped_column(String(500)) + meta_description: Mapped[Optional[str]] = mapped_column(Text()) + og_image: Mapped[Optional[str]] = mapped_column(Text()) + og_title: Mapped[Optional[str]] = mapped_column(String(500)) + og_description: Mapped[Optional[str]] = mapped_column(Text()) + twitter_image: Mapped[Optional[str]] = mapped_column(Text()) + twitter_title: Mapped[Optional[str]] = mapped_column(String(500)) + twitter_description: Mapped[Optional[str]] = mapped_column(Text()) + custom_template: Mapped[Optional[str]] = mapped_column(String(191)) + + reading_time: Mapped[Optional[int]] = mapped_column(Integer()) + comment_id: Mapped[Optional[str]] = mapped_column(String(191)) + + published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + user_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id", ondelete="SET NULL"), index=True + ) + publish_requested: Mapped[bool] = mapped_column(Boolean(), default=False, server_default="false", nullable=False) + + primary_author_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("authors.id", ondelete="SET NULL") + ) + primary_tag_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("tags.id", ondelete="SET NULL") + ) + + primary_author: Mapped[Optional["Author"]] = relationship( + "Author", foreign_keys=[primary_author_id] + ) + primary_tag: Mapped[Optional[Tag]] = relationship( + "Tag", foreign_keys=[primary_tag_id] + ) + user: Mapped[Optional["User"]] = relationship( + "User", foreign_keys=[user_id] + ) + + # AUTHORS RELATIONSHIP (many-to-many via post_authors) + authors: Mapped[List["Author"]] = relationship( + "Author", + secondary="post_authors", + primaryjoin="Post.id==post_authors.c.post_id", + secondaryjoin="Author.id==post_authors.c.author_id", + back_populates="posts", + order_by="PostAuthor.sort_order", + ) + + # TAGS RELATIONSHIP (many-to-many via post_tags) + tags: Mapped[List[Tag]] = relationship( + "Tag", + secondary="post_tags", + primaryjoin="Post.id==post_tags.c.post_id", + secondaryjoin="Tag.id==post_tags.c.tag_id", + back_populates="posts", + order_by="PostTag.sort_order", + ) + likes: Mapped[List["PostLike"]] = relationship( + "PostLike", + back_populates="post", + cascade="all, delete-orphan", + passive_deletes=True, + ) + +class Author(Base): + __tablename__ = "authors" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ghost_id: Mapped[str] = mapped_column(String(64), index=True, unique=True, nullable=False) + + slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + email: Mapped[Optional[str]] = mapped_column(String(255)) + + profile_image: Mapped[Optional[str]] = mapped_column(Text()) + cover_image: Mapped[Optional[str]] = mapped_column(Text()) + bio: Mapped[Optional[str]] = mapped_column(Text()) + website: Mapped[Optional[str]] = mapped_column(Text()) + location: Mapped[Optional[str]] = mapped_column(Text()) + facebook: Mapped[Optional[str]] = mapped_column(Text()) + twitter: Mapped[Optional[str]] = mapped_column(Text()) + + created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + # backref to posts via post_authors + posts: Mapped[List[Post]] = relationship( + "Post", + secondary="post_authors", + primaryjoin="Author.id==post_authors.c.author_id", + secondaryjoin="Post.id==post_authors.c.post_id", + back_populates="authors", + order_by="PostAuthor.sort_order", + ) + +class PostAuthor(Base): + __tablename__ = "post_authors" + + post_id: Mapped[int] = mapped_column( + ForeignKey("posts.id", ondelete="CASCADE"), + primary_key=True, + ) + author_id: Mapped[int] = mapped_column( + ForeignKey("authors.id", ondelete="CASCADE"), + primary_key=True, + ) + sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + + +class PostTag(Base): + __tablename__ = "post_tags" + + post_id: Mapped[int] = mapped_column( + ForeignKey("posts.id", ondelete="CASCADE"), + primary_key=True, + ) + tag_id: Mapped[int] = mapped_column( + ForeignKey("tags.id", ondelete="CASCADE"), + primary_key=True, + ) + sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + + +class PostLike(Base): + __tablename__ = "post_likes" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + post_id: Mapped[int] = mapped_column(ForeignKey("posts.id", ondelete="CASCADE"), nullable=False) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + post: Mapped["Post"] = relationship("Post", back_populates="likes", foreign_keys=[post_id]) + user = relationship("User", back_populates="liked_posts") diff --git a/shared/models/ghost_membership_entities.py b/shared/models/ghost_membership_entities.py new file mode 100644 index 0000000..5e3542a --- /dev/null +++ b/shared/models/ghost_membership_entities.py @@ -0,0 +1,122 @@ +# suma_browser/models/ghost_membership_entities.py + +from datetime import datetime +from typing import Optional + +from sqlalchemy import ( + Integer, String, Text, Boolean, DateTime, ForeignKey, UniqueConstraint +) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.ext.associationproxy import association_proxy + +from shared.db.base import Base + + +# ----------------------- +# Labels (simple M2M) +# ----------------------- + +class GhostLabel(Base): + __tablename__ = "ghost_labels" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + ghost_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[Optional[str]] = mapped_column(String(255)) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) + + # Back-populated by User.labels + users = relationship("User", secondary="user_labels", back_populates="labels", lazy="selectin") + + +class UserLabel(Base): + __tablename__ = "user_labels" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) + label_id: Mapped[int] = mapped_column(ForeignKey("ghost_labels.id", ondelete="CASCADE"), index=True) + + __table_args__ = ( + UniqueConstraint("user_id", "label_id", name="uq_user_label"), + ) + + +# ----------------------- +# Newsletters (association object + proxy) +# ----------------------- + +class GhostNewsletter(Base): + __tablename__ = "ghost_newsletters" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + ghost_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[Optional[str]] = mapped_column(String(255)) + description: Mapped[Optional[str]] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) + + # Association-object side (one-to-many) + user_newsletters = relationship( + "UserNewsletter", + back_populates="newsletter", + cascade="all, delete-orphan", + lazy="selectin", + ) + + # Convenience: list-like proxy of Users via association rows (read-only container) + users = association_proxy("user_newsletters", "user") + + +class UserNewsletter(Base): + __tablename__ = "user_newsletters" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) + newsletter_id: Mapped[int] = mapped_column(ForeignKey("ghost_newsletters.id", ondelete="CASCADE"), index=True) + subscribed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + + __table_args__ = ( + UniqueConstraint("user_id", "newsletter_id", name="uq_user_newsletter"), + ) + + # Bidirectional links for the association object + user = relationship("User", back_populates="user_newsletters", lazy="selectin") + newsletter = relationship("GhostNewsletter", back_populates="user_newsletters", lazy="selectin") + + +# ----------------------- +# Tiers & Subscriptions +# ----------------------- + +class GhostTier(Base): + __tablename__ = "ghost_tiers" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + ghost_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[Optional[str]] = mapped_column(String(255)) + type: Mapped[Optional[str]] = mapped_column(String(50)) # e.g. free, paid + visibility: Mapped[Optional[str]] = mapped_column(String(50)) + + +class GhostSubscription(Base): + __tablename__ = "ghost_subscriptions" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + ghost_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) + status: Mapped[Optional[str]] = mapped_column(String(50)) + tier_id: Mapped[Optional[int]] = mapped_column(ForeignKey("ghost_tiers.id", ondelete="SET NULL"), index=True) + cadence: Mapped[Optional[str]] = mapped_column(String(50)) # month, year + price_amount: Mapped[Optional[int]] = mapped_column(Integer) + price_currency: Mapped[Optional[str]] = mapped_column(String(10)) + stripe_customer_id: Mapped[Optional[str]] = mapped_column(String(255), index=True) + stripe_subscription_id: Mapped[Optional[str]] = mapped_column(String(255), index=True) + raw: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) + + # Relationships + user = relationship("User", back_populates="subscriptions", lazy="selectin") + tier = relationship("GhostTier", lazy="selectin") diff --git a/shared/models/kv.py b/shared/models/kv.py new file mode 100644 index 0000000..1a0563c --- /dev/null +++ b/shared/models/kv.py @@ -0,0 +1,12 @@ +from __future__ import annotations +from datetime import datetime +from sqlalchemy import String, Text, DateTime +from sqlalchemy.orm import Mapped, mapped_column +from shared.db.base import Base + +class KV(Base): + __tablename__ = "kv" + """Simple key-value table for settings/cache/demo.""" + key: Mapped[str] = mapped_column(String(120), primary_key=True) + value: Mapped[str | None] = mapped_column(Text(), nullable=True) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/shared/models/magic_link.py b/shared/models/magic_link.py new file mode 100644 index 0000000..1c8ed7a --- /dev/null +++ b/shared/models/magic_link.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from datetime import datetime +from sqlalchemy import String, Integer, DateTime, ForeignKey, func, Index +from sqlalchemy.orm import Mapped, mapped_column, relationship +from shared.db.base import Base + +class MagicLink(Base): + __tablename__ = "magic_links" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + token: Mapped[str] = mapped_column(String(128), unique=True, index=True, nullable=False) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + purpose: Mapped[str] = mapped_column(String(32), nullable=False, default="signin") + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + ip: Mapped[str | None] = mapped_column(String(64), nullable=True) + user_agent: Mapped[str | None] = mapped_column(String(256), nullable=True) + + user = relationship("User", backref="magic_links") + + __table_args__ = ( + Index("ix_magic_link_token", "token", unique=True), + Index("ix_magic_link_user", "user_id"), + ) diff --git a/shared/models/market.py b/shared/models/market.py new file mode 100644 index 0000000..87a6b72 --- /dev/null +++ b/shared/models/market.py @@ -0,0 +1,441 @@ +# at top of persist_snapshot.py: +from datetime import datetime +from typing import Optional, List +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from typing import List, Optional + +from sqlalchemy import ( + String, Text, Integer, ForeignKey, DateTime, Boolean, Numeric, + UniqueConstraint, Index, func +) + +from shared.db.base import Base # you already import Base in app.py + + + +class Product(Base): + __tablename__ = "products" + + id: Mapped[int] = mapped_column(primary_key=True) + slug: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) + + title: Mapped[Optional[str]] = mapped_column(String(512)) + image: Mapped[Optional[str]] = mapped_column(Text) + + description_short: Mapped[Optional[str]] = mapped_column(Text) + description_html: Mapped[Optional[str]] = mapped_column(Text) + + suma_href: Mapped[Optional[str]] = mapped_column(Text) + brand: Mapped[Optional[str]] = mapped_column(String(255)) + + rrp: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) + rrp_currency: Mapped[Optional[str]] = mapped_column(String(16)) + rrp_raw: Mapped[Optional[str]] = mapped_column(String(128)) + + price_per_unit: Mapped[Optional[float]] = mapped_column(Numeric(12, 4)) + price_per_unit_currency: Mapped[Optional[str]] = mapped_column(String(16)) + price_per_unit_raw: Mapped[Optional[str]] = mapped_column(String(128)) + + special_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) + special_price_currency: Mapped[Optional[str]] = mapped_column(String(16)) + special_price_raw: Mapped[Optional[str]] = mapped_column(String(128)) + + regular_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) + regular_price_currency: Mapped[Optional[str]] = mapped_column(String(16)) + regular_price_raw: Mapped[Optional[str]] = mapped_column(String(128)) + + oe_list_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) + + case_size_count: Mapped[Optional[int]] = mapped_column(Integer) + case_size_item_qty: Mapped[Optional[float]] = mapped_column(Numeric(12, 3)) + case_size_item_unit: Mapped[Optional[str]] = mapped_column(String(32)) + case_size_raw: Mapped[Optional[str]] = mapped_column(String(128)) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + images: Mapped[List["ProductImage"]] = relationship( + back_populates="product", + cascade="all, delete-orphan", + passive_deletes=True, + ) + sections: Mapped[List["ProductSection"]] = relationship( + back_populates="product", + cascade="all, delete-orphan", + passive_deletes=True, + ) + labels: Mapped[List["ProductLabel"]] = relationship( + cascade="all, delete-orphan", + passive_deletes=True, + ) + stickers: Mapped[List["ProductSticker"]] = relationship( + cascade="all, delete-orphan", + passive_deletes=True, + ) + + ean: Mapped[Optional[str]] = mapped_column(String(64)) + sku: Mapped[Optional[str]] = mapped_column(String(128)) + unit_size: Mapped[Optional[str]] = mapped_column(String(128)) + pack_size: Mapped[Optional[str]] = mapped_column(String(128)) + + attributes = relationship( + "ProductAttribute", + back_populates="product", + lazy="selectin", + cascade="all, delete-orphan", + ) + nutrition = relationship( + "ProductNutrition", + back_populates="product", + lazy="selectin", + cascade="all, delete-orphan", + ) + allergens = relationship( + "ProductAllergen", + back_populates="product", + lazy="selectin", + cascade="all, delete-orphan", + ) + + likes = relationship( + "ProductLike", + back_populates="product", + cascade="all, delete-orphan", + ) + cart_items: Mapped[List["CartItem"]] = relationship( + "CartItem", + back_populates="product", + cascade="all, delete-orphan", + ) + + # NEW: all order items that reference this product + order_items: Mapped[List["OrderItem"]] = relationship( + "OrderItem", + back_populates="product", + cascade="all, delete-orphan", + ) + +from sqlalchemy import Column + +class ProductLike(Base): + __tablename__ = "product_likes" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + product_slug: Mapped[str] = mapped_column(ForeignKey("products.slug", ondelete="CASCADE")) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + product: Mapped["Product"] = relationship("Product", back_populates="likes", foreign_keys=[product_slug]) + + user = relationship("User", back_populates="liked_products") # optional, if you want reverse access + + +class ProductImage(Base): + __tablename__ = "product_images" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) + url: Mapped[str] = mapped_column(Text, nullable=False) + position: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + kind: Mapped[str] = mapped_column(String(16), nullable=False, default="gallery") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + product: Mapped["Product"] = relationship(back_populates="images") + + __table_args__ = ( + UniqueConstraint("product_id", "url", "kind", name="uq_product_images_product_url_kind"), + Index("ix_product_images_position", "position"), + ) + +class ProductSection(Base): + __tablename__ = "product_sections" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column( + ForeignKey("products.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + title: Mapped[str] = mapped_column(String(255), nullable=False) + html: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + product: Mapped["Product"] = relationship(back_populates="sections") + __table_args__ = ( + UniqueConstraint("product_id", "title", name="uq_product_sections_product_title"), + ) +# --- Nav & listings --- + +class NavTop(Base): + __tablename__ = "nav_tops" + id: Mapped[int] = mapped_column(primary_key=True) + label: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + market_id: Mapped[Optional[int]] = mapped_column( + Integer, + ForeignKey("market_places.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + listings: Mapped[List["Listing"]] = relationship(back_populates="top", cascade="all, delete-orphan") + market = relationship("MarketPlace", back_populates="nav_tops") + + __table_args__ = (UniqueConstraint("label", "slug", name="uq_nav_tops_label_slug"),) + +class NavSub(Base): + __tablename__ = "nav_subs" + id: Mapped[int] = mapped_column(primary_key=True) + top_id: Mapped[int] = mapped_column(ForeignKey("nav_tops.id", ondelete="CASCADE"), index=True, nullable=False) + label: Mapped[Optional[str]] = mapped_column(String(255)) + slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + href: Mapped[Optional[str]] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + listings: Mapped[List["Listing"]] = relationship(back_populates="sub", cascade="all, delete-orphan") + + __table_args__ = (UniqueConstraint("top_id", "slug", name="uq_nav_subs_top_slug"),) + +class Listing(Base): + __tablename__ = "listings" + + id: Mapped[int] = mapped_column(primary_key=True) + + # Old slug-based fields (optional: remove) + # top_slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + # sub_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) + + top_id: Mapped[int] = mapped_column(ForeignKey("nav_tops.id", ondelete="CASCADE"), index=True, nullable=False) + sub_id: Mapped[Optional[int]] = mapped_column(ForeignKey("nav_subs.id", ondelete="CASCADE"), index=True) + + total_pages: Mapped[Optional[int]] = mapped_column(Integer) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + + top: Mapped["NavTop"] = relationship(back_populates="listings") + sub: Mapped[Optional["NavSub"]] = relationship(back_populates="listings") + + __table_args__ = ( + UniqueConstraint("top_id", "sub_id", name="uq_listings_top_sub"), + ) + +class ListingItem(Base): + __tablename__ = "listing_items" + id: Mapped[int] = mapped_column(primary_key=True) + listing_id: Mapped[int] = mapped_column(ForeignKey("listings.id", ondelete="CASCADE"), index=True, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + __table_args__ = (UniqueConstraint("listing_id", "slug", name="uq_listing_items_listing_slug"),) + +# --- Reports / redirects / logs --- + +class LinkError(Base): + __tablename__ = "link_errors" + id: Mapped[int] = mapped_column(primary_key=True) + product_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) + href: Mapped[Optional[str]] = mapped_column(Text) + text: Mapped[Optional[str]] = mapped_column(Text) + top: Mapped[Optional[str]] = mapped_column(String(255)) + sub: Mapped[Optional[str]] = mapped_column(String(255)) + target_slug: Mapped[Optional[str]] = mapped_column(String(255)) + type: Mapped[Optional[str]] = mapped_column(String(255)) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + +class LinkExternal(Base): + __tablename__ = "link_externals" + id: Mapped[int] = mapped_column(primary_key=True) + product_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) + href: Mapped[Optional[str]] = mapped_column(Text) + text: Mapped[Optional[str]] = mapped_column(Text) + host: Mapped[Optional[str]] = mapped_column(String(255)) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + +class SubcategoryRedirect(Base): + __tablename__ = "subcategory_redirects" + id: Mapped[int] = mapped_column(primary_key=True) + old_path: Mapped[str] = mapped_column(String(512), nullable=False, index=True) + new_path: Mapped[str] = mapped_column(String(512), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + +class ProductLog(Base): + __tablename__ = "product_logs" + id: Mapped[int] = mapped_column(primary_key=True) + slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) + href_tried: Mapped[Optional[str]] = mapped_column(Text) + ok: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") + error_type: Mapped[Optional[str]] = mapped_column(String(255)) + error_message: Mapped[Optional[str]] = mapped_column(Text) + http_status: Mapped[Optional[int]] = mapped_column(Integer) + final_url: Mapped[Optional[str]] = mapped_column(Text) + transport_error: Mapped[Optional[bool]] = mapped_column(Boolean) + title: Mapped[Optional[str]] = mapped_column(String(512)) + has_description_html: Mapped[Optional[bool]] = mapped_column(Boolean) + has_description_short: Mapped[Optional[bool]] = mapped_column(Boolean) + sections_count: Mapped[Optional[int]] = mapped_column(Integer) + images_count: Mapped[Optional[int]] = mapped_column(Integer) + embedded_images_count: Mapped[Optional[int]] = mapped_column(Integer) + all_images_count: Mapped[Optional[int]] = mapped_column(Integer) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + + + +# ...existing models... + +class ProductLabel(Base): + __tablename__ = "product_labels" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + product: Mapped["Product"] = relationship(back_populates="labels") + + __table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_labels_product_name"),) + +class ProductSticker(Base): + __tablename__ = "product_stickers" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + product: Mapped["Product"] = relationship(back_populates="stickers") + + __table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_stickers_product_name"),) + +class ProductAttribute(Base): + __tablename__ = "product_attributes" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) + key: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + + value: Mapped[Optional[str]] = mapped_column(Text) + product = relationship("Product", back_populates="attributes") + __table_args__ = (UniqueConstraint("product_id", "key", name="uq_product_attributes_product_key"),) + +class ProductNutrition(Base): + __tablename__ = "product_nutrition" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) + key: Mapped[str] = mapped_column(String(255), nullable=False) + value: Mapped[Optional[str]] = mapped_column(String(255)) + unit: Mapped[Optional[str]] = mapped_column(String(64)) + product = relationship("Product", back_populates="nutrition") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + __table_args__ = (UniqueConstraint("product_id", "key", name="uq_product_nutrition_product_key"),) + +class ProductAllergen(Base): + __tablename__ = "product_allergens" + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + contains: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + product: Mapped["Product"] = relationship(back_populates="allergens") + __table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_allergens_product_name"),) + + +class CartItem(Base): + __tablename__ = "cart_items" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + # Either a logged-in user OR an anonymous session + user_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), + nullable=True, + ) + session_id: Mapped[str | None] = mapped_column( + String(128), + nullable=True, + ) + + # IMPORTANT: link to product *id*, not slug + product_id: Mapped[int] = mapped_column( + ForeignKey("products.id", ondelete="CASCADE"), + nullable=False, + ) + + quantity: Mapped[int] = mapped_column( + Integer, + nullable=False, + default=1, + server_default="1", + ) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + market_place_id: Mapped[int | None] = mapped_column( + ForeignKey("market_places.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + + deleted_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + + # Relationships + + market_place: Mapped["MarketPlace | None"] = relationship( + "MarketPlace", + foreign_keys=[market_place_id], + ) + product: Mapped["Product"] = relationship( + "Product", + back_populates="cart_items", + ) + user: Mapped["User | None"] = relationship("User", back_populates="cart_items") + + __table_args__ = ( + Index("ix_cart_items_user_product", "user_id", "product_id"), + Index("ix_cart_items_session_product", "session_id", "product_id"), + ) diff --git a/shared/models/market_place.py b/shared/models/market_place.py new file mode 100644 index 0000000..8792e36 --- /dev/null +++ b/shared/models/market_place.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Optional, List + +from sqlalchemy import ( + Integer, String, Text, DateTime, ForeignKey, Index, func, text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from shared.db.base import Base + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +class MarketPlace(Base): + __tablename__ = "market_places" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + container_type: Mapped[str] = mapped_column( + String(32), nullable=False, server_default=text("'page'"), + ) + container_id: Mapped[int] = mapped_column(Integer, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), nullable=True, + ) + + nav_tops: Mapped[List["NavTop"]] = relationship( + "NavTop", back_populates="market", + ) + + __table_args__ = ( + Index("ix_market_places_container", "container_type", "container_id"), + Index( + "ux_market_places_slug_active", + func.lower(slug), + unique=True, + postgresql_where=text("deleted_at IS NULL"), + ), + ) diff --git a/shared/models/menu_item.py b/shared/models/menu_item.py new file mode 100644 index 0000000..d041869 --- /dev/null +++ b/shared/models/menu_item.py @@ -0,0 +1,37 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Integer, String, DateTime, ForeignKey, func +from shared.db.base import Base + + +class MenuItem(Base): + """Deprecated — kept so the table isn't dropped. Use shared.models.menu_node.MenuNode.""" + __tablename__ = "menu_items" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + post_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("posts.id", ondelete="CASCADE"), + nullable=False, + index=True + ) + + sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0, index=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True + ) diff --git a/shared/models/menu_node.py b/shared/models/menu_node.py new file mode 100644 index 0000000..d4b49cc --- /dev/null +++ b/shared/models/menu_node.py @@ -0,0 +1,50 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Index, func +from shared.db.base import Base + + +class MenuNode(Base): + __tablename__ = "menu_nodes" + + __table_args__ = ( + Index("ix_menu_nodes_container", "container_type", "container_id"), + Index("ix_menu_nodes_parent_id", "parent_id"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + container_type: Mapped[str] = mapped_column(String(32), nullable=False) + container_id: Mapped[int] = mapped_column(Integer, nullable=False) + + parent_id: Mapped[Optional[int]] = mapped_column( + Integer, + ForeignKey("menu_nodes.id", ondelete="SET NULL"), + nullable=True, + ) + + sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + depth: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + + label: Mapped[str] = mapped_column(String(255), nullable=False) + slug: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + href: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True) + icon: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + feature_image: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) diff --git a/shared/models/oauth_code.py b/shared/models/oauth_code.py new file mode 100644 index 0000000..3973dcc --- /dev/null +++ b/shared/models/oauth_code.py @@ -0,0 +1,26 @@ +from __future__ import annotations +from datetime import datetime +from sqlalchemy import String, Integer, DateTime, ForeignKey, func, Index +from sqlalchemy.orm import Mapped, mapped_column, relationship +from shared.db.base import Base + + +class OAuthCode(Base): + __tablename__ = "oauth_codes" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + code: Mapped[str] = mapped_column(String(128), unique=True, index=True, nullable=False) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + client_id: Mapped[str] = mapped_column(String(64), nullable=False) + redirect_uri: Mapped[str] = mapped_column(String(512), nullable=False) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + grant_token: Mapped[str | None] = mapped_column(String(128), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + + user = relationship("User", backref="oauth_codes") + + __table_args__ = ( + Index("ix_oauth_code_code", "code", unique=True), + Index("ix_oauth_code_user", "user_id"), + ) diff --git a/shared/models/oauth_grant.py b/shared/models/oauth_grant.py new file mode 100644 index 0000000..01a0718 --- /dev/null +++ b/shared/models/oauth_grant.py @@ -0,0 +1,32 @@ +from __future__ import annotations +from datetime import datetime +from sqlalchemy import String, Integer, DateTime, ForeignKey, func, Index +from sqlalchemy.orm import Mapped, mapped_column, relationship +from shared.db.base import Base + + +class OAuthGrant(Base): + """Long-lived grant tracking each client-app session authorization. + + Created when the OAuth authorize endpoint issues a code. Tied to the + account session that issued it (``issuer_session``) so that logging out + on one device revokes only that device's grants. + """ + __tablename__ = "oauth_grants" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + token: Mapped[str] = mapped_column(String(128), unique=True, nullable=False) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + client_id: Mapped[str] = mapped_column(String(64), nullable=False) + issuer_session: Mapped[str] = mapped_column(String(128), nullable=False, index=True) + device_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + user = relationship("User", backref="oauth_grants") + + __table_args__ = ( + Index("ix_oauth_grant_token", "token", unique=True), + Index("ix_oauth_grant_issuer", "issuer_session"), + Index("ix_oauth_grant_device", "device_id", "client_id"), + ) diff --git a/shared/models/order.py b/shared/models/order.py new file mode 100644 index 0000000..4f2f547 --- /dev/null +++ b/shared/models/order.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Optional, List + +from sqlalchemy import Integer, String, DateTime, ForeignKey, Numeric, func, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from shared.db.base import Base + + +class Order(Base): + __tablename__ = "orders" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), nullable=True) + session_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, nullable=True) + + page_config_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("page_configs.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + + status: Mapped[str] = mapped_column( + String(32), + nullable=False, + default="pending", + server_default="pending", + ) + currency: Mapped[str] = mapped_column(String(16), nullable=False, default="GBP") + total_amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) + + # free-form description for the order + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, index=True) + + # SumUp reference string (what we send as checkout_reference) + sumup_reference: Mapped[Optional[str]] = mapped_column( + String(255), + nullable=True, + index=True, + ) + + # SumUp integration fields + sumup_checkout_id: Mapped[Optional[str]] = mapped_column( + String(128), + nullable=True, + index=True, + ) + sumup_status: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) + sumup_hosted_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + ) + + items: Mapped[List["OrderItem"]] = relationship( + "OrderItem", + back_populates="order", + cascade="all, delete-orphan", + lazy="selectin", + ) + page_config: Mapped[Optional["PageConfig"]] = relationship( + "PageConfig", + foreign_keys=[page_config_id], + lazy="selectin", + ) + + +class OrderItem(Base): + __tablename__ = "order_items" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + order_id: Mapped[int] = mapped_column( + ForeignKey("orders.id", ondelete="CASCADE"), + nullable=False, + ) + + product_id: Mapped[int] = mapped_column( + ForeignKey("products.id"), + nullable=False, + ) + product_title: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) + + quantity: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) + currency: Mapped[str] = mapped_column(String(16), nullable=False, default="GBP") + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + + order: Mapped["Order"] = relationship( + "Order", + back_populates="items", + ) + + # NEW: link each order item to its product + product: Mapped["Product"] = relationship( + "Product", + back_populates="order_items", + lazy="selectin", + ) diff --git a/shared/models/page_config.py b/shared/models/page_config.py new file mode 100644 index 0000000..adb6561 --- /dev/null +++ b/shared/models/page_config.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from sqlalchemy import Integer, String, Text, DateTime, func, JSON, text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from shared.db.base import Base + + +class PageConfig(Base): + __tablename__ = "page_configs" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + container_type: Mapped[str] = mapped_column( + String(32), nullable=False, server_default=text("'page'"), + ) + container_id: Mapped[int] = mapped_column(Integer, nullable=False) + + features: Mapped[dict] = mapped_column( + JSON, nullable=False, server_default="{}" + ) + + # Per-page SumUp credentials (NULL until configured) + sumup_merchant_code: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + sumup_api_key: Mapped[Optional[str]] = mapped_column(Text(), nullable=True) + sumup_checkout_prefix: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), nullable=True + ) diff --git a/shared/models/user.py b/shared/models/user.py new file mode 100644 index 0000000..473675d --- /dev/null +++ b/shared/models/user.py @@ -0,0 +1,46 @@ +from __future__ import annotations +from datetime import datetime +from sqlalchemy import String, Integer, DateTime, func, Index, Text, Boolean +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.ext.associationproxy import association_proxy +from shared.db.base import Base + +class User(Base): + __tablename__ = "users" + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # Ghost membership linkage + ghost_id: Mapped[str | None] = mapped_column(String(64), unique=True, index=True, nullable=True) + name: Mapped[str | None] = mapped_column(String(255), nullable=True) + ghost_status: Mapped[str | None] = mapped_column(String(50), nullable=True) # free, paid, comped + ghost_subscribed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=func.true()) + ghost_note: Mapped[str | None] = mapped_column(Text, nullable=True) + avatar_image: Mapped[str | None] = mapped_column(Text, nullable=True) + stripe_customer_id: Mapped[str | None] = mapped_column(String(255), index=True, nullable=True) + ghost_raw: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + + # Relationships to Ghost-related entities + + user_newsletters = relationship("UserNewsletter", back_populates="user", cascade="all, delete-orphan", lazy="selectin") + newsletters = association_proxy("user_newsletters", "newsletter") + labels = relationship("GhostLabel", secondary="user_labels", back_populates="users", lazy="selectin") + subscriptions = relationship("GhostSubscription", back_populates="user", cascade="all, delete-orphan", lazy="selectin") + + liked_products = relationship("ProductLike", back_populates="user", cascade="all, delete-orphan") + liked_posts = relationship("PostLike", back_populates="user", cascade="all, delete-orphan") + cart_items = relationship( + "CartItem", + back_populates="user", + cascade="all, delete-orphan", + ) + + __table_args__ = ( + Index("ix_user_email", "email", unique=True), + ) + + def __repr__(self) -> str: + return f"" diff --git a/shared/requirements.txt b/shared/requirements.txt new file mode 100644 index 0000000..900c63e --- /dev/null +++ b/shared/requirements.txt @@ -0,0 +1,49 @@ +starlette>=0.37,<0.39 +aiofiles==25.1.0 +aiohttp>=3.9 +aiosmtplib==5.0.0 +alembic==1.17.0 +anyio==4.11.0 +async-timeout==5.0.1 +asyncpg==0.30.0 +beautifulsoup4==4.14.2 +blinker==1.9.0 +Brotli==1.1.0 +certifi==2025.10.5 +click==8.3.0 +cryptography>=41.0 +exceptiongroup==1.3.0 +Flask==3.1.2 +greenlet==3.2.4 +h11==0.16.0 +h2==4.3.0 +hpack==4.1.0 +httpcore==1.0.9 +httpx==0.28.1 +Hypercorn==0.17.3 +hyperframe==6.1.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +lxml==6.0.2 +Mako==1.3.10 +MarkupSafe==3.0.3 +priority==2.0.0 +psycopg==3.2.11 +psycopg-binary==3.2.11 +PyJWT==2.10.1 +PyYAML==6.0.3 +Quart==0.20.0 +sniffio==1.3.1 +soupsieve==2.8 +SQLAlchemy==2.0.44 +taskgroup==0.2.2 +tomli==2.3.0 +typing_extensions==4.15.0 +Werkzeug==3.1.3 +wsproto==1.2.0 +zstandard==0.25.0 +redis>=5.0 +mistune>=3.0 +pytest>=8.0 +pytest-asyncio>=0.23 diff --git a/shared/services/__init__.py b/shared/services/__init__.py new file mode 100644 index 0000000..bb11cb4 --- /dev/null +++ b/shared/services/__init__.py @@ -0,0 +1,5 @@ +"""Domain service implementations and registry.""" + +from .registry import services + +__all__ = ["services"] diff --git a/shared/services/blog_impl.py b/shared/services/blog_impl.py new file mode 100644 index 0000000..cbdcca8 --- /dev/null +++ b/shared/services/blog_impl.py @@ -0,0 +1,65 @@ +"""SQL-backed BlogService implementation. + +Queries ``shared.models.ghost_content.Post`` — only this module may read +blog-domain tables on behalf of other domains. +""" +from __future__ import annotations + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.ghost_content import Post +from shared.contracts.dtos import PostDTO + + +def _post_to_dto(post: Post) -> PostDTO: + return PostDTO( + id=post.id, + slug=post.slug, + title=post.title, + status=post.status, + visibility=post.visibility, + is_page=post.is_page, + feature_image=post.feature_image, + html=post.html, + excerpt=post.excerpt, + custom_excerpt=post.custom_excerpt, + published_at=post.published_at, + ) + + +class SqlBlogService: + async def get_post_by_slug(self, session: AsyncSession, slug: str) -> PostDTO | None: + post = ( + await session.execute(select(Post).where(Post.slug == slug)) + ).scalar_one_or_none() + return _post_to_dto(post) if post else None + + async def get_post_by_id(self, session: AsyncSession, id: int) -> PostDTO | None: + post = ( + await session.execute(select(Post).where(Post.id == id)) + ).scalar_one_or_none() + return _post_to_dto(post) if post else None + + async def get_posts_by_ids(self, session: AsyncSession, ids: list[int]) -> list[PostDTO]: + if not ids: + return [] + result = await session.execute(select(Post).where(Post.id.in_(ids))) + return [_post_to_dto(p) for p in result.scalars().all()] + + async def search_posts( + self, session: AsyncSession, query: str, page: int = 1, per_page: int = 10, + ) -> tuple[list[PostDTO], int]: + """Search posts by title with pagination. Not part of the Protocol + (admin-only use in events), but provided for convenience.""" + if query: + count_stmt = select(func.count(Post.id)).where(Post.title.ilike(f"%{query}%")) + posts_stmt = select(Post).where(Post.title.ilike(f"%{query}%")).order_by(Post.title) + else: + count_stmt = select(func.count(Post.id)) + posts_stmt = select(Post).order_by(Post.published_at.desc().nullslast()) + + total = (await session.execute(count_stmt)).scalar() or 0 + offset = (page - 1) * per_page + result = await session.execute(posts_stmt.limit(per_page).offset(offset)) + return [_post_to_dto(p) for p in result.scalars().all()], total diff --git a/shared/services/calendar_impl.py b/shared/services/calendar_impl.py new file mode 100644 index 0000000..26a3f0e --- /dev/null +++ b/shared/services/calendar_impl.py @@ -0,0 +1,669 @@ +"""SQL-backed CalendarService implementation. + +Queries ``shared.models.calendars.*`` — only this module may write to +calendar-domain tables on behalf of other domains. +""" +from __future__ import annotations + +from datetime import datetime +from decimal import Decimal + +from sqlalchemy import select, update, func +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from shared.models.calendars import Calendar, CalendarEntry, CalendarEntryPost, Ticket +from shared.contracts.dtos import CalendarDTO, CalendarEntryDTO, TicketDTO + + +def _cal_to_dto(cal: Calendar) -> CalendarDTO: + return CalendarDTO( + id=cal.id, + container_type=cal.container_type, + container_id=cal.container_id, + name=cal.name, + slug=cal.slug, + description=cal.description, + ) + + +def _entry_to_dto(entry: CalendarEntry) -> CalendarEntryDTO: + cal = getattr(entry, "calendar", None) + return CalendarEntryDTO( + id=entry.id, + calendar_id=entry.calendar_id, + name=entry.name, + start_at=entry.start_at, + state=entry.state, + cost=entry.cost, + end_at=entry.end_at, + user_id=entry.user_id, + session_id=entry.session_id, + order_id=entry.order_id, + slot_id=entry.slot_id, + ticket_price=entry.ticket_price, + ticket_count=entry.ticket_count, + calendar_name=cal.name if cal else None, + calendar_slug=cal.slug if cal else None, + calendar_container_id=cal.container_id if cal else None, + calendar_container_type=cal.container_type if cal else None, + ) + + +def _ticket_to_dto(ticket: Ticket) -> TicketDTO: + entry = getattr(ticket, "entry", None) + tt = getattr(ticket, "ticket_type", None) + cal = getattr(entry, "calendar", None) if entry else None + # Price: ticket type cost if available, else entry ticket_price + price = None + if tt and tt.cost is not None: + price = tt.cost + elif entry and entry.ticket_price is not None: + price = entry.ticket_price + return TicketDTO( + id=ticket.id, + code=ticket.code, + state=ticket.state, + entry_name=entry.name if entry else "", + entry_start_at=entry.start_at if entry else ticket.created_at, + entry_end_at=entry.end_at if entry else None, + ticket_type_name=tt.name if tt else None, + calendar_name=cal.name if cal else None, + created_at=ticket.created_at, + checked_in_at=ticket.checked_in_at, + entry_id=entry.id if entry else None, + ticket_type_id=ticket.ticket_type_id, + price=price, + order_id=ticket.order_id, + calendar_container_id=cal.container_id if cal else None, + ) + + +class SqlCalendarService: + + # -- reads ---------------------------------------------------------------- + + async def calendars_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + ) -> list[CalendarDTO]: + result = await session.execute( + select(Calendar).where( + Calendar.container_type == container_type, + Calendar.container_id == container_id, + Calendar.deleted_at.is_(None), + ).order_by(Calendar.name.asc()) + ) + return [_cal_to_dto(c) for c in result.scalars().all()] + + async def pending_entries( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[CalendarEntryDTO]: + filters = [ + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "pending", + ] + if user_id is not None: + filters.append(CalendarEntry.user_id == user_id) + elif session_id is not None: + filters.append(CalendarEntry.session_id == session_id) + else: + return [] + + result = await session.execute( + select(CalendarEntry) + .where(*filters) + .order_by(CalendarEntry.start_at.asc()) + .options(selectinload(CalendarEntry.calendar)) + ) + return [_entry_to_dto(e) for e in result.scalars().all()] + + async def entries_for_page( + self, session: AsyncSession, page_id: int, *, + user_id: int | None, session_id: str | None, + ) -> list[CalendarEntryDTO]: + cal_ids = select(Calendar.id).where( + Calendar.container_type == "page", + Calendar.container_id == page_id, + Calendar.deleted_at.is_(None), + ).scalar_subquery() + + filters = [ + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "pending", + CalendarEntry.calendar_id.in_(cal_ids), + ] + if user_id is not None: + filters.append(CalendarEntry.user_id == user_id) + elif session_id is not None: + filters.append(CalendarEntry.session_id == session_id) + else: + return [] + + result = await session.execute( + select(CalendarEntry) + .where(*filters) + .order_by(CalendarEntry.start_at.asc()) + .options(selectinload(CalendarEntry.calendar)) + ) + return [_entry_to_dto(e) for e in result.scalars().all()] + + async def entry_by_id(self, session: AsyncSession, entry_id: int) -> CalendarEntryDTO | None: + entry = ( + await session.execute( + select(CalendarEntry) + .where(CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None)) + .options(selectinload(CalendarEntry.calendar)) + ) + ).scalar_one_or_none() + return _entry_to_dto(entry) if entry else None + + async def entry_ids_for_content( + self, session: AsyncSession, content_type: str, content_id: int, + ) -> set[int]: + """Get entry IDs associated with a content item (e.g. post).""" + result = await session.execute( + select(CalendarEntryPost.entry_id).where( + CalendarEntryPost.content_type == content_type, + CalendarEntryPost.content_id == content_id, + CalendarEntryPost.deleted_at.is_(None), + ) + ) + return set(result.scalars().all()) + + async def visible_entries_for_period( + self, session: AsyncSession, calendar_id: int, + period_start: datetime, period_end: datetime, + *, user_id: int | None, is_admin: bool, session_id: str | None, + ) -> list[CalendarEntryDTO]: + """Return visible entries for a calendar in a date range. + + Visibility rules: + - Everyone sees confirmed entries. + - Current user/session sees their own entries (any state). + - Admins also see ordered + provisional entries for all users. + """ + # User/session entries (any state) + user_entries: list[CalendarEntry] = [] + if user_id or session_id: + conditions = [ + CalendarEntry.calendar_id == calendar_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= period_start, + CalendarEntry.start_at < period_end, + ] + if user_id: + conditions.append(CalendarEntry.user_id == user_id) + elif session_id: + conditions.append(CalendarEntry.session_id == session_id) + result = await session.execute( + select(CalendarEntry).where(*conditions) + .options(selectinload(CalendarEntry.calendar)) + ) + user_entries = list(result.scalars().all()) + + # Confirmed entries for everyone + result = await session.execute( + select(CalendarEntry).where( + CalendarEntry.calendar_id == calendar_id, + CalendarEntry.state == "confirmed", + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= period_start, + CalendarEntry.start_at < period_end, + ).options(selectinload(CalendarEntry.calendar)) + ) + confirmed_entries = list(result.scalars().all()) + + # Admin: ordered + provisional for everyone + admin_entries: list[CalendarEntry] = [] + if is_admin: + result = await session.execute( + select(CalendarEntry).where( + CalendarEntry.calendar_id == calendar_id, + CalendarEntry.state.in_(("ordered", "provisional")), + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= period_start, + CalendarEntry.start_at < period_end, + ).options(selectinload(CalendarEntry.calendar)) + ) + admin_entries = list(result.scalars().all()) + + # Merge, deduplicate, sort + entries_by_id: dict[int, CalendarEntry] = {} + for e in confirmed_entries: + entries_by_id[e.id] = e + for e in admin_entries: + entries_by_id[e.id] = e + for e in user_entries: + entries_by_id[e.id] = e + + merged = sorted(entries_by_id.values(), key=lambda e: e.start_at or period_start) + return [_entry_to_dto(e) for e in merged] + + async def upcoming_entries_for_container( + self, session: AsyncSession, + container_type: str | None = None, container_id: int | None = None, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[CalendarEntryDTO], bool]: + """Upcoming confirmed entries. Optionally scoped to a container.""" + filters = [ + CalendarEntry.state == "confirmed", + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= func.now(), + ] + + if container_type is not None and container_id is not None: + cal_ids = select(Calendar.id).where( + Calendar.container_type == container_type, + Calendar.container_id == container_id, + Calendar.deleted_at.is_(None), + ).scalar_subquery() + filters.append(CalendarEntry.calendar_id.in_(cal_ids)) + else: + # Still exclude entries from deleted calendars + cal_ids = select(Calendar.id).where( + Calendar.deleted_at.is_(None), + ).scalar_subquery() + filters.append(CalendarEntry.calendar_id.in_(cal_ids)) + + offset = (page - 1) * per_page + result = await session.execute( + select(CalendarEntry) + .where(*filters) + .order_by(CalendarEntry.start_at.asc()) + .limit(per_page) + .offset(offset) + .options(selectinload(CalendarEntry.calendar)) + ) + entries = result.scalars().all() + has_more = len(entries) == per_page + return [_entry_to_dto(e) for e in entries], has_more + + async def associated_entries( + self, session: AsyncSession, content_type: str, content_id: int, page: int, + ) -> tuple[list[CalendarEntryDTO], bool]: + """Get paginated confirmed entries associated with a content item.""" + per_page = 10 + entry_ids_result = await session.execute( + select(CalendarEntryPost.entry_id).where( + CalendarEntryPost.content_type == content_type, + CalendarEntryPost.content_id == content_id, + CalendarEntryPost.deleted_at.is_(None), + ) + ) + entry_ids = set(entry_ids_result.scalars().all()) + if not entry_ids: + return [], False + + offset = (page - 1) * per_page + result = await session.execute( + select(CalendarEntry) + .where( + CalendarEntry.id.in_(entry_ids), + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "confirmed", + ) + .order_by(CalendarEntry.start_at.desc()) + .limit(per_page) + .offset(offset) + .options(selectinload(CalendarEntry.calendar)) + ) + entries = result.scalars().all() + has_more = len(entries) == per_page + return [_entry_to_dto(e) for e in entries], has_more + + async def toggle_entry_post( + self, session: AsyncSession, entry_id: int, content_type: str, content_id: int, + ) -> bool: + """Toggle association; returns True if now associated, False if removed.""" + existing = await session.scalar( + select(CalendarEntryPost).where( + CalendarEntryPost.entry_id == entry_id, + CalendarEntryPost.content_type == content_type, + CalendarEntryPost.content_id == content_id, + CalendarEntryPost.deleted_at.is_(None), + ) + ) + if existing: + existing.deleted_at = func.now() + await session.flush() + return False + else: + assoc = CalendarEntryPost( + entry_id=entry_id, + content_type=content_type, + content_id=content_id, + ) + session.add(assoc) + await session.flush() + return True + + async def get_entries_for_order( + self, session: AsyncSession, order_id: int, + ) -> list[CalendarEntryDTO]: + result = await session.execute( + select(CalendarEntry) + .where( + CalendarEntry.order_id == order_id, + CalendarEntry.deleted_at.is_(None), + ) + .options(selectinload(CalendarEntry.calendar)) + ) + return [_entry_to_dto(e) for e in result.scalars().all()] + + async def user_tickets( + self, session: AsyncSession, *, user_id: int, + ) -> list[TicketDTO]: + result = await session.execute( + select(Ticket) + .where( + Ticket.user_id == user_id, + Ticket.state != "cancelled", + ) + .order_by(Ticket.created_at.desc()) + .options( + selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), + selectinload(Ticket.ticket_type), + ) + ) + return [_ticket_to_dto(t) for t in result.scalars().all()] + + async def user_bookings( + self, session: AsyncSession, *, user_id: int, + ) -> list[CalendarEntryDTO]: + result = await session.execute( + select(CalendarEntry) + .where( + CalendarEntry.user_id == user_id, + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state.in_(("ordered", "provisional", "confirmed")), + ) + .order_by(CalendarEntry.start_at.desc()) + .options(selectinload(CalendarEntry.calendar)) + ) + return [_entry_to_dto(e) for e in result.scalars().all()] + + # -- batch reads (not in protocol — convenience for blog service) --------- + + async def confirmed_entries_for_posts( + self, session: AsyncSession, post_ids: list[int], + ) -> dict[int, list[CalendarEntryDTO]]: + """Return confirmed entries grouped by post_id for a batch of posts.""" + if not post_ids: + return {} + + result = await session.execute( + select(CalendarEntry, CalendarEntryPost.content_id) + .join(CalendarEntryPost, CalendarEntry.id == CalendarEntryPost.entry_id) + .options(selectinload(CalendarEntry.calendar)) + .where( + CalendarEntryPost.content_type == "post", + CalendarEntryPost.content_id.in_(post_ids), + CalendarEntryPost.deleted_at.is_(None), + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "confirmed", + ) + .order_by(CalendarEntry.start_at.asc()) + ) + + entries_by_post: dict[int, list[CalendarEntryDTO]] = {} + for entry, post_id in result: + entries_by_post.setdefault(post_id, []).append(_entry_to_dto(entry)) + return entries_by_post + + # -- writes --------------------------------------------------------------- + + async def adopt_entries_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: + """Adopt anonymous calendar entries for a logged-in user. + + Only deletes stale *pending* entries for the user — confirmed/ordered + entries must be preserved. + """ + await session.execute( + update(CalendarEntry) + .where( + CalendarEntry.deleted_at.is_(None), + CalendarEntry.user_id == user_id, + CalendarEntry.state == "pending", + ) + .values(deleted_at=func.now()) + ) + cal_result = await session.execute( + select(CalendarEntry).where( + CalendarEntry.deleted_at.is_(None), + CalendarEntry.session_id == session_id, + ) + ) + for entry in cal_result.scalars().all(): + entry.user_id = user_id + + async def claim_entries_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, page_post_id: int | None, + ) -> None: + """Mark pending CalendarEntries as 'ordered' and set order_id.""" + filters = [ + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "pending", + ] + if user_id is not None: + filters.append(CalendarEntry.user_id == user_id) + elif session_id is not None: + filters.append(CalendarEntry.session_id == session_id) + + if page_post_id is not None: + cal_ids = select(Calendar.id).where( + Calendar.container_type == "page", + Calendar.container_id == page_post_id, + Calendar.deleted_at.is_(None), + ).scalar_subquery() + filters.append(CalendarEntry.calendar_id.in_(cal_ids)) + + await session.execute( + update(CalendarEntry) + .where(*filters) + .values(state="ordered", order_id=order_id) + ) + + async def confirm_entries_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, + ) -> None: + """Mark ordered CalendarEntries as 'provisional'.""" + filters = [ + CalendarEntry.deleted_at.is_(None), + CalendarEntry.state == "ordered", + CalendarEntry.order_id == order_id, + ] + if user_id is not None: + filters.append(CalendarEntry.user_id == user_id) + elif session_id is not None: + filters.append(CalendarEntry.session_id == session_id) + + await session.execute( + update(CalendarEntry) + .where(*filters) + .values(state="provisional") + ) + + # -- ticket methods ------------------------------------------------------- + + def _ticket_query_options(self): + return [ + selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), + selectinload(Ticket.ticket_type), + ] + + async def pending_tickets( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[TicketDTO]: + """Reserved tickets for the given identity (cart line items).""" + filters = [Ticket.state == "reserved"] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return [] + + result = await session.execute( + select(Ticket) + .where(*filters) + .order_by(Ticket.created_at.asc()) + .options(*self._ticket_query_options()) + ) + return [_ticket_to_dto(t) for t in result.scalars().all()] + + async def tickets_for_page( + self, session: AsyncSession, page_id: int, *, + user_id: int | None, session_id: str | None, + ) -> list[TicketDTO]: + """Reserved tickets scoped to a page (via entry → calendar → container_id).""" + cal_ids = select(Calendar.id).where( + Calendar.container_type == "page", + Calendar.container_id == page_id, + Calendar.deleted_at.is_(None), + ).scalar_subquery() + + entry_ids = select(CalendarEntry.id).where( + CalendarEntry.calendar_id.in_(cal_ids), + CalendarEntry.deleted_at.is_(None), + ).scalar_subquery() + + filters = [ + Ticket.state == "reserved", + Ticket.entry_id.in_(entry_ids), + ] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return [] + + result = await session.execute( + select(Ticket) + .where(*filters) + .order_by(Ticket.created_at.asc()) + .options(*self._ticket_query_options()) + ) + return [_ticket_to_dto(t) for t in result.scalars().all()] + + async def claim_tickets_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, page_post_id: int | None, + ) -> None: + """Set order_id on reserved tickets at checkout.""" + filters = [Ticket.state == "reserved"] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + + if page_post_id is not None: + cal_ids = select(Calendar.id).where( + Calendar.container_type == "page", + Calendar.container_id == page_post_id, + Calendar.deleted_at.is_(None), + ).scalar_subquery() + entry_ids = select(CalendarEntry.id).where( + CalendarEntry.calendar_id.in_(cal_ids), + CalendarEntry.deleted_at.is_(None), + ).scalar_subquery() + filters.append(Ticket.entry_id.in_(entry_ids)) + + await session.execute( + update(Ticket).where(*filters).values(order_id=order_id) + ) + + async def confirm_tickets_for_order( + self, session: AsyncSession, order_id: int, + ) -> None: + """Reserved → confirmed on payment.""" + await session.execute( + update(Ticket) + .where(Ticket.order_id == order_id, Ticket.state == "reserved") + .values(state="confirmed") + ) + + async def get_tickets_for_order( + self, session: AsyncSession, order_id: int, + ) -> list[TicketDTO]: + """Tickets for a given order (checkout return display).""" + result = await session.execute( + select(Ticket) + .where(Ticket.order_id == order_id) + .order_by(Ticket.created_at.asc()) + .options(*self._ticket_query_options()) + ) + return [_ticket_to_dto(t) for t in result.scalars().all()] + + async def adopt_tickets_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: + """Migrate anonymous reserved tickets to user on login.""" + result = await session.execute( + select(Ticket).where( + Ticket.session_id == session_id, + Ticket.state == "reserved", + ) + ) + for ticket in result.scalars().all(): + ticket.user_id = user_id + + async def adjust_ticket_quantity( + self, session: AsyncSession, entry_id: int, count: int, *, + user_id: int | None, session_id: str | None, + ticket_type_id: int | None = None, + ) -> int: + """Adjust reserved ticket count to target. Returns new count.""" + import uuid + + count = max(count, 0) + + # Current reserved count + filters = [ + Ticket.entry_id == entry_id, + Ticket.state == "reserved", + ] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return 0 + if ticket_type_id is not None: + filters.append(Ticket.ticket_type_id == ticket_type_id) + + current = await session.scalar( + select(func.count(Ticket.id)).where(*filters) + ) or 0 + + if count > current: + # Create tickets + for _ in range(count - current): + ticket = Ticket( + entry_id=entry_id, + ticket_type_id=ticket_type_id, + user_id=user_id, + session_id=session_id, + code=uuid.uuid4().hex, + state="reserved", + ) + session.add(ticket) + await session.flush() + elif count < current: + # Cancel newest tickets + to_cancel = current - count + result = await session.execute( + select(Ticket) + .where(*filters) + .order_by(Ticket.created_at.desc()) + .limit(to_cancel) + ) + for ticket in result.scalars().all(): + ticket.state = "cancelled" + await session.flush() + + return count diff --git a/shared/services/cart_impl.py b/shared/services/cart_impl.py new file mode 100644 index 0000000..1438bfa --- /dev/null +++ b/shared/services/cart_impl.py @@ -0,0 +1,162 @@ +"""SQL-backed CartService implementation. + +Queries ``shared.models.market.CartItem`` — only this module may write +to cart-domain tables on behalf of other domains. +""" +from __future__ import annotations + +from decimal import Decimal + +from sqlalchemy import select, update, func +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from shared.models.market import CartItem +from shared.models.market_place import MarketPlace +from shared.models.calendars import CalendarEntry, Calendar +from shared.contracts.dtos import CartItemDTO, CartSummaryDTO + + +def _item_to_dto(ci: CartItem) -> CartItemDTO: + product = ci.product + return CartItemDTO( + id=ci.id, + product_id=ci.product_id, + quantity=ci.quantity, + product_title=product.title if product else None, + product_slug=product.slug if product else None, + product_image=product.image if product else None, + unit_price=Decimal(str(product.special_price or product.regular_price or 0)) if product else None, + market_place_id=ci.market_place_id, + ) + + +class SqlCartService: + + async def cart_summary( + self, session: AsyncSession, *, + user_id: int | None, session_id: str | None, + page_slug: str | None = None, + ) -> CartSummaryDTO: + """Build a lightweight cart summary for the current identity.""" + # Resolve page filter + page_post_id: int | None = None + if page_slug: + from shared.services.registry import services + post = await services.blog.get_post_by_slug(session, page_slug) + if post and post.is_page: + page_post_id = post.id + + # --- product cart --- + cart_q = select(CartItem).where(CartItem.deleted_at.is_(None)) + if user_id is not None: + cart_q = cart_q.where(CartItem.user_id == user_id) + elif session_id is not None: + cart_q = cart_q.where(CartItem.session_id == session_id) + else: + return CartSummaryDTO() + + if page_post_id is not None: + mp_ids = select(MarketPlace.id).where( + MarketPlace.container_type == "page", + MarketPlace.container_id == page_post_id, + MarketPlace.deleted_at.is_(None), + ).scalar_subquery() + cart_q = cart_q.where(CartItem.market_place_id.in_(mp_ids)) + + cart_q = cart_q.options(selectinload(CartItem.product)) + result = await session.execute(cart_q) + cart_items = result.scalars().all() + + count = sum(ci.quantity for ci in cart_items) + total = sum( + Decimal(str(ci.product.special_price or ci.product.regular_price or 0)) * ci.quantity + for ci in cart_items + if ci.product and (ci.product.special_price or ci.product.regular_price) + ) + + # --- calendar entries --- + from shared.services.registry import services + if page_post_id is not None: + cal_entries = await services.calendar.entries_for_page( + session, page_post_id, + user_id=user_id, + session_id=session_id, + ) + else: + cal_entries = await services.calendar.pending_entries( + session, + user_id=user_id, + session_id=session_id, + ) + + calendar_count = len(cal_entries) + calendar_total = sum(Decimal(str(e.cost or 0)) for e in cal_entries if e.cost is not None) + + # --- tickets --- + if page_post_id is not None: + tickets = await services.calendar.tickets_for_page( + session, page_post_id, + user_id=user_id, + session_id=session_id, + ) + else: + tickets = await services.calendar.pending_tickets( + session, + user_id=user_id, + session_id=session_id, + ) + + ticket_count = len(tickets) + ticket_total = sum(Decimal(str(t.price or 0)) for t in tickets) + + items = [_item_to_dto(ci) for ci in cart_items] + + return CartSummaryDTO( + count=count, + total=total, + calendar_count=calendar_count, + calendar_total=calendar_total, + items=items, + ticket_count=ticket_count, + ticket_total=ticket_total, + ) + + async def cart_items( + self, session: AsyncSession, *, + user_id: int | None, session_id: str | None, + ) -> list[CartItemDTO]: + cart_q = select(CartItem).where(CartItem.deleted_at.is_(None)) + if user_id is not None: + cart_q = cart_q.where(CartItem.user_id == user_id) + elif session_id is not None: + cart_q = cart_q.where(CartItem.session_id == session_id) + else: + return [] + + cart_q = cart_q.options(selectinload(CartItem.product)).order_by(CartItem.created_at.desc()) + result = await session.execute(cart_q) + return [_item_to_dto(ci) for ci in result.scalars().all()] + + async def adopt_cart_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: + """Adopt anonymous cart items for a logged-in user.""" + anon_result = await session.execute( + select(CartItem).where( + CartItem.deleted_at.is_(None), + CartItem.user_id.is_(None), + CartItem.session_id == session_id, + ) + ) + anon_items = anon_result.scalars().all() + + if anon_items: + # Soft-delete existing user cart + await session.execute( + update(CartItem) + .where(CartItem.deleted_at.is_(None), CartItem.user_id == user_id) + .values(deleted_at=func.now()) + ) + for ci in anon_items: + ci.user_id = user_id diff --git a/shared/services/federation_impl.py b/shared/services/federation_impl.py new file mode 100644 index 0000000..fa33d7d --- /dev/null +++ b/shared/services/federation_impl.py @@ -0,0 +1,1654 @@ +"""SQL-backed FederationService implementation. + +Queries ``shared.models.federation`` — only this module may read/write +federation-domain tables on behalf of other domains. +""" +from __future__ import annotations + +import os +import uuid +from datetime import datetime, timezone + +from sqlalchemy import select, func, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.federation import ( + ActorProfile, APActivity, APFollower, + RemoteActor, APFollowing, APRemotePost, APLocalPost, + APInteraction, APNotification, +) +from shared.contracts.dtos import ( + ActorProfileDTO, APActivityDTO, APFollowerDTO, + RemoteActorDTO, RemotePostDTO, TimelineItemDTO, NotificationDTO, +) + + +def _domain() -> str: + return os.getenv("AP_DOMAIN", "federation.rose-ash.com") + + +def _get_origin_app() -> str | None: + try: + from quart import current_app + return current_app.name + except (ImportError, RuntimeError): + return None + + +def _actor_to_dto(actor: ActorProfile) -> ActorProfileDTO: + domain = _domain() + username = actor.preferred_username + return ActorProfileDTO( + id=actor.id, + user_id=actor.user_id, + preferred_username=username, + public_key_pem=actor.public_key_pem, + display_name=actor.display_name, + summary=actor.summary, + inbox_url=f"https://{domain}/users/{username}/inbox", + outbox_url=f"https://{domain}/users/{username}/outbox", + created_at=actor.created_at, + ) + + +def _activity_to_dto(a: APActivity) -> APActivityDTO: + return APActivityDTO( + id=a.id, + activity_id=a.activity_id, + activity_type=a.activity_type, + actor_profile_id=a.actor_profile_id, + object_type=a.object_type, + object_data=a.object_data, + published=a.published, + is_local=a.is_local, + source_type=a.source_type, + source_id=a.source_id, + ipfs_cid=a.ipfs_cid, + ) + + +def _follower_to_dto(f: APFollower) -> APFollowerDTO: + return APFollowerDTO( + id=f.id, + actor_profile_id=f.actor_profile_id, + follower_acct=f.follower_acct, + follower_inbox=f.follower_inbox, + follower_actor_url=f.follower_actor_url, + created_at=f.created_at, + app_domain=f.app_domain, + ) + + +def _remote_actor_to_dto(r: RemoteActor) -> RemoteActorDTO: + return RemoteActorDTO( + id=r.id, + actor_url=r.actor_url, + inbox_url=r.inbox_url, + preferred_username=r.preferred_username, + domain=r.domain, + display_name=r.display_name, + summary=r.summary, + icon_url=r.icon_url, + shared_inbox_url=r.shared_inbox_url, + public_key_pem=r.public_key_pem, + ) + + +def _remote_post_to_dto( + p: APRemotePost, actor: RemoteActor | None = None, +) -> RemotePostDTO: + return RemotePostDTO( + id=p.id, + remote_actor_id=p.remote_actor_id, + object_id=p.object_id, + content=p.content or "", + summary=p.summary, + url=p.url, + attachments=p.attachment_data or [], + tags=p.tag_data or [], + published=p.published, + actor=_remote_actor_to_dto(actor) if actor else None, + ) + + +class SqlFederationService: + # -- Actor management ----------------------------------------------------- + + async def get_actor_by_username( + self, session: AsyncSession, username: str, + ) -> ActorProfileDTO | None: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + return _actor_to_dto(actor) if actor else None + + async def get_actor_by_user_id( + self, session: AsyncSession, user_id: int, + ) -> ActorProfileDTO | None: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.user_id == user_id) + ) + ).scalar_one_or_none() + return _actor_to_dto(actor) if actor else None + + async def create_actor( + self, session: AsyncSession, user_id: int, preferred_username: str, + display_name: str | None = None, summary: str | None = None, + ) -> ActorProfileDTO: + from shared.utils.http_signatures import generate_rsa_keypair + + private_pem, public_pem = generate_rsa_keypair() + + actor = ActorProfile( + user_id=user_id, + preferred_username=preferred_username, + display_name=display_name, + summary=summary, + public_key_pem=public_pem, + private_key_pem=private_pem, + ) + session.add(actor) + await session.flush() + return _actor_to_dto(actor) + + async def username_available( + self, session: AsyncSession, username: str, + ) -> bool: + count = ( + await session.execute( + select(func.count(ActorProfile.id)).where( + ActorProfile.preferred_username == username + ) + ) + ).scalar() or 0 + return count == 0 + + # -- Publishing ----------------------------------------------------------- + + async def publish_activity( + self, session: AsyncSession, *, + actor_user_id: int, + activity_type: str, + object_type: str, + object_data: dict, + source_type: str | None = None, + source_id: int | None = None, + ) -> APActivityDTO: + # Look up actor + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.user_id == actor_user_id) + ) + ).scalar_one_or_none() + if actor is None: + raise ValueError(f"No ActorProfile for user_id={actor_user_id}") + + domain = _domain() + username = actor.preferred_username + activity_uri = f"https://{domain}/users/{username}/activities/{uuid.uuid4()}" + + now = datetime.now(timezone.utc) + + actor_url = f"https://{domain}/users/{username}" + + activity = APActivity( + activity_id=activity_uri, + activity_type=activity_type, + actor_profile_id=actor.id, + actor_uri=actor_url, + object_type=object_type, + object_data=object_data, + published=now, + is_local=True, + source_type=source_type, + source_id=source_id, + visibility="public", + process_state="pending", + origin_app=_get_origin_app(), + ) + session.add(activity) + await session.flush() + + # Store activity JSON on IPFS (best-effort — don't fail publish if IPFS down) + try: + from shared.utils.ipfs_client import add_json, is_available + if await is_available(): + activity_json = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + "id": activity_uri, + "type": activity_type, + "actor": actor_url, + "published": now.isoformat(), + "object": { + "type": object_type, + **object_data, + }, + } + cid = await add_json(activity_json) + activity.ipfs_cid = cid + await session.flush() + except Exception: + pass # IPFS failure is non-fatal + + return _activity_to_dto(activity) + + # -- Queries -------------------------------------------------------------- + + async def get_activity( + self, session: AsyncSession, activity_id: str, + ) -> APActivityDTO | None: + a = ( + await session.execute( + select(APActivity).where(APActivity.activity_id == activity_id) + ) + ).scalar_one_or_none() + return _activity_to_dto(a) if a else None + + async def get_outbox( + self, session: AsyncSession, username: str, + page: int = 1, per_page: int = 20, + origin_app: str | None = None, + ) -> tuple[list[APActivityDTO], int]: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + if actor is None: + return [], 0 + + filters = [ + APActivity.actor_profile_id == actor.id, + APActivity.is_local == True, # noqa: E712 + ] + if origin_app is not None: + filters.append(APActivity.origin_app == origin_app) + + total = ( + await session.execute( + select(func.count(APActivity.id)).where(*filters) + ) + ).scalar() or 0 + + offset = (page - 1) * per_page + result = await session.execute( + select(APActivity) + .where(*filters) + .order_by(APActivity.published.desc()) + .limit(per_page) + .offset(offset) + ) + return [_activity_to_dto(a) for a in result.scalars().all()], total + + async def get_activity_for_source( + self, session: AsyncSession, source_type: str, source_id: int, + ) -> APActivityDTO | None: + a = ( + await session.execute( + select(APActivity).where( + APActivity.source_type == source_type, + APActivity.source_id == source_id, + ).order_by(APActivity.created_at.desc()) + .limit(1) + ) + ).scalars().first() + return _activity_to_dto(a) if a else None + + async def count_activities_for_source( + self, session: AsyncSession, source_type: str, source_id: int, + *, activity_type: str, + ) -> int: + from sqlalchemy import func + result = await session.execute( + select(func.count()).select_from(APActivity).where( + APActivity.source_type == source_type, + APActivity.source_id == source_id, + APActivity.activity_type == activity_type, + ) + ) + return result.scalar_one() + + # -- Followers ------------------------------------------------------------ + + async def get_followers( + self, session: AsyncSession, username: str, + app_domain: str | None = None, + ) -> list[APFollowerDTO]: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + if actor is None: + return [] + + q = select(APFollower).where(APFollower.actor_profile_id == actor.id) + if app_domain is not None: + q = q.where(APFollower.app_domain == app_domain) + + result = await session.execute(q) + return [_follower_to_dto(f) for f in result.scalars().all()] + + async def add_follower( + self, session: AsyncSession, username: str, + follower_acct: str, follower_inbox: str, follower_actor_url: str, + follower_public_key: str | None = None, + app_domain: str = "federation", + ) -> APFollowerDTO: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + if actor is None: + raise ValueError(f"Actor not found: {username}") + + # Upsert: update if already following this (actor, acct, app_domain) + existing = ( + await session.execute( + select(APFollower).where( + APFollower.actor_profile_id == actor.id, + APFollower.follower_acct == follower_acct, + APFollower.app_domain == app_domain, + ) + ) + ).scalar_one_or_none() + + if existing: + existing.follower_inbox = follower_inbox + existing.follower_actor_url = follower_actor_url + existing.follower_public_key = follower_public_key + await session.flush() + return _follower_to_dto(existing) + + follower = APFollower( + actor_profile_id=actor.id, + follower_acct=follower_acct, + follower_inbox=follower_inbox, + follower_actor_url=follower_actor_url, + follower_public_key=follower_public_key, + app_domain=app_domain, + ) + session.add(follower) + await session.flush() + return _follower_to_dto(follower) + + async def remove_follower( + self, session: AsyncSession, username: str, follower_acct: str, + app_domain: str = "federation", + ) -> bool: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + if actor is None: + return False + + result = await session.execute( + delete(APFollower).where( + APFollower.actor_profile_id == actor.id, + APFollower.follower_acct == follower_acct, + APFollower.app_domain == app_domain, + ) + ) + return result.rowcount > 0 + + async def get_followers_paginated( + self, session: AsyncSession, username: str, + page: int = 1, per_page: int = 20, + ) -> tuple[list[RemoteActorDTO], int]: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + if actor is None: + return [], 0 + + total = ( + await session.execute( + select(func.count(APFollower.id)).where( + APFollower.actor_profile_id == actor.id, + ) + ) + ).scalar() or 0 + + offset = (page - 1) * per_page + followers = ( + await session.execute( + select(APFollower) + .where(APFollower.actor_profile_id == actor.id) + .order_by(APFollower.created_at.desc()) + .limit(per_page) + .offset(offset) + ) + ).scalars().all() + + results: list[RemoteActorDTO] = [] + for f in followers: + # Try to resolve from cached remote actors first + remote = ( + await session.execute( + select(RemoteActor).where( + RemoteActor.actor_url == f.follower_actor_url, + ) + ) + ).scalar_one_or_none() + if remote: + results.append(_remote_actor_to_dto(remote)) + else: + # Synthesise a minimal DTO from follower data + from urllib.parse import urlparse + domain = urlparse(f.follower_actor_url).netloc + results.append(RemoteActorDTO( + id=0, + actor_url=f.follower_actor_url, + inbox_url=f.follower_inbox, + preferred_username=f.follower_acct.split("@")[0] if "@" in f.follower_acct else f.follower_acct, + domain=domain, + display_name=None, + summary=None, + icon_url=None, + )) + return results, total + + # -- Remote actors -------------------------------------------------------- + + async def get_or_fetch_remote_actor( + self, session: AsyncSession, actor_url: str, + ) -> RemoteActorDTO | None: + # Check cache first + row = ( + await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == actor_url) + ) + ).scalar_one_or_none() + if row: + return _remote_actor_to_dto(row) + + # Fetch from remote + import httpx + try: + async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: + resp = await client.get( + actor_url, + headers={"Accept": "application/activity+json"}, + ) + if resp.status_code != 200: + return None + data = resp.json() + except Exception: + return None + + return await self._upsert_remote_actor(session, actor_url, data) + + async def _upsert_remote_actor( + self, session: AsyncSession, actor_url: str, data: dict, + ) -> RemoteActorDTO | None: + from urllib.parse import urlparse + domain = urlparse(actor_url).netloc + + icon_url = None + icon = data.get("icon") + if isinstance(icon, dict): + icon_url = icon.get("url") + + pub_key = (data.get("publicKey") or {}).get("publicKeyPem") + + # Upsert + existing = ( + await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == actor_url) + ) + ).scalar_one_or_none() + + now = datetime.now(timezone.utc) + if existing: + existing.inbox_url = data.get("inbox", existing.inbox_url) + existing.shared_inbox_url = (data.get("endpoints") or {}).get("sharedInbox") + existing.preferred_username = data.get("preferredUsername", existing.preferred_username) + existing.display_name = data.get("name") + existing.summary = data.get("summary") + existing.icon_url = icon_url + existing.public_key_pem = pub_key + existing.fetched_at = now + await session.flush() + return _remote_actor_to_dto(existing) + + row = RemoteActor( + actor_url=actor_url, + inbox_url=data.get("inbox", ""), + shared_inbox_url=(data.get("endpoints") or {}).get("sharedInbox"), + preferred_username=data.get("preferredUsername", ""), + display_name=data.get("name"), + summary=data.get("summary"), + icon_url=icon_url, + public_key_pem=pub_key, + domain=domain, + fetched_at=now, + ) + session.add(row) + await session.flush() + return _remote_actor_to_dto(row) + + async def search_remote_actor( + self, session: AsyncSession, acct: str, + ) -> RemoteActorDTO | None: + from shared.utils.webfinger import resolve_actor + data = await resolve_actor(acct) + if not data: + return None + + actor_url = data.get("id") + if not actor_url: + return None + + return await self._upsert_remote_actor(session, actor_url, data) + + async def search_actors( + self, session: AsyncSession, query: str, page: int = 1, limit: int = 20, + ) -> tuple[list[RemoteActorDTO], int]: + from sqlalchemy import or_ + + pattern = f"%{query}%" + offset = (page - 1) * limit + + # WebFinger resolve for @user@domain queries (first page only) + webfinger_result: RemoteActorDTO | None = None + if page == 1 and "@" in query: + webfinger_result = await self.search_remote_actor(session, query) + + # Search cached remote actors + remote_filter = or_( + RemoteActor.preferred_username.ilike(pattern), + RemoteActor.display_name.ilike(pattern), + RemoteActor.domain.ilike(pattern), + ) + remote_total = ( + await session.execute( + select(func.count(RemoteActor.id)).where(remote_filter) + ) + ).scalar() or 0 + + # Search local actor profiles + local_filter = or_( + ActorProfile.preferred_username.ilike(pattern), + ActorProfile.display_name.ilike(pattern), + ) + local_total = ( + await session.execute( + select(func.count(ActorProfile.id)).where(local_filter) + ) + ).scalar() or 0 + + total = remote_total + local_total + + # Fetch remote actors page + remote_rows = ( + await session.execute( + select(RemoteActor) + .where(remote_filter) + .order_by(RemoteActor.preferred_username) + .limit(limit) + .offset(offset) + ) + ).scalars().all() + + results: list[RemoteActorDTO] = [_remote_actor_to_dto(r) for r in remote_rows] + + # Fill remaining slots with local actors + remaining = limit - len(results) + local_offset = max(0, offset - remote_total) + if remaining > 0 and offset + len(results) >= remote_total: + domain = _domain() + local_rows = ( + await session.execute( + select(ActorProfile) + .where(local_filter) + .order_by(ActorProfile.preferred_username) + .limit(remaining) + .offset(local_offset) + ) + ).scalars().all() + for lp in local_rows: + results.append(RemoteActorDTO( + id=0, + actor_url=f"https://{domain}/users/{lp.preferred_username}", + inbox_url=f"https://{domain}/users/{lp.preferred_username}/inbox", + preferred_username=lp.preferred_username, + domain=domain, + display_name=lp.display_name, + summary=lp.summary, + icon_url=None, + )) + + # Prepend WebFinger result (deduped) + if webfinger_result: + existing_urls = {r.actor_url for r in results} + if webfinger_result.actor_url not in existing_urls: + results.insert(0, webfinger_result) + total += 1 + + return results, total + + # -- Following (outbound) ------------------------------------------------- + + async def send_follow( + self, session: AsyncSession, local_username: str, remote_actor_url: str, + ) -> None: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == local_username) + ) + ).scalar_one_or_none() + if not actor: + raise ValueError(f"Actor not found: {local_username}") + + # Get or fetch remote actor + remote_dto = await self.get_or_fetch_remote_actor(session, remote_actor_url) + if not remote_dto: + raise ValueError(f"Could not resolve remote actor: {remote_actor_url}") + + remote = ( + await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == remote_actor_url) + ) + ).scalar_one() + + # Check for existing follow + existing = ( + await session.execute( + select(APFollowing).where( + APFollowing.actor_profile_id == actor.id, + APFollowing.remote_actor_id == remote.id, + ) + ) + ).scalar_one_or_none() + + if existing: + return # already following or pending + + follow = APFollowing( + actor_profile_id=actor.id, + remote_actor_id=remote.id, + state="pending", + ) + session.add(follow) + await session.flush() + + # Send Follow activity + domain = _domain() + actor_url = f"https://{domain}/users/{local_username}" + follow_id = f"{actor_url}/activities/{uuid.uuid4()}" + + follow_activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": follow_id, + "type": "Follow", + "actor": actor_url, + "object": remote_actor_url, + } + + import json + import httpx + from shared.utils.http_signatures import sign_request + from urllib.parse import urlparse + + body_bytes = json.dumps(follow_activity).encode() + parsed = urlparse(remote.inbox_url) + headers = sign_request( + private_key_pem=actor.private_key_pem, + key_id=f"{actor_url}#main-key", + method="POST", + path=parsed.path, + host=parsed.netloc, + body=body_bytes, + ) + headers["Content-Type"] = "application/activity+json" + + try: + async with httpx.AsyncClient(timeout=15) as client: + await client.post(remote.inbox_url, content=body_bytes, headers=headers) + except Exception: + import logging + logging.getLogger(__name__).exception("Failed to send Follow to %s", remote.inbox_url) + + async def get_following( + self, session: AsyncSession, username: str, + page: int = 1, per_page: int = 20, + ) -> tuple[list[RemoteActorDTO], int]: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == username) + ) + ).scalar_one_or_none() + if not actor: + return [], 0 + + total = ( + await session.execute( + select(func.count(APFollowing.id)).where( + APFollowing.actor_profile_id == actor.id, + APFollowing.state == "accepted", + ) + ) + ).scalar() or 0 + + offset = (page - 1) * per_page + result = await session.execute( + select(RemoteActor) + .join(APFollowing, APFollowing.remote_actor_id == RemoteActor.id) + .where( + APFollowing.actor_profile_id == actor.id, + APFollowing.state == "accepted", + ) + .order_by(APFollowing.accepted_at.desc()) + .limit(per_page) + .offset(offset) + ) + return [_remote_actor_to_dto(r) for r in result.scalars().all()], total + + async def accept_follow_response( + self, session: AsyncSession, local_username: str, remote_actor_url: str, + ) -> None: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == local_username) + ) + ).scalar_one_or_none() + if not actor: + return + + remote = ( + await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == remote_actor_url) + ) + ).scalar_one_or_none() + if not remote: + return + + follow = ( + await session.execute( + select(APFollowing).where( + APFollowing.actor_profile_id == actor.id, + APFollowing.remote_actor_id == remote.id, + APFollowing.state == "pending", + ) + ) + ).scalar_one_or_none() + if follow: + follow.state = "accepted" + follow.accepted_at = datetime.now(timezone.utc) + await session.flush() + + async def unfollow( + self, session: AsyncSession, local_username: str, remote_actor_url: str, + ) -> None: + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.preferred_username == local_username) + ) + ).scalar_one_or_none() + if not actor: + return + + remote = ( + await session.execute( + select(RemoteActor).where(RemoteActor.actor_url == remote_actor_url) + ) + ).scalar_one_or_none() + if not remote: + return + + follow = ( + await session.execute( + select(APFollowing).where( + APFollowing.actor_profile_id == actor.id, + APFollowing.remote_actor_id == remote.id, + ) + ) + ).scalar_one_or_none() + if not follow: + return + + await session.delete(follow) + await session.flush() + + # Send Undo(Follow) to remote + domain = _domain() + actor_url = f"https://{domain}/users/{local_username}" + undo_id = f"{actor_url}/activities/{uuid.uuid4()}" + + undo_activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": undo_id, + "type": "Undo", + "actor": actor_url, + "object": { + "type": "Follow", + "actor": actor_url, + "object": remote_actor_url, + }, + } + + import json + import httpx + from shared.utils.http_signatures import sign_request + from urllib.parse import urlparse + + body_bytes = json.dumps(undo_activity).encode() + parsed = urlparse(remote.inbox_url) + headers = sign_request( + private_key_pem=actor.private_key_pem, + key_id=f"{actor_url}#main-key", + method="POST", + path=parsed.path, + host=parsed.netloc, + body=body_bytes, + ) + headers["Content-Type"] = "application/activity+json" + + try: + async with httpx.AsyncClient(timeout=15) as client: + await client.post(remote.inbox_url, content=body_bytes, headers=headers) + except Exception: + import logging + logging.getLogger(__name__).exception("Failed to send Undo Follow to %s", remote.inbox_url) + + # -- Remote posts --------------------------------------------------------- + + async def ingest_remote_post( + self, session: AsyncSession, remote_actor_id: int, + activity_json: dict, object_json: dict, + ) -> None: + activity_id_str = activity_json.get("id", "") + object_id_str = object_json.get("id", "") + if not object_id_str: + return + + # Upsert + existing = ( + await session.execute( + select(APRemotePost).where(APRemotePost.object_id == object_id_str) + ) + ).scalar_one_or_none() + + published = None + pub_str = object_json.get("published") + if pub_str: + try: + published = datetime.fromisoformat(pub_str.replace("Z", "+00:00")) + except (ValueError, AttributeError): + pass + + # Sanitise HTML content + content = object_json.get("content", "") + + if existing: + existing.content = content + existing.summary = object_json.get("summary") + existing.url = object_json.get("url") + existing.attachment_data = object_json.get("attachment") + existing.tag_data = object_json.get("tag") + existing.in_reply_to = object_json.get("inReplyTo") + existing.conversation = object_json.get("conversation") + existing.published = published or existing.published + existing.fetched_at = datetime.now(timezone.utc) + await session.flush() + return + + post = APRemotePost( + remote_actor_id=remote_actor_id, + activity_id=activity_id_str, + object_id=object_id_str, + object_type=object_json.get("type", "Note"), + content=content, + summary=object_json.get("summary"), + url=object_json.get("url"), + attachment_data=object_json.get("attachment"), + tag_data=object_json.get("tag"), + in_reply_to=object_json.get("inReplyTo"), + conversation=object_json.get("conversation"), + published=published, + ) + session.add(post) + await session.flush() + + async def delete_remote_post( + self, session: AsyncSession, object_id: str, + ) -> None: + await session.execute( + delete(APRemotePost).where(APRemotePost.object_id == object_id) + ) + + async def get_remote_post( + self, session: AsyncSession, object_id: str, + ) -> RemotePostDTO | None: + post = ( + await session.execute( + select(APRemotePost).where(APRemotePost.object_id == object_id) + ) + ).scalar_one_or_none() + if not post: + return None + + actor = ( + await session.execute( + select(RemoteActor).where(RemoteActor.id == post.remote_actor_id) + ) + ).scalar_one_or_none() + + return _remote_post_to_dto(post, actor) + + # -- Timelines ------------------------------------------------------------ + + async def get_home_timeline( + self, session: AsyncSession, actor_profile_id: int, + before: datetime | None = None, limit: int = 20, + ) -> list[TimelineItemDTO]: + from sqlalchemy import union_all, literal_column, cast, String as SaString + from sqlalchemy.orm import aliased + + # Query 1: Remote posts from followed actors + following_subq = ( + select(APFollowing.remote_actor_id) + .where( + APFollowing.actor_profile_id == actor_profile_id, + APFollowing.state == "accepted", + ) + .subquery() + ) + + remote_q = ( + select( + APRemotePost.id.label("post_id"), + literal_column("'remote'").label("post_type"), + APRemotePost.content.label("content"), + APRemotePost.summary.label("summary"), + APRemotePost.url.label("url"), + APRemotePost.published.label("published"), + APRemotePost.object_id.label("object_id"), + RemoteActor.display_name.label("actor_name"), + RemoteActor.preferred_username.label("actor_username"), + RemoteActor.domain.label("actor_domain"), + RemoteActor.icon_url.label("actor_icon"), + RemoteActor.actor_url.label("actor_url"), + RemoteActor.inbox_url.label("author_inbox"), + ) + .join(RemoteActor, RemoteActor.id == APRemotePost.remote_actor_id) + .where(APRemotePost.remote_actor_id.in_(following_subq)) + ) + if before: + remote_q = remote_q.where(APRemotePost.published < before) + + # Query 2: Local activities (Create) by this actor + local_q = ( + select( + APActivity.id.label("post_id"), + literal_column("'local'").label("post_type"), + func.coalesce( + APActivity.object_data.op("->>")("content"), + literal_column("''"), + ).label("content"), + APActivity.object_data.op("->>")("summary").label("summary"), + APActivity.object_data.op("->>")("url").label("url"), + APActivity.published.label("published"), + APActivity.activity_id.label("object_id"), + func.coalesce( + ActorProfile.display_name, + ActorProfile.preferred_username, + ).label("actor_name"), + ActorProfile.preferred_username.label("actor_username"), + literal_column("NULL").label("actor_domain"), + literal_column("NULL").label("actor_icon"), + literal_column("NULL").label("actor_url"), + literal_column("NULL").label("author_inbox"), + ) + .join(ActorProfile, ActorProfile.id == APActivity.actor_profile_id) + .where( + APActivity.actor_profile_id == actor_profile_id, + APActivity.is_local == True, # noqa: E712 + APActivity.activity_type == "Create", + ) + ) + if before: + local_q = local_q.where(APActivity.published < before) + + # Union and sort + combined = union_all(remote_q, local_q).subquery() + result = await session.execute( + select(combined) + .order_by(combined.c.published.desc()) + .limit(limit) + ) + + items = [] + for row in result.mappings().all(): + # Look up interaction counts + user state + object_id = row["object_id"] + like_count = 0 + boost_count = 0 + liked_by_me = False + boosted_by_me = False + + if object_id: + post_type_val = row["post_type"] + post_id_val = row["post_id"] + + like_count = (await session.execute( + select(func.count(APInteraction.id)).where( + APInteraction.post_type == post_type_val, + APInteraction.post_id == post_id_val, + APInteraction.interaction_type == "like", + ) + )).scalar() or 0 + boost_count = (await session.execute( + select(func.count(APInteraction.id)).where( + APInteraction.post_type == post_type_val, + APInteraction.post_id == post_id_val, + APInteraction.interaction_type == "boost", + ) + )).scalar() or 0 + liked_by_me = bool((await session.execute( + select(APInteraction.id).where( + APInteraction.actor_profile_id == actor_profile_id, + APInteraction.post_type == post_type_val, + APInteraction.post_id == post_id_val, + APInteraction.interaction_type == "like", + ).limit(1) + )).scalar()) + boosted_by_me = bool((await session.execute( + select(APInteraction.id).where( + APInteraction.actor_profile_id == actor_profile_id, + APInteraction.post_type == post_type_val, + APInteraction.post_id == post_id_val, + APInteraction.interaction_type == "boost", + ).limit(1) + )).scalar()) + + items.append(TimelineItemDTO( + id=f"{row['post_type']}:{row['post_id']}", + post_type=row["post_type"], + content=row["content"] or "", + published=row["published"], + actor_name=row["actor_name"] or row["actor_username"] or "", + actor_username=row["actor_username"] or "", + object_id=object_id, + summary=row["summary"], + url=row["url"], + actor_domain=row["actor_domain"], + actor_icon=row["actor_icon"], + actor_url=row["actor_url"], + like_count=like_count, + boost_count=boost_count, + liked_by_me=liked_by_me, + boosted_by_me=boosted_by_me, + author_inbox=row["author_inbox"], + )) + return items + + async def get_public_timeline( + self, session: AsyncSession, + before: datetime | None = None, limit: int = 20, + ) -> list[TimelineItemDTO]: + # Public timeline: all local Create activities + q = ( + select(APActivity, ActorProfile) + .join(ActorProfile, ActorProfile.id == APActivity.actor_profile_id) + .where( + APActivity.is_local == True, # noqa: E712 + APActivity.activity_type == "Create", + ) + ) + if before: + q = q.where(APActivity.published < before) + q = q.order_by(APActivity.published.desc()).limit(limit) + + result = await session.execute(q) + items = [] + for activity, actor in result.all(): + content = "" + summary = None + url = None + if activity.object_data: + content = activity.object_data.get("content", "") + summary = activity.object_data.get("summary") + url = activity.object_data.get("url") + + items.append(TimelineItemDTO( + id=f"local:{activity.id}", + post_type="local", + content=content, + published=activity.published, + actor_name=actor.display_name or actor.preferred_username, + actor_username=actor.preferred_username, + object_id=activity.activity_id, + summary=summary, + url=url, + )) + return items + + async def get_actor_timeline( + self, session: AsyncSession, remote_actor_id: int, + before: datetime | None = None, limit: int = 20, + ) -> list[TimelineItemDTO]: + remote_actor = ( + await session.execute( + select(RemoteActor).where(RemoteActor.id == remote_actor_id) + ) + ).scalar_one_or_none() + if not remote_actor: + return [] + + q = ( + select(APRemotePost) + .where(APRemotePost.remote_actor_id == remote_actor_id) + ) + if before: + q = q.where(APRemotePost.published < before) + q = q.order_by(APRemotePost.published.desc()).limit(limit) + + posts = (await session.execute(q)).scalars().all() + return [ + TimelineItemDTO( + id=f"remote:{p.id}", + post_type="remote", + content=p.content or "", + published=p.published, + actor_name=remote_actor.display_name or remote_actor.preferred_username, + actor_username=remote_actor.preferred_username, + object_id=p.object_id, + summary=p.summary, + url=p.url, + actor_domain=remote_actor.domain, + actor_icon=remote_actor.icon_url, + actor_url=remote_actor.actor_url, + author_inbox=remote_actor.inbox_url, + ) + for p in posts + ] + + # -- Local posts ---------------------------------------------------------- + + async def create_local_post( + self, session: AsyncSession, actor_profile_id: int, + content: str, visibility: str = "public", + in_reply_to: str | None = None, + ) -> int: + now = datetime.now(timezone.utc) + post = APLocalPost( + actor_profile_id=actor_profile_id, + content=content, + visibility=visibility, + in_reply_to=in_reply_to, + published=now, + ) + session.add(post) + await session.flush() + + # Get actor for publishing + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == actor_profile_id) + ) + ).scalar_one() + + domain = _domain() + username = actor.preferred_username + + # Convert content to simple HTML + import html as html_mod + html_content = "".join( + f"

    {html_mod.escape(line)}

    " if line.strip() else "" + for line in content.split("\n") + ) + + object_id = f"https://{domain}/users/{username}/posts/{post.id}" + object_data = { + "id": object_id, + "type": "Note", + "content": html_content, + "url": object_id, + "attributedTo": f"https://{domain}/users/{username}", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": [f"https://{domain}/users/{username}/followers"], + "published": now.isoformat(), + } + if in_reply_to: + object_data["inReplyTo"] = in_reply_to + + # Publish via existing activity system + await self.publish_activity( + session, + actor_user_id=actor.user_id, + activity_type="Create", + object_type="Note", + object_data=object_data, + source_type="local_post", + source_id=post.id, + ) + + return post.id + + async def delete_local_post( + self, session: AsyncSession, actor_profile_id: int, post_id: int, + ) -> None: + post = ( + await session.execute( + select(APLocalPost).where( + APLocalPost.id == post_id, + APLocalPost.actor_profile_id == actor_profile_id, + ) + ) + ).scalar_one_or_none() + if not post: + return + + # Get actor + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == actor_profile_id) + ) + ).scalar_one() + + domain = _domain() + object_id = f"https://{domain}/users/{actor.preferred_username}/posts/{post.id}" + + # Publish Delete activity + await self.publish_activity( + session, + actor_user_id=actor.user_id, + activity_type="Delete", + object_type="Note", + object_data={"id": object_id}, + source_type="local_post", + source_id=post.id, + ) + + await session.delete(post) + await session.flush() + + # -- Interactions --------------------------------------------------------- + + async def like_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: + # Determine post type and id + post_type, post_id = await self._resolve_post(session, object_id) + if not post_type: + return + + # Check for existing + existing = ( + await session.execute( + select(APInteraction).where( + APInteraction.actor_profile_id == actor_profile_id, + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "like", + ) + ) + ).scalar_one_or_none() + if existing: + return + + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == actor_profile_id) + ) + ).scalar_one() + + domain = _domain() + actor_url = f"https://{domain}/users/{actor.preferred_username}" + like_id = f"{actor_url}/activities/{uuid.uuid4()}" + + interaction = APInteraction( + actor_profile_id=actor_profile_id, + post_type=post_type, + post_id=post_id, + interaction_type="like", + activity_id=like_id, + ) + session.add(interaction) + await session.flush() + + # Send Like to author + if author_inbox: + await self._send_activity_to_inbox( + actor, { + "@context": "https://www.w3.org/ns/activitystreams", + "id": like_id, + "type": "Like", + "actor": actor_url, + "object": object_id, + }, author_inbox, + ) + + async def unlike_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: + post_type, post_id = await self._resolve_post(session, object_id) + if not post_type: + return + + interaction = ( + await session.execute( + select(APInteraction).where( + APInteraction.actor_profile_id == actor_profile_id, + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "like", + ) + ) + ).scalar_one_or_none() + if not interaction: + return + + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == actor_profile_id) + ) + ).scalar_one() + + domain = _domain() + actor_url = f"https://{domain}/users/{actor.preferred_username}" + + # Send Undo(Like) + if author_inbox and interaction.activity_id: + await self._send_activity_to_inbox( + actor, { + "@context": "https://www.w3.org/ns/activitystreams", + "id": f"{actor_url}/activities/{uuid.uuid4()}", + "type": "Undo", + "actor": actor_url, + "object": { + "id": interaction.activity_id, + "type": "Like", + "actor": actor_url, + "object": object_id, + }, + }, author_inbox, + ) + + await session.delete(interaction) + await session.flush() + + async def boost_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: + post_type, post_id = await self._resolve_post(session, object_id) + if not post_type: + return + + existing = ( + await session.execute( + select(APInteraction).where( + APInteraction.actor_profile_id == actor_profile_id, + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "boost", + ) + ) + ).scalar_one_or_none() + if existing: + return + + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == actor_profile_id) + ) + ).scalar_one() + + domain = _domain() + actor_url = f"https://{domain}/users/{actor.preferred_username}" + announce_id = f"{actor_url}/activities/{uuid.uuid4()}" + + interaction = APInteraction( + actor_profile_id=actor_profile_id, + post_type=post_type, + post_id=post_id, + interaction_type="boost", + activity_id=announce_id, + ) + session.add(interaction) + await session.flush() + + # Send Announce to author and deliver to followers via publish_activity + if author_inbox: + announce_activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": announce_id, + "type": "Announce", + "actor": actor_url, + "object": object_id, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": [f"{actor_url}/followers"], + } + await self._send_activity_to_inbox(actor, announce_activity, author_inbox) + + # Also publish as our own activity for delivery to our followers + await self.publish_activity( + session, + actor_user_id=actor.user_id, + activity_type="Announce", + object_type="Note", + object_data={"id": object_id}, + ) + + async def unboost_post( + self, session: AsyncSession, actor_profile_id: int, + object_id: str, author_inbox: str, + ) -> None: + post_type, post_id = await self._resolve_post(session, object_id) + if not post_type: + return + + interaction = ( + await session.execute( + select(APInteraction).where( + APInteraction.actor_profile_id == actor_profile_id, + APInteraction.post_type == post_type, + APInteraction.post_id == post_id, + APInteraction.interaction_type == "boost", + ) + ) + ).scalar_one_or_none() + if not interaction: + return + + actor = ( + await session.execute( + select(ActorProfile).where(ActorProfile.id == actor_profile_id) + ) + ).scalar_one() + + domain = _domain() + actor_url = f"https://{domain}/users/{actor.preferred_username}" + + if author_inbox and interaction.activity_id: + await self._send_activity_to_inbox( + actor, { + "@context": "https://www.w3.org/ns/activitystreams", + "id": f"{actor_url}/activities/{uuid.uuid4()}", + "type": "Undo", + "actor": actor_url, + "object": { + "id": interaction.activity_id, + "type": "Announce", + "actor": actor_url, + "object": object_id, + }, + }, author_inbox, + ) + + await session.delete(interaction) + await session.flush() + + async def _resolve_post( + self, session: AsyncSession, object_id: str, + ) -> tuple[str | None, int | None]: + """Resolve an AP object_id to (post_type, post_id).""" + # Check remote posts + remote = ( + await session.execute( + select(APRemotePost.id).where(APRemotePost.object_id == object_id).limit(1) + ) + ).scalar() + if remote: + return "remote", remote + + # Check local activities + local = ( + await session.execute( + select(APActivity.id).where(APActivity.activity_id == object_id).limit(1) + ) + ).scalar() + if local: + return "local", local + + return None, None + + async def _send_activity_to_inbox( + self, actor: ActorProfile, activity: dict, inbox_url: str, + ) -> None: + import json + import httpx + from shared.utils.http_signatures import sign_request + from urllib.parse import urlparse + + domain = _domain() + actor_url = f"https://{domain}/users/{actor.preferred_username}" + + body_bytes = json.dumps(activity).encode() + parsed = urlparse(inbox_url) + headers = sign_request( + private_key_pem=actor.private_key_pem, + key_id=f"{actor_url}#main-key", + method="POST", + path=parsed.path, + host=parsed.netloc, + body=body_bytes, + ) + headers["Content-Type"] = "application/activity+json" + + try: + async with httpx.AsyncClient(timeout=15) as client: + await client.post(inbox_url, content=body_bytes, headers=headers) + except Exception: + import logging + logging.getLogger(__name__).exception( + "Failed to deliver activity to %s", inbox_url, + ) + + # -- Notifications -------------------------------------------------------- + + async def get_notifications( + self, session: AsyncSession, actor_profile_id: int, + before: datetime | None = None, limit: int = 20, + ) -> list[NotificationDTO]: + q = ( + select(APNotification, RemoteActor, ActorProfile) + .outerjoin(RemoteActor, RemoteActor.id == APNotification.from_remote_actor_id) + .outerjoin( + ActorProfile, + ActorProfile.id == APNotification.from_actor_profile_id, + ) + .where(APNotification.actor_profile_id == actor_profile_id) + ) + if before: + q = q.where(APNotification.created_at < before) + q = q.order_by(APNotification.created_at.desc()).limit(limit) + + result = await session.execute(q) + items = [] + for notif, remote_actor, from_actor_profile in result.all(): + if remote_actor: + name = remote_actor.display_name or remote_actor.preferred_username + username = remote_actor.preferred_username + domain = remote_actor.domain + icon = remote_actor.icon_url + elif from_actor_profile: + name = from_actor_profile.display_name or from_actor_profile.preferred_username + username = from_actor_profile.preferred_username + domain = None + icon = None + else: + name = "Unknown" + username = "unknown" + domain = None + icon = None + + # Get preview if target exists + preview = None + if notif.target_activity_id: + act = (await session.execute( + select(APActivity).where(APActivity.id == notif.target_activity_id) + )).scalar_one_or_none() + if act and act.object_data: + content = act.object_data.get("content", "") + # Strip HTML tags for preview + import re + preview = re.sub(r"<[^>]+>", "", content)[:100] + elif notif.target_remote_post_id: + rp = (await session.execute( + select(APRemotePost).where(APRemotePost.id == notif.target_remote_post_id) + )).scalar_one_or_none() + if rp and rp.content: + import re + preview = re.sub(r"<[^>]+>", "", rp.content)[:100] + + items.append(NotificationDTO( + id=notif.id, + notification_type=notif.notification_type, + from_actor_name=name, + from_actor_username=username, + from_actor_domain=domain, + from_actor_icon=icon, + target_content_preview=preview, + created_at=notif.created_at, + read=notif.read, + )) + return items + + async def unread_notification_count( + self, session: AsyncSession, actor_profile_id: int, + ) -> int: + return ( + await session.execute( + select(func.count(APNotification.id)).where( + APNotification.actor_profile_id == actor_profile_id, + APNotification.read == False, # noqa: E712 + ) + ) + ).scalar() or 0 + + async def mark_notifications_read( + self, session: AsyncSession, actor_profile_id: int, + ) -> None: + from sqlalchemy import update + await session.execute( + update(APNotification) + .where( + APNotification.actor_profile_id == actor_profile_id, + APNotification.read == False, # noqa: E712 + ) + .values(read=True) + ) + + # -- Stats ---------------------------------------------------------------- + + async def get_stats(self, session: AsyncSession) -> dict: + actors = (await session.execute(select(func.count(ActorProfile.id)))).scalar() or 0 + activities = (await session.execute(select(func.count(APActivity.id)))).scalar() or 0 + followers = (await session.execute(select(func.count(APFollower.id)))).scalar() or 0 + return {"actors": actors, "activities": activities, "followers": followers} diff --git a/shared/services/federation_publish.py b/shared/services/federation_publish.py new file mode 100644 index 0000000..fb26ea0 --- /dev/null +++ b/shared/services/federation_publish.py @@ -0,0 +1,92 @@ +"""Inline federation publication — called at write time, not via async handler. + +The originating service calls try_publish() directly, which creates the +APActivity (with process_state='pending') in the same DB transaction. +The EventProcessor picks it up and the delivery wildcard handler POSTs +to follower inboxes. +""" +from __future__ import annotations + +import logging +import os + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.services.registry import services + +log = logging.getLogger(__name__) + + +async def try_publish( + session: AsyncSession, + *, + user_id: int | None, + activity_type: str, + object_type: str, + object_data: dict, + source_type: str, + source_id: int, +) -> None: + """Publish an AP activity if federation is available and user has a profile. + + Safe to call from any app — returns silently if federation isn't wired + or the user has no actor profile. + """ + if not services.has("federation"): + return + + if not user_id: + return + + actor = await services.federation.get_actor_by_user_id(session, user_id) + if not actor: + return + + # Dedup: don't re-Create if already published, don't re-Delete if already deleted + existing = await services.federation.get_activity_for_source( + session, source_type, source_id, + ) + if existing: + if activity_type == "Create" and existing.activity_type != "Delete": + return # already published (allow re-Create after Delete/unpublish) + if activity_type == "Delete" and existing.activity_type == "Delete": + return # already deleted + elif activity_type in ("Delete", "Update"): + return # never published, nothing to delete/update + + # Stable object ID within a publish cycle. After Delete + re-Create + # we append a version suffix so remote servers (Mastodon) treat it as + # a brand-new post rather than ignoring the tombstoned ID. + domain = os.getenv("AP_DOMAIN", "federation.rose-ash.com") + base_object_id = ( + f"https://{domain}/users/{actor.preferred_username}" + f"/objects/{source_type.lower()}/{source_id}" + ) + if activity_type == "Create" and existing and existing.activity_type == "Delete": + # Count prior Creates to derive a version number + create_count = await services.federation.count_activities_for_source( + session, source_type, source_id, activity_type="Create", + ) + object_data["id"] = f"{base_object_id}/v{create_count + 1}" + elif activity_type in ("Update", "Delete") and existing and existing.object_data: + # Use the same object ID as the most recent activity + object_data["id"] = existing.object_data.get("id", base_object_id) + else: + object_data["id"] = base_object_id + + try: + await services.federation.publish_activity( + session, + actor_user_id=user_id, + activity_type=activity_type, + object_type=object_type, + object_data=object_data, + source_type=source_type, + source_id=source_id, + ) + log.info( + "Published %s/%s for %s#%d by user %d", + activity_type, object_type, source_type, source_id, user_id, + ) + except Exception: + log.exception("Failed to publish activity for %s#%d", source_type, source_id) diff --git a/shared/services/market_impl.py b/shared/services/market_impl.py new file mode 100644 index 0000000..71f8771 --- /dev/null +++ b/shared/services/market_impl.py @@ -0,0 +1,128 @@ +"""SQL-backed MarketService implementation. + +Queries ``shared.models.market.*`` and ``shared.models.market_place.*`` — +only this module may read market-domain tables on behalf of other domains. +""" +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.market import Product +from shared.models.market_place import MarketPlace +from shared.browser.app.utils import utcnow +from shared.contracts.dtos import MarketPlaceDTO, ProductDTO +from shared.services.relationships import attach_child, detach_child + + +def _mp_to_dto(mp: MarketPlace) -> MarketPlaceDTO: + return MarketPlaceDTO( + id=mp.id, + container_type=mp.container_type, + container_id=mp.container_id, + name=mp.name, + slug=mp.slug, + description=mp.description, + ) + + +def _product_to_dto(p: Product) -> ProductDTO: + return ProductDTO( + id=p.id, + slug=p.slug, + title=p.title, + image=p.image, + description_short=p.description_short, + rrp=p.rrp, + regular_price=p.regular_price, + special_price=p.special_price, + ) + + +class SqlMarketService: + async def marketplaces_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + ) -> list[MarketPlaceDTO]: + result = await session.execute( + select(MarketPlace).where( + MarketPlace.container_type == container_type, + MarketPlace.container_id == container_id, + MarketPlace.deleted_at.is_(None), + ).order_by(MarketPlace.name.asc()) + ) + return [_mp_to_dto(mp) for mp in result.scalars().all()] + + async def list_marketplaces( + self, session: AsyncSession, + container_type: str | None = None, container_id: int | None = None, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[MarketPlaceDTO], bool]: + stmt = select(MarketPlace).where(MarketPlace.deleted_at.is_(None)) + if container_type is not None and container_id is not None: + stmt = stmt.where( + MarketPlace.container_type == container_type, + MarketPlace.container_id == container_id, + ) + stmt = stmt.order_by(MarketPlace.name.asc()) + stmt = stmt.offset((page - 1) * per_page).limit(per_page + 1) + rows = (await session.execute(stmt)).scalars().all() + has_more = len(rows) > per_page + return [_mp_to_dto(mp) for mp in rows[:per_page]], has_more + + async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None: + product = ( + await session.execute(select(Product).where(Product.id == product_id)) + ).scalar_one_or_none() + return _product_to_dto(product) if product else None + + async def create_marketplace( + self, session: AsyncSession, container_type: str, container_id: int, + name: str, slug: str, + ) -> MarketPlaceDTO: + # Look for existing (including soft-deleted) + existing = (await session.execute( + select(MarketPlace).where( + MarketPlace.container_type == container_type, + MarketPlace.container_id == container_id, + MarketPlace.slug == slug, + ) + )).scalar_one_or_none() + + if existing: + if existing.deleted_at is not None: + existing.deleted_at = None # revive + existing.name = name + await session.flush() + await attach_child(session, container_type, container_id, "market", existing.id) + return _mp_to_dto(existing) + raise ValueError(f'Market with slug "{slug}" already exists for this container.') + + market = MarketPlace( + container_type=container_type, container_id=container_id, + name=name, slug=slug, + ) + session.add(market) + await session.flush() + await attach_child(session, container_type, container_id, "market", market.id) + return _mp_to_dto(market) + + async def soft_delete_marketplace( + self, session: AsyncSession, container_type: str, container_id: int, + slug: str, + ) -> bool: + market = (await session.execute( + select(MarketPlace).where( + MarketPlace.container_type == container_type, + MarketPlace.container_id == container_id, + MarketPlace.slug == slug, + MarketPlace.deleted_at.is_(None), + ) + )).scalar_one_or_none() + + if not market: + return False + + market.deleted_at = utcnow() + await session.flush() + await detach_child(session, container_type, container_id, "market", market.id) + return True diff --git a/shared/services/navigation.py b/shared/services/navigation.py new file mode 100644 index 0000000..d9e15c5 --- /dev/null +++ b/shared/services/navigation.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.models.menu_node import MenuNode + + +async def get_navigation_tree(session: AsyncSession) -> list[MenuNode]: + """ + Return top-level menu nodes ordered by sort_order. + + All apps call this directly (shared DB) — no more HTTP API. + """ + result = await session.execute( + select(MenuNode) + .where(MenuNode.deleted_at.is_(None), MenuNode.depth == 0) + .order_by(MenuNode.sort_order.asc(), MenuNode.id.asc()) + ) + return list(result.scalars().all()) + + +async def rebuild_navigation(session: AsyncSession) -> None: + """ + Rebuild menu_nodes from container_relations. + + Called by event handlers when relationships change. + Currently a no-op placeholder — menu nodes are managed directly + by the admin UI. When the full relationship-driven nav is needed, + this will sync ContainerRelation -> MenuNode. + """ + pass diff --git a/shared/services/registry.py b/shared/services/registry.py new file mode 100644 index 0000000..23d559b --- /dev/null +++ b/shared/services/registry.py @@ -0,0 +1,105 @@ +"""Typed singleton registry for domain services. + +Usage:: + + from shared.services.registry import services + + # Register at app startup + services.blog = SqlBlogService() + + # Query anywhere + if services.has("calendar"): + entries = await services.calendar.pending_entries(session, ...) + + # Or use stubs for absent domains + summary = await services.cart.cart_summary(session, ...) +""" +from __future__ import annotations + +from shared.contracts.protocols import ( + BlogService, + CalendarService, + MarketService, + CartService, + FederationService, +) + + +class _ServiceRegistry: + """Central registry holding one implementation per domain. + + Properties return the registered implementation or raise + ``RuntimeError`` if nothing is registered. Use ``has(name)`` + to check before access when the domain might be absent. + """ + + def __init__(self) -> None: + self._blog: BlogService | None = None + self._calendar: CalendarService | None = None + self._market: MarketService | None = None + self._cart: CartService | None = None + self._federation: FederationService | None = None + + # -- blog ----------------------------------------------------------------- + @property + def blog(self) -> BlogService: + if self._blog is None: + raise RuntimeError("BlogService not registered") + return self._blog + + @blog.setter + def blog(self, impl: BlogService) -> None: + self._blog = impl + + # -- calendar ------------------------------------------------------------- + @property + def calendar(self) -> CalendarService: + if self._calendar is None: + raise RuntimeError("CalendarService not registered") + return self._calendar + + @calendar.setter + def calendar(self, impl: CalendarService) -> None: + self._calendar = impl + + # -- market --------------------------------------------------------------- + @property + def market(self) -> MarketService: + if self._market is None: + raise RuntimeError("MarketService not registered") + return self._market + + @market.setter + def market(self, impl: MarketService) -> None: + self._market = impl + + # -- cart ----------------------------------------------------------------- + @property + def cart(self) -> CartService: + if self._cart is None: + raise RuntimeError("CartService not registered") + return self._cart + + @cart.setter + def cart(self, impl: CartService) -> None: + self._cart = impl + + # -- federation ----------------------------------------------------------- + @property + def federation(self) -> FederationService: + if self._federation is None: + raise RuntimeError("FederationService not registered") + return self._federation + + @federation.setter + def federation(self, impl: FederationService) -> None: + self._federation = impl + + # -- introspection -------------------------------------------------------- + def has(self, name: str) -> bool: + """Check whether a domain service is registered.""" + return getattr(self, f"_{name}", None) is not None + + +# Module-level singleton — import this everywhere. +services = _ServiceRegistry() diff --git a/shared/services/relationships.py b/shared/services/relationships.py new file mode 100644 index 0000000..c7ff084 --- /dev/null +++ b/shared/services/relationships.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.events import emit_activity +from shared.models.container_relation import ContainerRelation + + +async def attach_child( + session: AsyncSession, + parent_type: str, + parent_id: int, + child_type: str, + child_id: int, + label: str | None = None, + sort_order: int | None = None, +) -> ContainerRelation: + """ + Create a ContainerRelation and emit container.child_attached event. + + Upsert behaviour: if a relation already exists (including soft-deleted), + revive it instead of inserting a duplicate. + """ + # Check for existing (including soft-deleted) + existing = await session.scalar( + select(ContainerRelation).where( + ContainerRelation.parent_type == parent_type, + ContainerRelation.parent_id == parent_id, + ContainerRelation.child_type == child_type, + ContainerRelation.child_id == child_id, + ) + ) + if existing: + if existing.deleted_at is not None: + # Revive soft-deleted relation + existing.deleted_at = None + if sort_order is not None: + existing.sort_order = sort_order + if label is not None: + existing.label = label + await session.flush() + await emit_activity( + session, + activity_type="Add", + actor_uri="internal:system", + object_type="rose:ContainerRelation", + object_data={ + "parent_type": parent_type, + "parent_id": parent_id, + "child_type": child_type, + "child_id": child_id, + }, + source_type="container_relation", + source_id=existing.id, + ) + return existing + # Already attached and active — no-op + return existing + + if sort_order is None: + max_order = await session.scalar( + select(func.max(ContainerRelation.sort_order)).where( + ContainerRelation.parent_type == parent_type, + ContainerRelation.parent_id == parent_id, + ContainerRelation.deleted_at.is_(None), + ) + ) + sort_order = (max_order or 0) + 1 + + rel = ContainerRelation( + parent_type=parent_type, + parent_id=parent_id, + child_type=child_type, + child_id=child_id, + label=label, + sort_order=sort_order, + ) + session.add(rel) + await session.flush() + + await emit_activity( + session, + activity_type="Add", + actor_uri="internal:system", + object_type="rose:ContainerRelation", + object_data={ + "parent_type": parent_type, + "parent_id": parent_id, + "child_type": child_type, + "child_id": child_id, + }, + source_type="container_relation", + source_id=rel.id, + ) + + return rel + + +async def get_children( + session: AsyncSession, + parent_type: str, + parent_id: int, + child_type: str | None = None, +) -> list[ContainerRelation]: + """Query children of a container, optionally filtered by child_type.""" + stmt = select(ContainerRelation).where( + ContainerRelation.parent_type == parent_type, + ContainerRelation.parent_id == parent_id, + ContainerRelation.deleted_at.is_(None), + ) + if child_type is not None: + stmt = stmt.where(ContainerRelation.child_type == child_type) + + stmt = stmt.order_by( + ContainerRelation.sort_order.asc(), ContainerRelation.id.asc() + ) + result = await session.execute(stmt) + return list(result.scalars().all()) + + +async def detach_child( + session: AsyncSession, + parent_type: str, + parent_id: int, + child_type: str, + child_id: int, +) -> bool: + """Soft-delete a ContainerRelation and emit container.child_detached event.""" + result = await session.execute( + select(ContainerRelation).where( + ContainerRelation.parent_type == parent_type, + ContainerRelation.parent_id == parent_id, + ContainerRelation.child_type == child_type, + ContainerRelation.child_id == child_id, + ContainerRelation.deleted_at.is_(None), + ) + ) + rel = result.scalar_one_or_none() + if not rel: + return False + + rel.deleted_at = func.now() + await session.flush() + + await emit_activity( + session, + activity_type="Remove", + actor_uri="internal:system", + object_type="rose:ContainerRelation", + object_data={ + "parent_type": parent_type, + "parent_id": parent_id, + "child_type": child_type, + "child_id": child_id, + }, + source_type="container_relation", + source_id=rel.id, + ) + + return True diff --git a/shared/services/stubs.py b/shared/services/stubs.py new file mode 100644 index 0000000..eb46cca --- /dev/null +++ b/shared/services/stubs.py @@ -0,0 +1,314 @@ +"""No-op stub services for absent domains. + +When an app starts without a particular domain, it registers the stub +so that ``services.X.method()`` returns empty/None rather than crashing. +""" +from __future__ import annotations + +from decimal import Decimal + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.contracts.dtos import ( + PostDTO, + CalendarDTO, + CalendarEntryDTO, + TicketDTO, + MarketPlaceDTO, + ProductDTO, + CartItemDTO, + CartSummaryDTO, + ActorProfileDTO, + APActivityDTO, + APFollowerDTO, +) + + +class StubBlogService: + async def get_post_by_slug(self, session: AsyncSession, slug: str) -> PostDTO | None: + return None + + async def get_post_by_id(self, session: AsyncSession, id: int) -> PostDTO | None: + return None + + async def get_posts_by_ids(self, session: AsyncSession, ids: list[int]) -> list[PostDTO]: + return [] + + async def search_posts(self, session, query, page=1, per_page=10): + return [], 0 + + +class StubCalendarService: + async def calendars_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + ) -> list[CalendarDTO]: + return [] + + async def pending_entries( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[CalendarEntryDTO]: + return [] + + async def entries_for_page( + self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None, + ) -> list[CalendarEntryDTO]: + return [] + + async def entry_by_id(self, session: AsyncSession, entry_id: int) -> CalendarEntryDTO | None: + return None + + async def associated_entries( + self, session: AsyncSession, content_type: str, content_id: int, page: int, + ) -> tuple[list[CalendarEntryDTO], bool]: + return [], False + + async def toggle_entry_post( + self, session: AsyncSession, entry_id: int, content_type: str, content_id: int, + ) -> bool: + return False + + async def adopt_entries_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: + pass + + async def claim_entries_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, page_post_id: int | None, + ) -> None: + pass + + async def confirm_entries_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, + ) -> None: + pass + + async def get_entries_for_order( + self, session: AsyncSession, order_id: int, + ) -> list[CalendarEntryDTO]: + return [] + + async def user_tickets( + self, session: AsyncSession, *, user_id: int, + ) -> list[TicketDTO]: + return [] + + async def user_bookings( + self, session: AsyncSession, *, user_id: int, + ) -> list[CalendarEntryDTO]: + return [] + + async def confirmed_entries_for_posts( + self, session: AsyncSession, post_ids: list[int], + ) -> dict[int, list[CalendarEntryDTO]]: + return {} + + async def pending_tickets( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[TicketDTO]: + return [] + + async def tickets_for_page( + self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None, + ) -> list[TicketDTO]: + return [] + + async def claim_tickets_for_order( + self, session: AsyncSession, order_id: int, user_id: int | None, + session_id: str | None, page_post_id: int | None, + ) -> None: + pass + + async def confirm_tickets_for_order( + self, session: AsyncSession, order_id: int, + ) -> None: + pass + + async def get_tickets_for_order( + self, session: AsyncSession, order_id: int, + ) -> list[TicketDTO]: + return [] + + async def adopt_tickets_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: + pass + + async def adjust_ticket_quantity( + self, session, entry_id, count, *, user_id, session_id, ticket_type_id=None, + ) -> int: + return 0 + + async def upcoming_entries_for_container(self, session, container_type, container_id, *, page=1, per_page=20): + return [], False + + async def entry_ids_for_content(self, session, content_type, content_id): + return set() + + async def visible_entries_for_period(self, session, calendar_id, period_start, period_end, *, user_id, is_admin, session_id): + return [] + + +class StubMarketService: + async def marketplaces_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + ) -> list[MarketPlaceDTO]: + return [] + + async def list_marketplaces( + self, session: AsyncSession, + container_type: str | None = None, container_id: int | None = None, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[MarketPlaceDTO], bool]: + return [], False + + async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None: + return None + + async def create_marketplace( + self, session: AsyncSession, container_type: str, container_id: int, + name: str, slug: str, + ) -> MarketPlaceDTO: + raise RuntimeError("MarketService not available") + + async def soft_delete_marketplace( + self, session: AsyncSession, container_type: str, container_id: int, + slug: str, + ) -> bool: + return False + + +class StubCartService: + async def cart_summary( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + page_slug: str | None = None, + ) -> CartSummaryDTO: + return CartSummaryDTO() + + async def cart_items( + self, session: AsyncSession, *, user_id: int | None, session_id: str | None, + ) -> list[CartItemDTO]: + return [] + + async def adopt_cart_for_user( + self, session: AsyncSession, user_id: int, session_id: str, + ) -> None: + pass + + +class StubFederationService: + """No-op federation stub for apps that don't own federation.""" + + async def get_actor_by_username(self, session, username): + return None + + async def get_actor_by_user_id(self, session, user_id): + return None + + async def create_actor(self, session, user_id, preferred_username, + display_name=None, summary=None): + raise RuntimeError("FederationService not available") + + async def username_available(self, session, username): + return False + + async def publish_activity(self, session, *, actor_user_id, activity_type, + object_type, object_data, source_type=None, + source_id=None): + return None + + async def get_activity(self, session, activity_id): + return None + + async def get_outbox(self, session, username, page=1, per_page=20, origin_app=None): + return [], 0 + + async def get_activity_for_source(self, session, source_type, source_id): + return None + + async def count_activities_for_source(self, session, source_type, source_id, *, activity_type): + return 0 + + async def get_followers(self, session, username, app_domain=None): + return [] + + async def add_follower(self, session, username, follower_acct, follower_inbox, + follower_actor_url, follower_public_key=None, + app_domain="federation"): + raise RuntimeError("FederationService not available") + + async def remove_follower(self, session, username, follower_acct, app_domain="federation"): + return False + + async def get_or_fetch_remote_actor(self, session, actor_url): + return None + + async def search_remote_actor(self, session, acct): + return None + + async def search_actors(self, session, query, page=1, limit=20): + return [], 0 + + async def send_follow(self, session, local_username, remote_actor_url): + raise RuntimeError("FederationService not available") + + async def get_following(self, session, username, page=1, per_page=20): + return [], 0 + + async def get_followers_paginated(self, session, username, page=1, per_page=20): + return [], 0 + + async def accept_follow_response(self, session, local_username, remote_actor_url): + pass + + async def unfollow(self, session, local_username, remote_actor_url): + pass + + async def ingest_remote_post(self, session, remote_actor_id, activity_json, object_json): + pass + + async def delete_remote_post(self, session, object_id): + pass + + async def get_remote_post(self, session, object_id): + return None + + async def get_home_timeline(self, session, actor_profile_id, before=None, limit=20): + return [] + + async def get_public_timeline(self, session, before=None, limit=20): + return [] + + async def get_actor_timeline(self, session, remote_actor_id, before=None, limit=20): + return [] + + async def create_local_post(self, session, actor_profile_id, content, visibility="public", in_reply_to=None): + raise RuntimeError("FederationService not available") + + async def delete_local_post(self, session, actor_profile_id, post_id): + raise RuntimeError("FederationService not available") + + async def like_post(self, session, actor_profile_id, object_id, author_inbox): + pass + + async def unlike_post(self, session, actor_profile_id, object_id, author_inbox): + pass + + async def boost_post(self, session, actor_profile_id, object_id, author_inbox): + pass + + async def unboost_post(self, session, actor_profile_id, object_id, author_inbox): + pass + + async def get_notifications(self, session, actor_profile_id, before=None, limit=20): + return [] + + async def unread_notification_count(self, session, actor_profile_id): + return 0 + + async def mark_notifications_read(self, session, actor_profile_id): + pass + + async def get_stats(self, session): + return {"actors": 0, "activities": 0, "followers": 0} diff --git a/shared/services/widget_registry.py b/shared/services/widget_registry.py new file mode 100644 index 0000000..f43d8e5 --- /dev/null +++ b/shared/services/widget_registry.py @@ -0,0 +1,90 @@ +"""Singleton widget registry for cross-domain UI composition. + +Usage:: + + from shared.services.widget_registry import widgets + + # Register at app startup (after domain services) + widgets.add_container_nav(NavWidget(...)) + + # Query in templates / context processors + for w in widgets.container_nav: + ctx = await w.context_fn(session, container_type="page", ...) +""" +from __future__ import annotations + +from shared.contracts.widgets import ( + NavWidget, + CardWidget, + AccountPageWidget, + AccountNavLink, +) + + +class _WidgetRegistry: + """Central registry holding all widget descriptors. + + Widgets are added at startup and read at request time. + Properties return sorted-by-order copies. + """ + + def __init__(self) -> None: + self._container_nav: list[NavWidget] = [] + self._container_card: list[CardWidget] = [] + self._account_pages: list[AccountPageWidget] = [] + self._account_nav: list[AccountNavLink] = [] + + # -- registration --------------------------------------------------------- + + def add_container_nav(self, w: NavWidget) -> None: + self._container_nav.append(w) + + def add_container_card(self, w: CardWidget) -> None: + self._container_card.append(w) + + def add_account_page(self, w: AccountPageWidget) -> None: + self._account_pages.append(w) + # Auto-create a matching internal nav link + slug = w.slug + + def _href(s=slug): + from shared.infrastructure.urls import account_url + return account_url(f"/{s}/") + + self._account_nav.append(AccountNavLink( + label=w.label, + order=w.order, + href_fn=_href, + external=False, + )) + + def add_account_link(self, link: AccountNavLink) -> None: + self._account_nav.append(link) + + # -- read access (sorted copies) ------------------------------------------ + + @property + def container_nav(self) -> list[NavWidget]: + return sorted(self._container_nav, key=lambda w: w.order) + + @property + def container_cards(self) -> list[CardWidget]: + return sorted(self._container_card, key=lambda w: w.order) + + @property + def account_pages(self) -> list[AccountPageWidget]: + return sorted(self._account_pages, key=lambda w: w.order) + + @property + def account_nav(self) -> list[AccountNavLink]: + return sorted(self._account_nav, key=lambda w: w.order) + + def account_page_by_slug(self, slug: str) -> AccountPageWidget | None: + for w in self._account_pages: + if w.slug == slug: + return w + return None + + +# Module-level singleton — import this everywhere. +widgets = _WidgetRegistry() diff --git a/shared/services/widgets/__init__.py b/shared/services/widgets/__init__.py new file mode 100644 index 0000000..d063a76 --- /dev/null +++ b/shared/services/widgets/__init__.py @@ -0,0 +1,22 @@ +"""Per-domain widget registration. + +Called once at startup after domain services are registered. +Only registers widgets for domains that are actually available. +""" +from __future__ import annotations + + +def register_all_widgets() -> None: + from shared.services.registry import services + + if services.has("calendar"): + from .calendar_widgets import register_calendar_widgets + register_calendar_widgets() + + if services.has("market"): + from .market_widgets import register_market_widgets + register_market_widgets() + + if services.has("cart"): + from .cart_widgets import register_cart_widgets + register_cart_widgets() diff --git a/shared/services/widgets/calendar_widgets.py b/shared/services/widgets/calendar_widgets.py new file mode 100644 index 0000000..d9fc2ff --- /dev/null +++ b/shared/services/widgets/calendar_widgets.py @@ -0,0 +1,10 @@ +"""Calendar-domain widgets. + +All calendar widgets have been replaced by fragments +(events app serves them at /internal/fragments/). +""" +from __future__ import annotations + + +def register_calendar_widgets() -> None: + pass diff --git a/shared/services/widgets/cart_widgets.py b/shared/services/widgets/cart_widgets.py new file mode 100644 index 0000000..a45ab90 --- /dev/null +++ b/shared/services/widgets/cart_widgets.py @@ -0,0 +1,10 @@ +"""Cart-domain widgets. + +Account nav link has been replaced by fragments +(cart app serves account-nav-item at /internal/fragments/). +""" +from __future__ import annotations + + +def register_cart_widgets() -> None: + pass diff --git a/shared/services/widgets/market_widgets.py b/shared/services/widgets/market_widgets.py new file mode 100644 index 0000000..480d42c --- /dev/null +++ b/shared/services/widgets/market_widgets.py @@ -0,0 +1,10 @@ +"""Market-domain widgets. + +Container nav widgets have been replaced by fragments +(market app serves them at /internal/fragments/). +""" +from __future__ import annotations + + +def register_market_widgets() -> None: + pass diff --git a/shared/static/errors/403.gif b/shared/static/errors/403.gif new file mode 100644 index 0000000000000000000000000000000000000000..940f6db47ad21a06199023cfb2f3bd703fda2c8d GIT binary patch literal 103278 zcmeENbypM)u%%;@?pnH~C6#WF66x;l&IOhwrKP(;x@7_k+3-D#-+Fj>d8u*w z$uk6rQU)7~hv^E1>+nP}<42lHM$0oG)w$xMqY^v~lT5^t>=g?)PVd~|984QoOzYvMec^n{u+gPI!}TcWMo-mnHk>XX+K?KVpT zw>|}KRYmWLN$-W)A1sX>c~~6l86MAe9Iq~%R3)FZq@R9tIQ{bJbg=sJQ{ZJu=;iME zkGiPa?_+nBX?Ic9!m&Gw!>~?uSb5Pxc>1o1f;|p4LX557(ZrPGK)VbvYev z87U1pK5ia#)c@E3d&a;7p@gAu{x38Bw@pxRUQjS`sT3Ou`y+4&*#$aq=9x7Lg{&u9 zPGObrpd7kX))R~D+9h;}6e14kd7Cd-O5S=X=b>>eDoGp^p8TG@T2PjBBy({jBbr(~pb6)q{ z*mGI8na<{O940y2ob)x9NeJTz8F*sblljQ50rT!2+ z;AxOYLV0Ois0efvMyRD!@~5<#MoZIPeR}D6=Dp)6&vKWaS^d(iwpj74qP$CoWw}=U zhuB|s&hIN`?Gk10btZBLN!+jo26vSx|NbE)AYj^ zm8kq>4YaE6N@*O0ahbAx-sFv~hS+_PrwuZaz|08Mzf{cN`D?$F#@Y7dQ)q|_dHo%Z zCsdUiNvwvU=S}8gH}4&lylC!|Tyy|aX1De|RPEmNGEtXvj!uc?KGP*+W1Al%dT(m| zekr%E6-!mOJxOkvcXTC2H~A~ft}sPK=;WXnh}kqoPocz?sfdS)P1zwd!l1=P^;}Fpm?Z^SsMz z-`0K7Og|6t_Gons?L*BXy97{^dsod1+v@?rkKv|5#16}YV0e1$`+{VSw(d&(OT zwZz+h$EsTR@F2@N+fT0IH3w9Axk)OePiPWYSQq_rX+9UAEY6`waNLckgogryw02uB|;;&c=>Wz&4f=2$p3J%B1? zEH(hnC4z0mUskblzBB!>6t0F%Dqi13CciKL4G|lYFB&(X#LO`1@O=A)Ed%Lp_ z$FnSjAxi+;r{`qn01P^tMMw27n%loDj(Z=u09vzM8s$ksvC~GU{7DR3-v{ne-o>1- z87xdxhkTH{Dzgt*LI(%DxS_sY3ceJ4{28lzQ&RtX$T>(Ib!0>Sb;WWkCiMxA&c%tv zKomZ`DAAyfEGr)XrT1DZRm><(`hf|GAH{ULmcbe13pDb=CQ^%&$-!=lnxwCV zT37irmQ;a$lnCd2nWDX~U^|Ym;!BTVv~d~C8iy05n|XZZwXxtyct(1+JPTYR$1RzQ zHn@WNHAAT_DOOR4_`Bv3d@|FhXLj%v()C6&2Ig6fSMedXTKz1090egwbZ2Oo8Rmd7 zPL0Mr$xliNE3h<8z!#(iMOTc24N_aTi{MbNNu|O#jc@2SkK-dM$R@I+^v{XB@tP*o zpRAI4c?^qxN@Qj}i$BrRqwGQTX3Q^N-Rl_?XM&2s*npy>2q$7E!BvKIlHFITD_8#| z^Zcr=TFL{v0V(yCC~`}a|DsbTdS5({^?eZSH<#a=F`Y8;m`VNUEq|fPaj@>=A+H+Nf?LA z1qCu)x&N)6Ea&kxb0X)QO-tL2aPfm7B**A4+VyxL@;tC6gBVc@@xiRe=!O+yv4{ST zq@#K-Bd_ZkbB{r$p<|Ym|Dq_}pOxBwJ>dyi@HWApTWdmYRyp7KHwn)0t{itUaVO

    +h%(Y*Ds; z*)XkAVO#eyz}sPg7l)x`hrTbzCckYRFEF7BFJpn?3I>1O zAj^1+?=s&F9?FI_1?iHp5|H^5DS8{9MCr;KXXpYx_W{>^5&IgDA-a%EX55+`S~9|D zEDu`EHGHJIErGn8i;+U5)1!h>-8xRwqZRLdD$Cm}^`ErUoCgl&+=g&=hd zqq2op?^<4sHh|u-fCBmCJn79l8^A8Rb|fcJ8*bKdbbw|aV9#~9Z3ko@58T{|4Sq*>f{4e;bO5WN$=-55emk54ae(hz1Ge+#Hf1E0M&xR*@so&^pi zfu;)~)=|i6!I(~L#{r~od2681NT|qhC<|NEVPw#zueUBhB3UvB9&Or9|_m{ zz6=-&1I0gw%XdQLaKYVu06Jzs9|~j%B?)a+ZPxuQ0V<$YQe0mtVsIbGRUTNH4qbHv zU!j2Ku0f-Ep0)QDuXLfm`ygix&@exUZ@WpWKJAgrtlENMHH^)7Sr@g4+84HGP0Gyl9xYF`Z3P zk&^h2WBksfd$Hu!0LIS6_Z)8Y0;&_9(pQ;yWn9mg8SvNATVDt5&TBL57z+JUU`WYM7l zCpEy+*T8nXq_Y@f6+(iwsPubWU>}|5b73+#8~Udcva|z^AS;5dK`;BDzmuR6PZr!l zWui|GJS0J1KK<8{5LA{#0MZND9@93Q8$}ZSnjlZq-+ZABylk&z@cLsyRqP%f{OTGK zat0ok1Tyaemo&iQXJV+&1ozt1J}-X4+58RBlK%m-^|0<*KHL9B6U zagQWuz9DmrE-i=xIt9q@-Vy850EdeLy)dFRCtf(!2Z6^n<*ubB|*KC&YkqzgJ9AK!5DGf87C2qg0-b11PUD{vm}&8L29FP+oJrC?Vko3K>kNX(@L!x=B=`N-t(=k z%@M)bwzdAEZAWWWs=pvxD3Bj0kip6P5;_1QS_BCUKa)puC0&|}5tPgB=`5e~2M@|m z0e$*j#&28I+!lJ}jdWaXHeRy{^Q#uN?~NrhEtlhbS^`hadb#ZD20f?z{QZeWhh=}D zrdlrYVF&52otMFgsM2ff!2>on#GYS6?%b+BC__)(pcEWgpWS>oSMbkmqakwrFex%! zI8g9|uU0_kvm3M zT}N`);7eGdoN8pS?GRX{Vn9?ZWfu4CDsX&ls8IuP!~=;AtoC&ZeZqsTp`^R}O>mR3 zVYBh}+!D+uGcoUi!pMpi4ZqwKLJGPvUVmu07qzqiM5c*N-ac4VdAo}E8VogoL#VXE zrxJz^H76TDDz;sRmg=!nV~OHJ=ZX~#lda)9Uq6R|q*jNlkN_Itw6684BB!#)hCVd% zYyx7YS!o!d6G2%+2uVLa{du}`OrURk-ML`r_WC5Tato_?4)v*pG%)gCVeeBb_>9i4 z{C96mMfn0KF(_jU&Ij5^YDmoedOns4owUzOij~Z&t(?VW-wa>DgWnd;oaaJ!%qE6j z_JK`F6GSOJtd2=cj%3P9`SP)oEkSM9zv@sgzc6v8+*;4NGAHMW)n72p)>!L$YHA7n zNeDg_IwWlU6RAGtGst&OM0Gj$?|N@DKzqS@CRsyBlctKeCS29*cS^PS3u44^>SV=rVFu6wB$g z*F6r)c?!#^!A)#W4$=#rb6W^vvuAPGoul3C0t4-tb;g<1KrYlkw`m0R7_d`p_}rX&xgKMSnUb2jD4D<*@}PrdZ}$Z z=TO4;UpWLI3f89{Ggj*|PEg-I*=sS6x%)dO@3*}){`ERSiHh~3)#|pS2(U5?*MoY; zI|2H28hVNcIy+@6!DC9H@6T+&r6OR8f&oh0fUkpqU$23)C=ha!O}whjf8Qrns7>j& z7E{HRYUPHR--ouZf_JaM33<>nH*old->m91?fq7MnN!>WC|-By-^=k>N#^pmmT;%d z?KbdfTi!f7HE>1ad+(Q-GnB&sKdyNg6H#N29;9@3fUJ)Pe4+&GraM}phN5@P!m5r2 zSC2~Xk9OCSv8y3niOu(grmJ+w?Lx?I-*L$JySWWV$pKLJk?GXVUN?NMa|d+5Gh|

    Q0ECVbFYo&_4~Q7xn=R;Z1*V$iVrym*Y(f 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


    Gg2Z~8NZ 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 zⅅ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

    tyIK5dJ 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

  • cz79Gn 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

    eQe!f9^EZa-f?l`)yEPE++Aqekb=! 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 literal 0 HcmV?d00001 diff --git a/shared/static/fontawesome/webfonts/fa-solid-900.woff2 b/shared/static/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..824d518eb4cbbd1fc837dcac2ccad718119d1ac9 GIT binary patch literal 156496 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­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⋙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

    nI*Jca(|TnplJWe!PLLzxL*e9M*N>FC~k!IzL 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 literal 0 HcmV?d00001 diff --git a/shared/static/img/search.svg b/shared/static/img/search.svg new file mode 100644 index 0000000..648171e --- /dev/null +++ b/shared/static/img/search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/shared/static/labels/_blank.svg b/shared/static/labels/_blank.svg new file mode 100644 index 0000000..9d600b1 --- /dev/null +++ b/shared/static/labels/_blank.svg @@ -0,0 +1,17 @@ + + Offer ribbon (top-right) + + + + + + + + NEW + + + diff --git a/shared/static/labels/new.svg b/shared/static/labels/new.svg new file mode 100644 index 0000000..ffa7f27 --- /dev/null +++ b/shared/static/labels/new.svg @@ -0,0 +1,17 @@ + + Offer ribbon (top-right) + + + + + + + + NEW + + + diff --git a/shared/static/labels/offer.svg b/shared/static/labels/offer.svg new file mode 100644 index 0000000..d4752e5 --- /dev/null +++ b/shared/static/labels/offer.svg @@ -0,0 +1,19 @@ + + Offer ribbon + + + + + + + + + + OFFER + + + diff --git a/shared/static/nav-labels/new.svg b/shared/static/nav-labels/new.svg new file mode 100644 index 0000000..04c43c3 --- /dev/null +++ b/shared/static/nav-labels/new.svg @@ -0,0 +1,14 @@ + + New + + + + + + NEW + + diff --git a/shared/static/nav-labels/offer.svg b/shared/static/nav-labels/offer.svg new file mode 100644 index 0000000..41b2a37 --- /dev/null +++ b/shared/static/nav-labels/offer.svg @@ -0,0 +1,16 @@ + + + + Offer + + + + + + OFFER + + diff --git a/shared/static/order/a-z.svg b/shared/static/order/a-z.svg new file mode 100644 index 0000000..c25cfb9 --- /dev/null +++ b/shared/static/order/a-z.svg @@ -0,0 +1,10 @@ + + + + + + A–Z + diff --git a/shared/static/order/h-l.svg b/shared/static/order/h-l.svg new file mode 100644 index 0000000..c487d57 --- /dev/null +++ b/shared/static/order/h-l.svg @@ -0,0 +1,10 @@ + + + + + + £ ↓ + diff --git a/shared/static/order/l-h.svg b/shared/static/order/l-h.svg new file mode 100644 index 0000000..2bcf700 --- /dev/null +++ b/shared/static/order/l-h.svg @@ -0,0 +1,10 @@ + + + + + + £ ↑ + diff --git a/shared/static/order/z-a.svg b/shared/static/order/z-a.svg new file mode 100644 index 0000000..f544a32 --- /dev/null +++ b/shared/static/order/z-a.svg @@ -0,0 +1,10 @@ + + + + + + Z-A + diff --git a/shared/static/scripts/body.js b/shared/static/scripts/body.js new file mode 100644 index 0000000..a7dad81 --- /dev/null +++ b/shared/static/scripts/body.js @@ -0,0 +1,822 @@ +// ============================================================================ +// 1. Mobile navigation toggle +// - Handles opening/closing the mobile nav panel +// - Updates ARIA attributes for accessibility +// - Closes panel when a link inside it is clicked +// ============================================================================ + +(function () { + const btn = document.getElementById('nav-toggle'); + const panel = document.getElementById('mobile-nav'); + if (!btn || !panel) return; // No mobile nav in this layout, abort + + btn.addEventListener('click', () => { + // Toggle the "hidden" class on the panel. + // classList.toggle returns true if the class is present AFTER the call. + const isHidden = panel.classList.toggle('hidden'); + const expanded = !isHidden; // aria-expanded = true when the panel is visible + + btn.setAttribute('aria-expanded', String(expanded)); + btn.setAttribute('aria-label', expanded ? 'Close menu' : 'Open menu'); + }); + + // Close panel when clicking any link inside the mobile nav + panel.addEventListener('click', (e) => { + const a = e.target.closest('a'); + if (!a) return; + + panel.classList.add('hidden'); + btn.setAttribute('aria-expanded', 'false'); + btn.setAttribute('aria-label', 'Open menu'); + }); +})(); + + +// ============================================================================ +// 2. Image gallery +// - Supports multiple galleries via [data-gallery-root] +// - Thumbnail navigation, prev/next arrows, keyboard arrows, touch swipe +// - HTMX-aware: runs on initial load and after HTMX swaps +// ============================================================================ + +(() => { + /** + * Initialize any galleries found within a given DOM subtree. + * @param {ParentNode} root - Root element to search in (defaults to document). + */ + function initGallery(root) { + if (!root) return; + + // Find all nested gallery roots + const galleries = root.querySelectorAll('[data-gallery-root]'); + + // If root itself is a gallery and no nested galleries exist, + // initialize just the root. + if (!galleries.length && root.matches?.('[data-gallery-root]')) { + initOneGallery(root); + return; + } + + galleries.forEach(initOneGallery); + } + + /** + * Initialize a single gallery instance. + * This attaches handlers only once, even if HTMX re-inserts the fragment. + * @param {Element} root - Element with [data-gallery-root]. + */ + function initOneGallery(root) { + // Prevent double-initialization (HTMX may re-insert the same fragment) + if (root.dataset.galleryInitialized === 'true') return; + root.dataset.galleryInitialized = 'true'; + + let index = 0; + + // Collect all image URLs from [data-image-src] attributes + const imgs = Array.from(root.querySelectorAll('[data-image-src]')) + .map(el => el.getAttribute('data-image-src') || el.dataset.imageSrc) + .filter(Boolean); + + const main = root.querySelector('[data-main-img]'); + const prevBtn = root.querySelector('[data-prev]'); + const nextBtn = root.querySelector('[data-next]'); + const thumbs = Array.from(root.querySelectorAll('[data-thumb]')); + const titleEl = root.querySelector('[data-title]'); + const total = imgs.length; + + // Without a main image or any sources, the gallery is not usable + if (!main || !total) return; + + /** + * Render the gallery to reflect the current `index`: + * - Update main image src/alt + * - Update active thumbnail highlight + * - Keep prev/next button ARIA labels consistent + */ + function render() { + main.setAttribute('src', imgs[index]); + + // Highlight active thumbnail + thumbs.forEach((t, i) => { + if (i === index) t.classList.add('ring-2', 'ring-stone-900'); + else t.classList.remove('ring-2', 'ring-stone-900'); + }); + + // Basic ARIA labels for navigation buttons + if (prevBtn && nextBtn) { + prevBtn.setAttribute('aria-label', 'Previous image'); + nextBtn.setAttribute('aria-label', 'Next image'); + } + + // Alt text uses base title + position (e.g. "Product image (1/4)") + const baseTitle = (titleEl?.textContent || 'Product image').trim(); + main.setAttribute('alt', `${baseTitle} (${index + 1}/${total})`); + } + + /** + * Move to a specific index, wrapping around at bounds. + * @param {number} n - Desired index (can be out-of-bounds; we mod it). + */ + function go(n) { + index = (n + imgs.length) % imgs.length; + render(); + } + + // --- Button handlers ---------------------------------------------------- + + prevBtn?.addEventListener('click', (e) => { + e.preventDefault(); + go(index - 1); + }); + + nextBtn?.addEventListener('click', (e) => { + e.preventDefault(); + go(index + 1); + }); + + // --- Thumbnail handlers ------------------------------------------------- + + thumbs.forEach((t, i) => { + t.addEventListener('click', (e) => { + e.preventDefault(); + go(i); + }); + }); + + // --- Keyboard navigation (left/right arrows) --------------------------- + // Note: we only act if `root` is still attached to the DOM. + const keyHandler = (e) => { + if (!root.isConnected) return; + if (e.key === 'ArrowLeft') go(index - 1); + if (e.key === 'ArrowRight') go(index + 1); + }; + document.addEventListener('keydown', keyHandler); + + // --- Touch swipe on main image (horizontal only) ----------------------- + + let touchStartX = null; + let touchStartY = null; + const SWIPE_MIN = 30; // px + + main.addEventListener('touchstart', (e) => { + const t = e.changedTouches[0]; + touchStartX = t.clientX; + touchStartY = t.clientY; + }, { passive: true }); + + main.addEventListener('touchend', (e) => { + if (touchStartX === null) return; + + const t = e.changedTouches[0]; + const dx = t.clientX - touchStartX; + const dy = t.clientY - touchStartY; + + // Horizontal swipe: dx large, dy relatively small + if (Math.abs(dx) > SWIPE_MIN && Math.abs(dy) < 0.6 * Math.abs(dx)) { + if (dx < 0) go(index + 1); + else go(index - 1); + } + + touchStartX = touchStartY = null; + }, { passive: true }); + + // Initial UI state + render(); + } + + // Initialize all galleries on initial page load + document.addEventListener('DOMContentLoaded', () => { + initGallery(document); + }); + + // Re-initialize galleries inside new fragments from HTMX + if (window.htmx) { + // htmx.onLoad runs on initial load and after each swap + htmx.onLoad((content) => { + initGallery(content); + }); + + // Alternative: + // htmx.on('htmx:afterSwap', (evt) => { + // initGallery(evt.detail.target); + // }); + } +})(); + + +// ============================================================================ +// 3. "Peek" scroll viewport +// - Adds a clipped/peek effect to scrollable containers +// - Uses negative margins and optional CSS mask fade +// - Automatically updates on resize and DOM mutations +// ============================================================================ + +(() => { + /** + * Safely parse a numeric value or fall back to a default. + */ + function px(val, def) { + const n = Number(val); + return Number.isFinite(n) ? n : def; + } + + /** + * Apply the peek effect to a viewport and its inner content. + * @param {HTMLElement} vp - The viewport (with data-peek-viewport). + * @param {HTMLElement} inner - Inner content wrapper. + */ + function applyPeek(vp, inner) { + const edge = (vp.dataset.peekEdge || 'bottom').toLowerCase(); + const useMask = vp.dataset.peekMask === 'true'; + + // Compute peek size in pixels: + // - data-peek-size-px: direct px value + // - data-peek-size: "units" that are scaled by root font size * 0.25 + // - default: 24px + const sizePx = + px(vp.dataset.peekSizePx, NaN) || + px(vp.dataset.peekSize, NaN) * + (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16) * + 0.25 || + 24; + + const overflowing = vp.scrollHeight > vp.clientHeight; + + // Reset any previous modifications + inner.style.marginTop = ''; + inner.style.marginBottom = ''; + vp.style.webkitMaskImage = vp.style.maskImage = ''; + + // Reset last child's margin in case we changed it previously + const last = inner.lastElementChild; + if (last) last.style.marginBottom = ''; + + if (!overflowing) return; + + // NOTE: For clipping to look right, we want the viewport's own bottom padding + // to be minimal. Consider also using pb-0 in CSS if needed. + + // Apply negative margins to "cut" off content at top/bottom, creating peek + if (edge === 'bottom' || edge === 'both') inner.style.marginBottom = `-${sizePx}px`; + if (edge === 'top' || edge === 'both') inner.style.marginTop = `-${sizePx}px`; + + // Prevent the very last child from cancelling the visual clip + if (edge === 'bottom' || edge === 'both') { + if (last) last.style.marginBottom = '0px'; + } + + // Optional fade in/out mask on top/bottom + if (useMask) { + const topStop = (edge === 'top' || edge === 'both') ? `${sizePx}px` : '0px'; + const bottomStop = (edge === 'bottom' || edge === 'both') ? `${sizePx}px` : '0px'; + const mask = `linear-gradient( + 180deg, + transparent 0, + black ${topStop}, + black calc(100% - ${bottomStop}), + transparent 100% + )`; + vp.style.webkitMaskImage = vp.style.maskImage = mask; + } + } + + /** + * Set up one viewport with peek behavior. + * @param {HTMLElement} vp - Element with [data-peek-viewport]. + */ + function setupViewport(vp) { + const inner = vp.querySelector('[data-peek-inner]') || vp.firstElementChild; + if (!inner) return; + + const update = () => applyPeek(vp, inner); + + // Observe size changes (viewport & inner) + const ro = 'ResizeObserver' in window ? new ResizeObserver(update) : null; + ro?.observe(vp); + ro?.observe(inner); + + // Observe DOM changes inside the inner container + const mo = new MutationObserver(update); + mo.observe(inner, { childList: true, subtree: true }); + + // Run once on window load and once immediately + window.addEventListener('load', update, { once: true }); + update(); + } + + /** + * Initialize peek behavior for all [data-peek-viewport] elements + * inside the given root. + */ + function initPeek(root = document) { + root.querySelectorAll('[data-peek-viewport]').forEach(setupViewport); + } + + // Run on initial DOM readiness + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => initPeek()); + } else { + initPeek(); + } + + // Expose for dynamic inserts (e.g., from HTMX or other JS) + window.initPeekScroll = initPeek; +})(); + + +// ============================================================================ +// 4. Exclusive

    behavior +// - Only one
    with the same [data-toggle-group] is open at a time +// - Respects HTMX swaps by re-attaching afterSwap +// - Scrolls to top when opening a panel +// ============================================================================ + +/** + * Attach behavior so that only one
    in each data-toggle-group is open. + * @param {ParentNode} root - Limit binding to within this node (defaults to document). + */ +function attachExclusiveDetailsBehavior(root = document) { + const detailsList = root.querySelectorAll('details[data-toggle-group]'); + + detailsList.forEach((el) => { + // Prevent double-binding on the same element + if (el.__exclusiveBound) return; + el.__exclusiveBound = true; + + el.addEventListener('toggle', function () { + // Only act when this
    was just opened + if (!el.open) return; + + const group = el.getAttribute('data-toggle-group'); + if (!group) return; + + // Close all other
    with the same data-toggle-group + document + .querySelectorAll('details[data-toggle-group="' + group + '"]') + .forEach((other) => { + if (other === el) return; + if (other.open) { + other.open = false; + } + }); + + // Scroll to top when a panel is opened + window.scrollTo(0, 0); + }); + }); +} + +// Initial binding on page load +attachExclusiveDetailsBehavior(); + +// Re-bind for new content after HTMX swaps +document.body.addEventListener('htmx:afterSwap', function (evt) { + attachExclusiveDetailsBehavior(evt.target); +}); + + +// ============================================================================ +// 5. Close
    panels before HTMX requests +// - When a link/button inside a triggers HTMX, +// we close that panel and scroll to top. +// ============================================================================ + +document.body.addEventListener('htmx:beforeRequest', function (evt) { + const triggerEl = evt.target; + + // Find the closest
    panel (e.g., mobile panel, filters, etc.) + const panel = triggerEl.closest('details[data-toggle-group]'); + if (!panel) return; + + panel.open = false; + window.scrollTo(0, 0); +}); + + +// ============================================================================ +// 6. Ghost / Koenig video card fix +// - Ghost/Koenig editors may output
    +// - This replaces the
    with just the

    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 literal 0 HcmV?d00001 diff --git a/shared/static/fontawesome/webfonts/fa-v4compatibility.ttf b/shared/static/fontawesome/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b175aa8ece8b1881ed7a23ec6ee6db6ad6b7cd94 GIT binary patch literal 10832 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 SW@ke+i9ap#|HA+S0001);};|V literal 0 HcmV?d00001 diff --git a/shared/static/img/filter.svg b/shared/static/img/filter.svg new file mode 100644 index 0000000..f8b6af6 --- /dev/null +++ b/shared/static/img/filter.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/shared/static/img/logo.jpg b/shared/static/img/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..edac7173924bf0517b764607414d94b56c3651cf GIT binary patch literal 366136 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%0D3U8H5!>I>xn(oZ&Q zgb-Ci*V)Sp9?dVsdz>MeoVzlq{&a6P6B+u(bp&FjuNGb4tI%TM_oYQa5O3oO(*97} zEt!3ii?2y<@eJamMh${Bhi$#o=A-WTPjj<0fDvamHd%Yh2*Xlgu}hKQaY3h|SVbpS_sgST zBYT;ZX-SCXnrF3IY_Dx|cVVsFX5BB(Vj1Y-3)(`HH*MPNN~U9@u^%|wf@LC5Itt%UE( zx@x0qITy2T^Uuz?{E6uLMUTLPARRyC%+n#}Z=eMK&>V>(z1}{_Z~J03t0t-k5Bn4Q z)Noiq`U*ovULh9ko8@Ryh0k1r$igbZ;xRYYf#yyN9|lLSjWd(Ixh(HW(_hvf(!31z znYeq>Y-_mN^q=hY(@#oQ-rQK9_8`?Tv$7PCWDA$dvYRLxlX#XnxIi>pc#J@=0ibM1_k<`8q@jP1IlnJ zwQ(!T(+9x|5p9hh>(evY&%o~#>-ZHCxQm&H@!1614wHl7U*7+UTasCkS`+q1eSwp@n$JoffXV^=^>kPST#!V zVdOaZLjJmRL;CivlHTD?V}P%f{^gf01&&vS&WiooFm?Z>Q+Eb?|!qzCf-szpp8>8EF5fQ2Is~Za>Py z-!qUip>~IxVm}ymXu^P;paq~_1zzO)@FVD7cMlW)0nSHWo?P$s$!;m;+-%6di* zLJ8PN-|u*A!@-_Ihe!?K?Kf?jw&85%V|mZ$#UU3T61WD1TiH#qPO?AV&E+J=-|m-( zfml%c1!+Z08(PPeVw)@3NNP*S)a4TM|2QQy`42(3dM?MAuL@6P42*sYN32?mWh zkv}G;OgG`xcSE9sz#8DoObXuC_S znr{zk>I3(u%wegb{A5BGrBxzte^j)^FxD)a-m`OR<67&%`X_}BT&$R<3jjan+p@`K}~`3YP6vBUK@ByH=}U68ZNu;w>miFipSGoiVWEm?`&1s z{Ed@?bQ9|Q^$fn6&UWo;jJvxhza*Wf?X7nzl=XeliTB5Y-ZI(%?=`CTBlF=rr4tP*tN4lwRxw0|zFVco0mwAfc zJTq0*S)N04`dLlyZeC0Oagb4C&1oQWY5iik5Ur>-ta}(aOkPc~8_##nVY*f%Iu=)q z+Si30QUX6L(&}rq$B#Mg#}V2h7XQ+_MjSkP+e$l!b=_pZ+C?)U!s+?n=91hcD*;0!oXDMSj(bIOL_dZ+ei8Pfd>$D|B!QoUhwf*YElBU2*W%?$eHU@?(Lzls9T6(a1 z%V&a+2*deNPVvA_z-mmj5A4zNBd-=fG$T<@z(qb}s3u=uoAt#wYKMYmd29NF^Vbgq zmn0pS4ImFqA}ZR}q`iJw%zHyShpQ@*%9@R5NJWmv*50hc5-!Zp(MH7Crf~vwn?Ai)L*xh5Yrj3t_9qIj2 zIzvyl=wAKA%_bvt79^KYPv ztJ%n~!rW;3!?3v-c|VQek>PfF?wPnttPYABo47YXcL?TcXd0o)Fr4KdK*o0z#jWW{^SZf8Wx8JhWIbK=G) z;$_}@nE0b1{eGvd^DQZe!nXacKYDRU=s_D#u`3?Us*2P(W-qW})Z(9wluu9LpP6X4 zLg;aaqwzJJleOj(oRA(@W-Y%qQlBrVD$nCNrQs98t?E%(OA5O>9-XRI zfUwwxiLO^Pryb9mb>9Y88L*GDAr0zE*Rs}>> zB&~;;)seo)A$`r zn)t}WX`CBRe>b}jxOSi}Cwg{*g`cmAQqjrAPkPunqsq?!m`#ga^R4Y*!ND0Oz|=$| z)8sc-Kp%%PwAy41RG~7I*;kEvQA`l)Pe#7iD8F`k(RArrb`vad{}z4h+i%&=i<~iq zR$xsK-x5Zk4*poZx69B!Z+#YE_|X2mGaT7-<%ch+UEL`6&4h?fot|O|)R^(w056h>MKFEM}S&pJ<@9B)P{!+<32YKb-oF@bN}tA)z^;*Rf6EPR%CjY15pEzq%T=S&smc zp;+)L!JkK05b3;~{r%LEd8uVU`&b=yWSJ(_C;qQ|tsWeF*hYpJF z6glKBHdVQ*mrIu1gc8?Hd>qpI#Y}~OFb1~rk%Shq+~KG0%36lU!JlHh{bCUWyXjiV zvG~dP#4QEXc8cjQxQVQ`Y_@w6c$Djb939#O`rY=80DFu{!=J*YQ|@3XiC@2Ph$GYDjT;CFK+jE za1XY&!Ac=KF9NHE-u6q*ec+qyAQ;^Kt&7-;xA!2!j~~2mV_Dayees~Nu8|XY(dflr z2(aDm+H!r+%&?wj{J@$}j4CFf+*s~Ju?80>oo{Plxa{jKd1z`di1+mp-CX~#+7vm2 zz&(5FtEU} z8wDZ(_Z7wY^HccZEoJ8RoigvhI}6iutcKFb-14Aut889}Q$POD!!*b;ekn zs381#L1&_3tPULUsz=qEKfK4jgo=4IZ84_~|Bmi>HSLFo-L(qxqK>1nMAWXAds~x2 z15($?(gL59NwC@JpRXeywbGyx(jj!@#`WMoP0}`J?=Zst%pay(^O4BbyI~=RS!JV^ z4}d$%T!a=E59We(QzV`a)8a)4ysT{_pcb&3K7tWjowv!{sU7~X5Z$pMAhIK`IgvXv zR zUehmE9>Vz8);5rEc5(8wfWen%Mj@6ruoTjpyq+!NOANxfFL;0TR5fjENbtuA0iR(@ z*v~mD@LT2H9Zx~r)yr4@e^>27|x85>)H#cwUXh)qq!Q<r?tZZI!=G`XT)v5%n!{f?>2!+(Fq=7MnxzO?%*Vl7@eOs>ph6^`3HHZA zklzf2$}=>|hhljj+VtP!lY5$;2xEt{2_XjNZ*B)dYI2p$Ki0rY%$d?z+xc}!G1rpG ze*1gla66CE$1Jx_DPo$Myh7z0e{@(P>rtXH7S}{YncpM_a;<&yYjUQAbn!Gd0?~<; z3T4*MZ$`o5Oh$f#&TZ-N1?#i$d$ZB-!gQvI0kXT7tY+5Pn=G~DqfTV!xoK7Nlze!< zbnXWzXWhr=gu+Y1`b};0cyEjMO60V4J)bt)%Gxv}mirD(md1Kqt?ch$MADfXtDg~n zKWdG%VR9CXp0nevk0U^d>~Lq|R>os{(9?O^DT zhcH60`bnwLhU_%54g^jM(``OOZ}MMG;7V7LODTw7MUWS!0<-vbTs8X%+@QVmG!H>D z&ULNqH(xiiH!luUf9w8>#Ykt?@QdVh(bx+@ou5uMM7yYTCgZ)$_2OWRDd%g@pa$BLzRT${~5*d(x}C@pJsUhagTBd(ZCLGD(tzDzTe}2R+Yu@ zwNnYE{FCCUumgUbwi9;_t$m~LZvW@4trJSGO1EUVz=^KOry6jCd@MR`GFUhDo67yI zf#(aagX4PeGWkT_vccproaMfcZeImGtbL&#Axvd*Rp=$78*7|Kg#ymAZfF=n-DqVh13~dLc#Dp zT6|lDl|2qffD+gjIN)$gDpyb4z&kY<_j<}PQM^!Q{E6x8M4$?5()S5Gem;CSBou21 z0K)p@1hg>{j)lf%`MC5I@6QRXPfoEFiGq3+KFaH#rup?uq;9A;Pv)vFi`e0`5KTRl z+s;?FrmK^T7x`<2Rs_w4)rd9=p%2bK^jUB72<9_5dw6ahMGP4i4Ze?4KzxhAxJ%Q@ z4+~d{PjzqfhMf_DAe?nYyQ{&v2<8OKL_Y5#+kQ4(ZNXR6X9kKJYaVtEi-lXA4+jTJ zC&1UhA`b-&{{|Em+wJ+iBB|+sb$4|5ie8e6Q4yXL5zy<(n5@`hcDIIj>%vY-^$Gbs zpdSZ5Mt}v8RKACXzneQS-@qmmPK}KeS8=F{q1ZorHk1eVLst8nVieR9AZacoA8!f{ zu~II2FXu;pJfM)9_baq3g-{ILVU@#L#rhWVj6F1S37gffn-5OC%%gd>y`EZS+G<#o zUAwAqQQ(ycb+7A=Z80~h7esP20M4unCs>(%aM_#4&qcpu`M?dc^HASOkoyQP#QQV+ zDWJ#bI+tZV^r9Cwr%=L7O+tyU933NK}Q(tFEAPvNn-U;2g^rW;e)SP zzl1^3S9qSpcGf_%ztG-PNSoEa5r6fh<(K*U=+wXG5>z9+;4-?!?`ddet3QJ}^oI1F zbofC{SFIIurj~w{B@uCT5PQ~E4i@Jao^~_ESiI4vkedOzO$pAQX6+VQx5S<@`1>^R zx!x+6fF#nv-bBT0Xwc~1O*0Hr&ZcCZHCiPfo0$8%$wST$dRzrR)nX)KEzK9cEvb$> zPS(_sxxJ$QaEARZRlf0OI74SMZJ|zZ4c}fK)(3fxVXQD5A~K&uzkCZtc%@_eo&>iO z?!WkDs<@c%AFm(lfn8*g=PM)2ztwq#DgL#Ut5XsEg@T3$QW)z>^@u@3Pn0l?BD2L2 zEsPtQy!e{@jxuSzzhP}dM{M!H4>8MV!tjn*6FB`UDm`H(^yVVf2yzyZIg5L7qq|*1 zi-&_nHv_PjV&A=)^#u#^OsLp;X4tlgiqz(1-r^j%XUaQTx+|U$yOq1K&VZ|vAo>W& zF%4i{{e+8o&NKmQQoPFSTzL4HaREuf6_xYhc3hbZ=rPi*MT>v{Lj{krUYuj$K1TxqrKp zhjnG!&r{f~)W{+`=7KE%=W6?=__(x$FMC4i{M_7`P^OlpOG!LGMVx!)+#F&Ex?LK; zB+839`=*gwPu=oA&qjQKD@vAZdkNK(EVL_;TxEc zgu}ac;(zaT`{i2!yb?d&Zt-2@abTp{-{%OO?cH-mwLz_YdevW$z9W-T(?F&R8m!Kl zdWZq!h{Ke}VXn4AhPR)j>;RfXASy598TRiRVou_`1cQn@U)qX)LwqsOr@Lp%-Y1fL8S>4HQ=y;_> z(lgD1jKng0+rm<`n6&tryC{>vxZUS>G?IbDjzq7V8BAWT=pO{xU}T0qOF^&I)Co)% zOGAm`b+><*l1+d#E**G+wyeBwd9hr_1? zI;6J=3XOfMs>)q4K$CS-plw3uHbwzi|gJDsH*IRU)28Dv_t?f==Vi*yI0rMhDlk zI+{i*+bm9P@`@vwc;loQeuWXL^~jOdU+QQ}p$DFs*!S#Bn;Um@z%QOo%0Us6O$$!R zi&Y3eS}^RiZdZ!lGAwEVA*fY{rMfS} z$br+XZ%V@4wz;w(-XCUNYUW#L=Be4fhxLKAQ_@sTY8n8@_I>x*&x?sCf<-ut@JcWv z`EVLh-Qm%?2@^?lv%aye4`$sqlDjoyc(_=4x!~?c5IC0MIZd%+-#*NGnf;{@2!^Aq zvJZ6nDMZsXZ}k_Ooaw{d^Z3S|_zwGkRK{|v^DMT09ywB%E`j!pVuaNpWYVc5R?^#4 z5YX-Zu*xkaZO4qgY5qkn9EuqzEq7|sMvYk0hCe#n@QvE1B*({Lj!7XEMY`|t*}&qD zNM~mC)%~umZ}J?zy{$aW(HVX)v|4cx8rGmwH_^7;8?x&)!kn2cdHJQ%!`-Vg$%E}a#+#$<>*YcL;n!gW*)W#Ch4b$>*{v7MNn$l zS{)wD_1=mX0+~th9=Duzf7f4K!!J45!2|)WBn*8B;vG#}AF%snW zs;13hTfim(O9^csz04e%4#Fb84;s$=D_SQ|^^|Pz>%z61# zt&g|j4=r(~-^QHicSgp-xI=iP2Tzf_d2=7DP>P40o+R}wx(Z`B>l2oQIY!;o=y~pC z9>+2H4}yR?z#e{S#0$ycw;e71)>Rj;&?Q8jIiFSw9!3bwIAzcrgyZFU`~2|3I#q8- z)f)*Hg_CpssQ#M+m{i3&fMml-e$sZROin7G$BG`kIq#|lj?x#T?T|KL-h_v_xa{ki zb7>#TciO#6Bw6t!!s^PKq?mDb&hrj+IXyW{g*nPMjkd1I!?_T1=CpO4b3B=Ak88!G zYh~53f8Ht*hNlgRi8t_^En@*jtzvd|Hjy3nC_6TezRK*7JNZZJTlH5qp;;WJ(HD;m zPgQqB_;u-&hAD3t>z8q!&9(`TBs;~*E0Y?Y2?UK9pT;bk@dtmE@2CrT4YuRJkfd*( z-yxO{4+q;^5WD`45`|L)wJ}R~UxOyX7(J70m@i{p4Xqx-7D<&&hZ~5Nk;5JVD}d@) zBwpV=QhLE6Akb%Rc@mz@=W;6gJ65(4ckb8blhWOrMyv0J4zwO+GClsl3V!*%p%t(Z z4aPGa6MvAloo8xRKPgSFK#u)nIhYr`Y~o6krMPMx1Q{DE9OlKy&3Fl#+z|)rWkZ-? z_#1JpKzpVq?Puif58mXvp`3f;7S=WEk@^N}%tLj6w{i|TtC+t^B4*0DAK~kgxc(w5 zG2N(O_&=I=tg3da@yX3De&p>zn3;zm0GR=iZn+(K5B~fK-PkB57N-}>5V1uShQAS! zL|H+BV$`b6vnr6kv5m%YkMU)Kn?F08(ZNQww_l3X)8 z;TarTI+5p@=4m&|xvjxnGhPz!`6n~u-_Y+(tb~sF1Xwp}Rnp3xuN$Zx?AaF5npbBO zSw6unpbJ*L>+a4(R>X~e-nxYp!;Va7h$=!yMwjqG->N=liSL!Aj<7js$9g+R3mYJ9MvLADhG8^m4xZ)$-bcMvS}snE9i1L zeI@L;($J4W!?+f*4OWAtL_uvBM)WYBr5o~#OhfcPcJsO*~;hIn%RWR z0AXEbfRz2>ia#169{F$Pfg{Pp zf=*&{nC0$PWuKG|=d1ARh;yJjrJ_ZIQCu182NAwR<~x3b%;Vf+VEZ zEDTRxC=Dsi9MfMtW&x#IF;y^6#7uZkYa5n~HFSy27#6iIwsZD-C^V4rwrqmO+D=gD zkNor2Pyf92JWsuOYKn?G8FIK6zN`^v%y>y4-s=BQ6nYRElWs9TR+xm8 zY;xiV4J(rA`VF_IhhjA@8c?(hqA@LZAm46R$N=dKRYQ>xpCK4 z_O26ZfV0nyA#jJ~;?L#+AmHu6`}~hz)pdoi%3Kr-^8Y=Ut%N1w-Y!tWk`rR4{)gN` zShg2GC-i?$7WJ_++(N$il-iMTn{k>cWwpG!N z7kcd2Li{OG`(D9xQ^l7Ld%d}fwE4W8qcNl&)*|%$7-Kz%7B19^8f3;^dwKJZ^z`xr zi}i%P++6NoNuG^4tShofjcQI-*}xUmmlQ@;P&d$z zcKhfW-=|i;;*=k{*5tKeFQ1bX+V{`Zp(Yg{sB=orWzZC}B*wn%JGbkg} za%WCB$8_Zs;Hzx^2YR%TaYu)JoQDH?PES^)DN<8uB>S5FP@FLHu_b{&vK;@gM{VcQ zLaSxe%JL^Uw~)ugL}vVowt_blyH>6xvLCKbDt^TWi;6yDD?%YVk=7uOOKS}4FaAG{ zu00^BgYEBqx7B)GvDMPDl(lYKrqlOVGbLj#%`2R}Yw6M%Z#kuD z%Bg*mG@b;avnH912L1a;NQV1(QANS6f=-jgz&CqNg6WJ&9 z?EB0Xu*dFP7`@^$B#2iESJEBJI_n@3Z{6br>pL|*dso*~-od%gpuNu7b{re47SUCFwZ&EV zlp&46m&tEO9IQy+XQnEdfk0lRyzEhZ=ovou@W`ZPc~`Z>;yyHkDu{yJhz<@? zycs@1V!BK+-Z2wG;RxEVUlTX8^4S;Uy4O1Ai;BO=bs8pr&A*}){1v!;zm=n=w z%1g|v?z#%yRmPrTdHru*+R?4{IImB)fcyB z_@pR%L*R>_otr9N*C_r-RMT*9Q@=L7PzcXP$&`e;=q-VSQ;chPFx{kK|HVH@_nK|H zAylZ-dcRk8JV6J1c;3)kuLp7zAcuRFr`xKHoJIDVlXN>pfpDBuZ$i=nhOOYtsH z55nD<==HO0%RG7Zzbe+}l6rHs7O5w8=GRR%rXrSTTMN?p_bQ{BRxWW26lNOIdax*k zYp2Vd1E9RV?5>U02Xbwe3-}2I?MmVn3;*Xi;2N39#ue%X zNnFiWxQEz7{$>oVx$z44ms-kAr2{6|bHRGG)i7$oSPZpiyyFWd)FQ2}ubX2R%c^w&gW)nLk=<(-_B(jheZOZ58`r z`-kF4yi-!+!E^~|(ZkPknL9H>>Sh~4zc|Y(mwkjD@Dw_xyUx-wHQRkcd7(oc3hOr* zrzD29=>8?dBiMuJ=)6RB#g`OdW?R3HX<-)iX8z=}^X|R^W?uvTQv4Cv#SQ^)u?Nn9 z=g1SKNzc0n9v(eZG*dnrtrl7QZ)IlKUviJ5zSmwakvA44QST=Ol#F{3XDP^%?-MRO7uAvfgT{4#k#YUSJYC-oDJ-2eI69UBw?FZ`nT9#RoH+oEn%-G`a|70eW%1~2qvTS$x zv?ZqUzEs8>32M%%4Yr_~khn@wTDT>>xaZiy=q$~)l48ouJzh7!@HrT*KPRVqg>gJZoC0rBYW7$#m?6 zc;4h_cgQ$f!CVm+G!fcwD6PxZ9)0YDUy73Lh6pD-;!*o*bE);v@}!49#hCt?C-`-cq0H?!}by%m08Z z(yy`WxQCe**jU%+S$LV@k;dB%jL^R6YF5W?Zi=bLhhT}fS&EILqz}ywh;FHWGt&#` zhTtRaw%yebcT^b=ddufCXsk5DNF#YN|BpxX`PexMI^cmF5(WpP*?sQH8UT{OTi4*( zjD`-(t~0gEL+03PAy000K_sMK&4iU zmNlljZ^d?v#jm||=7e6$%eNS9X=l?%t()*?asu)_t#{Mu;r$_1RNEzh+IS2^TVBvdFYYEc^P zF`(6Dw(imBF5$n-(H%Qr1s8CNxAkk|3~}xcP4m>zD)Ws;!J8yUP`Hq1ZRhBKxq@(tpez}*y_8p|_IEL3X86C;cEm{)5ZkE*KC zaGrirD=OxTSABI&298tzykR0|xQ_DsmBiBxwIHe0(k4073;AABj!A<3Las*}=+w^l zRq|CH+1b5LfVR{m<{Z&<2P7u$h{jGuGJrU$kY`QMHmrnRY_mY_s{VD7+;NRD4oj znGIQS*uTS_<<6|a#jH%xFeJCo);aGwmYn3L+Y~}Bs#YW4&Vn&w{PGd*)?cTqcqL5& zV{13fl8Hx^vV`g1Vb?XJ9buu#l@YTeb+^nJu%7m|yzp_4OpKD_q#C>f&=}(2+6nF> z^wV~?^o{0fC|I3@Xa!+O^2N|P-P_`Gx5eAtYCL%N;1B5O9m&0l7nLW>un%cFPT{rZ z=KH6I8`Dx2Zd^~*^IB4kbR(WGEncTw*FD=B-X|{&Chow9|4=pde=h`p^6XO zmY$>`(d;>ir^rlDu)6}?Af{(CH*>quh4DcwR~3|+QC=Y=6|x({rO0>Eh(T2o{>%Y( zmbDWG%aYN*BDUr}#0eQghHl-tTK6GF=LH9TiKj+3XzL`k(HO>s0;R!)4nMh`J z`iymlS>08O3t4OG#LjciB(CX|)#Ptl8fw8JG_8lXFn7Ne^596tTq*iAjhJCbJNY`m z?yaW%a6Q-X9dG;Ln@l#WMl8Z_AX6Kz8h97GAJ%o9BBsS*$Y1S+Q}N0jGX%!>GjYt_ z@W2XNUZ`9X_)XWO6V`J|;fZUs-O`t|g}DceML0eR6(ag=#FfeFhp5ct$oS3H(;Gm! z*Ob-K`94;&Jm_hBw5bYDHfUUg=_v}s(Yeqt^)e5n?OroqoPK$N55>AR3IB;DC6tTX z!_DaE$LU++ZziWMx|jQw$Y-g zh|6STU!xH|3RYlr>fV%-suL`HoWW*|RHJ6>U-?v@kZUV583evQG^C zJc8BUJt-gApU@T^0HG5wjO4V9ySuvOltYLpQil?(c2r?Oy4cx?wlX%zf%6`)l%&;B zx!PmbNkd6OjP*LTV079i@z2TBo5|O3{pdrgJ|I4FJNR(c!6Fg5c-R;yT@-;km^eJs z_pCglLEpmaS)noxEJR4!T4v@gL|R6tz&D?m=IUO)AADh$>HufBl`@=??a7)o3A~jt z4xh$4riZL3k5?8}YPSiGEHeF(YB47;sip+m12Ix(X3dokHS>F_II}N9xLy>1F@8=)v>K#arRqhz zxc~lc)>Ogs9qw(c-CYu&XyvVBR@(1i)^Qgw_;#HCc>_zeRvyn)vVYdj51Dhr`wRhp z*L-CCo@?TLG)GyZHbJuD;Tx$1`kiZ1o5)k}XID1x##7&eQL**Z$LAvp7Ys2Pc~7FM z@&Q&xKAqD3@z)*!%TGPiX{M*$GG!lGFI3_g|3AI8ecs*1+MXU}rYVV;?b~4w3D|38 z)s{+SQdRsU@?GuE>k5T*ImPgGfh7PfOalnYOoBpZM$E3}y#pbR50MJ_Jy2L%6UQMm!2u!IxE;yEkI*inwnbraT##HfHl*Y&y{iY>!DVD zC^Y9q<9oMM*}gEJFHPPSK<7fU?)uiksxXDxrCm-u2>(#MJDgiBbCU$U-rY>a-<>^I z9j%lD;eF4d(MNQPIGqfEO7z?gp)D;Wb0sTpi@@s$0tBa!kU9mL z6S2!2t#q-{u_b{d_@i`Ag2p}N85*~e_B#8;IUyz~E&VI##M0yy9m&SZHwt z81}P5iZcj16Q*!ody!3mxk_3mkqF&XL4NhEUPZPw&JPaxK>Q{e=fwzQf8s^>MUhUe z4O^N+{=OdYvW*=n>q(ogZSSdJ=8Nsri}t2vK&p%9>Jj2EXf*jZX?KptXNhFK?F+#` zR0F?ZfZVvb!QnXwkt4L*XDZbQ0T;D9*Y%CpHR%YKBNz>KC!?Ah#@AhelcIBkaW;{h zM4fM?!xaBOWIQ}k{l|+Q-SyMP>~N`2SSIO%q>WJXmWo+PCz&9gJB0SR6FWKYud@=~ zOQC|93x#|ZDm=Qt1TRiQ4G1KYxGV8ZXH3!+rE%xa--)s8_E-l4`|H&!N}~H`U@!gi z^1PZB48L*oMIWZ(b=^Q*Ltyej0$IvEU8_I0{h9yJcFbEsRkZpp_2UO{3CYwW0daow z{R=2p0I{%`0YbDQI5W_l9#HTE<+FsPzbcPy7 z$)n4Rly}`>VqX6x=PJ@Y#?6lV_9AZ(hIbg+nm_@T7H!P(MeP$`eQI0bj0+EAUQ`ez z6bAPT8c}{53gl?{nuv(d!LSr6$1CKxiELhG(l9fLdFi)i8IL&n7OIf)l>Kn^^e+mc@g)KhH5N zZwKj$EQvi_KAu8*6z&%c+esXC<$E zogD&t=vJgoIt(_T9R83S)NoECT}k{+{%)cRdwrl%=G~K!t7+85a$DeB5RreK$n&AX zyZY}@Txd?B$4xK_MnVw}fWs*wu;=vocFt@Z^+40n-}RpoV(_|CpLi6sN&*f1GPxL% zQJ=rrdF|};$du);uPw+Y=c&i!a1wPxRIGfmFl&_LVB6IX zEcl-2+-5z*p3F>FrCSk-<_6Tvt3q2RqCBy1iZ|7AsiAnGN?YUJe-KHtR^B2l8a|LO z1d)eE;n7!zu{d{qxM!0ib#KD{7S81KRp&F{$@aXq=Lro@y6Ri6W zR14qxc8DRkdU{jqQ@Ls01nslG_2a|^{FhUyz*~B9tSMvSY0})y#tc1WWJuNp=1AA! z7b2ER^fy~7U7bzdzZ8tizWX%b+~#YaBR`P3Ax!uzBK#moklkt{|NV42L6#I#b9Bvx zz}L7Tz=1oF0RMKK<$tN8)uhqm1)s?^;@Xbog7)L6DyW@Bv01+F9%Ln!(4&{Pb^M`} z$8GAEzo5s#tA6p+pBewd)hYJ(oSiZz7jt>q#K96#%6(?8W+Ojrg|Ca~GbZl_3)ZEr zo_8Z6z}uHn-{FZ)*QF#TTDjJQ`*_>V?j?mld-L$}a*vDuG_=bq`&FArSvWs|mk7>x z#ZEQ;z`TX6b)AtvnLB_E#eJ)14YADz4YSqM6?^-_1&jC1kf9kklw}1^lO+(kvXHj6 zTM?2*CU@ZPG^;Kk?1f@R$`6)?f-tObo~-ZD5-0J4Gk6*yqM>}O^Z_}hK(RqD;MKj9 zG6@!Ft+aBd$_u`vb(c5P=~?ls1rl0x3)Wo@z0ilrSlIoe|Bt5UVG%y(Q)sG{Febop ziDk?jzzR(bv|+uCmo(N)4o)(K@5WJ{uW*l`KS9PWvekd3e0u+&1DCCj9cwcd$X-7D z-`{NF^eOB9;94sg+XDtXzRj=oAGZkZ)#Yu;?72Eh`noOdRi?HGfjTrcU!Y^;a6jOEZEq+6v_DVm*2~Zny181#QaE8Z%Fu70tBx z((tYpu;iviQ4u8<(&4qb*qfRnA6&%>1WIpN*?G;+!Bv-kBbtV z&L4+NCbE+H`lQN)K-buB-3szFm@oZ&<~`-MB(v^L`hMN0In%nzdZ^fNaVKxBgXL-baCpoPI`M1+ln7qK5Yn~Jy$+{DDOhZ*L3#UUYloDU;W12- zj);S&-D>`XT)Q-(C9SRfMY{|-O(s#d>uwU)-ZR|Kzgd*n>Pw1S3cht7O#)(cBrjN) z5`RlNKCKRFy*~@ZdG8B<+t(gN|to&#r1gNOe~mZ<-{OPECs}F>-vd&An-J@3T3Ss^Bg+ z5fR3kpT9OtiTAQKkK#8_3r1JWz3V@1V6xIo{ql)<9%Cs_cg|87dVfLAD0(*0L0Vq- zTzgiIeeLz;lbteLnbL;E+tiHC;%$w6hP3%YZNZ+<9=z`Ofa~^e3-MxBcN%j63ddw_ z=l>>6=Sg=t?+LEG3~>0vm3K7;9vT2N0IDEVv%>Kg#D)XyV6UxkT{5oNP}c(5z@|FT z2F>T25WO)e$pmTaL_n}|lFdF&Va(80DV!;>G#fig{rf#bl}6AQPMmD82@jhuTZ5)* zho)M{85_jzp5g9V`&zn#LlAd}$^`_46H2LtG&|JR;enn_vdMLskKj27?_B|QA<~S} z=sUr1_Zmr-2VArgj|f`pV3FM4kMMV;3D>Ls5LTy5HBV)_03eQuD=*)D?wF8F?kMSo(7K z%#ZZleCV|hJ3b7v*ghNlY;%ZmX-;}*nVRa3{Q*2|&*IF;_;EFH=$|(txkrI+w)oKv zg(x2Z(J4HRohZIHuh-_=pPo`KhfrrhW*29sCOEXG>SZi37KLnj-cTI*`Ip{!Ub)HF zu|QT%kD15$qQ8IJ9Z7|sOOP6Y)6T<)Ez_6A!`y0>}ZPq35c{$vBociQRf zBP~sRiPsJ4%96W7i?Jg~iw?1FfWK5DhiLz?9Y|3U2r7P~G*1~C5INpe{_+6*Eu!iM z(lZHUKcjkvw)Ohy7qdpj0x7IRXagRa>Q@ zM;e;5dz!M;Y7@^OH*@eJe@~mrujCLA|UJ3 zDKVxQ4Ymq@==7NlEXm<@M~*hJwQP?hZZxJj>i^!H!n($YA%F93wC_&e+NdkfFlt>w zt)!m6YkUSrK?|HlZj8c7LRlL~I)6iVB&W7_KkJD=%T?tSYo0ptC{pd50q2$Rp5j!W zy<7eu{JPd>A5P35Zd2mwuqy!N4=Y5T zN?u0su70yWBQ;EE-4#{UL zD1;QTw5-DPPTcfn48N$bdKXd}CcTo9z}DbWj_FpENwC~Ia3IY}m4A!nZ{f@&wpRu$ zgxjhY250XXRFObhXsarm0W#3)#!MCM0Xe&Pml+uibm?+_U%(E`kRC&L$uw)Ua1`4S zj#VK&)1=41XLrPIz~1!gCU;q!Z5MiUAgWR43o3~Ya3gx{^10RB#taDoA@lBT5%I9g zLYFy8L4IpeH%OPB$f#9lS5crm9cZgVTiR=1Oz(vGTM(s{$99oBSnu@Nm^!-?@XP+& z=~%GtM3-bJvFOJMEiH0(&>ECtW!TVIvYs%3-`ExL<*w}yG5V0qzfQy?zd{}~SNn8K zaRn4L{iY;9c5{I#(@6S>ja2Oh9<<%1woR=@;kzdvo7m<98qIXgpM2|Q&?8z&8|rJj zL4q1V{(JQ-(v?H()Y%*aFStjW;IC-ZW3gYDr z%yXmo%5c)u$is&(hwavfsvRI~E4IG=J|K8MK}VXlnC_WFg44Iwb$Exi1_e#Y)eDA& zr=lH}apL>h1U|JiH^$bEvAwW7xRsStaYJgaDbrGcCl-X;gwN-lgS3?*68X2By&Xs6vb;0s$8JDG7hu z{8*7Kw;v3n$)||XX!a4wcF%(o@cK4<)=*#XAfJpVXmsi6{GLdCC=UQ5Wfb9*%r!B8 z7OLpj=udeIrX8itI@KeyhI9iN+vGxXV>)Xo^0>EcpkW$S~B5( znKYV9`$9SuBpn+YxIyx{I=vrlxijEH zZRU2&n6D0njvyt_aHgMhs&KAxbv3FzSl{eg6DNq&?R86+fBcSZooSM+ofc`xlVRt% zspfYN9BOb$J7Lk5%ds<83X+i#>!Is82|=yQ)|j%EmzhWVECz3ag>t08 zPqg^W;OkHL5{@^ugII?1n^v1%dpW9U7QH$VYP7l!k%}&*GP^II9T$ziXmCyIwii*{ z^Mt=tr<=Xcn~8YakFYvr9CW6W*CqEkAM}w`d^GD1yy1V|sF=8d#cPjQlF=lDF`UR-&v8aW;-QI}8xmq@7_SbVpx- z*m)J_$-NgFvqXNi1#-NN+`V4e>fL)(`j|zz?07qHT%}i?ntMQ0qy3A%i%o3WM40?R zQOLu&5M5nwq6rHUo@~NJbEJW&*VvWGA*>R<^S_KHPi}oB(M##IlE#D;_q z2rjJjr{AS49n#E!Eop0DY=_kw&Sm3kjl4Rale&M)9$4af$4k8i$w;X5IX+wq(bq$yuZg$#cgC((PVP+B?Q|W#(0> z3w$#;KJURu+#|QpOwI+{*%2=>muemL628VvBD{MhUld#LyXBX52?qf1MdmF)ygcS{`!tqrh;Q-UO zE#9f!XsYh3S0+j?Nco!FQ9D(cZu5BF`ACFlVT##|_kMoFFoco~elakGH*S2+3a^Z% zui6vQUw(jZTQ@(w=lXnh!=aS4fCG}yleE)VOr3wrFiRBVNXg(4{~B3&ZAv;K>1`0% zWE3rEp(UxRlW5{shhKBA)IS90cxKI=Dc1S-r5Os4bwr_lRL8t5el6y7Mmgeo$cbc5PgO zjBi_&XU?fk<{!}`ic{teKd}N#O27n1eZ1Jt*N=Oj$-}&hyYao5{O6}*oZoy=HA=BJ zKcssT{xj6YIFE7HTEDEUco~tn=$6i8GAmJbz%1_p`vsztTv^x3vAA^f2~FuBCV$- z7MgT==B%fy-z)?>vpQ@q>48zIMCQL9^j8`{1-70F z?Tv#fQ#BWNp1|jo8((Gh<_7GY8+xg!<7*k9B<04{TGC9TrKyR&`Of(*mW6hS?11c; z_6OU}{38KUYp;h!BT6L=Cl29D*(!W#J@_Oz6U_(4Q$F1+GTPY%Z_UJuQ575qBx)L& z)mQwl+1+eKAx00L?J8QR%bu6mr+QY+L1A>~PTokg37yN@B;BiG8~0=sR;xbw{(rJA z#&-UmOP3_vnZ&DCxD#bAujR7;khO$MEH|;J)5~6Je01^7JLvssLG~K7?+2vM{VZBH z!%PFa)g4Xjv*N-AGt!Y#8S=E{j|(gFwi#J})>QKX9qHu^_agQLxr^mt{w8IVU@td+ zDmz|4)N1$je{5)~Z*`?=7mx4|e8QKP%lg9unAdb?{%k zjkO-_!Wpg}*d|sgKXrPv-ymNf5fibC9eQodZL`SWbnGF*fRW%AB4|Jx$fGbOQAgH6 z|LB!tmyqWM8+9@`HaJFc;&4T1_?~kEt@s&T$`tiNY39Ks?fnJmNQ*nyaN4?Y(sHi( z6?4@CWF_nUGpsfD>YgWxkKXcAqzS3o!$? zPiV@0oPDHExwzJu=TtGkV#ipi)L!z6gtn!7&F;5k^2z^&nNtIoNIyyVY-T#T-RfJ! zvoT|Q;aNUBu+XOz${YgCd&{S2vsDR{_maoOf%}v$G;OH0k_dD7;1+O6AfIK<0ma{Z z_DkZbW2=8P1;?!m!Nhd>{6doVjs@0gnL3#1(O^>PfhVsgA7X7`ffIy)kN5W(wWl63 zyzCpPOT?CUK*R>6XqK#VdxZGe=2(NYe;Kvz%GLbLG7-|4_?7nDW_<7pm}A0&SXv%Y zJPb2#e#t*?y!{_D?-m9&jy1J+hRmV8;DGIS&7F*s-B1y^k2G8ng)Jox`QV=2wzIme zw(W%XLemdiqkmokL=eMC&vl`dM8kgp-nf8%2k)=kK-7E3aK`i{IoMQNKiWIpM*e%e z>-F03Bx6C~R$Il%>Rb`4z({jN96j=MNgb_4gopfjQ&O>0|8$LK(b2$TntL(FRP)?9 zyLurobdrdNcSiJBeQgf2NH=9|GC#Okz`ytWoLjy5=4{*3c4kg-Neo_yRoouVp3xHS z&3eZq4y1oIi(WcwQ1^J<>$CYm_FY?b#;p`r-gMlI+oiDq35Fz0v)T*gb>ys73htE+ zXn*}MxyzX#(k58#tZBXW{mBHG8`?+iBP&neiI9VZ#fho6Q?F;O-eC?q5E8ySB-Pv9 z#qeCGTYKO@waZXqNM$%{9i3GWZ0YWV0I$rNTil}p9U;T!OX4`>(eJ=>w&Q1)OemFgo|vLgbW|KPWhYG^h>{7a&? zmjccfV@2-Oh~KBlt%uAH6xSIVl0!22#67-sKUEh4bNp!<^X4;&_7Cfptc2lxCae6L z7r-(?#`@nUgr(Z&(^|H z;cJYIt>_oeSRL>!4*X&EH#8nvE7Y#8ik-sWH9xgD)%OErwUXX*Iic?DF_bvDsN=T8 z!%})@bUk&{?hI|E2eRtqR7n0sQ;|zB2mALs29#vc1JMbp_ljz)8G{GbnQVA}e!dIP zMb;)AH`^zM_3OOzx_$wn>Sc($yN3BY#j}lTOAWVc-B|Yju-M@K*c%sWeq+&2^qcrr zzF53K7q$9I*JjQ*Z9m%5IKp-1(NpRX%BRV53e5;Xx8h=Da;6^&@uv=XZk$(URC!C# zD<=23{qx3@6IZ9D?V^j)_gwkvm_Ox%(2#CCn;w5BJYO! zsg6XGs)o$5eN92l5`f_wj#f$-#oRHoCGK^U&M+>i?eKLBlr@@K^fQ+A-P-qOX^L{T z&(f5*5H(={g_L8tw`1;!M%YGC&>EwB)|u(a>I<}ohjs5`z@EJF2^y6 zkao?sx-Hetj@@-i8?d5E%$}ys$!1>KhWc$ON~YfBHt(Rtb-ZI^L#5Ui|9C zc1uhWqLOkzm2k0*xw zRSO9K`wi{w1Kl0K#5=kt)SU4Uuplr6o3RZ7!}|N_<=YsI%=^!#r}Q(LJsisl*UujF ziV{);2MO7rh+cNdhiGEF7huAo!JWM5s3jTc&lxM@RK|+oln6FRlgR6}>WJY(z!`)f zQ}_<=&hS%B7Qe3(~ph z#jmYS)$nQ{S0yhCbh~_t4UJSagTp&5YBr`Y-(B%3=ls_^GTf>!YM z`N$-NcHgrO|0e4JTST<27SDcTGuDhamp@J#>U@OMMNlM&7tj8eZHmH{XG*4!mu? zcu158=$)7(yl^VtoBMLF=U<8Sb_enfZzOL@_;{-Zj7*Y(ZY`6O7C~G_kgb zi{uaa+^8!O{ABVI_1}e>B!8R)3P)m;8P156r{?_oG;zh4&vJ#;*FWrcTuEN1d^$yyomk8cV2TkQYUii?V%~!#HD8 zSy&~vrb4=&#Dr}9<#pqg4()u%BTnM^yYj&JXw_o8lEENsxInCHuPA$&ocriJ{lZEl z=3wIZ;6Xf_=_7`Q0(K<$MG1bxa|Ns617J%)t^&k5bl=)nUf=mDVN#MeU2hv6n(OY& zIUhk=sg|xNC{Dal8``tF-6vb^o&EH@2^JL#azio+emkWL+-2Ggbw`@A>-WM^cqdXdNPWIXBe6JV<(^TM3RBjjE!-mw z13v6JBZ%28go)o$=N{KQabh7Q8D68zRjH{z-a0ouif>kx4voMP)i#&jnpR?kvkx>j ziEMpe7%MY6nsG-y#V3U~;3XI^1{V}C|D`N?SbuP2td|G>-#0oSNaRN$cEB2V~QQ4a|#L0-bIx{0$JbvhB!d zHy<*_+}=(MY3LMd<^m(*!xj%~$RT2hxyf6%7>vwI$q?5y=HdP;v?n$yOgK}MCIRr}+CL+7ELhpO2> zh}I!nVs0m0qZ#7^5+{0rei_Sg?7bw2((EQjcmF@8z(DKphfC8fSLkXp;tp3; zrfJBa()+CMSgYM;bqx2i;xuu0W^$^q!;!@P)7fHOb&y+1h`%v?mP}RY>cVkM=%!R6@WwkMp*IB~oIM4}lT8N7$z5V!em^hQn z)I8f)Es|TJXSrpQCQDGB0!tk^$#69mEwR%EQ0lBsTvK?tjY6*9&TFtEGLKoQcocEY z?oNGZCBxp#&=xq?#(OrjWpMAF*=?DS)>j7!gZqGsf%hcEM8TYT2I*#lRqI-7wC!iQ zmYfz~X+wEMf~#|K{ro&CiEFVKeS}@aH;uHF$u}pC%sPObU9`!Hcy&7d(sFw?upFT7 zy)5T`2C7&m((qaU!<0HE*?z7sdc2 zon^g@`wV~D58LP$1sEMG#NY1S!X;!00^+Pf16$KHmL-0NB!q9WF+ZP%G5Q5xi`H6s z?i7*4I31Vy0H4PR!#o=5*NFXLTN?f9pEs6*x77l>`myO(-i+Jkxrt2NXYt2sTOdx9 zi>S_415pq*L*-E8 zqOCZ(M>6KsIdeIrvv~Er%)Qprz1SGejl6sCl{GXx$TkTj0suUT6o1L;!fpYDVI{nI z%lAY3jG;w!+YVJeuzi!VqH;7m;Hb_Ht0mu`c0FbNntTo{GR4opYx*D*Kj0vS5NKoU zQ?qdO>l$$=@}Rw&{0nU=-(Ek(;hvUMeVYY0%E`s3te~x$Ao657oNW?$bv^e%cGZ8F zH>L;t$gkt+&a5^Gr4Ap!O zr8}cuZ4H3T-A^L zE7AxSbA>grA>eBt5+m2Y3+j)qbP$_$uy*8jJFf%SQg2U`c6$V>r zQoQ31XyEsl`qlVmOMZE&B=A7M3X^;q=ash4T`S%gOCgNiH4MT4Jw;9r4P&iV(%&jMeX$VuVAtDs3?z^VlY8iD$Be=% ze|kLhl+&`o5Y-Wwlb~==xF#6%Kf&_wKjf=l)7E$ZNhhXK!Jn;ZC|>|l-v;Ob{jX{> z7ni8QfY$9u`FVI(_kPW`If`tA>NCX~VEZtgtE_-KESzy_U27`tgs0%~(Dv$j$K)~) z&tIBfjI{_S>(%q%UtwW!cM)Im#(i5mkmc!q2e3qz6 zHE$7U~s{HOGyFW0ASNXqYUZms?+EspILJgtbJ-k`oSeE>tZcD`-AQeBW ztQzr)j*L%q{{t_2iw2#A9Z=a8(u4n3KFKw&1s>BntemvjyiG|Y(4BW!(@8{lvwGqj zrZw7!U2c$j*8?rW#7{nd?9_?if5yJS7OJ<@;PxPtAeP6}s@T3{yl zC2K*e(Uuh`&z#$&7yP}s;UahL%8i2lrat*mtBdc`E4qN!Jx*N`Y|pp|bL+8skxO`w zh?xj~tL>w?Gc(m{v1R09{9R_gOJ~-Uo%Y{Ky;z8EPJ-o*7^k9Dv=ht_m3TS7aoH{P zg``hU_KeH1Vt2z#{YmK%mP?)_OPyGfV&4|(W2JsRGZxkxxzJ)G+%B7Z(N;Wy%tly{ zARiagPgtITmu1iQ;#j`BToa?uIqDk~kUyQGuttMQk7m1=pgYB=_KrnfP{$~pgrRb! z|1Um!D}w3xI!E4rZv&1}#$8=)M29rmz_9^w?(HF@^WhV-oCvjOAq(l|VwcG9rryjP z+T0TM^lBT|lsSEyZ{0N48(&Nw>&D)Jjl&xBs6vtOqJF-)xiLrazdsV+F%lXO8P^id zQOM@TTl6-4&ARy$aC$@Z2CXvcQ)n?DaNv)rWH|BIf@iLRn(>VN{j7?7wyp^mW@~hv zUlV0(1%mpP*82NX$T$%y)69achT%U;`3-ke=R0o`C4Oe}Q=I(hg}aerr_wKY$%kyn z=@2O*)S7O%1K{y`<4YY)M23&d`)kCf_75YC5L((;Lceoh{AAZ_q6CjM;1Dz zO4!}jWwy<6hkIuLHecyPq8Y+TzQ_LlnNbTfRyA~nGV;T=ZGW_F)1HX4EDX~btrpou zLS|*=UCsc{j4w|F9uMP7ggo-Ox$hO@GL-hAmJ=BjWg{UWzy@fqI6t8-I9HMagzf@F zbd~&_jWrZO7`N(~Rxxk0RtSiJ~=wI<{JFC*XAKHd7B!4dk757mSkjGsI)j(c zL_c9yH3o0QDQqN^d1XC(60p}{&|g!$EUsJ&)#8BP@O7D#|605{gPb$6<^{RF-s*9s zV;+^Rls-sD`m4HaaG60SKetA}@Ul|-X?p;O+CTW`jkKGpG!{97qxU)6enXLew>V4I zo~_#!>%a*ib_SjRX6ESkwWYC+mrzr9JG)dm)#7{w5s^pxr+e_Qgc)lV?rU`k*B`X& zsJ2C3+643O`uz6&bN`A3e{8bO9r$8^QaUGJh)LgDJ$zffW=nsKTfNm>17-Gu5MfkV ztOFOS)YVjx^?W&T?tSuM7e?T0cwP+$nu&ca1gpOXt#B}Q<&(sm==nM36_J*!`hRf( zS$L)Vz6s4Y>1CAG>Z;V^ZMW)@6XV=)t#2Y4*buV5rWPyR+{G_aQnLOx^yvHt*Mb)x zv~J0pjf91N-#MvX_NTfcE@f1hO&q!(V3oV?1NN6c5n%2BD`DX?Q_ZK64(&oc%p%B{-{c=sb8BL~I#~ec!^ngnMKj5r}O>B{Lux)$ud5>BHZ9 zXIsPk;(0dlQ7pH{=@*$DL4AAB`*N8laS74Y1?sl2q3VCoRg@_k6HO-u(D(#HKu=FV32^AN1atnN zbOn4cuY4!(S3goaPkl0ALQjf%Bye;xe2#uVFX)=MI5O8o<=ChG^Tw~_#7Ko%3x|I| zI!ci;=1wNJZ5aA@xjb?`1{10_1l5QeIMd{Y1_$lGhem0 zs@RltWwKm3(<;-wq5k} z!>=>dPi&F1<(-z(UC*j@8y~iRIO|-hT$Z@}=>L)Q?O{#U|NHgnU}i{aLSnKsl}{p& z31S?!ubPn(&Lb#;EhQzGun3UTwx?1+Bw1>(j}R<0M2-Vt3}LVoa2W!^fWZb#Kn`OY zJFtzh?RomW==Vq0)pfyz+w;8NufzSiU-zwScltRr+{p3{PU4)e7XY9GM%2uwCr;Rh z2!kkGqDPAx(FIp9>avPgGRl`)Oon01Ia@1#@{8Hp9`HECJI}(+vK<04!^(@Hj2Y?g zMGH_P9I%pGmQid)gHMuFX`qfH07JZPrX0$MqWj0~sfE|N`Ae*y+2$o0PKGxIzpO+u z|3u!zb^z_$bSX8Kg}dtInp~wKjDN__{tF&RR!3FgFL2m`Ae)2WXT5D{y`hWS5wyR% z%LBi~Tr}2&e-!6v!sieqVW2hZE>T}Py(dYoKxWpzC$GDNwIVL!d!pX+>p#7C(4%tV zxTU+mAS*HSB9CU7KXp|n2TS%Rd>k25@aj%sB-#N64@Jaf>#LbgXcgYJ8O#c6FlB@y zqx+eo;v<3RzJ|dj4{p6Sm%LMrbw-+mW_|Z3D-;xev)FWH!0&9OO9r7X(Qd_~J&U1N z9>7u&R_N7ID6t)mtu}Y5ud3(vPCsbfklgua>gw7F`P$@6h`?h31$IvU{|BA5C4yPqk92p=J+Veu9`Sa)~cye`J%9WvU~U3j-iWtHiTOLXyS1@iKL-v%#~W? zru{9dSV4B-KO9JWzseYNe4z~_>vGE#!i#(B;Q9TYo||(QgTS(js16&3nlrQkztMaF zlRr)sW%(hD_&dooHKj#fp5iEu#?694tVyo+Xc(F!EWT&*&wLfE2iAgpmlNBRo56nP z({P3`R3Kb7h>0%+35`(5gH zPwE9yJUSXKr6Ss!48Ao$#nF9C7)HA5@CO#*6p}6NHyK`asR=FhYHmQhYdh{eKjI{* z>+EI$yKQV^p9XtWqPVoUNcmbIhJJJzKa;5Cx`~~P`qqKm@n(?mxHL9%TIXV7@EVE} zujA#!Y{D$+hJR{uRRj@#uUw4! zs^#Bcx$Rw(f#~7BRo`R`dn`K$xCS<0omPfnaAK}|0SjwN#2!|I%~7h|d@+Llh|u1u z%bV~&J_h#0rghN{NpOvIv#Dv5^06i)9luWUQsU=oSCuJq*0&wi^HwES(dzD#!_Wuw zga76(W?K}>yb);boLc&bv%2V`^_e9>goi?*Mrix2MB6jY$-=wsVU+|#Zk4d}ON3Wd zUaTFUp@s)U%n(R)pH%P<2va$B20s0kYjORhW4&szm0!}a5&`!03X^Y^Xi zxgS{x;_BOy%Q?JSEi4mf_^m1_>6C>9lttqd`lqHe%Kbb2eaI4vSoZ)ta`)1%X!}2AP}jGzNK`JTlg!rR?nm{Y z*>C#(i@PQ z=%Ug)Y4r3#TFOi6T5|Lk9uGREza!LxZlW5X+rh?c?%HonsecT9XKw2jEW~cf zlb_^)Y>%_ujI6{~BL^Sfk@-&V%ut7~MVNmY_zg7QhFIdk%hg7E=)d}Xqmd(6C`g0lPQyFn?Y zZjR1sFmQi`h3HV^szd6{sHW}37cJq{#nfp~&m8?(`v`huTxvkOFqx5;|97D=GQ*V6YLGD*a+`f44C8*s|3|Lqvwisxu$1nSVlx>V8+5}59iL0X z8M;m^+X&~R*{kjLCV)5FW6G;G7jPiCP`f(UzKwC9&3PlM9n5}&H-kJn);+qn%ni85 zsIZdYYOytZ#w~20{C`|&MW^p)^^CPJK?^!)Ag)CsM=6TMwR3E-(y3#LYlHOt*w2#y z)597m@+|Pg%!X1>Mv>aLSs!x!zUEx$yZexz*VZ=SBpJ`W}AYNom$d!z1qvS$7i}bX1i}V!MgFJ7OmpmSX~P=(jJxY z0VC5&0BSD>GKmy7oPV!EB4H(MZQps&Esa8HTbNJXd$F%1F}h{#0|`;dLx~sa3P?KE zb)r(8E>+W_ zi7wPF-5q1(z-Ru(x|%63JtsaY;vV@QS4MmN+c9U|ij47Ic>PBO5BZQJuiR=|lR^$K z<-zulshMKI90=@|^9B7C`^tZ9u9on`=q5Bs6O6R9uw%F}GAqAsG>PDV|8T#j@G2Wy zKR|zzzH85Qx9;rw(GH97Rn|h1n$*uL(Y;W|XWLs>BrbNukKwSt>mCj|2IvZ34qlob zYlBDIl3r;dd|UUlxd|I=sddVPCpZd@Krj&HTivbN5m^6h2M~XRb*^)sK zG-ODl@v^s1CY;QZs~Rq)8Y`W@a9fsxd&*#-zkAe0St7W^{+0vd!bhB|AfIepSqX#Y zy$=V0Rm`f1-lL2D8xbveQbVaXOFMW++lwCXVU8_icjC1@1!-^BMCg!sA~TY!8jI%L=#+@yl}9yKn}gG@k#+NEJ z#{bg)7cl_MEng;LnSX(#QYytHImkpf!&E0ykO#PQO2K2~;&Yq7-IK2U*ccYRe~k1c zyvx!9QBggX)pt@k7>^!UNTjM0^+zNv1_hK)Dar4QUH2L5e2?E0>e0!k%!!JbAefVz zGTF6b=Fwv49}vDumO@KS?G5swqA#3$&|2%gN8sv zRgoQbp!Dh8ly&G&3+U>33F?V!z!?IGM*t7&lomT z)g))1L+>>xP*+lf^zTm+YLkEa!evzq>Rs12>c9$|NAQ$}5Y=KO-Bp*{_ibJ)v2D}W z0L6%Kpd;T4u|td8i`qk84{;7>5H7*3YA@ZO>7t*iq&sTAD6X7q-P&sm4+gnUWLO<@ z@gp8jI}O956qs^wYLTcY6GckUzdHakFX3HCcH>346W8rkQrQUT9pxQ%O9+oAUXz^a ztKaw|H4@9N*ysu#Kdu_NYd%7Hiw@BS(l;mzW!M z7%Otz*R^1&YW$&qt!gT1X_4HZC)yi22@eVaJyXa03$ zgfizKqz-!6u6n_~c0>3wF9RIWzj&gZb0V6)vn*B+R?_WXtU}g~D9D?I0%5^X3GZix zg;52_jm#-CW4_Xu-F4#7Br{&9z42r>@>Y?aLd)khQ;g|<% zu^4-^8QhoZZimkUZ=lvAnEdW=TjA7YQg}eNPG3Xc#{s2o>575)QK|a zK@)*7xX;BqA?!EAIh5T$v}~Sm1oZW+Z}k&O{gpaGyYmiRNP9&mbEJo1^*(bO?WGpD5t>O6NyIPpaVTJ~wOe!=S9K21t{OR#Sob&{N_tgpwgt0;rq8?O!)myj#n0KvTBFkm)c`Kk|1n2c$PLr4j50nzZ2vp zkb6@52WKD(4ii@UI+eyx_SCSYxr<(81gZ`<3CR(Q8tOq1F=^fd@)(guzzY z8nNvwQ_X35(C3_h7v1|WCLb{) z9Z5c64OqyKfg0|r&V%s^JO*B090U6)@q3Oc4s zH)U!P($c8Hnz>+fUZ}JlX|4a~mA%|r{HsqV618sKwJ9j)olgVrha(ZPj>MS-s)%CN zTUPV56!rRhN(tV-pKJ8kO``4vGx@!DtnX-FOrjn(?1PX&&g;fiT_B?Vab!7KcNnVu z^DWOWV;qm=%>gd)NL=^AXezUEA^Cfr&u<5^#1)$Jyn3UoH?xDdo|-gB^J*yAo@HXy zr|=zgp#X8QIA(aeoO%C%{t$m?Cn-N0JKq~1ryOrPVf7yXksQ6oc-WNXb+x75CdLpk0RlQe_nAJ^CN(12-WU4anyGsvQE?KO0_zM4k{1E zfA^y;$Svi|{1_k99VQoQ)TBE6Fu&_~#~$@|y~`07D5;MxbQ<|UJQ zDLOzCO&H@AwNeIzFK!FJP0MCv?ddGI3U6WTGoq|g4NxkfH(=(8XSe@w4yyIsboN zDNNY&J#wD$$H-IKZfLAKJ)|&|YQV!cb|#3$Ui$|n$MZaIi;326khx`>0mZ`qe^V+8 zW7jFcU-TZst(mZ;1|3sRs>m9DLYV6N+lqb)jZ~5Ake@tCO(5v!;6&VDPKG_a>3f@F z#dojDqTj!JZEk+$G~~svjICnC18eXTw}&!%hE>~FUI3jw{Zia$%0iI51&jzi4ukHO zv;Gi%xKuuJF^4iXk`nsIf7XmiP+wfBqT_2c7o-v>Pj74{GzoJils$UTaaI0|`6Go< z)*eb@f(0yKG7BlmKj}9(OW&&txTSVJcGLBYfJ1}?iNTvSYQ5E)b+y=<*`_hBw{)$3 z;)DbHgG;!^L3a<}J;VMINR$Xmm{*b~PZC>BDX>DYr^mD$8Sw;n^@7Fr6DBegI{!9kL8pp_b&8j`Wb-wXiHc>t;)?G z;{nr*mF<*QraN%h?DoPfrjNF|Ic=P>O3}oX z!X9NnW!S-TYMr7r6h$SwA~G2|(?SFZvy7vD3N#A3?=K5!lI_)PUx`Xz=nVHTU; z*zWv>q@M6@bm%*vra#Vlj|ygS1AblZ^A^!pP@>3|Ett>5r!BW;-7VID-V0h(a@ljE z-5&Yp{k-fGhUABuzL|dM<3#FRbo2w2G`DEm8R=PfALcTuG(u+4t%GhdC_^GBJq3H)Pgh zUD&z-dfbHlc1ZxASf}(}gIr9Z49AawD$(eudn_A61xvX*8P}p8ngeksz{5HlTF_kMf3;B6t5-a`% z=cD6|lWN2`g_bZVEq3>}mt9!=L^xbDxk!mwsM|*VoZ3z6SQiYb%fV#=YLl^ z`}J?O1}aj6y4fBD>x)=tqh8mJkdYp_44K`+IFe#R_s%Jb6?vB&IblE= zlJ|5^>#^=HHxKOelkc{=4_d?EARfCATSa)e@&0RKY@f*Q61T2xVn@HtnqWhEo-Z~1 zsz@ViT7)kw)tbm^I3LM%PeM7FbR1IVOOnr(s@J5XJ}ILTOthAQ zooI`GN#M<7!G*ri768RthVlBj_aXGU#n9Z;bnC{>%h5Zr&I+V`;keZfejYe^4C_?F z$Rqj%{=Z?T58X>{fC3n(Cj)eUmtl26%eg?iWZ4+inVrN2;ZF$CZxkkTUwv+istgz( z7Wl`!@V&nF{VCkny4NqY(jAgOq#bPAAZT<29_hWvNVHdV?M!+hsW2vz^G($~iTY9H zCE*Lhrx;FKZ}T?tDbE(Cv@s1lD%YQLrJnk1<1vs*jVvxY7^laWl zd)!oo6)o->O7RZO9CB|5kM*R29`moTETBOT8tr_+!UlB>9-9O-PRp#FkcF=}nnCR` zpvQxJi3>CR)gFW?$qK$q$et2eNBje$sC2e%6P{c!E?I0q{ev2{$B z=-}^4Wj2|8U8uC}^*xKrmUGRnYf6#MlHYM(C6+sheoCnDy)p>CI?k9Ze89^tdc?9*}mL7DiMg?vkcaPPI z^xR{;^s^J(%S`|0^h4vUcSUysoXfz7x22U$Eq|24GWa%=nmb)L1!9H%y2z7miBx-s zurIun)V~rsJ?c8pOp~2^vWJ_RFr`#2FS_iv{a;1?d}HQu?UUU2$T)DOcNdh!5_s+} ziZ-VCL1WG_x#g7HKXVSq8C>r*s%Ve%rGeB%5`@RP-baE{Z_~mJj97zAOk|HSIZ=rA zXk_pFPEV26BbUoyt3ylf)w-8Oz@m!{J)hZFF z;Yb;X#i4hQm+N2@+oO;eB&Jn4|GaXN)n4RM5MW)wF2C*$4B>?r{~LxcR5Bo6p(4ZL zB(?q7r#A2w5_8YT<|*XZ+qH^u-Fk|S;m`~sh6@^bgpt^pd6eIfKeLDrBZu7H+E5b~ z2X$5+{Bs&h{uHhHBYurgC^xNoYoamljF{67K94HIh>b+JrG1-Me!w5M+1Gca*qOM=wZmeBTm>IAN z+e%v8%$NK}{NE@E_h@OQ3a1-Bqr9PHO%Sg-TKaEjdOaZloR^K9W=>+QLNQS(Q3fO5 z{u}_U+v$^;-Qw8o`f6cf!G_vI8T{h)RLx_U!zz~7YXB&vf=3c(&)DQ^=tmgA2b!$h zt368{DW&T0ackj$ak7Bzv5m(crdPaWBQP(1oiSNzp$rLkFI{McKi^!UrYWln@u-`u zv~wo;W|JjjOFqwbb5uIWCv&v+Imp+|Nfc-5_sVz24#dz9cyMpcNZpXQx8Twc%+{7# z10X%OUWc2^yYD1!5OY#MO>KEa-(#+F1@Ukog) zKHr$Fc34Cd;sU~w722AtU!eHU$~1Atf216y$Ffcf%w;b`yt;lb#JtF3KkKoft3e{9 zzJ;-5Dn^<;s8oa6Oy|$hzo1y3@zPuw7f{l0=vW$)oO!G~Idy~#51P?JU8(l2lsPqTm9e8}s%>j~W(|Gc6(Kj;-m(iqZq zV{B?vHnbg3W8O!^TpDN?IH)I9R@3m#3V3jL;LNr^4o_ZMu(udmur90P@E zz1lS!-b3F!)=_u>#^~~E=-R&NYkl77SntbWX-Y%waEEoNmI@t#&^TE%)~2NFVh%bw z=FdEqRndZY{FsO%Y}DZp^bJpy=dmo)7+anh(Z{a<4=da7>~uY* zzC(O!aAy~gE%(t(6-tevlWTzKI~)?^wcn5nOu9s=`4dS;v{iX{v2A{;W_V?sY1T%1`(33{3iRp7+{Wm<28^kjSC1?&vON8Rj`?YJ0EoI_;32zUz0e zLbrR#cic~SAeA$ICMPsm!!@v4U{Dm{bB(1xf?5Yhl(*;m0N0^M29kzs6v-cL~h- zELH$z8KZ&_4VjS8BmOOTL}_)n%Mz&~f*nSc}wGilPTc@hk7Z$pdVxw1y|+9O+8hjpq=vQ55R+F^x>iDcT=@z?A-!XGHklqJbx z5K@f(TfOju=qAyRbjO&enDUV*@@C!V!8lqYpVwqfEMl3sJZV%?(xnh7`|3`v@dGSZ zKggTt{7Y~sJ`yP(LE8IV3(mws{a8Nq2>TV_IavHHFoD+UCa5m-Ep;87ivt2cS6A9! zO{~$Rlt-@}iURME)>cgPV7Xo)1#R!d{2CWsgyJrPb(7=i3{1c9XI~V5jHXFU>kLm7 zEgL4lL!12D8Uz3Zap|feH?yunjc|oR+a~Y+rHgIObEhm-{+|qyTT=-I!XT5FUwoSS z?y#Bsr)>DC*R%Z6T({PY#6z~i{mZ!dTwhNd%Srq1IG8;TCedu5MuGe9RrId$BVI*u4_RQq)78gq(Fs&jlKq z%7L(&n#y2O%HBsJRBXiTA7f_v+$OKkww90We7;%hxyO>1oAQocKIgcG372r@I!TQz zSnzF0Fvj`Z5@B*N#hVdo>F}_*h_upkbWb-aJKTM2a`zI!sEA^9w|<7+z+Qb!KXVph z`}nLnMl&ybFwvBWkz0qMA2(K*77jdU=ri;`5FhU{3{Am2et|HRlY%jtld8TmBT*p~ zA@m$f3NjnE;nu{CQ>Rr&a6x(5pH~Wd$X)c?XqC5RQ+MZ;8qiP4`!*sgG(W7Pcuf!8MD9pYj;eK^T4F<~Sp&?S zq$p?HLZ#oSQdyYDBbW66DgWn{r<%^CL3!JJJ~L<(+lYh^jbvGjI-c!JZ#5ac6otJo`R6*_N3Tuz}OBFvuSB%jBGCY znh*0=+rEhSZ5>^Opli3uX8{_o28X;P6JN{tJGV*(^{{l@Vg@47qF)w0tId zGMVpd*%>xi43dA{*pz*=k!MXRypJ)$$oHZDfWGS3YYI4SBVklas#mFxuhg zy2rwnWoFVGg}P5#ME43i0?-uTX0~$tp-0`l@SAAOiIrDR#UvQ@4Fw4b{v1R0q6PQ4 z?n&9bx%lL?FRb5OYP!(3DwV2tqs3`hVH796@1f=j`4fRwc->QOr}H(7-*ROc_31mXgvV!2pnOXcJV*LY@e{mxV=T6YLSTr0Zjl}%-UA4pQZ ze>J;&u4&Ph0vxzo1SuH~YZiy=Cov3IkI+}t=v;m<;NBe_I`hvfF^VM0P-yAZo%6!U zPMAC_SIpK+e;_mPDB49|iO2)y_0}M|k9P4-p;J`r=0VTV10)a~hgSVy+zWFxx4xo_ zfQ8Lh&o4K`vJSbet-i~Ha6N4U1F0B1d5SL{R5A;Z&h8q+uG4g)=G!!G5bC8CR>-SM zixe^Alux07(BX6AR_B^XtnKoX*^8D)ONfj)fZl?-W*u%RO#=`rGk2K=0P=?UKC{d> zwUMS=k<3OwrlL~YZ!S4X^8IIQEuk=%S(q6Ve&ENLv^RgrW2w;{)7bDqy0k7!E zzbyPg!i=?*@RyB*mVj@KcCkIgW4wmYU`VUl|1oozT7Ft_BFdCRxmeGux5i}J5X6Go zC2(Gc?fO-O4bcwF_lfHRN`j`B8l2MG8~D%3N!}r_nNJ&PT2$O3r(3_JAJ*oTjU#WH z^KkDYQDo{`nIcIaa5j-)@((~)so0d+5E!gE1QVeBepHom=KGGXb>yd|w6?&MP+5}S z2w~nZhRVl5_I=2v(^6P|{@}B-ag*!O#(v)Qh<$uExep4QIL0_d4Ris*1G`X@$VyPl zX~67Yl{Q3A7mn9!J)YRBp^bf4F>6X)G<5@OpgjC^4=gi=!T}l^6d!64;&guf%%?q9 zJ=MDd`&cyEZ_s~)yZL>WU+EKDJ3Ys~1XYyojj7T8g)Zo_^LhVnbadtvngAAP2LUb0 z#YyYYc=DrCS=R*1<#X2O|GaXIM$A^5oL;g`b#r{SqX4Z?n&*<(FpD+VgP+|J2DhVL_q{#!N4?pXI{Y}zQ}7Z6^;{)ZK9wz`faXSO*Z znMKo&eeJy49UP#`*gZga@IsIDBE5J?6zqPZ*Q_d+d~1A%6M#-?59_*<&m#(~`z3#8 zAFUgVNvKc~f{u&P6(2Z(FwK^#gDS4sb&1KvRvL!Sm0WG}3nG!#G2$6T4DlKg1$*%4 zCW+R_A1dMPjIV73I55jN1=fkCszQ;iDLRj1vLR@Az|8e0Y%cYkqW23(br18$urb%5 zh@;t@x>-{LGJ8)049*#BUlGrWD^K=#;$Si{*AJ>D`zzhutKDL&{bXnUYyjKBZNJxB z--W4QD-?b8sjO7nRkX}+)dVp;b*$xCSXNR zn@&Z?F#NcSqyb9x{Uw`xN6Y!Fk};Wk6B43`Y0_y{nZ1t+GlP8S67MyP^6&%dVEzJ7 z;zq;a&%jI;`D_2*B3&0_qm^Jw*3mNP0VZ$oD(-<+f>bIynE3iSKnd+J3WND{X~Agz zbYT+WMLX_Qw8X&Jv1!j3HW|pax=U2}AgK1G;qX^Wmz8@vJ&CtQ_CG!y;S zvhnW=1q@s}PH|!%s4N_=r!tXAI4|fb^v!uOwhgX%YN?q%tg9ObD!L+M|j%QSD7%1?@{ObkM}V<7;vZHzXx-)RpvXiOju&s zti4e{tQ$!1wZlsSCGNW_h>?5YbXmw%@WWAjC%OXLFGbc{R$0HIZ+T94)V?Zl*V+b>C=(;7z&{^H8HG#wUh5-+PNQ-w>I#d{=gM)`*iYTpLR4-(UlVLzXgk1W6pq;WYkjP!XIHQv5KBc{GzWjf>Q+$|^HEhi$@+9?E z2EqD$JOsWY9H7u)qooWkL2qY~w52ktev`7(>DwlOoWHckdGocg$AhrQaq4GNl0z_` zL*-$T2a`TI_@Z1{vh&7HoWtqlkhX?P3*Y8Dw9u#~hPyJ*N;45wcwge^r_jP4+gDz= zrBw`9_J%yB5^2E*8G0j*@Mjn4wzR#=ZGYFAH6V8MTC3?&( zNhWD+Lg}x}uB3HluvgC)**;3`0GV>P!j|gGadBvI$})M!fU_7Ti(W`ZKw8CyeJWOI zHtSq+)&<;xjBxZ2U~YnSV^OHRL&(^GuJM_0&Pc;9*_sqsWM$nu>)$>k)(@fy%3_n~ zO3d+1DckFG1e3w1BU3tGuNy~Hgj(4cdXcD9tHX#tx=-_Ur9Y1koE3a-MTTR(`6cwx z3MpP<>Om|C8e^5mkLUgNl}9z@W?u8^itUzf3C!*!o%Sq)0}h)OAtT_NFzVXNbTo&M z`tduH^%(Uptc&Je>{iv%QB|pplPEe;PAag)2DuBrTb4>_4)Z^p8+}$8TcsaOy;ntR z>t7O@er25+5S$S{E{omHwmQ3o9KE&p;@@1YIG-bd=b;Q4Lf))TD?natN&n3!?CJf3 zG|ylYa-RRg$a3QADV^lZP`26VVjC=qib-{8A8$biIoR!K^xDeiNWBHwMG3Tlvtos-4ndv&>I!-UxV=07Z7lXz!mHZi~71XsRo5D;O2X;MCzGF{X+u zd@j_UQIMnV4wFcVOHX&t+1m+k>^GwANP8uEG5*|+OMu5DV$E(%^xf9)=wX@T@-{}`sf>ldI^kM)LAZ-~ee^IG zsosmHbm6XU&78P!YeLo)(vII+1In~k)`O>aM-fqpC@>IQF*s1R)Xe$sR(CP4VzQ#G zjb`+I*sNvM$o7KqmJrnOsCWV@XlgbXd!~8wp@FUKVtUcP?Hkt{XSAETAA&n~0FUpz z=tL*eot_kfi8#GpVa`VbTpo!_(`ATTeT&I}Pup;{!*4KfLLbj}>RN=sS)#k$C(M9|1ra=w~)D16O)SHyO@R}#Z5HT~*SQq|r> zE>5Ao+{ayocO&q?c@n!qIMVu4bI(ul=H@5PxCagWm44o5Q)qXAz;GzCJapg8vdZWt z=ptW%W25QeC17uwX_5CS)Imx9>#;j0pv-LC(AwA9n?eN)zrbS|Gu{UvKF2>*QbAly zW8!A?7~e17P*8^Hq;f5xg!`Auw)=(uj>!OValw&=d)5^)W8lxyxfk%?GtB3f*^{~-r!ac><#Jj5l*x1a1@Br{UoS%1k_909gQTE(}Lr?#q>np(LH zz^{rI$)Rnh8yX8v=~>ch{3Dp0qb*2j#8P}wTF8+4gRaCpg6Dc%vj2ZW~?I`*`KvQc^U7}o=YZ~8PJSx^2) zF9X(fXXGV9U4o9Gq4d#tl1V*5kMqUt8XmQc0S%5OVJa-6e_E zSN#m_#vNU`G<)X*;=qP3OCdGsgD9~a0gvSRpFQ50w| z;FfeYUB2}L{N@R_;9ITNl%+OrB7x-msflX-)oCCcl?+79Rmu;{a59)UUD9>GW92Q+ z2-+;dm5HJ=G>{4T{khQBZ^R4)aHUbm=N44mPkRJ4f+t`?R)D zdX2(_G+NJh6cHvt?Uvil}p`L)x3OEh#%=fz+rrH0(s5c32v&p z?kRyF5xbL+=*%&l@bkvOY~)`>HO!BB*=FCZQ~2Q7u_oHtO48Qvv(EE#B!4y5StHY$ z1LaF`lQ@txIV?@T4~zOLxB0tzAtNqs{V+#pOa11bSAI*Cu%jt;^0t`o7u1%XOpe+c zr~Lf;(Uir)I$&sM=wMinC2~yZzTBvVz|KVUqIEv7a}L; zrgB+=GvgJIvPcT@YrRw6i;Iq&XwIqrYG8_}@_@D3B2@hs;m*%owtajjnRvLNrl!v7 zx#znjav0Kh8Mb|U!c6b4$EDls;irMtJy|id$Pz&ElnJQ2d)m3dc@1O*7F?hVZy`hq-D(jeE@eBn{j8qS|yf>3L1b;a33Q+ot~Hr%qE@w@QVF{MPfHW%f#N-dAjFURsZ(SD^P+S#-3;Q z6yX{WQK_LvNe-zmQ#0}kg*ajW8fHu^RV>CVk@U8YZN7t^+%>aNDfg6sg$y@cp+x5e zrqDSC)>4A?Lq~b$EcH5C%*z5)HgnP2*L{1&1c~A9iqtSMnkCgrv3Wh#e^XpG#jw1o3561T2cb3BjV%1IiH0qC24R!e!Tv46sPhax*fTU|gi zWqrXe4utuv8plIQMxzHgXT|ka4Wh;os~9xht+0WMOt)k^PhxG?>o;Jzn`k}Nzs?$t zd^3C+kc%M`B-M~w^}?cR@_zE}0r*jrKECDUdhlLQgvROlH}E z|IDcwK1C5AWhc`V8Fdm`QMhoIiMBu4+g3T{Y8MdOW#UGHO zSjO$~%RxbSr5ZqS@qFuOKiE!Vxb7B@WBr~RHEUc$nSp`1g~`8K>MgxlJ@SIp{iG5?A6D zW`OzD*rQPAFE$!iS^cA@cB(5UYpg2@5^gLnfh{bW7j2eBGjTHVXFuWu%bD+9CNSIm9_QckC;)Ux^KJ?J&&yYi8M z_{oA!>ea83CP=3>o0}G?wIo?PJ?EDKmjZ64!l*t&dCRsPbJWJ> z{&8jmy+?Z>8x0Qw&m^boA4ycfQ|~$4Pyg)l z=?@$~;_q<{Zy4~$TPfPjgMNpcS*2stq+euRXG@2|!UJh>iKh2=Vj=)Q13~A&OQ}48m057#o}xm1PjJN=>E#I* z8yv38rcvmS_+2TEL-?NQ(WqboP*9ag6^tuUi+8b!qPCwUe^Kf<^j&nsK%%EU7k9HHGXH~qiz4KZ%Fn$r-sF)lwSsoDSX z3yz8tbJv`-Z_w|EJ{mSj!v(=>1CVFKio{C<++b@35Cv12IXQ}wW>8jlWJ7soD*zE5 zjSfpG`$Kmyqj{HhjFmJ6Cld1AaeX(%NQi*cMEWYiODj@8(p?VH5;9b_t&8mZnCV&% z+ZDHoXYb0S;fR>M)HBE}%Jre7Tb7swrXd5^di?b%?^s3i*1{%riDhnBM{DoQx@+!; zUpc5Ye#~1{VWoV!k`QMnk^<=(!QYiBie@I>D9p^T;fq#c5 zEoo_~#s%V)J=jLA0rG&CP=27K(`4plvkagV*w4hF%-h^;jpmY)X!&|xc30rxe(yQ& zU|8}6)kpXi$Wj`wZn+!BAv5yV783ur*YC)7tB})4b1yAU_I{k2`{d!-%5O1(wuOhW z)<0rfk4wmoVv?cx12|QhC6O0Vluya>s!Dq*9MpKt+@$bm{ac#&wS%4jE%N$eH3@!W z@-)S;a!G$|&e>T@u8p5?^KM^sSh7FN~K7icnp>_sf;W7UY-S(35 zZ{dluvt#{;b~Bd5Om-#OWQW6wTHye0w&%w$F%y;)|GG!M`L!a=)tLHsC$k9x)1Qj? ztXpjy`@}B%#+XBX$HrfRYx-zD{<3LPaJL-uceA%0Q6+lt5&Tf885zea?k9J`zO40N zQ97t_g@y|)VUE(=mLkGSw+aBNIu~W23j(1t=pqtLb#%CtdN94~Nv%h&$!d4pmsGJz z1ty_V5JuVvR^jRqqs>*(GfaGJk!Sm*#6(t}CV!RBkVtuftarmhyWreVLWATsVJZ@7 zoK2N{T4wEM+i>Alri**{l`@uSTycEPIM#EzImJl$5?f{4-Vb&nq|dNn8d?(PD6PIO zXlVW1zlzX9jBPa8`IWT16#GweV(foCxA4wAdy5rlw-p!5Mr@e0K?2XyS0U7Y9_=`aOnHA7X07KiRJ3$;kdJ|VNyDDe*)ImZAY><8SED+W}$&&o_V=mIA4({nw`f;f7VpywD$ocbI}q&< z`60!WbKtGQ^n%Xc0l(gy*)g`gpgsQ-+T1p7CH9kJADpf1ASV>!}PDMyNf+&wpi)iM3KfJ@ z5h7xU2qDTNsXRoB5G6oJB1AwQAqkL>+=SfR+uvsX@caBU%FR9J?6ddUYp*q5CVJ@z z!Y_~5u8q30+%m!@X}x8pR?Q)J#$E5mQGxx&i)splV{&!5-vY${_x$%Fcj8qEzWyo> z*UQrAwWIe(@ea8lq=r>eCPZ5@v&#glpQ($1~b#nJIdihc;WCrn@OLF_t3U2 z%I2QzO{ruzVDdeLd^VEqVj~GN+=dfF9#J+uc;U$8L+{8AazXt^O#X&1O$ySnk0PdF zw3vjyS&#d{NcF*!xX>7vHD$1B!PB*8w}1S|wC1l;+gjslG^Js#^n%#2fquMp z#J!-ppoQEHLv@eX<vA^y{}(*oqX`H_m;HJ`|YEB8%+LO zs24O!iW*WIjgZ!9&*`JAbNKc(vr}FLuy%4IUN{a{;F7kW=6mz{gc*n$T-t7K@1CYR0r#mnLlVzwQJDUQLPh1>mk$I3G6VY>jx5D9FLpLIlXSWa-II(TgP zD%H10gV)*%n}-jb^t+G+V(;q$OJt%*c7f}eBKzMrVS#@x5zu2sq;K0;(Z-5CAA3S^ zQcJS)4bQ3@oy_#tAYfTQ6(N-uOTC@+(8~Ncn$((@{>Tc7Bfxc1iB}Y&;*}BXa(!di z)ykJZS}!aL%ZFrqDqKl21Eu^yn7f+S&znuE4-*e*hbWD4FXhT!sYpz-Z4(Kz5#BXj zgPktUbZg1{t&(@#&bf`;A2ZcIeO31WBiq+0>`D1mu)}qOVV(LbbJE8gPutJM{;~?4 zn;X#w5@Uv=ZPu`RYXbra{~2>rNQg%YM*Uib|BX8Gf%h1wVwpXXW;7AX#L>=ve|1Jp z^Ue)5RqPvWtW4O~q_83+$+(e+j?GqC{NudsSLdj$#K*S%Bbd`_ZCIMs_nqX-@W_r% zm1HP0x%-u&Y6_7ZJ2sb*5Vmy< z*3c^t2V*`!GdgpGpYv%}S__Y7kv_v2wsGUKP#c4<2Fli_^oUU65_7q<``@lRO}I}a#_s7A6(GK?>r!^%F!J!p{3LG= zyfdl@iq|B0>N2ni9(t4ALt4M*R*A5sDG9QM@48Ilmi;RYahVHoOh=2a{r!}jy$wJ_ z{jNtA^sw&Q>qfEk{O~ZD3MyIYfs2o&as0U?jk-%GBzu$HPJDanefgedDbZDEngsut zuvqRw0n!>?LdMRbcEw|k*BtLBv&?y8ZYC(*hXh?AHWsD3<}_CPL^{z71DQSy)c|Pf z1`(~>@f~JqzVDg*(*{e+sILXe!H}&T<@dZe;MbAycc^BHh0E5 zbla;i%}~~?cGBv1ZFK&fko#3>g=HmCjAur&DysEAj&HB*o73d2(inX6F0fqeA4;_Y z=`OjO^t>aB%mpLxns;Xqyb}6yTgc4}a+u;S*+D|SC-&Yu8%*4s-wRH2?4v|rm+l>L&>W4`x`il>bpt8zlm z^tbfY(jy{uyo3kXXU)#P!o^lJ2wz~Y8}DicwNv)KEx(QUXJ0)#cnfHr6@V_#cc%FR zqTGYU!(9aXUn$vmIT*yea`?n%8e>OOODFU6X8g!nzZlBE+6yU%oPEPCZZVb!A#s#T zI|XcR|2kyYFwXYsMRg)e6jNDNMvkmjDu)Lev!th-wnRbj@|zZ1(I&PiL5f5iKSqi(jDybDNq$0)Ua=kZX5eS@Gv zqZWDmoACqkEXZP5*jM&ObDeQhUZp8mon6sIcD%ho?_2+zkh52?>94bi&D{^kQ7omhJ zyQ`vqNJ&+_PZF@d#&&d9#bmTSeh!u?TjB2n&~bT{3#KVJGJ4GB<~}yNT%EBs$T)er z`HNSjzN3UHgfT=v6vH9b@@x*73RcY)-cPM#o!vru%zL-l9Z5l-mP9bQARKs7AkrZQn+ z96+;vxfG@8$V|qjwn8@BP{^zLFe|*D^tT!Rz)h!#P66chx-`<137$L_Fe)cI*|95M z25O!BWS{)C{3HmZAA5j#^u0@ia*xWG%CLN(i-DAgb((ORODa595eNK(go5tIi`xY` zct|BjLFATYb141eeO00&jL@diLZ2BbQgZ| z;ZfC1ZV=`dAVZ5vG2swro>xy#Mj4P@Tv0zih;`{O=QD06{h_GG^tw=IRf5T`%zGhau`OQG>b6$;{A4I~4|crv zJv%$EmbFGPm_R6@G9odU7=p7c9h`y;bvsF!6|F$uRSRdDHT6Lr{THiIF7_jgXeDyB zx&djaRCN?RThhT;#|u&j%`G5#7a#=Lmt`eFgRK69=4f5NfIaZsN8Bsn zt$&2=k|<^(XIn5_&7M5>Yjz|jYtv8;7<(I1<_7{&+Idc|K|wDig-<_GROosJ)>B2c z1kYCXkv$i29}<#8Jp!RMgk&VkaXfQD#ab0;-1G?Q%)@i9vo=<+0Q%%d*fIx5=G##=kY@=X(dQ7d6&O-jeANE#n8s zyNgKoprCL6{i2_JzqfRmQYqk}%g;0-@fkmALM{dz(P5kIYuRpKgadc=xvLhX8Elr$ z3-c>*Bsgj!hqD#R6_eD`fFrAY+WuU!%+O5wZIGdj+|V<^!R4BXjE;O)%t+6i|-pdgg-9f567^%dNDYbYk6iTyBl-Z@rLoQ zCQaLXJbkmAkb|d?R7qdoJL{2i)~g_!YAEmv5o{t5>2uDL472brHrgwhEtIk;bj1CF zJ4~AO`v^3AP;8BoZ9y9Gu<;gnrB+QQ8j zUNvpc`2uxU`>|?o0H>hw^04Ab8EXMBI~pqe~mkI5^EUEWf!IwM3Y zTQ+^Z(`T@T%P{NlCUd72^{kC8@2u!4WU-dl{|JG7(YeHq5dl;j`E8c5^!jmLM73U< z*<`?3K)a{V^58Hx+Rfi-(k->n@c#Mfy88jYra#zZOcjUqG&s*ozhbp=| zXKr4mia3+J-MO=a;k=BPvf+S*tx@0kYZ{`zW=;a-=i#!=(}^=4cMG5FzVwpssI3zh z>F0o1&}sh|qQ>-k9w_MONb^?veBAjizPco$yrIZChgzosTJChkZ}pOIkQPa3!wZ~n>Qtdro7U{+7j{+B zI3M*ib9w$#R_ZuX9>Sb7YV^6($%+`-G1jzo;iL*xJiJ!TVl%<@`Nx-=irzi%p7{wN zp-rVxh6hA*@}8&sTbGK8C#V~PkpZOZFAG^KlmRW%Yu8cYoW~~`9u%vh;Tp@+UF0tu zP_=VoS&2!7Kek>1K1?&*41jwu%zT=#%d$f!xY`tFHTws3@<7>rY?+@eH7b!ZP92{P z$!NaV0?PH=`RC#Dvsh4$3oL1@@p58#Tbinpdy84vfI7RLE_5}!9`L6xYZ4k*i@21X z^T2RNVSc*tV$rH#l$pq#SlHM3DLsb-NDxc1n!9UdsE4_Zzk&G zIbS40Ehp7yQo4@f7q_MpSK}7%kyS9)dJT@xlOVAqMJKHX)hY*qZJTzM#oo@iJm037 zvU`Q+TV2DIME@~VYGBp=1tnwovHsy_UuKj)PJtLitejmFg`c9rBsa7V$(PvP9g`05 z$WVi_+D=gO?kT#y$QyMVE)rErXaLd*AC+yoS7AukiM7@W(4m-OlSdFVKNq=|+U-ND zGu1ao#FvH7jtn?eR1{Kq9E(=-7H3OF{;U7Wla3gdgT%I{H_iP3S1)0keMlBt3XyAw z9unp`LdR%M(x3E>C!QxUr~4|dC?;ZYo>Zn$8OT6ihCQIz*c`RT_>V(cYuTy>8VqOd zJwH7>O8&&Dp3c56(--D-5mc6B2;Hr4_Q=j+G>dTG>O{Hy`Rt>9dkc;E9oqQ95|>H{ zCo8zx@DUnRGnpez%d^ybR<~uks7FJ%FQJ;!p%#0%xbx_z$<3d){7&=f#{82bWdO@C zj$(AG(Ax-w1WRB`A0bW^&%b10Z%VHuCm()|lmkGI+yp>_UTTC0#17z$h#T|wXtoub z*p)Ke<=I8@dVD?pFO!@nK5murDK=UA*k}9s2ZfD9u}Yo9rnpr^{(=jNiXBZ16FXlm zL=(H}jgyZ59^)-vQlKJTX*b_r$ZO(U~HERz(kUMWr{p@;F(59K1=B{eS`b82U z*}V3IE0j2w2}QJjmK{6a-8(S(xWOJ;e1exyVhFRM zD~FM;NxR25PbJk_3Cau>PIWXB(qE;kWS?Ba1i42UjXjBFsJtex+NumVD*>p z%x1nu5~5&J^z-Mn3}2$R%+q*~*A@^q!R!iI{@I4yp39~I9YLY=_w0D2W3W1Zn-&+& z9h$Cv`i8QDg+?KkcMqvmkw_UwWRe-qjo{m;r#PEv^)C~y=m{Vg&nyDuzae8YR$>Fn z#usYt2KY*3R!NhE6VoiFgrAK~aNeX$j9sYYeI*spa*-eEgh*@fa-t>Cvjn+=oQMVe zBEd(!LJy*U5e_OW+{Q5C=)2p^Yb=wGnHQ4AI^5nW+4WHT@ZLq@@1l^zeTnh`gT2N3=lM1oG_)izDM7OztP~sr(5!l05dQA8 z0+JIp?#recbNBQLh>tUN#fhQ<+`tkq^H`t9thvSgVBWCoKywq^bs8I9I|k**a1Cij zN_|0ETXU<-m7Nk7x=M#T^0&i|-N6|C;EY)=p0&S)TH;0k*; z5GG~EBehxle4y;!zX7q1r=;HZX1Y6>HS?+)M&5-jE*);G6jIR-3Z5Yhb-k7D${0*I zj*$ju!PVys1UjAeqNsv)Om7}0(pxOJkCu78|BGL$2HFx(CjWBK5UBpvQHr@4ON$QQ zj{9+e$AsBmk4%Rj;_J}ad`A2Ng3z-A8vz62>LDTH!|g4sby*xU4$&?U2T^i6W2G8t z6*n8hE+F@r!0SJBamNurlH~dsi88$tV%Sn~H}C$fBKkj#MQ3w@x1DqQrN|}n41T8h z$3K@;JtX>M#3d)Ks*+`Wkj3^D4=FWvwK3qsKllS*DB09c9TFL$Fz&`bYLW7tAY76MIEp{tVkfq~igHjG7&@2Xg*hRo><` z@ImHlk@!5>4BcNXW3Svk=l+zkG2><%u#}dUy>2d4wHHdi7lHCLoT6`=uZ>ZxkwG^ zAO5Cf)ahQyhjzvDV*WW{Ip{L}&9sb47O*zfNjpGci1m|12?ZGe?p+cJlfEo#;KAt=!NvSb&sItziS&Tn(T!EHbFx;L+PhFHk`7wlVBKN* zxbPmB66YvSE6~MyKv6a*EXp|q$ehnv@)lBCTHCH>F7_n>%I!sT_D_oKpgg^cBjM%m zLOPvXndwE5!?hKG8^(KTA@XZ^C$Izaf#qZRu(XC;B3e2of1kJ>z7)?N(Zx`M8j5G; zL;E=W{H`ror^R&k)yFl+*uX5w_ywHmwX`<#ZK~h1FVJd>YotnLV97&$99z=dJ}6vU zeIffRaNVd_@F7k8>m{87PouZ;rqYTS+6F2xy*E#YUafN*YtvR7;t%S_q+*c>`x+mp z_E#Z@NX34D{DwEmiAVH)s9gQt=TswiNkbD zZAM)ju{z`l%P_Ma|75#kCtk&*RL4TB<^3R@llp6;(kct% zEa8X7zGf+M zpUuhV?lK#Eg5{~KWopJS6}_{JA=+h!ULn6j2}>Wj;Lj`M1Gp+t&aio)FE39q@9*H@6^ju7Kp zC9$lO6!ehy@}&eq9S1c=B44hH{*DK*T8OT_!6uAl9gao0n?rvj`^Vy24MkrK4?Gu@3B*eYXjF_HSM_7 zJaF8&>34O&F4gebv*lk*7gv~F3ay0=Y3b?VJgw>?5B-oWB)+5?y&Yftbaj`=H)>uF z{C7&w%Cme%BRlbhqro)4BPObY^sXqRpd_w2e2z=+y;V3FQf$)b>oOH2^RlM;7KPUk z)u1^HnfGt8@;(cF-JJi^5kEX@engr#>#c(gbuS{$j+}i4z%#i80z&9|gGPxm82TQK z>Frs1_?--&-_w7j?{sRTxy6MI7@4lO=pTyvJ$8eRIB3bMr1y~qQ2$&oYt0i?DjTi! zeo(@E26Z&=Dw>TU_kwS)+sqCl)eb>iXuk`;&Mrv?8UrCZ31h&RtaDr{%RVW+_B3SW zW>Py%oJt_Q0pVang8>U8HFl2l*!AG+bb6woLwdp7G_-jw*PhjzAmom5=_18qmr3Gc zo9_r4{%7*xa@(otu>%Fzmkcot&6dGP>TGW(ahPar-ZttJx2i||i2B~gh$w2|el*i( z|D#I$qE4Tl#rJi4lj~@wj>dG_W5*}y|D2PaGgy?(?x_{b#w77*NK^s-)>yQ(k!L8J z=;PhTg+z13Jg z@#T(nYMMMlUTP;#0yY&gY19xa?jSby+tx{@#r54oE!o%{NAGV)#!e^J&b>r zvTC|)+1v%$#gp&#ZLAYFK4_4>FjP?IE=~E6j!}OQVXzFprP~=vW_{VzTorwJf1Lys zshSi_7z~+ux=jp5v+9g12cet57+ZRJTr2_!f!E@&&*Q_aMc?$u;jVCTaA4^{x${5n zL*{_}QGNAdV}dFve`NODFzNiTg8pKxv9Xp%f1w9WkXb`oVI&F7_Xp=K8o#%BGd8+3 zKPPTZ2osYvEj-7k#8QR8zyiAu(7hPb7#K%;lcZV{`wrXa4R+4;ccfK0-?OP<&vR(8 zi4{88-4oRB74FuAsd>z^k`QXGA6d^G1~Bi0G4HAM8V? zwP_->1>1&BG3LV0er%km64=V-qnags&0pNf9B9b;skJ?j#WgqWJ%NlKhQf$yUSZI7 zWTl^iUQhNb>RHLBwfRqjSKI*Nd`JF_*YM}mUSbH4V9&2J!XF8C8tSQM>D%tj9p}O% z<1((9z0YI6YhuSlkkSK`NlI?V>M7R;7_G>Al-gT1@$arhmnx{|sH1Te>X#v@?4@Wv zEZ%FExb)2b{p=(N@ac{a>E_RE++jOG&2RtzIMI;cL39zN z+iE*__86W6oXe=PV$@;s2)0;O*?S?2h^7lSUIoh6-y{sS^3gnd|@P zR8T%)!UTJw-i?_^#@t(h-W4P0=ulkx+vkVo(^Cu7qy@rapcjq6eVzL|@mHXr_VtO} z{aE^@sO%^KdDr=AC)=+`=SUL-%bgRLo@D&I6U68{swIU-!vqI+%{E9qQ8pzqKJ9zF z@ROTu``b4l?omk?+(R)=zP7qKr%+q+2%gl|)_C>wKvs_Q`^FbQ*!@uS`60DZh2NJO zGJOjn;c}x|R`t%Cy?KwAY% ztN4A`_W^rToBieIQ^hwkLLv&V*k>4Wi7kwGDXu6j3G47KbPFdMovt}4m1~x2A%h2IJMe^sQCogLOwFvPLmR_ zV*;LISB-)C`eWU~*u6W+T5I7?_7(T+>+*Al-IG%%V&ZwKhBPam;LIkZdITCYNsG(! zE1wD`zVG!l2OMi){S8`}e%+hL@Xfl%#T9}+)c0yf=>P{j(-!r5ziN`(kz=GtdxBfc zuB&VIy2ss_CL*)^(D@*z`bz1<2h~~bZqJ7|&{k!z9l>71yM1%QOkJBwm`8I`mvu!^ zK+|!Y%}+BSBm~?TSoBO#0FFeH%KD@rKW=p5q0|WxHG5}8y z#@Wc&=gOxy|6Fn;=i7!W5!vSwM;p1ce$TtHbwGNGEfJc@He9le`a{olhkG>`*SESS zQrjC3@=oQ%-He7jDCnrNs*qZ|G}wGUkrC`py|Lj8@Nw!Bk(~zFcN8=+h+cH=gK?m!ke2w)eSOr)Sz-gw=+0Zokm)=I z4T6y&SsNQnakQx2=ZbR`UPR&L7&d9MqN3rdt{DbqihTLb_9$KbFXLAc9lLaCFYU|f5KOfHQ z=^{uK#A|q`ryU-6p?I4rj0<{uGFs6eax*3qfqhg?T5W^#RM();QT-d_I1gYfX7Z(JJ*qEnYZ*_;Jk=MC>0# zloA~2%Cqghzn1jr@WjaqN!Z5n>eLXShRBYyE!4@Im(EE?V>?1U19rs;i1wuB!CPll zgO64qW3wO<=$k%>0h=PcP17K2ldjG2xv1Ru{HSpQUu_sYKNccdoT1E?*)UNvK8PLi=7e2u-X^UTrSyY4NZ{e~jqiv2-${1nsk5e=Eo^SM6%Zjv1nE5Wj4+P`T8L z(s$(Tt54XkXd_Q6(6q{!*g*N%8+wM8F~!rSOU+{o1Do;)S1tYR7m3Z;3D&ZA9tJ1Q z)WA92JZ-l#5fPnTqX?^qtWxL3vW>LH@eXuFtH@H!j=toY>3?&BosJRwm9= z`ZzaM<{yn^5&5yixw!13po&&*H)@1Rij(c!tHkUM7wRBoGt@_?%UYw(T0#mkYWSy2 zuTtWean-P&5PmFi*v=)&+~+kNt_i~=gfJbA8*Z12aN9@+XsQ-ccwj#v@@^$0J=-?btTVFovky? z&mKj^VE1~-xT%BWvc=3;6Cas}donhA1nwycIkiky3xI0P<%GMb@H@c6EWUKF(A!;tg$TqZI`mhmU#ozw?aLo}k~6v!)y$TdmJe zUJC+q7c--VDq3$`YER40yuB#!7Y3Nc&m>?lmJsJ)isPo2)WB#k>lk8ct>$dy0Oym8 zEA53(lYb;W!YEbD*w|EN09UoxyaW*29TW>JbD4Q-={$vOMn0o`-?!)vE}Sq3homq6 zT;fCYm3>raWOXR&2E4M*H3!EY9*+p-$xu!1DTd|GDs6{j zDG4hYu~~G2&8?;}JzxK8{e8$GK}app3hc>&=G|ZKsu$o4%e>hlN1MI31RTVM|FCB< zW=xzYo?w_Q>gbJHR^lfc$`tPdpBVgkryU*C)wt3>hzZQ&lE_9IJ80In6;r^Re|qlW~4pCSqtz| z_%~{9BqnDRr77;y12;M9#1aut22aC%L|jAl?0mCh!M$J(BpZiiL3wMLO(&t~X%LCz zV?|E@)S#zPC(pZ=m)*V0Jl+VhF>BotBTU`5*iIFLZ7j5_bl2f`RQdL5#1CI;a7Ij) znWd!f@M)1r`7$Od0rCAK80i&^ogYB(Pl)aguAPCuN!=T>{PC05*9_9S$&7JR^Fu1~ zH;vh9ccVUj7!ygvmE%^WqXem62qR!_!@C1$J=`d$mE+>qb;{?3oFb#5%j|zn--3LT zlY;HZ`Rd^u_cUm%l|w5mK&DLc@Zh{aQY&`)06Z+J@2`(C`0arJ`iPg56>fTQH%jN` z)$eUMym4MyE3_>CQg`5TREX~HiL)LFvz`Voo@m0*-WN&f)$}D&hl~bs?RieNc{uLh3g!3Tp>A|G%qFRZyEA#- z+}qP$7CAq3%K&e~#O#X_H38E_<)j+=fED8%! z6C0Pf8cWU+UN4#4J1^C7AZni%;&auRJ24dT=zriFv(Mn+Ei`Ol#7WIga_M~|`V0$C zx#?MPH#oLxw<#{nc|ej_dE-XMWCmcCAZTf!=}X}MKZL|0H#t|$LD#$u(;SOIJjB=7 z)+=%n`O|b)d4m6jJ6iRG@oUB4Cm@l)SpNhL_y8FyL^XTX-05v!iLG(ClyW|b$Po1o zc>!6Luzn+M#eg)jkyq^Arw8qJfb=eV$=Dk(X)2k0@P3pFhuIwj3 z9s|5(xvtB%m*!}-$JwzLVEPy z0jiPw`ea8=!{PnPks{VWm)%jw|IV$<6|JGjYU%bXaZ$q6ZqQFS-@eIzzS zDieZWi#3t9au+{44o}3li2{xc_b_FbCs=yLe2!)zmGWICEaHqK&0Lilaw0KDSNhq^ z$KQ^Ak(puIRIK67{})d=F#ceE{oCHx&)Oh-MXeLg);VyhAPhSu`sqrvW^h-b#`K4# z8>e(Gz)RRX0jo$wXQz{*#r@ven;D6co^#_!SErp@pKjtQE_9I2-;qibl2kE^()I? z*NOQ{5^iq)$t~GpPk9PintYO7G)BbksJIh&$6>W@J#@Nn@$O2QT<~#5=B-4o&OF6- z-@JY7Vl4K<7u{a5tJE(c=wSxlR1TI*WC~{MPyD&$2W)HGf3##Mi(kxmH9;}j`voKs z$fV;xhsAvYg@Jl}m|&G`xeUCKHndG$g_<4R%sg;LXoQd10{l*p+HnP~odnE`7__d;WN0r%0|rCXTrXqiP?E!nqm+mI7=P&Oa@kiI5kHmbR18Lojx|4^ z*=lS6y_n86n361+h?@FWMCSd@P0AbyWB?Ff)$kdN$#7ydAXPmD0Sp8`{rjSAvLnTyXf=XrNOrs zL&R{hAwhE!R}yc2`bG0T7&a?%nI(E)n0-zcW7$fJ3klLC1|T(JxvV&kf%W5Q$fcmb zxj#I4vCGuo%qdmiW1oRZ%BzF&>OvVREtTYG2AVpTZLAiz_&*&|^^>*-SN4w!=<(6g&U^ z!XH=TF~+deeIb2*!7`man8oQqO=hijew)V_=2ljfh@Sp-^g#nYp3n2ACN#Dzc7a16 zI$YcivKf_B*B>--7l%)j5y66Tw**F`7VV-BHKqC64{VK5d!_sv?e%;5S%F~{V`!&_ zU{l(FWQ2ZF$=#PXdgPps-Mg6)52BJbnFDr}f-4vPJ78C;$2e1|X33-$dB{ONrg<1A z;UPs&Ow#jNRJA`T)BCPZ(biy>Vh@K}_zB5e-9=ttllXEj?N<*Zei`e{XaM{qpU%yK z^|U(;RO!g8;+>@r@(toex+Ayie*!REDZrl3U9s#4jCLJ_SO55X^KF4)RCEeNW5rS} zoq8w7ueW*chSv)FC(+H9fB(Bw#$3v_I9qR9J(fnm&%)>s?RfC0@Y zI5u>gX}Id$^33;~3*&^i$S@o<2=MB>!w5>#*zQ2&PR=&PmC*bUqf-&!?gHu`V0M6W zQ!~}2fSvFaRqeK*x%r6aIfqh4HVlljR}HuAk>0yZ6S14%dE>FeA}p5p>y_R39q*?O z@~iBUr-B*}@C3stA0&BX(M*y1G8$ zN$LF#XD+$4LWcSY8`y=jmtCQ#ov*z340k)@_0VA*$) z@{~!G0K>?^Zqi;?$a$_?%-u&jHOo1JWgj&+i;GAnKw6tNKyvhy!Q?XfGSscp!+k?LuQ$K21@hfn!BpMR-dMuntOui#LUkr2}uKpBKTw0mP6 z3APR8jy*7(;AHn@zEHu8dh(|dMEwP`q=|9gjj!K+vy`g48M7)9VO(5j2;3(*P#Y1i z(*^U7aW0&U1X)JA+Z%AFeW;auZd;!*Pxn&?9lkx~`;0eL#x9lAGEqO8&9n%N;B5D$ z=+*~DO4TU&{RsB|;U&8o#UkQ4Xy&|&U!KH0r;x!A{)jw)b9Am{T=po@ljRg2rf?}< zQCk`B>eU$VyQnr73)GjpglGKjxWKN!)=m$*$sI0tzboQ>-;0b*yCd$hviUs|;FD{{ z6l*R}M+qxZ2phpKs%BujN&j_pn$J6TTK2>_15s(=Lrqc4%>aI^={90^npX?no zZV>0!XOg0fL9@un7Ly<~O4m(=vuL@cLb|R(q49u`lvf(cVvU2EaUHdLV0B^9fODg_ z1eIFY+_>IUTE1k}oc#plHj(fr~$IemN+MEM@m2;Z231j&|{hp$?PbRu)3}70Zjxy{Ui{jGJbv{HXRU}#- zL>v)c6V_3GV1qIgt*<#y7|@beo4iI+<;lAf5eb=ywxQgR9*&q{qwjTE_y;h*!l>=i zB{rwg^%=a!S^mBpI68lC!6ii%w#&nq^$^0Uwj9+C)S`7zB7l#*&}$QJ@05EXm4>X5p1cuNtj;d}t~ zfQZ*4hpAqv4fCf1qzF;bk5`eRS)reGxW7W6XY!Tlu7|lL@SA&}KKkU8X>{(m92C|M z?~wFKd*Q+5+iPU#CF8)gjSuHEJA(!Kkr$&_PB})IiokEet={uo#ct@t{~k|03Kz%k zGTG|U@fPtJ;se{!*0A(2R6%?$m0N<3_K}Gpf?NKR)BQ9TjTV{oK0_Mckl)Yg;|1H{KGmVwG}7QUG_6qC*mjJB;c3Uls@ScMfi`EI$r|kuu!*i7Lja0j`o*Wdz=2bUf&N zo)cGIb8r5{*7)L1)hCbnAMTCR>_iKwX1|c?z?+O@A{Rb=W^23nXIq+|k8*6;f~cT6 zJ3?SEw`66Iy)cO3F{)_=l0?X01Ffam-!?7`P~_GIlll*}T^+(;e+yUjGF3kzoNjR` zD^5^6r`B2XPIkv*dPR6R`0w6E5>EKiZ;%|deN;KMw+6OCp}nE;?wl4NwgKb?M{!KE$kslWKi(Jqnsz#A=5E3W z;uJl6B1<5t0xb=rMzyf*D8c^b$}x6TRwM#?L(koFHEU3A8?~TV&|w{`zFj*&;xC z4V-}lr2b9rVA2dQC#ChNw1u4RoXn%uq2-Q$xAZlCmWhLsK3R2_S_cfyMfkm|5+<26 z_GFnO!~#CR;V|1HQXJV3I#((V{&V(ic zTUREiU<-vbz!v(#5B`2z1QSYvon|HVgyuauJvwkgGwqzUqUr`vm=6t|K+}pMLo13< zmfb0aN*v)`NXV$$gWvE55warN+DB+wZ$wnbnzEvbDWB(urq@yWae;v0w7_vL!;O(; zF8vbUZa!YJZ@}SFG0Z;o`s`<#ULO) zNh^{C-scZ|OtW=Wo;y;Xr16mK>Sk8`p=}^mL}mX%J4gB4cyOj@P4m7EH0rj$uAqwt zSw<(AOz+--#qb@wsoI{F{fsE;S>E3aqL{-oTP)Iidv|>;_+OGf$@b?p#K6blYf%sD z=+xU8)uTaquOF7sjUeT3t`ZoMjurbA#JmMx62BvZ8N*Pg-F?(?xyC4D&3TNA3;e00 z)zsCt{19+WyWl#C>9e9G-&}QC9P#g+sd)s?5=u2{Kz8U8u_t{&p zrX+K^Mj?J;WRu!Z&c7~Lq^BFNcJMCLV;DO&Vlg-Q4m92hs0&%xrrJ95*g;{rXQWCq zf)f>^{N{|LPfVgviiYrK`3C~>LuUNY@*(gQFjo`f|FZmtNlccrIxzM(TNWJ8J@>F< ze=c!kOozdh$;z3=%!{DG%2kmf`)O-c( zW9<`MyM;|;Go@3hBhD_<4~mO7O~%%oi?nK$CnHZ%R+Z2m)?V|v#iNz2Ex`ZXQLc;x z8;Skw`#`+fJR1yk4x+Ay-1|x3UiiHb|I!|Qho0f7K9Hwky%TpoV7yeQOuB2{Uc)Ck zVYKJ&tmjNU_)#zXrw^H4Z+WZ5_pxk5an`G8`O=*a>mCkx;jy%W1AX2FP=vjYK;O=v zC9P*=!C2x02c8bC0LeNalXM7AH@nAH*sjpAx5`p!tD=LL07Ts&uH9w-GNeS}z_-~? z&!Z3dk}N3ZK;^8y1>no6u4?h)<-sw^O?8QB;?RVgs8>?w3~*-ve+|0@pSW5X9df%P zmCrsk-MOo!p&nw@SB>?ZXwnV*A}Ajl`ED?PocdGBY0hm^D}>dp>&rY>KLy~Pfb z?{OCMOjBD@ZrXA8LQv-S8R4F>i*p@OBT~bYoYj+zt|}Wsdm$D3>$0EWH9Gtspug0G zYw*8y1l6HHrIpdBI$7UT)&HUICj{(@D9}`1RQrj9-4NgqC9kN&`svNNMYM`Q<$ ziQ~W4-4oyI1Et>wg#)7*iF(5pJybI#y6NOSiN75AtW{Whcc`9CZ%oHpA_L%pVL;cn zVvy++`&wGyCA`N_zU)G$Ha;NwAc{zoFeZVK%_E(Q$FBz2DrM4^+m4P%c)!^idt|gr zj-8&oHF$?qb(fzv7Sz~`LAHs4^EsT56L8r-3&cJ@s#s$uze;;({6Rrgr)8a)$St30 zJR?-jcz|Jy9^4O_8tcbkoQjUidL&*pDhcG!M%f_|M*plYGbjV2P&$eq0&nK63RY$+ zOxog?gF<~VP!&jqaZrL>U)`(eUEl~9DG%9)jiX*Wd2QX< ziBvswUmNNcGzpCZ-UT6G<>KK)HIAj@H=qzS&|rYy~0^M>Ne_VV0ift)!4L zLUzsl;!}J~)GmhlD1Xp{RbTCwO>LmFi-=8O(CwA#KOPy{8bbL$BO`^jI{2^{_^ODG zb!}Q+wTqXo#i<=cJ_*~IK@T9eN;cKKuoQS?cb(Vl@Ara}*d%ao&q@N^9u#_7Hg;h? zGzo;m-o(Gi|6DTR&eNZNE%|`cV_nns(MY-S=_Jge@hAYoH^JU`vP}I6g`j z@{`$dz~)~8Yy)Ax28J=}=(!^oC`{g(mw0uDSR7`h$2#uSHPiu+e6;vd3nWs|oTVD( z!tW{_AJvJVP(jX($Q|44ISuv-U|5#uKL^<%_WgMvARVbCEvh`ga-0+M$7I~I-P8N= zp@39{v`b`m$bRiHrXmy0Gu3zzgHc zrUz%R7d(HY>7{?#=00mC{w3_DT7#4s9+qXO>wkIQHy$2|EI#42oE5irjVi!Byt1MM zb=5LYH|cs800`$idTq&Fn9cpu@UM2(iFpoNdXN%T>?h<1%yh~{V%QO^csTr?wTZ9n zSXrqO<>20%@%dDhQJVhDdE02{9DB4+HMkY@a&XpTDBB-{dhTx?yq+H(+Wg?l;>m<% z!{p-NeBLsv0zm1{5Vz`z#R&tl7cFUwYt=-T^bT5Yfszrtp7;SG28PGU17~Lh#su z_1Lat&imm8gR0tT^BMQ97X3dqC*+oY`h}F{9M+(bSgfI_=_5d5_Z^80bdEWqf2AC1 z4QkIa^-4{bVW~8rV+Rz$Omt3kZuNU9dnsLncg^<*0dvTGMPztSGR~3%oyAO+CD?`_ zLb{D=5SPF2x_Hx^V%RC%d-Y-2I%$p0*?s#iO*ngXsB)MXqQR+&hA*v4h6|rt<)2X* z)^H=cMd#;$3&P@l2j>;Ev*Bb(ytYffv@~0=#3~tJauTRlplsAAm?<;G{}m~pB6w3dAPf9 zgt#iM_)1-V&7mav-@F(ujL?_s`)wrK@*_sTP%+T8KOf1>()Jaznb&O#eMQae|# ziq#Ov^T`crF(H!wToi!cW!rr36|jJV_eNgjR*}NiX0haGl|uBLmm;~l9&{Z{{``L- zogL4FAf17}3tU+;9H6p!ZdF`Z(RFEY@Nc{z=d8<3bBSAX-Om}@J+x4PutL!~iMZ2? zKg2)vbZJu}2eED9&q6u0p0fBJ^q3ZpOCt~UWhEpwNmh5;By|kCT4~Biqd{}KgU}pC zWgo@_1N`Qi^O|*S{KXHDdChnRlAXggU+ToRNO@~&QYXIU7L88#4QT{N>iaM$Iqhe= z;jw}&l$jPo=S(>TVfV`4M*sA9e%PtP#2D_T_0t<*WNZ0p8yyc<)WYOE~3nrCTT5^3$`C9?llW_oV4}VTXc@&9=r!R}VSaRk;>NZtyH@ptHkfAW;Lna?*?3hnT8O8ed1EUN`nCgY*7xuGJpupuWwsxZ&}yi( zTTv?d2X9|FCQ|+jT#-leY8Sk&qnAQ&XhSbyh@DiXL)6yAd7ZskYwkr}EpkM7-<4fC%ghq~9!Pjc*j39t6>CU^xiWzsN$CB=61 z9MZQ3dv0^p^0SqYyhpYeDolINE)>@`-$~B-dC+grw*XP=uv>_Tld+p_-D6HrJ~6Fo zztUUQOzTUDesI==i|*(H7 zV_e>Kk!UcpGfNxna-vAdhw&|31i2@xLK#pY7u@yvI-+6-Nq!LbaU=mmwX=vuD9fM< z*&ej$v1VEwADc$0^kER&^KQP2H>sHqUP~@hPjM+-pIn(WLKS=i;zyF886rOfwcG~SWpcZf(8FkDo;*l@b_mmu0@<%>zUi^4H za3F9~JP{1637olKeT8AO#LpCd*X}8NZ@CcuGUc4U{u7zMM1iMbN%$;>E2aE_lx>iD`I-~EA&1>}5J ze{SClCa6-_FIv{NDjtD*rL#A77e9oUK5!KjOyXiq(Jbv!5FYc29{m0o#de5ulCG8IuRwo)~ogABjgo5k>7^Y`3+k0 zp%}upD9Khk6L@J7(JY`2K*%Ga7ZZuNDdtaxlcH&AM&}0?iQ2=NKf(+a@{SbCAp?zhK_72wUq2XReQ<>KhcSTz@B4DmL{%LNdwaqQvLMtYlWx{H+T-98S6UfQE zAT;W9rQl99>s6XG?6XCX*2@4&q?HXLY=-DFMly+72lLRE{C=&U5@kx?opN}Y z>1%lJdFcr9PU<^BV0(vm2-j@Xp1gjzWQnp&H#SnK(B9!CZS}av*5?!j5>YW(FIJ3j`YYtFl8}l_OwEY67JKNX_z9WIaJG$>y8-2b#1G>fBjQ$v zVXv8JgC%w`*X(cFYR}(rs-Tu9xScL|M%?)H|7McHg+-m3Sg!b-`-rFKz33?1 z{41Q+kE6n1BxHjCBnHGhLOPMg0y&T&FQpODrz|oL!uo242l+V2S;a1@;`NY?fK^EXgyXH&fQ%;587@L3myMbLK z4nY|a6@#vw(M{ur7WhNMhzjoT>Z;hQwnH^$!3lAu>&~t!v}{Tx!Bpd})t{_~CS&v; zKp4#n>=(M_X4idsewMbzIKchkAQS_Z^TEd-dv+&5A76LW*R>Os;1=2u903H*DKl0A z>F~C&aLeKlNivaRb2e|SndC~+7jijy`KK@+>Lo(U!~WM7*A@7|v4Z-plg}Pg)>84i z8A*v7!ove97K;F*W=xwvP0o}GSDM)H(F!=C#3PUHsVgb5c0I4;pchJEe0K6-L;w)K zm}(_KtRdu=QtGa4ufwNc)&K1NV;`1ip*cLx?xe;(Q|j?{t&Uj&D_%oWFeM9p`k_}( z>)jK7xrQzLGlw94J8d1MEtQ<*wfFzpwD$zXlNW)OxqqWJwehIw=XC_^MXJu(+4k2x zMsI=q8Q!3S>O8gj>!=9Dp`DA~{ovQNJ+TWaZTOCw-c!k+?DcsL5OdDuRr^0;9NHno zI&c$8OY*3%@1Q{5J?G2m{EvL%S&NOnmO0U$xcg!6<~bgT@<#qFb7<4(bB$^LMJiIB zezLPaLgV;?_0JAoUk{e&%$M=90qjB7uou6nOZxCnc=VO?0VI^>0rGuGI~V(ghG68% zSUAZ~fbZu5I`;+-8$#Ic-;rOp`T+07v=sNq8ZdUHW=$`sDzpy~Nsr)X9GMK0q&dmf zdB@*Kbdb)>oI_>@arE{@xpsL{qNC-+F9&@7;%nz@ zb1%+UR`5hO&Vc(;s%l4=P=Z)9K#PmYbYWITY@-%`8l8m(4P5sZVees?RO+5(Rv8VU z&zt2hDm>esCN_}20bvG^;Ys69l<{JVsrk3QKJ>Nk_*0%3qi>2zEy3Eqf#fai4Onme z!n*EuvCKn(&1HLMq^D?$ug=b%)!MXvxUD^4IXkd1PfTyHjKbgEw|_ooJlt{Cae~Xp z@lkHH0ngBN_hI+=>5(+x)WxEM5+;S5{00sjA(%PoOSQ(d2W-0G+38UwNZb$sx58sJ zG?7B#;@>VU4)v7#;fLBs7RP4~l~Oh}w~S(u2zp|TRi~pZ`-B_Dkh#@pYi8WfEus^J z$?vXug@|e^2+%scZd6C!y1t+C;kRmv=}*ViJ-9+Epq8b0H4oYQ^M!|U`P~Rsdy{na zVj!kLQ_#I{43=Be^y4l0qt|<9;_nY(H~tlP$n?h>`dM=M36_D5UWaLM-1AzIJb>SwzP{{-P$zL&aVdrA)mP&|m9*kkcz%E!@=nXdX?sUU*K(QV>?2^Yz_0S# znS+0o=t2h;gP$9kYji#}r#jd>UY~TP$cf4xI^UMFVRlzF9$0JhO^wzRn>qbsso&sy zMK@*T)Q>Q;Z?^%&V7k^r8IOqa$4b3k!hSv}nM67*!i#?E$}F{R?)A#{`&>?HRvO&+ zjhq&UYlQ3G)gR!+UwI=Wl8I=tN56sc^$Sg~Q%Wa&nVNq_JYKAzxN6q|Nd01e9^3Lu z{Q%pNPGSy4FwG@F;>u}tdi>Np;!EA-Xa9wX;zlMeSZTp?DCJQ%iMFX-&ll#QyU|U& zM#*+J6)K=^?e z5ssqyZ;Q|p+5`yIMni3-3Xe1UF)RL=|Ia6d_BF4!JtZsaME;sFwt{w^HsH6e@dZcg z-&uS^xSO8n(A@}=CERhe5sUb{sYw{=U4j3>1&a5#5zy(Ye)ZC z6^r5VeSY#q9EFYl0fUtX-wDCG-n7-T>q?Nu;V>bzIKl-B#o`Q7Eqke1>`CA1arS$H zm4$lyE&?l+dxn*k93Nd3E+prSi`jc+=nc7XYTJ%a(v$HS)V*~|kGA;%GcVTa5}$4I z1xuZ>tbFsF&}EO|%!4AurqSY1QIEvq{jF}bZx{<_w^EEmyxGxTqNuQ2Ng!#HGQHEQ8hLg-Na+CSg5+r|5l_)K$wVbp*d!jDR%umChF)m}K%jI6XtHyhR*%MfL| z?6d|n)9uFT4?ejop4nhM?^sM_A`P$}>Fe9^CrvYyh7i4pW0alOlcxlB*^6XukhEU$XIqMBu9!1BOn=|(JpZ{K)S$S?+8-MP6tv{BDN$*3- z#=}krNpLGMq!o8{e896eIH;d@*rnu`1b%XbT#2dX{!iGGN6ouMQnDqF=*8&x2KK?z zfM7nhvJ>Q?dfOjn_vd4ty$)&{&ecR4*%Q=`pHDlW7?hw6@9dQ zlI~<>Eq;g*fJAOePor+l?r>s?S~F-J#buFJyx9fk7!{Iw#nzHc(f|r`0^1*OeB#?*D?)LZfcn)+>~2kp_+jh=f;Ih|N3HLJ8yHvC8b} zesmZ3m;`@=UfmYLJRQ7ReAJ?(?&1ei{ZPWSWPh#8_vhxrOZ{o}v5tlJNJS>csDopV zfwFnKz5^pZ>$qzs=#|=Hys1<9;Vv{`Vw&FySlnghwAUJHBjyW!qOMx6=_sbb-9r6_ z`IgPqU@QJdwr~mL`H@aPhe7u`zo!+Ttx&TD3SnXCgk`hRc6mGh9qgT{NZe!EYR=Vv z876?3ml}t6?RC1!tRl;5^Gmy!2xo{0c`*^!&9=8W&e}x5UpyYb+# zUUT&muidRz-pJU~VwHbt@(27r&m6xmG_tzF%7~%iQ1rUyaR2#mt-qE(Bpv2B?>sU1 z%QDWHN$j2eadDrg$FJD-Vu?bhJz12Vm6trZ_39{FR_oUl4S5f~>utIz3jd=Qon!x7 zFhf>Ua@+QAL#Kxc^oe>Y&RN%BZ`{^Ox{!h-=oLF}LC_~DpS9TS9u=fVJt&e*bm736 zhn!cPeHX&<cl1yqx4kd>)i=RW+uxAqDblnUF*;=*m>YAY|m)Yq;k^&5-Fj2Z7S ze|=M5ET2aHAFqU*O)o?rC7bd-O-O=D7Kw*h#M=>!?2a$uqTVT9N zH?s?wZuuOR^9-i#OG~{{dD-Bzy2{rSr_?IxfrkUUL=!ph-niP7US8w@k2GyqW>}Jw z!daIgrzH#B_7`RLSscQLN_BAYc+Ne=f%t^`hIfex7uV63%-sH-StiEibhBlPw5$W$ zPSL8S0tFR0ZEgH=DUG>1`6RM>yQlq!;iNj0lr^?71A5vkyN~6(ivOd@JJPtyS?3bb~RB^p|COu z*Y^o*NmrxqJ1;GT%+%s1p4pIr18+;(SB&^EqFH}RZwRq}JEQ>w2zQ%rS=0k1hg2RVp&+0aqDSU1uC4t}X_iueEug`pOmfU-4biX)Zv(>|d*fNPQ5e6gz zm_nE})FS1X?e~`eQ(JlQeC^2$JJ(LfU`L1#?d)qNUq2BRnXrgI)i8h%B7);%WA$w( z4E9%z^7u6&QV+M84keO^=2@4#)Tm)149{;mw3pi+s=t+UTb}99(^IeBVVzwf0<+LE z$rQ3hF{_ydp@%e`C8nPVbOy%ad7|{cyuFnlzAVV-$PbOjitNcnBsj6Z;z{R)!*LBsJ4sKk=m1l(liTjvHup^c+|oT|U(iFlwH5o4?tVI#c^oM~N5FpzbPQ311?3%Zl@LH~jVd zY}@`3Z$QX3vP{}K>AhBNiNE!rgi-akr=5!i+lS1pw#NDuSNQH!)ShxZI#9K{HCau; zyD^JO0VHUe%J4Yz;Cwq8FP#mxiQdvjwL!RFe!7fX#Wx+JV}6ZTDJ%31jvIY{MA;MES5%A z3*vI@8GIjXS94V#B(K<+*Y4^}<^P-dso|aOBE_dKrhQzaV@+NI=i4$Cf4(l%O7LoU zyJS)dJd|L?C4fFc*tHp_T1G zK6B{^b++o==cU10${$+ht}t~irfnSg-WfoEh>+OwlWXjc#Sf3 z=~Rr0jOgb4&cLW^beUoE#k0ls**^Q@w=h6;+PQWB3*bXmf~Pt}L}F3ue~x;e?CL73 zzh`qaUwLhx>-UZxyT9Nh8uIz{v&2Y(s1(_ zM4gPae8VpBe2!bJ{*5vQb~1+Ica>~M;$v>9Ed>>5wa|7ki5T#_4KJ`gFD|ItsKHIE zecfXF!V`)$RaOggo$U*8X6S8F821X#19@jEaJU3Itoq@weaj$X(U$U;iYxxTvj$cZ1h*F-I-~`?$w>iPq{%69NTcBb+3r;Y%ouD@LBOt889d z3Z2V-vnwraA7@qhQ|#*KiH^EyXzB2S2&Sq(tV_mQd@zFwuX^_=skSPkgx=POf_4<1qBDMl?$kCYyS^*%l0rvB=n! z`six(k&C9F?el#^kwLxa{L)8NDr&Kkpqb z%cqz=ctgieE`VyjTC6rF5lfZ&W9V*Cx687@09iQl4A<=2DE2Yznt#r1I#^qzSA{mns_RqNSQgyCg2#OTyD?XMechsV-Z?WdUdW+RrFY-HaQobt#G+8dc3H(1l*KN}XZlQ&}Zyb|8g2B%aXFPXNj zZPNDwJY;XLoLt!LbTRxedM7!$28oNba}AXwWyGeiP&WLs9J(#_#+0w-O#nc^0*QtH z`}s&vHNGI5ELIZw#~KH;_4h(ZpDtl>KeX!=%$lLE!Xsj^Ev1O+_+$1fd%5?By=~)H zlRHVrUPZ_wNGVPWrpPX|u5g*m8zcn0F?_)x+hs%*M9jmj_4)C1)#0T0Bu~NAkI&qp zD{jDg@Q5+MqjUF#P(7+$m~&XTnW7COa0X2_5BaRm;V0~d;4n&p%+06GPwuwMDPsVAns>S+#ep)p7 zy-qynZ8UQk=^g~L5jXp>t@^VA>T}{?(ElH7^|dI8DeqSv4@qZ`nr?{2h?xC`cF`;t zU6#co8DQBuN)HpjSYJNayZ!hy*_v}69v76BQW;+b^{vM+!bnF;&|Ap?u9!pS&TkiR zBwpUJ+^M?Ii3RW(L)UR%A6f;|RRw8=)l5SM9%0! zq)XYpz`~>JPbhLYZ;BMTptj3QQipZZO%Es|SBG5`#0H^B)bQ9x38ifX&x+uqs58&q z?2mE%OUQqw-OKB>;w{@@PCIWok7qi}A|^ZoD>e0gJ7M*c_U7W}T>uS7=@>B;yPZ;Z zL8)@gZ$iGtuy{Y`-5=C#C{l$ zan&Ljqg4}Ok`s1#g0NB7XFUJ?3_rt^5gxA5K?D;?$I6B)7n~yG_rcYpzPGcvfm$Rz z!o2gTbu+BW;A)|fT(P7kv-~tu-h7xGKaK*aGo0wB0v9wQrQT+JExijpYHxenqem^g zrM-#c@PNpEcvs!r-`Y?yv=2W_G|Z!)_P&aUkjwvJ+&KJoTGTo2>VstKD9m6OG586W zfneXNI7Y{Y#w)AWWoL@ambneHTeARjP-EF1AfmbWp=$xZ*+d_iJpZD?%3;&8_)!s| zvR+ToOV4GEvvte`$8Y!k z@>h%Wk+xosWBoc6*Sg*G6$)f%%ugiePYAJ7xF}bnVx0gzRb^tdgWSX-p7^)<)f3n| zw#c+{Lz^E7cDnENP->!5DS>)l>kuGGC}ZSNyhnw^Z-|_B=Dk61Z5qK@FWiI19Twcw zECX@k-mfs+qMp+N2ZSdlx|Av5Z)V7heZVAUP^)6#EAACzhfdchlM+bhhhB#+iT$2oJAn2#Ql!^cMCs| z6%bC#BW{+@(~5DM@hUc+Lds=bHW<54)nI<(|`!-WJ1tgW@VFSGNDl&P+&;2@M+6Hbm$&%S|$g&3t%9$U| zFZfnpCxQZ}AuE5EFD3BN^@&4`YfT@M3|$ZxKu5`lCpW<1V$SeoKGMHqoGODC=(R?j zayDK5$Y>_@n_R2K<2J@(hy@v8{&t>v=vYa2Qs~WO*PP5drtJE+xLPbnY@45QopJUc zwAPEonSFAw0H{AiAw$6ffVTPu%KV+y!5Igm;Xn8K(PPieg$v@HYd$_`qBjuy>}}~8 zP)BHzqOPzMq1_7q;qa#2vx~%7`of%G9_yN@K;F2%sW04iqWZMBL90QI4?ecyt2DFl zrHMs~NISYa**oRHA~uvfE{{m-*k{F~7Xu1dSjI<^kBTgUgB$Pfw))8FyGPMa2==}^ zOgIU*cE&w^#=~VD`QMDy5c)fR0k^*t#QH(#;2w7m&Wg*tM-Q*d30!o5c$V?sdLBgD zy6wo*iQ);b9qV7{Mz62?^AYdS%^s`{|0-6A@sO3uD;E-fkO&|04u~II-)|dhWWw@ey}Kf)>aZ9C3?#y;DYA z3h8Kiv(jx|Y20NhBhqbe&mJD*zSJH^oet9mw?nu75V`*gzJGd}-^9VqiS{2;TUfod zoflrDWtYVZOEz7jwj!4K#v$`wv#s!$BMw^Pgt75kgc&!r6)n~S4Tm60 z;GN%s$NvXK{kwJWCX$jc!?^ZJFa{*j#6-^;mjEHbc=V=DH2db{$u}@*>R*@Ad>ywt zJa11z#a9urMjaNgO8zp!t*7l3eh3rwK1RX$3`D~R*qEuhLXd?HH3E%YXRkhcv|GibkSZIE@Z>4X#<+$7Y3UbC} zAPNXe6HUml&~b~>0L$|xj8@r1D;wco{@)nSD`5K4_&u^ihR)!OX(TwN#z%-3o=t=uzy!b#$R_50S$9s|}$#1B?&%v!NO@;7P zc*Z~f#Sr?}^mWn(ZZYxeNO$`!O1wG~ZPQz@Eu5Hwal&5Q;8{_@X|Zn} z?5Nc44eyzW67JlR4;f5EqX%IhJ<7GBj1ddX|IOp3b{dk;uf!eA+`;Xe5AgK+oBO5L5Us~e zXaOUtLu@hbIWN}q09Lq#7su(K%6$aU+WC2a$pK~2$`NsKXmnJJ9#O^So%c6e3N`d2Dv9=Me28V za5KI%DLDm#;`)`3go`2oaz63&XtbR*XOwH!kk^F5QS9;0=2>xOiwM+)X~QUUe!$at zX9X)$2!D8et`l+af!boS$l}|3E7;LeQ)E&cSOc)t7LE2o@bGlJH%cpOvj!41=^mYg zcZie4CsOt*|2);y!Ydkh37*xX{nBav98^*`L^jl2l}vn5K|mS&5s3_2LFLEfn(N-n_w(`2l_8214 zua-c;$Q9J$_VETbbHPVs?u4`kb}s?JVtfbh17W8x(!0y-B{M!h)}%58im&v_tfuPx zb<^A^$3ELJ%I)G;OU1%eQ4w<CvFR=@4Psjc_=3RHpz7uDitrGlHN09kt!&0MgO&PF>xeBlXh<%L1(;)2$fMtVpWk+4SwCA6mbW0xA9HzLI(!~RMdLGQ;q%gs z6~W%Zs9J6Eh34?^r~(~PU52vEwB9Dl^g3^R<5|E@PPaB*ccm<5LK>?_OUwq;)v(o6 zq;D<(DPN{OC$!v3PUR_AP8DBn=0{t7hlj^txLsjMF=fF&Ig6Z0m(H9}BnwT2rCeJ^ zL=t%ub5=N=ijQj&fqIO&&-#mPr(w$-*h-Q%`Fr~YxHYvQ+g$V5pHC4_Sx=V^1N;e} z3{x=l+txBp-TI6pQO_BQK`VJelR;b7pI8+2aMAa@t6|NIVsnZzo`PJjSK(I9(?9G@ zjKBTLoFT9jaj|O-!(B~b##KwA%P@Ra)%p``_{LrQn3rg9CG8n)H}3_@U^kJCjC_@9og<^R%I}ZKQ+)jz!Dxjv>flD{O!?JMUFQ5-TR_dp}0 z@3~}l4#w#A|5uww`(U1P%Fcj5QD@W_SF!ldhLlsYQ#e_1=5_QcvXn%m7Doy}+)gFw zDVek_1|Po*Wq*KEieunZ5fsjFyYjrl>rKZoTA|@;{cCHFXx*0Tdzzdil$o69fbt7X zmp*8MMNL?rg=*U+Sc%5XO~GwK ziZj%P)YuA?;cQ9NYWiF7b~aBZ+8uIbToSoBXuL9+vNq2I|I-am)@6u{J#5YbTqpJe z~wb7bW5CL-Whk-#wq` z{aS|J^>{eddge|VmSqYj0G8!J-S69Y+~4dvws`L`Yr08iR!)dBh{V559xGm@mL~Tw zmy@_IxQESRz`M1{`KP8d){|spabCahlHJ1BiS}7&H*9WtP^2{DRk+n@vXm<}0;rhh z#3zeZ5rkGo-3+Dbxra-yd?vBg^3zF&}GL9 z3Y7#iv7N(XxAXmmch@hx+Hvtg<*99)Gdi;TZlrWx3PIF&S6fEuFP0&nAOl&o$;0_d zSFIVQ>dDYDefzIcJ{e)AGDx6O&jGkyO4l zCWOQ3XLr2V)5d{t>-&LE@Q?+$k{M3`#W z+c9D`X4jVQ8w(LUm(znZr&d44?5RihQT_!g@a@uTNck&ju?XEZBcelr`of1+coB1F zx+2-VKbt4$sjt57sIIFXAodz?HLmimh(sEpE#h*-)I z`QNzb8c~bNzN@`gcSohy)iDfV49$2byuQ{J9ATjQ-*NOK!A66B0E) zw)!k>yWw(V;SKv1*FjnJba>Gr;RuEi(g|UXZ`?_7{&6Y_LJ6Wls^VUSBC|{ViCj{lRN#h z?~9d>?X%a06@nT@UPz+^;lvI!f!f@8YeH7ZdZRcM$NrP-AIlQ{qSJ?W+mE4Tk?XWh zy*0R)7Hd!`(-x4grJJs37v%KF0~f|Q$-Whe_6!e?7Q^6&A@pkN-nH!|5>ww+bG1fI z4fEZw+i44TP-~qm`b&%#^F_6pZ;!&9O1Wm^L%4%R^oZR%sN;$D!7Z)W zUt3Wd_%@&WZQyUNcyRQc-gh??WJ?1Xsz^{I(xJZ&r`mJF)6wI;dt zdo&9?5r5IsA;7`tk2}EOzn_|}5D0#WE53S(9T8!dZg+AHU4upM4bT>V!{tlU-ru^b z4_Sq55nnNbrOfVWq$pKIPWiPJuNiQQcd7xo79HkGeC%r#A!LQ4HQ?4sEb7h@x6fQW zWJX|=%fO-Dr>fOlr-k2a-YV*;C%AXa)%zGHT5_eGj`*MJrFik4w%tXys9e+=NAIsy z7h!LiJ@hk_Wy(pvZw&anoM@dVbX_rrFqvNgA9IZl61^v*rfpn60X8blJ(!JIdruSPnYnT{gLd}az=TDNqBLdSP!4Qx-pHdzOef% zxbhP~yJHw3`D>z(WkW-P*yzOZp3gd{^F_?lP<$}Sz!Em< zUY-o5`)s^7RZMNJh7=djL1zP{%+brRjv6?nd@i20OkF5qaAgcwYEtmtmkJ$=BRNf9 zs)oNsM>)G4gPo}KKk$uU_9X-|F%ot;FCXx1Cao}y*k}|t$rRqp)F$Kp$-xsI-OqDXWdr1mYGH9A@=#LR0>QAAM5nJ zRWPciHsZisWV0F*SyPUAu4I-b&1HVCzrUNdUSiwv+HHRTZ*W6PuE(kf|70I0(%Pl) zWEd@#B)OVKTZ_Q$Tx~e4m7$UDZ%OiC3L2 zk7FL?u#Olj`5Gf}kiLy|1VF{}?Rya>JJRvm98PGTU(9%%k$;MUQ@7HyIFbJ@J^o$) zJQOJtJZr4Y0=7jY(gI&bG_IPE){$o$O~0az?zAtyw{+7-bgM|s=~M}m)#KdJZb};& zz)w1+SW-m&{BQB`cI$-rwY=zRva~pYw3`Wqd-R>59BfCJkLnSQ$P91O6nkiNJ-0dBDez0dHwJd8)z3 zZ`msSd|Yj@cxHynPp+K&sVea5^5f<5lrVJ$(GYen&b({{$&T)yp=~yo39Je7kRoLi ztU*>L`>c(@PZlYS_FhA#%8o6mmjtveEq$I}f=9h;5IvM@JyB!{l9rucN?V|}>>$1E zpg2S^xC!WCV&4Xcgh?^Qc0oRY0`k0ONq~6vSI&;e4kH8-n8)b zudZX75H6>+V=%hK8Vp3snZ*G=0hR-c$_6-;MGgy|Ur8j4Ite8h-zjtomYKT&>S;oZ>3f!Nr`wC@#6Y9(?Dk07# zbwl@7`t6iJpm-lyrAaS(REaGRS~&f$`@mCa?cgFmRYuGj8}$6|=Pf+kBqLP9Zw$~b;EW%Qmb(twJgi>kn8#(? zd3~5T(Gwl7q`2o(9voyXuDt-s@b3TKB2%-_UDhc)JIo?hExY>7d8)8YJzNCcEULw5Loll z63Urc+}wPDR8fYKaY1_Pt2nT4a7w6;_pRuf>2IJCL~_0&~SeH~)ot2k+Oz zHf!lbm;J|2ihXdTmV&9#US2aFwe2<`lZZi`VL|Ou-B}T*Oq|TNVLZG=%tA%p9ET8ml-i?2R}g?A2N6Ny}w?%T=Y6?E|$nlxwzW0<*XWd51fL}e!#A7LWY(JWEHX~dAMP#d}H2pYj0;_*kcNMXC}=T z0H7m_WEJo?H)Xy9h7!4TnL$+ntZ=kkBAqrl2ET|}f}W)4hk`(>BcD4FY){Ub)6Bwq zV!fC-QGLwcp+K;^rtG;z9*$>_NiP>+*5<6I+Sa`urgZAf*YT_O-+@Y`YY(LKhDE0u zFGv1lW}GAhT*YBj!~d&Ibg6YAukL5VN%}}mCVtm1i>T$rb{F1<%@uWP zPRGWLP8Uilf4?S*!&n_YsT6F@B%Fp%)xnI18mG{rDSx0=`%&sAw1{!_0De@+TzsFF z#(u`kVXngd6)HqnsWF0d*2HBV-cgrK_lo-etpo1BuTXonemvl@M6ligH_C)b*!=$* zT>2Rtbv&$nded+_Mz&FDC{4re+0ltsb`4uqCERLx&Zi9_s%~&+kePeu7_pcJG!yEzZwNR4c;s1Km!osIJtp z{e;O_4oFyX;|?gPEs5y8N-R@))7&_^HJA6}-Qf_R++d&RyNVjYfbHI^v$BY}-U`6i zDuzIJ(R|ZIIlFRT{>Zr2C*D3Mx!2|S?A&yEe9OS*+~@w`AwDHkpmF(KQQ}Vlb^5iG z9e1-X$G5qUZAO@i^z+*SgZ_j0N69Gay5jirw86}+Xt|FsMdfXWJF4%zk;**Am~NO{ zV8+dL!;`|Fyptw~)d>Qjex7B%+|f4^e_y$!}hFVfHOe=lsj zDmvK=qNw%`!zkZ>a93*W&s=_MM}4~l|AM`Pl#(Ime`4D!1(t_{S$xvq^|#WVsA(iA zWdi?ZOnl?n+=TUz1;>TVwUcOn>nF7Ekk*r9l2}}~ipBH3Q8>1CE_Xh&UmV~&>^L9u z$gs9OdW7mCKhpe&JCJaW)sm}OIN7L#D{w#~5yBa-AQmwrn+P{cMDAGGFqgJG1YK^BT$MzfKx zdiFu^LcquWgm0McS1u^kxR!;_|TQxfUa34na6tJ0x z=l9TtShga^W^riOnE&Q3_RwqVAxtr0%lJ_3Hr~t=S2U+x#<#n~cU}vV|L?8)F_+@yePI*t# z_TZ0uZsNtDx@^P#3}yz8BKY32D;}}`t({XM3>!Wvcr<9775h4)^yH9@VfB#D+;8Ey ze1}X(0cHIVqxiX#X!ZtfL9$znaBr-PGVdFxk~|rG4m3*;fNO=9(b_avQ}f*&s3b4> z0p^MvAU0LxY&Qa>zIFcpW9hx)n!4Nfai6EdwhkOsS`pIL)yha)8Oq4{C{?Ny;{*gD zRaArsDP@o3w6%&zB~>3ug+i(bh>XaL1W2l&s0a}P1c(p;86gQFgRGOBesB8w{pst4 zA)NDm-{ZQk`?|1ZW=v#B^eSr(;O|aKw0m2WewIASJp5;n_Uaf$Umg6Qd$jM+x5U{6 zF;QxIs!RQ`$zGa^pdpbeP-IJuP;9m}DKg1DbVQ_m;y6ERd!cM={ZRnm0&uz~VR3%1 zbO;R%<=&`Z-BQ~;AzWs!az&QN^8)jiKXbJro%gXVgNer`yYW7~SIc3m2ace%e6f{n zEgB;lZ$t%8;lI?6@iKF~Bh?YUbDW=a2@e_@L~7gLns<(x(3-#U$=Ff0zC~zWV^|%1-~s*&(?F0eb)Dn6f17*OQEl+*S#{0>^tgaMI8h`QHR`%=L+q&PVIv{ za$+?5O5_VcubV#yX-lK&+b#P^y)ob+_|GEfNIjEEt#AxT7? zL`8ItUwgWK0nHUg8=IdLZ@Hl7iaB(#w5XxM#P-i@#SKHwf{xvW80&@+*I~oDJEDzW zdy{B`u~i$vBW;9zs;&k2EsKKC$TO^-4Wids`t&f~S*YgE_F0~cIA+Z>vm4pQ-31VebM1RpIx*O{J&Xr#h< zA424m5q;?Bm%P028Gmx*)CyDe@#7&3kh1AfN14(f!u92t0}k2!uOV-E7#Hfxvp$1c z%MzLr>+1Bf`MViS%Y6N@@3zrP20ABFf80FN|W_O`%*ge?kH0%Eq|G*P4B<zo6OXTiIt2F)9+92wlb9iJ8sXTaEs|YCpq{2qHneusXB;b=|xfHJ3Xc z>bsSrUWz=xyt(f>qK?)imlnFfrQqIM2DbQ%p0o0JyxCNk@S`a>he(R56g;Xzi~N=h zjvSLTfA!p@GS+#lzHn>>0%Ji+Il;dx=Iu#mYyLA(3*p{Eyl>Yv-R?=M2z2 zoZ(`-dk>)`^BldmoXPA#*)`f_emYBplOKY_jGMmP$C~>8%*s3{Jvk1DeO;KU)PzKq4r4H8=yk%QB9V_H2AGGE`2{=LC&N`nN>CW z={!%KzoA_w>-p1@DqmUroMFZLurhBBxTBs2NG_=$UQ(4>0y`Kn>xw%&C!s!eOb-oi zE-$ml(S`9D)8$;_j>_o}?f#f)7IRs1_>r2uRO*iAh1f8z5i4N438SvRnBxEb{xR=H zh4kFzF5By;ev}N;iw<}M5oBvqF$%?~sV0?&t zYC+h=!MxsEdEXv3TtD_IEP9*2KI5Uc_uza}`$9@+YvmSyJ%zb{J8IQCY_FWi^S@Nd z7{+fxZG|pK%rt>Bn zmAi($n{HN?Tycr0Z@f$#rt?ypV5|v!9zC^^vUczKG%p^xPWFvNy}Tp4lepm1UP?4t43k|Jv9}qXo$tLroh?DJ z{owc9VBNpP@8x`p@7U(7m?G#QS|VL;C4QK^)*-`?msr4&xL{}rn`^yJ5`@SQsy{uj zb<_u&sST~1$}erT9r}!?9fheKjtXyNihri~r`V3-S#u+!mc1p?S1{6U3qR73_A5VQ zg=8dTaKw(%u7HgeQ?0(Yq1Vmkg88xa!enQNecgWg!d4&O@VWmD5i-FE_U`UDwA5ImA%12Cd*wSrVRVjyGN=!d0>GAav9eUg z?=zpC3xE8^NPTTQk?LV(b%J;&7~yI&2Z2BF8%Tx%w>22d2T6oMW369rT|nU3S*Vw~ z(W2viFrA=O!^9PHsKi;SX)+~EQ5H3t5U7MAdhQTqj_l|0^ouj8u@03)XHP3D#(`(j zaSyBQM)zNjr`6S8;blwgy{sz$o$2RHFNTw| z{OV}>`xVk>7e_?TFOPRY+pwE$X$l)!&K@=6yN$%ZF@{S@p#4!7rxBk+A~*FXlvOHV!v&;b3h)coBK?BC+LQ&^ zQ>mlx)eV-2h#SAjk#}k=(pSFt4YMWhHe3`b*+4P-i#dpl#aIjEm#1+;OtW4vb!#>*%#GsIo#=1j4kk~-=FhC)UFEzL zc?g=9T};0QZoQV@4vAc6Cp22HE#}RM;Dy~Zj%~{+?Bl9ipR8kDz*iW{kM$OIj*N+( zW8UlsKZTBj*&!zNLXXK5n1CZ3o=Oedhb%Wn?$pH8=>+P{?4s#@TZbUFl}U@#BlW`} zHYRw7@WYFfoA1l}8wrxtZ9{>qxq?Z5YHDSjv2v^))~}uMZhdG+hd;)3XXt-v-g^@(Qd*BTce^dd_CR7-MXP;G zwcaR0Z;wRc-hweoy2zqtXD429ffC|@K+t=OD@qvL)Oa5k=D-@AbA;lyMg<^(Rc})h z{~LNsEEX*D_Kn}fufEq=ws(2)mY?P?Rk%1QW2b%rRz^=uC49~lCYA#|g!HA@J81l) z!~1%#lbb=ta-#QiUE>2-Ag5bvQ>p2ZQHlQoccKc0ScIca$kOO^;%So#^}$1R^}?sy zKDc2|q#nAUG}Un?ObJ2X!4q0Oyy;1ceo292+q$C@1s8AKJ1e%eodls-_DRfnxYS~V zcC)Rt%q&iQEm&$4jxEme_RibPbSRH69;UO7UGJ>N`|?J#N;cJiEzj4JjsJrWU3Dup zSdTk(yH)Oy1msDu7z?6Dbvbzz$hrF3W>CKE6TJB}J>stQ{y5o^S|!n^8>|0W^nsy$ z9fKJ&+T_aXbqnwf5itF;pH^p+U(?I+U);k1UYJ6a$8~0l;4pLR#drCejy^uifpYkf zf=4w~I#jankybPJ86#e&W61l_A@-9E=SUqKzX~}C9he7)+EyzoS-%0|=`3O=d?fH< ze3AlCX@51A}bTFc-$ly}&eF9-deLiU|& zm4PW0`w{>1#%>qaCgv6;D|{`s6n2)La1!$&$`ghb_`t2m6v*rDVh=Xn<0xJELllvj zJO7-{@bE6)cf0jOLhHtQQDThObOQ{9(w|(PgOolh{dG1m%wvrC%doB~%rlhp&a;Xk zDOxl*Os@^ZD0*jozTQ!FUuU24`U5M`xLUn!pE~kqg$qs8)mu~#<4Pt7Yk*f@B&@A2 z44N6k?IKP+1Azxi7zNqtpzy}I#aHTui{&FWP!_Nrg?}!1d7}D{K^c0ou1Dz>5m9RA zXuqcoZ5xOh7;mA6VV}0A#x0xpiN42v-`)V};4E>LQ}I2QppoARqd%ZuZnqN6)@ZiX z%R&mJOjOI}@VNbjI}Y7b%?lpNA{+A1f(oW+I%6-oF|F!Gi6knXUg+e%3G_Vr(P_as z>>!R#v2Ws^yG=G0QzR*wqkQJrhni-=*CTS+dm%9fld0J*U{?m`#l+cp&Cx5}4$Bue zH{^M<=YzsC`t?bIlNgbp#!TY2e-^EU{`A2tA^3cIq3fKs9gX`iFv%SD@|Ui0jEFNJ zF0=Qe%N@&l%MZqsH^+9)a7 zRgVqk&D)e=8yVhe@enX0ygybS2DtYOcC@WVA}5@x26XxK!tUxY7+%9HAV?}pVjDk3 z*$RIB6=axJN|n=9+}q#wF1^VmtgL|HP+5ZbD$koyVC0+%3)^uv(m z!Bx=NCSU<@En!;$EBG_%Zu7BQM(smh=1;nEwnNHtFP|U(S#-rgy0hms-tz!Vn)R^3 zu7Dji!88)PphWt6PASuT*-U$iabO>iExvmM6kbMzykVC@!9>|Y44{1!6*6@(;{g7y zc)soW0YUc!j|&XF@9|ENYMU5VE!1CE19iV3iLv;BAphZXUTjt|%mBg0qj~4u-BE8F za>uR9Rf<2#8K2KdFEBn&sU33?OtJokWTauzBqGo1~)9{ zFgRgId6Ld6-`=;~lU~qSm^Bt)^G#ltW7f%*2f1Ql?g-GwUWCAHmKZ9NhMXs>>2Ksi ztS1G_ZF?$Xot86iS?{GQBNRz3AO}HsgPL)F!UY{+>#}q|W8(OgqtK1*_E`7iF6dKi zfK`m);gBfg1RO_#5pXnlKayQXS#>;xatNja=?{^)E3K0f>5wc*GrW`jUM;M@L%?j~ zDk#&4`b%7BRv(1s3FS`JPb(~%5DP_QGVdmH9qmRrsmmLg#_1beAVTgh^5hCDQ`3oBU z3?5go)nEEJYB_N5>&3r$dyQhk#R2gS`SG-fiI7)RhxH&xQ`_%$#Zi&OKGoC$b(zxB ztipWp%>>OFbCBa*zcFdnR}U+YEahZlGmBi$>-CP~M5Wmq^z@0lwS9t`6ga8QSp@Zg zC{G4bPro-q!z#^^hWrG)zE}4{Du2lbg5-lWoZoRp%rwYHv1 zW(q%=8!0M&wHr=@JIlwQ&orN&3I#-|+UEXHWRBq64q145%xiRM=*VVx`|k;^c*nRs zVa4h1&Sz6zg_HGQ6JxEKeZF8f{(rqR!OUi4?5HW|7L~EOVXi=+kIhosE-;f^{h11E z2{_(cuOAm9bwp=IyR{5f^MDr6A|^e8>if-xvUz=_oVYqA-M^=@8A8?n!x zL3ZNyFD*WIL(n^xwT(AdInoqZx==~p@Ex4vrcg5MVBQJ_=g#->Zbb9J)VobKD^i?s zVGJ5MSJayHQt~)%Vp&AEhIp$Tk`Vn`2Yx)>Z2m3qcoP5PUZ+urBn}zhG}J!GK7bub z3%@coFhb7}E0WpNrWSrD)4aSAQZ3u8{aAHq?`!F=yplq56?2Sp$(Bh2%f8z15F7`_ zUqU@bu^5@?gg8i1RxW}E;nI(@74E#~raeqK_TXG{{)t9Fj^n4qJ2Q4yee?bL#` zU5ZNAq*6L&3mfPhB^vYZQc{8(S`l^lM6ty9Xxulym1lSi76H@_(K9$D^oy)}4}VjG zh?(Ago8l-S1(7hyX(0w=?lY#33)hZ>hf!?<*Zvp+UaR`-jv{MiBK(Dvp|?KkQNh47 z>j(TOH==4YbJ=+7+{qpM!8Of-$X?<+`7^uUpq{3}nU@+hkolSW68J*lyhq^&e}2J< zGRIcZSe%kPhl67_OD#Tgl{yY5<6CZXx>qG>roH23V!#pkf3?|7VIeAfH}ZL)ZCbz%&4O+hm+JaZ~Eo1zPF5h+4o>A{-0UpU_xIm63jZpfKDRX-=V za0t}p%geK$Wf0#s`?L?$-hs5!4nnAB1sWNIY@6l(EV@*+PwszTT~cf<9~WjfXwefg zG`cQlL>{iTZ#Ua>1^_G#u*FxlAPNLt#UEgzs}C~U$ESPNZ^gkLh`|-)ZLj{wn>bp+ z7ff1o-iycmBs!FkQlOJi=^m~8&+9dHF)lttp`aPuJ~*Uj$uRSQGD)n*AJ&-$Ne!t2Em*6+f64CJmjcbYp%6R!B!I^cMZ%;MP zJz+8!eWOiSy2-_@BQzMPtNUk>DS_7$iw~DYuMPA@dx_qW{VnH5g^J=))#a@C3QB9K zeD!hPyJ7SqWzd0ONa1o=Z-v!ees5FS3~Hs_8MQn{ksACKA!Azk6r}|{kaB$pinacHZhgkI@$VUc)EoCCgCy*u~A&#xvKT= z|8}}n?sDE0mZlk+k>frp7ZpkT?`iWV42t_p7fK&MrisY+!ym-#lk5~Fq|>P#7?QvK z+A!JMb0LuJXuv4e6e~xtM$h>P*39URkCOAE&zg}7%IoCI>@WaT4AESVY`GN3j@x}_aP*(&6koneFckK@ptDm1IHfWpY+k* zTzX91)K)44h^U-m2Gq8l`6gZCIAZrG?+G);wSO&xu+7gFCPP@zRquY7LmPk^a(_xG zWk)~jA^+yxajG9yo3Yi{;g!dO{jKHR!|@3*?;@;zzK!dXz9)3Jp%25-97+f_`|^fn zhble%$@~$j&N6Q7cF4bNoSl3m{HE8*+jmkjyu0GqKND zgj7%|%yO=*v#uLy#Yh^OGNJ-41`sZ}*Qs8Jy~m!oIw>l#vWBTDe!q${u0g!odQqi2 z{|`N&{imxWv{Ll&3&v^-(wA{;e91qHmIm0s#}#47;07_NW2!X{sMKUdc9WyM$AY0X zv#E<*`kl`DjxiGRcxBn3eutZlHoW20YASE{Iw^F`!o?Ej-BUwV_+v3p2o%%_^k=?P zzpF5-A&EC!uf*EXm^JwQ*xA1pYJfRP?zm&_iRz>__mNF*{DPx(NlGKFaS~Y9h1cWm zwV%QhHp>qH5AQ+t)53Eh)*6)nnf9Kaj_*vFQaZj4a~vZ+6s$VlEIosfSa)}?sc+#* zID$G-2YzY6IL9Ttq=ncvVpFSrx4JA~$*bVw?ZvQRvZO~jg)bl^@D=*u3l81Lt=9O$!lYHPrD)w_?FxBEn9Y1$# z!^GNU*pZAc#DaRK&2xsIbm*#-!(?&QrareN%o>vxUWSZS_zABLlI)yVvZp}rPc~=U zR5PK)r}$s*E8owlDIE?oNcyGBFq_BsPN;Vec^k~_1UoE8_h~SgDEXi5GLE@8EO4l&gxN|xlUdn)8eH0Ssk_PE`KGb}K2z*!KEaOx48E!?&`ESU^J>JOeV>4`C?)ZWJ*ixXU zGrDFpLM3Hz!Hz&@g7N@{tr^%-F_ZJ_z)M@4dcZnl=}lCDYsvY z9Ge*mzaFJukRY4(_%jkajQ?c`dUZIwGi)IVc$wv*r{)WpZ}wGEz8;m0|3Dw>S)QfOHT0i$rMdhtW?-2Uo& zpw+pOei(CcK6xyb^H(pZY(sZgjEb{rARk^tKfuqqT7AF6d;4hfO~MbuQ;@cgvoT^Y zkH=>p>U9Q#D^lN8ILQc1IR8LLDLs*vm4)s5xFSnDLuEG-Z5u*jtz~cOd&BtM{WtanXHsRHa#Q$FjFnVnXB~+MtK#gY1$cN`3~aoES=p%`$KWu21~E2*}yd( z_V%!cDNWQp`O3i-_RwY)`Mlym^S=%^OiB$-u*P-%82+Vk>ULCG<{~Gtgyc%+#2VyGhpey6pWHe(9K$g=T(}zr!iU8L zFQh)dqG!j(4IfPb{TDe$Z}Ac-bSWyA)m@l(|0D8N;x>gqt=5N6KA_vS_dB?XlqGXo z$*}KwUdGkE$gDSp*3)xT*^WwooWc_qKWRWCv?F%CjX924=Qe2g!nZxvgXp=Q(26iC z_c-L2VZ*u7Yg&yQB&2pDkyMj;UT0dLQw=aVDsef~HS&#U47dnf@S?KOG0h3)c=I~P zz<>kJZSLaSepTG$R0o$OWJ6N4(&W42`btd0mD-uaZli6vrCXh;d^pB5`r!`p*z*Q) zE{xKmG}Uhl&hao?bC?w97j;f8X*$l7_;I*vW6yl$>YaHpJ5{=rYSbaKVo2yy$i_2M zqY@)QYTk->SJ&ZPef7)9mWO7sl56>Jal3R-ur#rWttjuv$5La)8o0HW`^^lO3V<2) z0(NM6=c>)U&1CeB{C8u6f7mV4k9;rT&Mh zIR@noBl(Y$UwE=xB!YApJ(kkK$g7Rdij14_uf4&f@gwTXkucDDe}xuP^C}D3sqsKb z3J7X5Pv(zW*JAtmO^9YW-T|daP?h6PFD)BhTV1*c>j&sX;hK4Wx{G}Twg76(UDZ-S zjFe)$CPmxi3*KwG)trNBZno?2u1;6!iq5;#wP}+#N@d9hA%30t<%wN3?6B9Xmz4le zxO$1ifvth!^!Gr&H8OO$t^-+Yyxy8LRFw~djLeF<7LsEqvULXYt%V5B0=}JYy|rmS z+{*hDeURm$T)y>48F`sP+0v38HCbs5pSodlIuP%ev!cbOoBUNTrN>_$>Y~O(C|P;M z?Ke{I?M{5BZF84gnac`FMl~3IzMNoMoIVA3r{13nA!yVmuoZLfQadYSW-FY>SwhUA z2pVowv-!3s)IB#jzh;o{oz+k&L*Yv=jvuY%;==m^kmILtBbAwi=)Ni_!QhcqQtAnd zEgqv`8%Dvnq5xD>J11E*XYoxvg=GJKP(6~ro0^kpjur0%x4(<;{YC{r;w^O%M8Fi?lI})&)U4bNto$F~M#f zmQGDiIX4Qn>6*%kEN`i_Q;+zQY1$FK&6fo;Aq9%Uk+L-XjKFvL{xHIs+>Xhw1WFJ1 z|Jukr6&Gp(&~v&0kAqhu(2=#O9$ugN9lX+Ju6jM=IF*b(T)0Nv77%W{xd3OeS>B<^i4)oCP$dY z*FofER|=Y=Qe>a(KZ&_sGks)1|A3UY`N zyCWu&EgY|+3Cs)Z4eqb4`pCU8v$2ehNoHNhF!^o7xY&qpt>LbypRcd)kO4zu72D%^ zuK4LiU|GRma%+BcLC^8K=hQcn))w%=7@WinN>&I3|F5-HDV_G?njW9-q;2XYrq({g zg?VF?uf59edb9BbQ%YXngWCKT7d}l-y|yDmT6b(XRoe*tS;sXq za_H=d%m@iC@6g7*y#)tFY$77ueCOg0es_y=WzW!vBim8AGIa%d3+`g6g`w5Y`q+F& zPqcJIZMZPG%I@Z-uZQ=pcdvcuVTHOmQ`~=_(p*z+POvnHD0u_#o;BBed%t*M>bf4S z1sCJ1*Wqx!)FOnxo?1J^R^hJ=eR_I2tbH zk-_G)9~YpYr!#f*kk%uZYyYZOubvT9jPAHEs{fS0wj10YbKD(nZrrnY*Q?LYWMaGe zLoX>apu)imnX&0i_4ro14ysqXCZ_6xy=zIrjrz~BhYF~VWeM*^_ILh zQO{+=U@V=%sE;JhqAkW;TFkk}oY`22E!BhY?sg1wu&T|NU*G5iK_CkLzb9HZvnrq{car?VatWkR@C87%_B-(wlaINo3R zn1lUN*svUi*(<=du;thYnhAmp)v)a7|9-}?<;;8Xbzl4HDcr}UGL1>g)=`Y}O$5CG zE~~HG8SUS+yWgIm2Y!1_Io9iPi~GN!hi@csfjGu9&5PUEAIv+_!wIX6zG2P&SPJL@ zO%9n1thE_S!2n%lBHehLsjr(U9~n*%CpP5rg|iJMefFNpLo>~SWu6K1I^;l1LKCv; zcxZSOKXQ=&5E6hN(b5d|hv8}?UayT*o`}_>AIxV*_PRKU$O4gSB6mlI$_td^{uDg5 zY+aZzr~{`S=Nd?MYR}LGH7Mt{Lo&VEw0Z_~uuvK06g-+f;ZV=GU$c>#8iO+$r)GLzhN?|@W)_Gj*tchs zKFRQ`ZA^CsqsTRnFTg&50#;@LQr?GA2GN%l>DKG`FXu&naXhTQgFmtD*zUsoqQQ~y z@aWuizlL;`-3=K|ottlQZ(t=iAZpL*UK+o^^JI%^e3Dz1k~}u`lWvgzIE7^!2r7u{ z8}3utXR#oPSuJbVvnPTe@+F#aAD{uM{~Uv04N%8Hu3m8JMxzm}@?KCz+VKri~l4_D9p4 zZyXHZg$e=q2fk-E2R)IlYCemvGnQG)PehJo0}5K69(EWzBtFv9yu->CTGFDq0yW%b zLULGKmuCX!jsj>Y?RuYmQ^7rNx-AER3wmX9LHty~wgW;5Sb5{a?dBw;i%KvjT9b_+ zQ266_T{qn3_wf|0NSb465?xq`lg=jb?TkO)PMNe{HKI8?TRFzll6o@=eEsUNGuIQ_ zLsh0s^LXJaD|^lu9$jcZr@xmdx4O3;=)wCEBR?GZW`(sD!Ztx5lj23;JH{O^Wza_p z(*4AI;fXQIGU!QykB4?lS!<^J0MhEI_unrrQVy8e2ZKliBQGZY?@yg94+@2Q*48|W z`+}5wFc}caaD>J6^?Nvl=1S4{bKA-?+sB-thn=q#O^F4!W`nw4NmEX{B$OLF3tn(n z?gpM=s`%XnZ7VBqQ!JBw8|!^yy?lrFB16{Z@(JthV`$Bob_$7Tgz+)9sTlg60O62Y zWfH$4*<7OvmrUm!eZKeW<7dUItfhGRar!-N3u_dr3Tu^9ViRXw$~HeyqK)FN&dBe> zUNA7_$*!*1evx(Ypf=p=s!5q(2_pgQ^bt+TusC{1s=pC6@vp29qKh13zd zyapbEB9{fd>x~Sn{hJ}5CZjeWz}K(2&t@?3+d5Os_ZJBbVe}dmzRC{l|E$$m@?gBh z+{2y|mre_RZJ%^#|BoWd5-6RW=7mVJx-2$1t$|#1!Jl86JPhmHtBEj^RicwN(Tu0A z;2ufmKrx`EY>x)bc^wS}M67n(KWm`hg|$RCSfH=i;51Sz0j zp9_~h6BAd5HuRL_AHZK;cI@nQr;sfA{{A8AMr4`kcD)$R2WPn!g-7G`dzi{^z-xRL`+s9cP{uU&kpzS13gVhA}D z41^kqZO~f&8MgEA6qT66AYZw5El_`Z zH)2kBU||f6Na{;YK22ZH&t8~e{hm}h|L(2pI~$wDZp2klKl-J{Y^0pNtQqN_85tv( zFy6w{kmbZ!?DIG(V5~!E+{JkV($L0l&-5r`Kg)hm$16;&BDx4B-~?g2bZRm7rPVVq zJhQ#R=a?&AT^kshB>AymvJpV}JvpF9op*R7;I@9;-V=`B!3?83VUS{oH5 zozX#B($joUy-M45Nl%WsH4xh2l!jVR5aIEz(lxbgHhcy>Mb%g0D`FLLxtRxDdxs{p z5I-6)nbev_rTRFVWSkeKS_r%I%*Do_qrLT=4w^wXW*uedVW}*LB4Ooa4#wXZ4jcY= z4j4#kjsU|KKLr)`sdIU>-#*O@ZpcG)%T5D;F$JrT)*PXt1rhm~w}codLyxe1Jr=hU z%galTpH)?Wp&EFjhTh%M43i=HH7&jg<{lz9?c#^h%i$YQhm@diy63}u12X;Y_aZfw zs!jELJ|Glg3q1_lREmJe*>^z=Mhqe9(vUFdw#47Z)xTpZ zVm|VTc-_W4Z+BZ2tppIV-EmQgsR%g78Q^M!wq%6H#l=*GQPTqR$7zdaLIOcHqxkVH z<1hVUgLJw%#L9s~mBo}xrX^S0yyIRQGj{2RNY#}l?y4&!?Gaw#2pz;6jV;0ffv`R! zb&P${{6EhN%898*O2W~;SyJ~GnEgMCAgH+rBI>ri81fU~Z9PV|Ve1u*a)=abE~y{->Fxh%!QoEZyND$M&<-i%0F z2GM4j{4MfDg7N6cy8D_jZK6L~{~uCu97KE;?7V^{52!8fhb|JlczD5~B{yPTFY^T( zTFYZ<94&F`VI=${F*)!e{D~r|EN7q`w+-qex+M6s-v=60@11OzsEkPsbLblbVc}Q` zEK4oq_3jM8ZPLkNF94BLEmpMZpG9RoXNSljPjfGz!bp1^%Z#ajg|lFKp|maOepKK| zExt=SB#l|1l-)KX2%E3pg|qT)%|5QkJ&<%NYW!t97ct1i)V~Ni3ZK zO?Kh>AfK@;@2}>v+gF2Yor8ngSH_RMb>hU>`xY9g0Byidg18obJVEMDHf80UGi$?K zd1w~n;z&=i&kd;lh`CLLTdMY|52RclTgxrZaD-d<(}+I>DzXC)crOz>l>UIT{!!m2 zwcrC<5xyyrSqvhlJmWcek}baa;;HQCIP47c00m;fbdkBBz-TSfnG>pl#`+2h?m-Za zK`V_vyHeIur4n~u_?5&rWIXHaIe6dmC+ILTL{1PiHqh}tS9v=E0n@e>Sv(YF4m>~3 zTt8^M?U+@pb*a@O=ESC^7J?39^0fkE1yFAxeT9AeaDRST(9l={ucqO$bc)+GoK=C_ z&hS5x0?{wCy~){rJev%Mn!$D{ds}_nF4j2)l>FPxd5p5xlwZWz*7IY8%Y~ox&O2sP zwuYvAlizOj>ntd>zFm#~K=%wFwu8(yy{mkb@H5w7ARTQVNL;o{u6(&u??r)NR6NKw z;sC2;PJ5*>TlGdxYSF8iAo$IUkEadSd?$9t-wPSC^46mzr_jG!h5}`INOVi0MMMhW&IuoO5`6nk1UR-kI14mYC#}*<*G5%IFH%o})$dfjWTjrJ zv-|FKRN?$oki(jn@LNZlMGPCuslLR0b4h+`B<7vMu-`h?sti&ae*w|%UzS`U0NH1?QVf{nHql7KtT$mSKGj+r|C)Aj z=!l1{UL73$aeWO8Tu6OZW)$@-X}Api3pP?hA%=C8aG9eCc^6*1%%B><%h%Dyb%36; zS0D~uXwd#81z8vVWNDH}@Bv9U_rt4zWnE15hG4TLFbWEf) z>>t+@-w~hTr{l?Zv(?*Ee}kSYOO%iWX6+8urFEIVd3z*WKW<48z3vtCxKy4JDM)bi z33<@$Gs!xFNv)Ha#H_tkXr!|WKe>;SlOR;&^O+S)_lV4xq^BT6Y#@I&cSkS{xJ?e=rHtsZ|Xf z0BNo*l5&Eh_>1S-JXXyRXkcv?b|@tQHw3aZ%K-6B4)bg%-CrdTTf8iOm&nFIO zH8Q>;SP~_$VXT%V4QO*h31-Q(lD)7N#@S^V*?QP+U(g0fh8zvRo&KrX&edET@ zijNBV_}fw_eiz3mM&q@f{*+I-wOjCvioI{l(P-v+E#uQUe*V4CCP~JL#=dj2o4C1h zKki&he>tg`mYNKijFr~kxDc;;ge}Ow8$#aa_7K)sQ8IU)PC5tk^t})Sy^i)<2t|@m zKk(hK9niJB`~D;lk>G)m2c4ZN5{bI?^y~O8QZq(x!M>Ep8gQFtL9^icH-cUy-39Q@ ziWT%cEi&j1KE#l!m>SM+7HX04ia<}6*~UnB zoFxzDN{_N0&KmMIwfwUP{D7?G3oSf*TA==-37OttU9ulIZe~V8nMf%ONED1x(QtaI zCndZU3#H;zvp9ekJa0$0`FqHr@||&`FgWv2zjH=;c`DH|z4<{iuh{^xcU_;zd&lcP z=KXpr8JlL_GrXk`L|i{of8Jf63MbCrpJ-x`cscQ5%l@?0MCY!i7*BRdpmjZ)D?ZqK zu|LHV99mHt?RXIR?rxr;3%zKjb8p%XGDHO~s242ZU-?VcUK#@bfkkNsoD_nLepZtB z3gacnsDQdi=5{m{5O!W0b|9>uSs?*Nt&hqrpEB~#BE#}jrRvGnuIoj{xR`?qOUzyB z>d+3FQy?nQb6*0X;W(iNaEzS;g%^2!`3m##FCj0rVEgv!FBNkiWbS0|%XDvvAxQv! z{K$vA=Db63YO3BVs(G2-$GvNVbAQoB-tiT_BJ0+Ek-|F-(4wOeVnd+S9jHoD%lVao zH{7r)F^mu`9_V51>hVpLg6!tRao5`PUoT3JY3d6|&+Ryf!t7Q&FNYF zmtb4%Pfm7`SH24Aa9}6uRh}SyvNSBrdhwq{?tw^bofSn&7%MGjdyD37u+J-$bq^AW z_frEKH2#VDkI-&LI|#T}bzuGqpG?blsgJ`v^XVNH_Nd9kUi{OGF9ky>7qV)_-{{k0 zPRB|~c^|>B=0X9t6>j;lfYBzlr}+5gaq{I6DEG^HiSaO!P(kz8p|Z3mg(Wh+w@PUa%&`TBTx#SgifdqqLm0YsIpPS27+t zSS;Z7xy4kn+CF{LUn6nf}HJS|q?fR6WztKv2uAew2w< zqnY{}Gx&TcH^8Guh04$3oUUX z%CPQN?g6qsy@_J}ksuSu20tGE@SjB=hLc#ud@eecGTP8H#^v?7cuP$Sn=i2O+b*&P z3ol{D8Ifv3w?o&%jRE^LGg5_ZZV#OQh)%J2lX&ne*h^Er%#$IbHuu$$tu5()k8Bk5 zdMit&U0dVU7Jd!i~pJm|y`u-5eL&pS%1=jq`fSt=#uqP8<@qC}kiEE-==SWx`Ij@Cq5D^d-mFt z@7Lhr$sQm*B;Jgo`m{-5{^1W=7*GQH^Q?|{dhnGp{$gX9`;9TX0%B+;yx1XI{o0|7 zkmC-Cf%j6g|u^a$TiGpsHjMJ~GV zKa1ks5DY5ol@9orGtplQMfZJOdOA`K+30@&xEmyuNF7oKsfO8A1e%}fB40JFEg)jo zoX$N-KNQPyZkH%59Bwo(m#wc{51tjb(h{HIz2Gwv%;`aXV>#GB_-Lc7?W{QVY%~iL z{>OODnyPw=Lc-7ssQne&;gegf*I}wIn4)Y4J~?fawyf^j`|5INQBgNU~E290r^kUxxO*zy{B>in_nfUF|E~da>MAak$vX{zjDXpE1IEW9G4dtzYTO{DvyO z|HUr5gJU!|)v1IQf9nno;mb0tVh;vrP8AD_cR2_W`7IF(&nF3!W_@%P4PC|8U z&LM6M&Ca$#o(&{4a#k3CkO*bdZ{dq{_98@cxvJlPe>fA!?VY; z&eXmf70gLya?F|fjM`<0@~}w^q;3Lovda@HE7Er4ZJ^q+F4t$%)}#o7)o*{fmvI^v zPC%2(YzJzGO@?-ky@~T&{h_rbl%$$S;H!*2$FM+~mHP_UB#Xk5HA>OZZr!T|rw7~2 zop5D)D8br!rozM!-lYtaBi<1V(hyK+@rN0?bvq>lNN_&7>;3#Iy&UJN4c7+vWk0f~ z)%23>0x`jy4sV=@!J5weOg%fZuJ3+rLncO&ZN!C{9lZ$pMsiYX-8x6l6pQ6KRGK1b z#WR}$#T)73L4ay$`e3Im(9}ac@yjk8GSkgXbuzZ+*JpTX}f2OU_Rep{ej^t?MPc`UN&vY1=JM%8#8QRO13u@wT zuEWGpQgwMs>b(6)dk2c*Y~^^*>|fxcq`VgrEfx$>Vrtqm@~r!yy3fkkAP^JJ%{zw+ zkN2H(voX$Hb3P>A2jhx`N*Ew)_&Zy=qe@_BMF-$VxVgFI=aKjkn{keo?ncd;mwPs?s@_AE-^kGh%&RX=9hK=&<-?}OUlT;3VaZ3Sp3UdwkSw;*jjY@Q)_84 zToktD7ixbUdu4PWHqypkWOUVoxv7F^=ZQXIn^ZFylSo~7^IjQTi! zo%pB3rYz^GLt=b$NO50F|CM{7TNA$iB_ydH;SkfZH{)m-or^y-=iCIrBhEgR1-Tm) zf?lf1@F|N9V!;>yiiX6FYys(saA8!~qXM27)4U~Sn-mLaVUMLsAr3jBL5)+RdK2TS z&yrDxGENlkn%;4i28J5_F$$uR>Yv9CK?Cv)`F$fr&VsYw--4p3E;A3D_c; zytut*ggBqOAo%a_m)6prW-1+3Ru(X`+(PrqW3o1hZ#bnal!*+Za2#KwJCauX=MRc+ zGya!zaAM`>543#t^ahLhvhlawEAh>H?K_ z{#le8Sd+@5usy6idS(EGv*nH+3WIxTS99trnu)rea{2|S2uB3SM9>DzE&DUyWq-O$ zH9fDwVB#}gl4j+?*I8B@x3utw9D9YDF|!xo=%u>#3_jkwei9j+#1M(cvjo=Hrh}o6 zZB@9?WTSH!ls~(d_G@l`8s++rhP7gkRWL-gvZ3Oc_$?$%-TuIA^|u^!;BIDaBz+{v zksQ4ywyquDVqHECiS2I(!Rzf6_FLOS!!|#(>`kSi_ayktj6eD+SQsbM@0U)={%;-6 z=kc5VXYn?_%O*{GMj+%x;?kyM*+|7J-&8;fJq8=VTsA)ulak&SI24)EMb4^KJ629M za4&55|4vQOLz{5Yv-Ea3yT9t#OkQlUjbzo(4gi6McTduNI0E6~pF7{i_3U|6uI66d zSw&RcX|D6rD2u`1!Dg%6KoV%*fgsp@;*A6|H~+b&_`z2opRS%aGSHr%=_gE9 zsg;w5J&koE)O>g8clH)MWMnUvH7bYLTVV3puFQ*hldNu!kB?j$r9VHhdsACW8kRV3 z;pgN$u}ebhS{}dXhLJj`M18lxgyHO6YBcG3Xn3e#2uZfRsVBEenG++mWuhsw9Y!X$ z9MmE9FZ=W!zk{Ev&WW7g* z(mU6`G>K>^(NNuPV`*jx@Oy%?_9a_43v0uV`%n1WzaUvnN^A5FbH(XWbD&NumpuLd zDGv{^)A??b#-va`=jMa{lP2wyWxUWNd$RRKZq%knUc)z=n+{LVi?$U?wHT}bKd~7v zC+Ih|OHB^mP^X99{UjeRY)Zok1z&r)*7EY7Wxja;9nBWWY^gTW0kYn7^bGMV2W^_* zM$Vi)9dM_3%;Zj6x7xXxPq{&*>yd_dz!*>n$VhnUU?I`dw)Vk=4G2%WU0Vs5l2D0! z7b&^ibK_S1gG>Xmg@xV4``)BV7Zg!5DV*`Z2yWiT2S4>s2IH#12&Ofo-|8bYy(PQa zb#iA(@ZL=wtv{Z)4sR3=S-DDN+0OY&?CrVX+LQsA?(v`aW<$O9we4HhFVW0c$HAiB zb8Gvg*3WE4;D#|B3ewa2qFkHUYhC-(j&!%j>km5d&jkZA`IZHzgcMusJtkx$(H+^PcmZXL0!* zfDoY|7g)667l~4$`gJ7S^oai$yJ?*KmYLP`MCMCM$9kj?Y8W=- zDL=Y{m*G67*sPfH@YJeul~ee$#CiDaF6$kYLzG;Xl`P)bf;z$Myw;RxIlDpd!?txL zb@l{pi6z3k*|Tvb-7P1hN9qu({OZ9cm7#v9rX6Uo^#~ zW9==Mo)W8G(VAHJi7?L3f?ax3t1snt^c(c}Y}Gk!rp1|L*4vU;$g$?D^58LTzdoLQT|Wb4-)~`xZkLDNrxCj zC(bp0d@8gO5Ek4TKPp+7S*db3)ianL5U2%T=^I9yup~{r@M60#)20h5Lrxg0#(5{v zRPHYOQ9Yq7b#s;gejgGgtFEVRkxj*gS)8E4!8Q&-{yrN0*cGpJ*xa(8YOdLSmOj_8 zOB=>){_cG@tQA}(4xNs};xAi3wYx&JXNXB)e3Y@fs{*Dwc4|U>J7J>JY74Zj<@b3^ z^N3mycIKtzA2x8C(D;L0)yT>{r77vnH~J&Pp5F@zdu)`{B-kPcIHc*c!%*%pqWyYj zRiO4iy?H+lluh!cZqC(c6J{F4d@$)>zRMhSSE)IqXb>8a{FP6U)=u#T)|VOo>anL9 zmaPC?_oklVZhxhA@z%t#K1C?tZ2)d_g-qgJ+f98LYfD%_Ve#4?Zlc=# zo9(>B(0P!Y1CrG_-Oe$B$Lcj?!qq;h^~^$GIJIh8GklGro-8cGD_VowWhYy4c#zT^cOAmPD0^5&pg zS!3JEM()b)D_<%Gk-Nzf%e=So)T2MceVWN1-wExfAkmYAgaglQZ1Zd#yqMw|I4{T( zk=uT)4`*O8=Z(Jy z?>_zD8tORzP%L?=C#`k&|6Zxj>ZO5zvrBLZw!$Gdnk1F^Q%MVj&*)|~ZRYogkd{@} z!SnZH!h#iZ_4I9E0#kSQc2RF+>g#?Az7j1%7u&DpXwt}+H|iVu<$TEFXcr0bCvT`+ zsdMgxSqve_1AE0)Z1nB>f5;j34F@COet*BFj9x(kY_z%4^`3Wl0Q`55v+NrKUL0^q zAE1a1kmZS(5Gw$OW0hes7I_e^vNo~@98O6FQ7CKdC_TMuaD!B*<=v4^dC|X-Q-$Ph z?r<|Lr*D9t+N#7h_F;l~_5wJa!WeR?#1(A2#X#Z`<5pFzNm`XlNVa;Mod#29R^-eJ5WM(OmXs`f@tAL73e~E;#qpR*u+2$?G0iT)PsTY+izp^&=v^Cm7 zwX-N$jBQF$G~qw?Uo;e4^Pt1N=g)pp+zPRLX=D(Z9~TWev=Y1Jqq(m4(otHgl%#iT z5-&Z?`cgmeTM}|cAbe2wYnWWQ6NN1lL5M&M98669nUdzh4SH{^EdJC6G1m7w zaq*>$i<@mHC5o*v*yd}3DVHzx+;bVBYqrS;h21bon26UPilW zF>z7<=|<<*8ApumKH(j$_C2Cw4B5dN4aP~9T9fBC^YnupT=OMjrG1b1TSHcpg+48E zS7l$yuuZn(ryc<}6mGr2A|b)Nvc{#PN-7b~cIzP0)W2%8t*614HQlZyIUXvCZF#Ea3IRt0wMCw9#d z?k5@BJ4NBq@uewcO@nUf+{HkMD2lN@t)dwxfs8k|i^ew3poE;@X3Ce{Cpzp~c_l5% z#YznYhxRnY-3!4fWAQYY)L|p6v|Yqp1Z^=^mj=Vv&5n7knb?k)*_g_9$T{yzvTiFe zUftHqu8EZAi^0nn&BBJxTb#bD9zWQ8iq=d#PYXB2jt35?4I#8NIdSWfm z3ZOFkhX%BdTo45|flaaq+%Bn>xrddKkp)__Ib8MrnYqKAw;`fqG5R%NEPJ!}8#Zh z{EnZ2W(BXb#;xfhe5x<39I`cRS-X5l++=G?UD?o6dM|dF{PF$1enhJ5z0`_*gqkaG z%~oUaC?)n##*ca|pUxE^Q7HsZBI+U$_7Nv-3aN$~Fqm@lvE#CEvI@C^tn|M0SFZX+ zhgLUTo-s-D`=91p+H6wl*#<4#aDroY-vKSk;`;Rd!8%6n+d`j@q*usf46`5p8}m8h zB`5bkdL+QPU_YsmZL5KX^|TGSp0Qv6%UJ-je+Ygzw(;w#$_8-{w$V(G%RwPlbJRpV zJ6ply!%wp+P1^VloU;<6_@AC?kWc?fkh#c+F% zqQFRN%uSMZCv<`t5wZE>5KY=|gbn7_l-y;^KS|hCn>iiLl}cw?Tvi$u{WRZ}Ea=8I zq%={gs?hG2+<(`dOMTR^(5|~EQP!P?Sjmw1a&>xh3Tr7uJ3QE$dLSY;?>3m5iXmvx z=z`pyb}fEA2c6ErJ>D-;qK1%6_){AnC*BL6$gLV98|?v&KONYYX9*B2-1lnD!9=%) zf_FQo$;G+%m7cSKzfL%nJO+Cmx$@dNc#~*GW}% zo*qPv6!qityo*H9eo4L*-w^1n@ zU>pibCr`*ULNcYEdV9oWqjZk9IPj53kNnL$h=B#T@PTdjbZMe7+b_KhtgNdW8o}bl zb$OGCyK)128|$O02bq4Ms+o$<^z~Z#ay|W~WJ*?(f83Cl;oT9mqD&n?csx(8 zYPqj5&J4uXY{nv{ryHL~|IezFj~t^?Fv(G0-1#{h>a){eXSwHjR`o{4SQ9VsuI93_ zY)PNHCsNYc_)T}?>hWAZZ&%_4)^<|CH=bK$*fR1mnrxNt-vF@u0F{E|36ScGW(X{z z9>HEK@@1U&@Z9t(7=;h^^f;#=&*t!jaYw4r_nHo0 zpQb($E-?&XMwMTrCq~#|qu$d^%Ep7za(p`xW&;(8WY<$bEUv4t%_&|eyDx)WtHwCa zA?&DqlztKHcrPW0*YoXl@z^g2U+Dcjq~9F;9oE}bkr5qNj{F(BthHFtW;7FDgDF)# zA!nTODSNNf4p~{ts*-B~z~i3m94lONEeB7vwkdG$^O zqMeY-lf+H^qO*EC9SD}Qw#ee##3jnvrSM~dDYPkW-!3Df&*4os@Ftyp<9xy3MUy(U z)4Ys}bcyw((hjI`a}!3a1n!**`G?t4BFb~l4>%N51#!^gE)POmVzF~uwjZ2-8^cc0 zok6BoL)I<*P~J4l#r{3mNAK|>wJ!?l@dWf)q_B9ILFb~@BX`2smDCfxZ`kJFV|}AH zYa&+MXHxh&z!mr>(NNuB>uBGJFAr}uU#|U4(a4d>y7?UmgqP?Y|IK&DSu49ldw6zZ zT}0#As}L`HYud)l)uD@of+TJgqWuS56|_Xslx*_(fWDC@Hu~H$hI|S#1AwH1Xa4qn zBR6T+@iT&|>N>E^=Dn{JP4Wv_eaD56alkLJ1qTQ_ZN=~w~osw88Unm6ZJQa4i5N*$u;Qd^xtOphGZn- zjspVUFyKp>WYx#=O;fBQvq~3YNUA)Ble5}S7ESsx?bj`LKF|?0GBJ@m5`i=;b2|j(G>ObSnk4AREhxPswv~!vCB>y8j(_xhUT2*9po7v5kp} zkdrg(q@ZEe`6w~|UQ3v%1-6Q6$!c+yz%uc?aARqs%wp6*sz= zR>;Jv+*>L*@0FFtItr<0>IF84_w}{0L>;7$HL3@g>^bpqiNFX$sD6;{wwoGT+Syo2 zea;ZfEA|!Op0njmkDZj4{g3>Zck}6NILbZTO|0ICw&;360;py4Tj9NUUX*k0(dCo}f?XYW>Q&tC$=g&)dR`1zqX5^-VKPPd~ zs)68dg;klaA_9jW~oIS-Pyqbgo8M@`2^ES$D`y)Oahd!Mu>oRY+W9U(L0&!VL ztW!myn}=6ADa37^6zc|jkCTkdf+%o5+UU2Nq1F?<4!d=#f`T;Ol;x8T_Go6OR-Z#^N zx3Ybe7AuqJYZ6ptP3fB&C8&#QYDQaXO1{DcYass+{I~KU0`!eH>C~xoblYqQ*i8#( z6Wf3Egh`u@@ViVd-A3|7QPfbjA5wj{c%#661HC^+pf zn2TS2F|DrxBbD0TTl}73MUIxaJPfdzd_sQjWX8XQzdqHYcLS?#$l7Os%z(6l7=U_4 zZMYjVlltqQ>G|2wGa`P?9wSvh9U%VDKwNBo!B|V<_F4&L(O&Wlaf8kODDV*wYdVNz zP+IRiDZMQ`b?G!6*6o0RD-`WCn~qF=TKk16phTA*YElX{(&41Tu+1MxJUXwS*F$ra z`E}ImH#cU!#X7I+)@Q%CJfor#aMX-ASVm}ff4!$xoCjGzhQdtq_#F zl|BFA=5vLo8WP)C8kFmn( ziKgVGQH(SHxUw;|d{Bi+v=q9XwN6maebwcL^_ioLQR`o^VGdlo+hR_wK&t8i#;xay5DiuCM1&gBSus!ZG?!ol_mqj4#IUXO{(s-< z+AMWZV#`;1`RFUEip)j-5{v3;v&lUhD(fosV|_XzU~dnn?Yq*!b^*TKsQvgp`6&+I zMB$IixZ1tO?!8>?D#h-ZBs;I9r2R+ypVYV1`o9hAEYY_{c_iORsbYK#H?d%LYMLk| zJjZg~5Ou3uT;z`5FWvAuq2p`xnlrnqtwhnsCg7^r7TYr`6XDFBw{2*mN2|#n_EBQ$ zLc7n5bf{*%(#s-`zHMwPYxA6|ulJl%YAd*ad>TA}S~PntQ~VcG7qfalEY{0=(qiD`y*Gw9zpLP7@z zQXg8fOtazeRBqco#iGF-eR@?|#ng=(gR7|A4R;6COokPxx@puoGNYtm=>}S{&^Uh_F2{OoDqo%1vJ|mc0&WBv&?5 zI7Q4viN>ykZE=0H8;T7d|M#tO=(pkkq_7gvMypYMt#N)IX-B#IbjGb^L;=e<^t*YUn_i8+N?s5k zm%#fGOGRMS&m4%vSdY|#(_12mPsF=&Zb+JhHyHS6ahHg{<%zc`YSva{B=$3_#MaP#TEPsfV+1LI{)fT= zjeJxg+1x{Ky}9Nr$D{CH0#wJbvEwA_;GUuN*G;_ zKlMw192FY|gQ{<6FnUX?9WL3D2yUCg;v&N$GmMtOQ7?`1mFZz4eDRD){@TQ24#P1R zcX<=unxCFhA%+uD_&0tbIA9VYD5UBTZ8@RcquF#fY;C}jOl zoS(k8v27mQ0vhYbPlUFn($@dwsakxRu4+DN95d+H#oQ{5)x>Tqh} zgRhaln&OSy&k{%eLd0JxlII#SiLg8fC|2NlGzNzJ4=@0>HcUv~>S-02-0?N^prv`9;hYPA5BD5A@w zTwQfxQ+k{+Jsk0AgSCf|ZArA_<|dEWxYO9W|9Af1_#Skf%DAteuWZe+DL-rd?_2*g zo9?JoGZKtAsOt0Q&xxx+TN|+OaQSO~&N(l=!NZZa+VBQO=DGUl)(twqI0 zT8C*>?Wx;shq;=gU+&^gqA4v$t1_`Q#G4KNit~wUxUq#e`#ZK|*6(9^z1!j=!s|== zM)Mt)fuP>v5+sJ*pgfv5gW9VKb9e#bbRo$kV~Kn6*6`(;DUtXM^0(!Wq=xboMw2zZ zUpW}DUJfowgQ>yPWBG7~ID=V(kGOB!f9M!CtVBnzaOL}36XT90Zq6C&MT@XP?Go}@ z2oIXzbj|CGJ;rcks>)bKT_4)D$a-?1=Vmfg(|tsmRx&fsmq(F1)MkVp z@6a+@JQ^9^LNmUvg>TWa5S)!XBbb?4*1~k-5CkvfHUu!n>oUaddC!tEWy zdWFKAb+EQW#PbXU37Y+9=wRmR!-F=EA^|u6l*|V z=Sc137;cXEFcKz;O_4jK5@RBBw;^8Z%Wjsu*W`fyvh_4xGt4Y|)@a*ZAcOlG6rsd% z>bMUF2=~D!i6^JT5@cr3C~P}4%Rf~%oL^klAfFL%@5<8>Jh3*GTYdyw-a{)5!58Rr zyj|pMbjz!mi5``87Au4SQi_bOWwugZRb;TxyafEkJW5&ekhto!HD|byajO{3wQ2J* z&%pDB6W++5;VGGGi$L`ds~q+q_2Hh`S1IR{01IV zIN4C&Z$HuH#eK(oYKFd?VoqhyRH=)4Jp@6Aj?|rt5iF%~1(5>)tFU_!mz*K$O7}!9 z5O@2IvNqTDxL1JgAe`wD9zKb$g4Nn`7#i*|8sn)v#D(zT!oW0FpAQkzK|U>YOn#qN zjJxWkDJJC=qnKrN!}!D zq+0Z9oa#%Ii3G#>;}mAKzzgtb_Yv|0nlMnD9;!SmOdEV=og6|gd$XNbFXg7J@)NZW z#Cr6CsK)88JA;2$SOv8n#GgL`br(r9D?-erYKfV&{IQSk9BZ>Q!QA+mb}w85UV;m% zj8`+DyWIe;2$0f;AG&M}e}nOmqUWk~*TsGT_?@-3@2>Jgfi+t9KC6$GFBS~+!k>;; zp5UJ+o|ZVK0FP3cov6hJR=w7_ykMBWpQ~qY8Gr2EFv81rcLNP^ipWI>7y ze%WD_PvQH>@-~ff?-6bnPs)BRxw(WJ9|u6!>4@F5sCRmd(d>tjeaI=+q62Hvf`?^O zc*8&Jd)`#>l#@xOuPJEZ^cP@-bLQV&6>i`u0G5Wy=~3B=<*oMfkA{=@59Y*|_Y8@D z5&A+J(GLRms3TpUy-ujlE(=c`6~luNF{>R2CZ#R#2YX8rQs1ewvKpB5(K%bj?V$(1 z11~R4Hq#(3R_F+u%@*@DU;R(|UbutsSeJR~yU!{Wjrt^-C(7?FQ&>PvCijPI#4p+qDqaMtBny=-~%%A=m z`z2tq(?90k=|WttGxx!1odZ`6&K8$t+T|K?lx=N+63cQ)a1*eJ>?OxPITdg zgWo3vee>V9UUYl1+GxW;M!$$KWY_?&2D_xP)L2Mm+uv4iIb=*JOH0FCG6&g=`;*Hi zO6hORCQXt!Hk(|l|Ch}3c5M$N;T*PYR#Xovjf-`_z~4LKNfde^A4fdBq*~l85PV*2M*P<{)!!k?*rV^sv|P;s zxz?bDCHNBDt(OvsPS$CurQ+otAcXs%u>iY%hQqP4t}Y~ozGGLT zu2Wp2$+BO3Vd_RlqD3cWaoM}3XMAZ(b7f5)*>=9EPba2Ja^|R+gI|dB%z}}sacrBO zIZTZecg%nP%W_yxN%(yM$kdv2(`3XxHkiNGwbY#BtZ#DJ&@m#+CUtqd%Y#!KZSzP4 z9wD$?_570cH02^?N+GN?Y69N=!TeawSZqH3?Q2o|pU~#x#~)7QLe>0pvd2_#wodiJHbvx( z{5#`}IsQ#R)P9*@a&bY_PFXdCqnN8vk8p5r=(P1s0^& z=e${xbgn+c&hwlC0hwp7L_ttQfiWyqPNY?<4-f>|sM3(w*dv>6u-7y`J=A;<3_G$_ z`86^r-A{+cM2(aGnUW0b%_Z2{l4}_|hN}2M<-l4Tg_U~im|}aqpuW3YtU6`{%|sX3 zd!Ju6G1Dac(o7sGxjg@c;CY4uzyfY0GgG2PG#tcWW0pF@;`yrQjHY=U`_!CY49~6C zx1zYnObqFE^FmL_w2onvNB%GrofO$117;Ptz>l^Pp_BU+WQcG-T%!K7R73a_3_@=! zQZ_Lz8>yhyt_0?nS;?rrr?_H6zWo68M+VZo! zveWeq$x`HcLpiN*OT9pxCwcSHui}dvXU|r62L+SPEun*_pVSvz&qt)#rcdmy)%rqi ze3AJSHr9+S(v^-<|Yu3{7nDUH$P-L$7AEB3% z7=YoWCXzmZ>1t#a`1~le6g~*lTKOXE_+Uj?zcfpq5_?lFPt3fUn0)r02@@k~lWjY)ue(e| zB7ST}nxr&}vpJ^7oQA2WpNE@oM0y~VR^$%b%BrsK+A*)C9N^Kyo<53By%S;Irw{lg zc`0&7Vcti5Fd>e%xpU>1%;>rl)c4}~-Pe0znT%N87ZiSr+KCA@gyQ|K&^j&%yCmkU4dAwE>uj#kALO>i|bE?f)TK009l zZl&HLyrdfRID6AS@JVe$s~e!XWI# zHgb8VQ;)3m57cc*(4fhdjEdxgDP&b{KE z1JDI{@f_%E&c#zo7;$$sNoj|D*{#%1s14O%5f$s=d|7vdJGI~h+zHy9K;j$P6=+#A z=f`RV4{Fdt?GRZI5xJWVk*K_*IA{&Q4l|ci;}qR1b|9~v9LFz`o58W=)2x3z(Bpyg zgbR;d1-=O1{%A4jujCW8q{A^&M11A3c%Gw?2kqN=GNODC%!(wah zZT;XZsfu}gOqB(tFMYIWG$i)s)S=zqp5;%+}jN?aKz!WW|eUBY#sC4{iHG7vJy*1 z9VW!#YH~{Y0cv?)`Q-D_2iL+c^630^g7g1;HQ6wrHI>Y&9%SsqXR}hyU`t|>mQz6e zdGz{3p2L;pF?MFfghcy4>54jXM3lBEPd^xBNSeV{SsV|7OwZlxxf!$D~EHr(n3Zotk^Y|=X78#AOijMT9j zlHj>fUC*R=+CM{<%{}tO?=>@q=su_;WM_U?N@Mm+Vic`gIU-G+GkK3)H%nXH%cvU4 zIs@bUTxm7T&n+db#Tdr0rd4m=)z#P(RT2+@FYSjK(gL&nz4gehZjWO z1f9)w(zSXn)`Xqq6!(*isa|Uj9ck2IIWcH9nqM;CYH#vA!svVIoKL)@jLK>WD@N~@ z(&Ff&fi|4Ibnp65{o1b<8_Bps7BGp+9``N&x^Xba6Y zfsG=77xhcsLj+*V=GISaYtu@ArKjS(H@mY(hpmRs?%?Ho_23`|?NU{)Oe;UjjgEEG z5<92(;%Pj}eZ}g_t7FOd5`%Dl{qv@M70!-X0!aIoiRYODnQX4@3Fg?NJ%hE!mnrgT z7e!?u*mGKxKXKl4ZKnoc7s^9{yXRB9+%fEIerb3UE@yOs*J(R%hGGh;J4x{&Avgsv z4s;BOnZd8pFY-pSZA&I5a5dqBVfhk47|xck=CF@bweQ*6^t*rgg0W@GpTD7n;iYe2 zi?0D{h0cQ6sFVG&lY0v*bkIfq{NK0U-lgN{mX)!x$2!q;)Alz0V)K*9kUiX%8Z4;! zQ5{%8$N1aRne}?RWFh>8md(RAAs-CYaVa7Z^P9XYEVp@xWpkpM@svl!`+oSaj3);#dlCiy{cP(KTYzhm z-Ylfp@Wz3xUXP8JXAjiPLZ5oplc?_O>C|c8gd(kjarQDD_Z+fh%8Qt(`6}eJevq$-74nvHQt?3OP9G!8EOL||_RzM-{zG6N z@gY(r2nf!zWi=tI+(q~|0ZYBb4(4;OMn9nQ63|^!j_~fp#PlFD^D)Urtq&PK<~5?^ z%zx&;Aerx@s|IA8`4dwqt2g2QsS96LF@-Tm8W}i(Zia`y)}N5E`Ci1N(_z2mA=y)o zVQWQ4tHPV^g<2!gEG3`c=sFs(q-B~|&G+)^qVp=;&u;AYZ(Fi%lMEx6ClZDL-aXfN z*}%9e3O7Wx8hhlpTgAKXrQA^;&VGV@3LoTX**0!8EE4g_X=M8k(66yqnmjLH%lpk# zat#Sx6qvX6n+a9q!9B>xMPi#$!4CTZe&~=`!i38_2GBW_q)n5;%kZ0kP&A$?v7^4KQhA?wW;XD_Bf|zs z^qrI&6Y`!X-{$tDPC-jNCbL-66ZoABHuoL7r{_89)4GF!$3RigU>B}3ipPn4{VLhi z@BT8$D())LqBqL-HdP|=ATgzM@~1z0ok0Y0a0IA>+xV5}{h6tdwS*ToVTG&b|4b7; z`VO};vuvtz-*b@UgtaRlV%5_2X@4T!uxt>&sNrSZ=1hP5!r1ufEUO8-j%9HL@~EIL z;>=xIaI>;H@H}Cy`P6LF;4)DIOi^);MwmstKR8=`Zxa!^u6y0r3J_RD>0SpvYusV@ zmOwCEcvYrK=T4>5{K_u!GKdUMZZUEf0_)1Ak|GxCYUGg`YprjzCWP>L_d1zZI&o?> zkGsdl;=*aW*w{pe?s#yu5!$~aG`v;B(5A>B+JN_4;DuAJIKqxA38CC zWhc}Erxc2^mExl3H#aQbpBI1+o#E?nR$Rft0K0Cop5(fggubm+{;%6*xtgd(uHGe| zp&tMTqxPLTb)9s?1}{1DAa3)1;0JL@^@E=A%wCx)gMN41Xv&obX95y0+JoLw;am@2 zlSw+S-G+Tja$2!cfoN>Lh!K$uivKm#5VIx07LzOu^0v>T{Xpo(E-B!qnw+FwK>lHmQN&)?6)KlxA7 z&}5qr`|#z#&x?A_R){PxY>mmZCotsCilXL1eAa&HP*tW)>zgBLq+e)gKKcNF{*ejT zSR4zmBJ+k2;)nJF(G?lQ{E*IyjBU9t_-xdX*moQOh#p-A+<&CaNE~0os=aS@$yX^a z2Y)>1FLT;-iQ)dYpxeDwH>y4lmoEQhd<0R)gh$ulU)YeL6Td{;++N}r%o2v`3S0Ea zkGILU-A?sBKf#4tukh43b*GOVm5;W;c0$Y^yKK!x8UFR#&t|kSfLu^C5cZhJ??WE4 z2C#U=WwpO!QK&ikyUg~^A*_`)jNaKphZ}W|#>k&7EV(?`c-0%{H|0Y8<0LwDgJfLl zV{b$xmnOAVJ*^$7FPIj7_%Sfhc^wb~-=$qdewIpPLcBMEfj9 z@(c!GPxB1%#pR!)QnbfvW=>Qg^TEMeTN3`Aj}2PWuz>oG$ZFyR{AA^G38~7CytL1u zjc&Rr`x?&vi39E4BmaF%hg7eFfXaX|E57e6fV>SUI;w=z%yxOsNG%U|nY_yJ*WD!y|l5t zSs7JlqjUQREudD~0h0GI#}=10`BSFCb=}nckARzWwz#&cqav|=iv(Z_iKYT^mGZ53qV(=x0X6;Y7IwZAjf>$QJTc705e~Mov+M{AG+msI> zfa%|#s0TPgM(h+FVR|=KLQ}h(G3V#1Gk@>Ew(muTq0)WeW8Yi1cJq~^rYw7;PZrC` zzi=aYMam6KK>-2tSKXeU$w~}|pUyhw1@-Wv0i1O4kzY0P;1DeT$V}baDF`Q;Z)z5{ z;|5or7Iea>N|)$ z3ki-dvhO#i$3-J=0vLby%%j=d_F|JQvy5D>X_DI1J<#1llhU z1asR;$R@y)&W{dqrL+Dyu>gq+=eq~eTncP!&8#QHwJFZ6c`bsmOdUbVC~hg~-#tu4 zqEP6tUI;|pI;1KQ|6rjCo{p8Xil7HQ1WAQ=MVnLOW^mDsR&JgFzyWxaHyt{P0}8#{PY+A}(LFA;Mg7h|1UlwAB5*P%6< z!1=c^*?^wkFNJHZV<8$8zCP$EJv+sIDx9hf8-T4Ox%dUje<&MEr&<*w;$0oAuBaQQ zDKX38SVf~ss~MrT$t*{oa-7j?er+^bDnVi^{c$0nfdlMr$*k3-mj`;G6Q{^k4u7<4 zC=BMdbMVwzbN4Mz|L5y$8%2z0hU3j+_#Alnp4?dz;8Gu2mXDVL#`YolLD7{x#+ZtK zcO6T=DC$dme`It<`N_hVRj(LZr_b1%Nf*WGE30dMT`j&F zS2!T)JRd=vo=4cqbf(dg_M3LDd1i3dMi`7K=4ki#Q+CssW6sK&j}{`V{KZ7pd|CW| zYW@>KXX+U4V4&dN-`gIS3Og(AF<6MPpWgawaMxz}sXila(DKj%;XK4ig~A@&-g+~2 z$#Zw!C|Q8q=}-Oc{dLeObNLTLLjaOx)|Z+ld(Qv!bcLI4W#@?mvd@d`HZieLy`Z3f zM(B2E$qOHJd!CB#%bLC;yymGi^vJF$QASt^T9}Ik<)vOEj;TPh%=&a5_3i#dj)6aV zg6Qxpc-wS%TGbX10RZ^556?Fr5RA0DrMoCzWxcQk%iUWG-f?u~jwdIH&2NLi`2HCk zZR0TKi9(oEF7>2|KJ_P6!8#pFM`J5teV~2J1nj`N8RFjm8kV;)xI~; z#-4LSh-1N{wOKE-PfD(PU5{vSi<8F|23k*cbNk*MAlCi^H`K>blGotq)?7S9SUZT#)FlR&Z%<$@thpWWhm{%@+(p*?=e&+0STfe#jtN#hRH# z6f&+c!>Ex-C&TK}6dO2%pjVcn%kmI$!oMMfV?i30t-$MB$!ViRA^$|}BTlIsyX5JS zei|ew>ETA3XP^2O#W{ts)fh^js|P2jv@E+79GEKH0*zVorD{Ej(S?8I%D~8fRaEW- zvPr2u$~{rnYJY^Z4ep#Y5?`zm=95o6%A|7*I@4#JZ)O6)whc-d7c0LuKsy@{K>ShX z&@4?2UMBBn=9`v-0umGd3`CGLL-^`j2RBa-T_7YZ4R2%nnDAgLjS}Eh)Tjwxv0vps zzvXV%Y^mju*2BBo9yL7hNSHqr7lHfAD0>DpwlVP6yhdOdqO&*U zABHI47X#qKgxxyjPGlO}#K6vnKim_Ws`%)X(%Z8B_20+PyVHXWwj?FC*x%==Q{siE zkA{Z(TqMF?(#qzfRkG!v*BQts&iRQgKWdVx-(#G|UHuTpp=4G38UKhCo5g5b5$LQh;dB$`G=aY2Ld&ikM zI=&rWU?dX6(aIGx7iYuUX5!AuKC>Tk*dpTNPbqHok_4m?tM*ep8Iq$_6KodqpmP#WcW|~)r5wJ+t zY5dQ~8aIF1Z4KoU*_~@oM*VQEBvCEr*{rye?}oJ*f4~DG-&Vr6vl1lxNa{^oM1{jC z0kltNJM>RnXXZ9F=LP)SSeZFCCFE)X(M79}naOb?0oh)0$3T3HM(x$l5!`EgrV#;A z;8C%;%YQby1O?@`BXyi=7$yuroBFBsY2udGWyOVs@H#h@HR#NV_j7*yVXu)pEevAe zW%jK$#HI8j-);b4Kl-&#EYIM5(~qgOt1^(0KwgHx7Ll<`>ggpk)LimntJV@cg>6NO zwx`96=ot1~C2T7}**@;TfW)^u{sH{c`f+Ss-m9@@?zmAbe+P0*+j(4j9U?QWt@|jr z*nD*%*SdOu5o!y{e1!e%D92s*C(-Aj;nx5lzRUJT4w40I6x%dTDToef7ii~30Wt4r{ zPDdXQKBtK0+9ebF;&BS0*p~nS)X0upNkg+Lf2!Tk51M5iI8w0hm zE<>6@G*_ye9p0T@*ID6BoO%=mQ9g@T!*nycG&wmYJqnzpb#tVkU4X}NeMiO1i7`Zn z3Ceaej~~R333i$ZH7oaDZi*0Y`;{8nA1VjRX`fskws3(*D?>o|w^3=>igmTZ6*93g z@=+3X{&EQSq|~Zyl&6N5&g?iv-OF3|wy?dM*N(sP6*{b8c}f03Pz6;`>+T1EA9{6luE3u({I|) z-(po?_VV2MYW%*oBQt>;)mI^nt=xLlvMEo}oYWB#$Z+U38FSgGG$RqI>79VKTx*FV zn~_wXi}XbIrE`0{N-Nwv+%^hp>uet_UmW1^%Gf#4DJkX{ug-JFQYq<%OQBne@!^U~LbZ3IA94SrJ*Eu*ES;m_&bf*LNJZVbQ32LNK@ ztUW&;P@yp7&cZ2by;cOFeZVEH$H~{*@J%e=2hvkL?sMskPe^e{QS8z-<0qShJ;X8T z(aDoofvk3zi+GuTrqNfm#oV=>9IYya`Jr{lW&;gcpF9+PVDXs!zi%z?KJ>b_#qwgJbjW6E=hHbnpDPFLXoU6uq z0M@rRGpf*=Xwh9HcC@zT^<29rDV=^C5ML0Uf6>EGgKo3pQ4~mz;ORemZ(3TgYidVz zJ@&i{XWuKHSeCkSWY8aL%|EO+UA~QIT_u9>-pKGaW9WRFWGen9a3s>f$Nl6nqk^i% zSv~;Gkk_+mPxTJ~zQG>|l>AIw=&io}&`(pYR7OdjtK58_32THvCf z+>;eZ7tu!Mj}p@|8l1!Q{a^M)Xc+PL!cyy2l|-+UT`xszWZK-k68V%^MdkjT*p6&9 zx%l5aEOVPa-Z2G9XOm^Atk!!Z8e~SBI7g9ja4T(umO^Ldz(`O_Y{XZw1f?{JW2bn? zy$cy#4XCA=qmYj+Py}|{))v>?G{OoWH12?3KdE~#zv7G3KIULSl3hsL{pQNaE!j(d zPL3?sDfp7eD{X9?S@JlI-QsBYFx)KJTWbsG&g7PirW#OAYP1n}V33f#io16t5%+FL zitez!wOPAX-6@JLlJ|#@N-V?~7p&DrwYv8W$3+>%mPLqUth4dCAvv>Aaz7=#B`o-s z!ym}02V(9EL6(g~7L85S>d=BH#k@D}Vzqct3bL!FzHiMJn-*AmbJWjp)d}^sO$Zf9 zQ{|k&_U3&uN7~MxLl5>0s>UBP9sZUz_9fp>Qy*wF zsWntzjc}EVn<4rqe7HWBs|ttJ;5d}Op2YTVCMoA_K4ei{m8iNCh!Fg0Ex}}%fZn0) zx{1oR;C-%?1mlAH+!+eWbE@7lUgzv2yXr-DLYi$&_z&k7w{dau370kK2+Er`=EDph z8+twYFAedmbhi$%EQ&>DZ2Kp;izQ9SVhv@7p8S(a+_RxxI?}zq%X6A93cRZ^A!kI@ zb$B`?7+6@<)vFib|Bt0_4@mOf|KHAb)|xZBtXx)dwpML3<+OR}qHXF^`djntMc@m;ng!^N!eaO{{fQg>;z;p4>V5J zd2RSkggq{+9tORP#-l1i8Yf4d8SAL6C|r?96%5pph2=R3V{@4^-EJRcr-S!%-Sk-K zGT^)JSB|o0^R3h>CG{IfLKw(-nh6-$=t@1Z#U6DveMZG%e%1SKldL$0EFc|KlUpS#MQrix&qnC z@>sA&T#@xQ((a%Q8f__6Mpetw927`n8WhZm_YbFR*VA#2%5okUiTA1M%q!>IsqskA zRQ+PUJqF~Kp_vym7EP>MLEzpDRncw>_-4)j6Mx_;?t!?x-{-j)T}}b!-Mg@FmT;_5qevMu zK^IId!sv}%TCcU7|q>QukaM#&{37U>Wp$7YR2ZFz@WuX9_pdb5c{iXSd<$ji&P6Ff!5Am0gH&-h zp+2@JI-P&pHq$i|p1-gB6?}2a%9o7V1IT;-0CPzsek`~TtGEXm5%wRP&l8`QZ@J8K zCF;spz2c=e>P%Y?s*P&sOgRaOn2}-NYY8o%iui+CJcDX*H%z-2l&pfruGUgLgn_FD9#LOSp07=SU{0InV)2ZgXKnNs+vjdxn_OQgS* zp0m!)Up6a6gGV6VEG`i&A8&Yu&i^p_tYlEo(RD5@z@w#>OUaHk{Jp<9Ey^8)$R=KU zix!(H>=j)iG<4{$3AN&9d z?yOJ6z;^nEON$);=}D=v36zkM`1VXu4bk< z)sFoYhO4R_4cYp4-jsp_9^}>kYG><87ucpZ1Ql7j#rS-C!_46UXHdiG%O)gla1smp ziHRK8{XWQ#+D$JfrI&MZ<3E@!U9Plc>6r95u&;^^eEeRO6;#AOpe+az+-Gu}aEqQqlv^MC2e6AS=garE^&S%{Y20P8wmLl?E;k!^Fi~PgTw_HaXB+5~zwoDrphFPiI@5kEw4Pwt-Yyz-6?#BVi~CX^DI@pKTLSAwxC z8e-YbZ4i(-^01n3%C=lgK@jm~W&d>r_<#QdO`6}sh-e^)7<;hYF^7)Nck-;}rg_%b zJwA_rom|MsIxIwRfY0Uwg!+@vJU9tUp0Y?iar(9iHoK5U9|KFy5VBI&Xa2h}SGVR| z=hs%xC17CIg?IzB4A%_Q^8`#Xz9QtjHI>GfP6^SRoYKg)jw(fZ&GQLQtNgK5OkY}x zsT7(8oLNm{J}{m|3AiUz*_|E_Q6c6{wT;cC7Da^(U;`6gN3>>`560)7uo?pGKDO5? zk;%{Dr7Aks^*GqWu;p?C-S0v^>A}#agT1$)8M{(VHL71ICgqo%;Cj1Ii0nBsOxJqz zn0yU5uCkT#myA;Ev2oh^h60l4sy8iE*#-a6LYE>E;^v#-2@BcIKu$Hc z=?Tku^#NU0f@^dAv92R+fD>%HIU1TwqaA;3J8uLtcjq1;PXr#2@T1sD@*JHOs^_6A zfnFf_4cu8jUCYtVTUi|Y!IxCD-DRV7aX78lhJ^bcL1H;{TM6T;aJ7N?7{=LhyE5rl zPfD^6-Whw6WYOH8L?W&@Tze|&rz)yOitnC+4O3Wd5g}3{a$;al)~ECTJr_vIxKHzl z?B7Es_E8^1&!(+u=Zw;Q4d5`>l3GA%!g97JgQhuTTq<7ulnx~k5UTC?)2?aU0m`!m zoGLVjI@2(MFjHPw#A`sc|C6Uz%TO~K8r)BhyAu;y1*$6;XiNQ$7HcC3rpbh~`-y

  • >I2>SB8~C~A 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%0AgiW&Fi7J0?$CK*sd+XWy%b>p)PMI6=uWyzlafL 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 F06PWCD*6Bb literal 0 HcmV?d00001 diff --git a/shared/static/favicon.ico b/shared/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1b75201ae786c838e3c5ed9c32f3e37c71c1e11 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/shared/static/fontawesome/css/all.min.css b/shared/static/fontawesome/css/all.min.css new file mode 100644 index 0000000..cd555f1 --- /dev/null +++ b/shared/static/fontawesome/css/all.min.css @@ -0,0 +1,9 @@ +/*! + * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2023 Fonticons, Inc. + */ +.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)} + +.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bangladeshi-taka-sign:before{content:"\e2e6"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-child-combatant:before,.fa-child-rifle:before{content:"\e4e0"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-notdef:before{content:"\e1fe"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"} +.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-pixiv:before{content:"\e640"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-brave:before{content:"\e63c"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-threads:before{content:"\e618"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-opensuse:before{content:"\e62b"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-debian:before{content:"\e60b"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-square-letterboxd:before{content:"\e62e"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-shoelace:before{content:"\e60c"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-square-threads:before{content:"\e619"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-google-scholar:before{content:"\e63b"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-signal-messenger:before{content:"\e663"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-mintbit:before{content:"\e62f"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-brave-reverse:before{content:"\e63d"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-letterboxd:before{content:"\e62d"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-x-twitter:before{content:"\e61b"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-upwork:before{content:"\e641"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-webflow:before{content:"\e65c"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-square-x-twitter:before{content:"\e61a"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} \ No newline at end of file diff --git a/shared/static/fontawesome/css/v4-shims.min.css b/shared/static/fontawesome/css/v4-shims.min.css new file mode 100644 index 0000000..13fa437 --- /dev/null +++ b/shared/static/fontawesome/css/v4-shims.min.css @@ -0,0 +1,6 @@ +/*! + * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2023 Fonticons, Inc. + */ +.fa.fa-glass:before{content:"\f000"}.fa.fa-envelope-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-envelope-o:before{content:"\f0e0"}.fa.fa-star-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-star-o:before{content:"\f005"}.fa.fa-close:before,.fa.fa-remove:before{content:"\f00d"}.fa.fa-gear:before{content:"\f013"}.fa.fa-trash-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-trash-o:before{content:"\f2ed"}.fa.fa-home:before{content:"\f015"}.fa.fa-file-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-o:before{content:"\f15b"}.fa.fa-clock-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-clock-o:before{content:"\f017"}.fa.fa-arrow-circle-o-down{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-arrow-circle-o-down:before{content:"\f358"}.fa.fa-arrow-circle-o-up{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-arrow-circle-o-up:before{content:"\f35b"}.fa.fa-play-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-play-circle-o:before{content:"\f144"}.fa.fa-repeat:before,.fa.fa-rotate-right:before{content:"\f01e"}.fa.fa-refresh:before{content:"\f021"}.fa.fa-list-alt{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-list-alt:before{content:"\f022"}.fa.fa-dedent:before{content:"\f03b"}.fa.fa-video-camera:before{content:"\f03d"}.fa.fa-picture-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-picture-o:before{content:"\f03e"}.fa.fa-photo{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-photo:before{content:"\f03e"}.fa.fa-image{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-image:before{content:"\f03e"}.fa.fa-map-marker:before{content:"\f3c5"}.fa.fa-pencil-square-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-pencil-square-o:before{content:"\f044"}.fa.fa-edit{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-edit:before{content:"\f044"}.fa.fa-share-square-o:before{content:"\f14d"}.fa.fa-check-square-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-check-square-o:before{content:"\f14a"}.fa.fa-arrows:before{content:"\f0b2"}.fa.fa-times-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-times-circle-o:before{content:"\f057"}.fa.fa-check-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-check-circle-o:before{content:"\f058"}.fa.fa-mail-forward:before{content:"\f064"}.fa.fa-expand:before{content:"\f424"}.fa.fa-compress:before{content:"\f422"}.fa.fa-eye,.fa.fa-eye-slash{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-warning:before{content:"\f071"}.fa.fa-calendar:before{content:"\f073"}.fa.fa-arrows-v:before{content:"\f338"}.fa.fa-arrows-h:before{content:"\f337"}.fa.fa-bar-chart-o:before,.fa.fa-bar-chart:before{content:"\e0e3"}.fa.fa-twitter-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-twitter-square:before{content:"\f081"}.fa.fa-facebook-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-facebook-square:before{content:"\f082"}.fa.fa-gears:before{content:"\f085"}.fa.fa-thumbs-o-up{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-thumbs-o-up:before{content:"\f164"}.fa.fa-thumbs-o-down{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-thumbs-o-down:before{content:"\f165"}.fa.fa-heart-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-heart-o:before{content:"\f004"}.fa.fa-sign-out:before{content:"\f2f5"}.fa.fa-linkedin-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-linkedin-square:before{content:"\f08c"}.fa.fa-thumb-tack:before{content:"\f08d"}.fa.fa-external-link:before{content:"\f35d"}.fa.fa-sign-in:before{content:"\f2f6"}.fa.fa-github-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-github-square:before{content:"\f092"}.fa.fa-lemon-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-lemon-o:before{content:"\f094"}.fa.fa-square-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-square-o:before{content:"\f0c8"}.fa.fa-bookmark-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-bookmark-o:before{content:"\f02e"}.fa.fa-facebook,.fa.fa-twitter{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-facebook:before{content:"\f39e"}.fa.fa-facebook-f{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-facebook-f:before{content:"\f39e"}.fa.fa-github{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-credit-card{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-feed:before{content:"\f09e"}.fa.fa-hdd-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hdd-o:before{content:"\f0a0"}.fa.fa-hand-o-right{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-o-right:before{content:"\f0a4"}.fa.fa-hand-o-left{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-o-left:before{content:"\f0a5"}.fa.fa-hand-o-up{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-o-up:before{content:"\f0a6"}.fa.fa-hand-o-down{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-o-down:before{content:"\f0a7"}.fa.fa-globe:before{content:"\f57d"}.fa.fa-tasks:before{content:"\f828"}.fa.fa-arrows-alt:before{content:"\f31e"}.fa.fa-group:before{content:"\f0c0"}.fa.fa-chain:before{content:"\f0c1"}.fa.fa-cut:before{content:"\f0c4"}.fa.fa-files-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-files-o:before{content:"\f0c5"}.fa.fa-floppy-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-floppy-o:before{content:"\f0c7"}.fa.fa-save{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-save:before{content:"\f0c7"}.fa.fa-navicon:before,.fa.fa-reorder:before{content:"\f0c9"}.fa.fa-magic:before{content:"\e2ca"}.fa.fa-pinterest,.fa.fa-pinterest-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-pinterest-square:before{content:"\f0d3"}.fa.fa-google-plus-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-google-plus-square:before{content:"\f0d4"}.fa.fa-google-plus{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-google-plus:before{content:"\f0d5"}.fa.fa-money:before{content:"\f3d1"}.fa.fa-unsorted:before{content:"\f0dc"}.fa.fa-sort-desc:before{content:"\f0dd"}.fa.fa-sort-asc:before{content:"\f0de"}.fa.fa-linkedin{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-linkedin:before{content:"\f0e1"}.fa.fa-rotate-left:before{content:"\f0e2"}.fa.fa-legal:before{content:"\f0e3"}.fa.fa-dashboard:before,.fa.fa-tachometer:before{content:"\f625"}.fa.fa-comment-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-comment-o:before{content:"\f075"}.fa.fa-comments-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-comments-o:before{content:"\f086"}.fa.fa-flash:before{content:"\f0e7"}.fa.fa-clipboard:before{content:"\f0ea"}.fa.fa-lightbulb-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-lightbulb-o:before{content:"\f0eb"}.fa.fa-exchange:before{content:"\f362"}.fa.fa-cloud-download:before{content:"\f0ed"}.fa.fa-cloud-upload:before{content:"\f0ee"}.fa.fa-bell-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-bell-o:before{content:"\f0f3"}.fa.fa-cutlery:before{content:"\f2e7"}.fa.fa-file-text-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-text-o:before{content:"\f15c"}.fa.fa-building-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-building-o:before{content:"\f1ad"}.fa.fa-hospital-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hospital-o:before{content:"\f0f8"}.fa.fa-tablet:before{content:"\f3fa"}.fa.fa-mobile-phone:before,.fa.fa-mobile:before{content:"\f3cd"}.fa.fa-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-circle-o:before{content:"\f111"}.fa.fa-mail-reply:before{content:"\f3e5"}.fa.fa-github-alt{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-folder-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-folder-o:before{content:"\f07b"}.fa.fa-folder-open-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-folder-open-o:before{content:"\f07c"}.fa.fa-smile-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-smile-o:before{content:"\f118"}.fa.fa-frown-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-frown-o:before{content:"\f119"}.fa.fa-meh-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-meh-o:before{content:"\f11a"}.fa.fa-keyboard-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-keyboard-o:before{content:"\f11c"}.fa.fa-flag-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-flag-o:before{content:"\f024"}.fa.fa-mail-reply-all:before{content:"\f122"}.fa.fa-star-half-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-star-half-o:before{content:"\f5c0"}.fa.fa-star-half-empty{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-star-half-empty:before{content:"\f5c0"}.fa.fa-star-half-full{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-star-half-full:before{content:"\f5c0"}.fa.fa-code-fork:before{content:"\f126"}.fa.fa-chain-broken:before,.fa.fa-unlink:before{content:"\f127"}.fa.fa-calendar-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-calendar-o:before{content:"\f133"}.fa.fa-css3,.fa.fa-html5,.fa.fa-maxcdn{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-unlock-alt:before{content:"\f09c"}.fa.fa-minus-square-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-minus-square-o:before{content:"\f146"}.fa.fa-level-up:before{content:"\f3bf"}.fa.fa-level-down:before{content:"\f3be"}.fa.fa-pencil-square:before{content:"\f14b"}.fa.fa-external-link-square:before{content:"\f360"}.fa.fa-compass{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-caret-square-o-down{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-caret-square-o-down:before{content:"\f150"}.fa.fa-toggle-down{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-toggle-down:before{content:"\f150"}.fa.fa-caret-square-o-up{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-caret-square-o-up:before{content:"\f151"}.fa.fa-toggle-up{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-toggle-up:before{content:"\f151"}.fa.fa-caret-square-o-right{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-caret-square-o-right:before{content:"\f152"}.fa.fa-toggle-right{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-toggle-right:before{content:"\f152"}.fa.fa-eur:before,.fa.fa-euro:before{content:"\f153"}.fa.fa-gbp:before{content:"\f154"}.fa.fa-dollar:before,.fa.fa-usd:before{content:"\24"}.fa.fa-inr:before,.fa.fa-rupee:before{content:"\e1bc"}.fa.fa-cny:before,.fa.fa-jpy:before,.fa.fa-rmb:before,.fa.fa-yen:before{content:"\f157"}.fa.fa-rouble:before,.fa.fa-rub:before,.fa.fa-ruble:before{content:"\f158"}.fa.fa-krw:before,.fa.fa-won:before{content:"\f159"}.fa.fa-bitcoin,.fa.fa-btc{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-bitcoin:before{content:"\f15a"}.fa.fa-file-text:before{content:"\f15c"}.fa.fa-sort-alpha-asc:before{content:"\f15d"}.fa.fa-sort-alpha-desc:before{content:"\f881"}.fa.fa-sort-amount-asc:before{content:"\f884"}.fa.fa-sort-amount-desc:before{content:"\f160"}.fa.fa-sort-numeric-asc:before{content:"\f162"}.fa.fa-sort-numeric-desc:before{content:"\f886"}.fa.fa-youtube-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-youtube-square:before{content:"\f431"}.fa.fa-xing,.fa.fa-xing-square,.fa.fa-youtube{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-xing-square:before{content:"\f169"}.fa.fa-youtube-play{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-youtube-play:before{content:"\f167"}.fa.fa-adn,.fa.fa-bitbucket,.fa.fa-bitbucket-square,.fa.fa-dropbox,.fa.fa-flickr,.fa.fa-instagram,.fa.fa-stack-overflow{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-bitbucket-square:before{content:"\f171"}.fa.fa-tumblr,.fa.fa-tumblr-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-tumblr-square:before{content:"\f174"}.fa.fa-long-arrow-down:before{content:"\f309"}.fa.fa-long-arrow-up:before{content:"\f30c"}.fa.fa-long-arrow-left:before{content:"\f30a"}.fa.fa-long-arrow-right:before{content:"\f30b"}.fa.fa-android,.fa.fa-apple,.fa.fa-dribbble,.fa.fa-foursquare,.fa.fa-gittip,.fa.fa-gratipay,.fa.fa-linux,.fa.fa-skype,.fa.fa-trello,.fa.fa-windows{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-gittip:before{content:"\f184"}.fa.fa-sun-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-sun-o:before{content:"\f185"}.fa.fa-moon-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-moon-o:before{content:"\f186"}.fa.fa-pagelines,.fa.fa-renren,.fa.fa-stack-exchange,.fa.fa-vk,.fa.fa-weibo{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-arrow-circle-o-right{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-arrow-circle-o-right:before{content:"\f35a"}.fa.fa-arrow-circle-o-left{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-arrow-circle-o-left:before{content:"\f359"}.fa.fa-caret-square-o-left{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-caret-square-o-left:before{content:"\f191"}.fa.fa-toggle-left{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-toggle-left:before{content:"\f191"}.fa.fa-dot-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-dot-circle-o:before{content:"\f192"}.fa.fa-vimeo-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-vimeo-square:before{content:"\f194"}.fa.fa-try:before,.fa.fa-turkish-lira:before{content:"\e2bb"}.fa.fa-plus-square-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-plus-square-o:before{content:"\f0fe"}.fa.fa-openid,.fa.fa-slack,.fa.fa-wordpress{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-bank:before,.fa.fa-institution:before{content:"\f19c"}.fa.fa-mortar-board:before{content:"\f19d"}.fa.fa-google,.fa.fa-reddit,.fa.fa-reddit-square,.fa.fa-yahoo{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-reddit-square:before{content:"\f1a2"}.fa.fa-behance,.fa.fa-behance-square,.fa.fa-delicious,.fa.fa-digg,.fa.fa-drupal,.fa.fa-joomla,.fa.fa-pied-piper-alt,.fa.fa-pied-piper-pp,.fa.fa-stumbleupon,.fa.fa-stumbleupon-circle{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-behance-square:before{content:"\f1b5"}.fa.fa-steam,.fa.fa-steam-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-steam-square:before{content:"\f1b7"}.fa.fa-automobile:before{content:"\f1b9"}.fa.fa-cab:before{content:"\f1ba"}.fa.fa-deviantart,.fa.fa-soundcloud,.fa.fa-spotify{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-file-pdf-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-pdf-o:before{content:"\f1c1"}.fa.fa-file-word-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-word-o:before{content:"\f1c2"}.fa.fa-file-excel-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-excel-o:before{content:"\f1c3"}.fa.fa-file-powerpoint-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-powerpoint-o:before{content:"\f1c4"}.fa.fa-file-image-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-image-o:before{content:"\f1c5"}.fa.fa-file-photo-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-photo-o:before{content:"\f1c5"}.fa.fa-file-picture-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-picture-o:before{content:"\f1c5"}.fa.fa-file-archive-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-archive-o:before{content:"\f1c6"}.fa.fa-file-zip-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-zip-o:before{content:"\f1c6"}.fa.fa-file-audio-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-audio-o:before{content:"\f1c7"}.fa.fa-file-sound-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-sound-o:before{content:"\f1c7"}.fa.fa-file-video-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-video-o:before{content:"\f1c8"}.fa.fa-file-movie-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-movie-o:before{content:"\f1c8"}.fa.fa-file-code-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-file-code-o:before{content:"\f1c9"}.fa.fa-codepen,.fa.fa-jsfiddle,.fa.fa-vine{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-life-bouy:before,.fa.fa-life-buoy:before,.fa.fa-life-saver:before,.fa.fa-support:before{content:"\f1cd"}.fa.fa-circle-o-notch:before{content:"\f1ce"}.fa.fa-ra,.fa.fa-rebel{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-ra:before{content:"\f1d0"}.fa.fa-resistance{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-resistance:before{content:"\f1d0"}.fa.fa-empire,.fa.fa-ge{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-ge:before{content:"\f1d1"}.fa.fa-git-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-git-square:before{content:"\f1d2"}.fa.fa-git,.fa.fa-hacker-news,.fa.fa-y-combinator-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-y-combinator-square:before{content:"\f1d4"}.fa.fa-yc-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-yc-square:before{content:"\f1d4"}.fa.fa-qq,.fa.fa-tencent-weibo,.fa.fa-wechat,.fa.fa-weixin{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-wechat:before{content:"\f1d7"}.fa.fa-send:before{content:"\f1d8"}.fa.fa-paper-plane-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-paper-plane-o:before{content:"\f1d8"}.fa.fa-send-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-send-o:before{content:"\f1d8"}.fa.fa-circle-thin{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-circle-thin:before{content:"\f111"}.fa.fa-header:before{content:"\f1dc"}.fa.fa-futbol-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-futbol-o:before{content:"\f1e3"}.fa.fa-soccer-ball-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-soccer-ball-o:before{content:"\f1e3"}.fa.fa-slideshare,.fa.fa-twitch,.fa.fa-yelp{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-newspaper-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-newspaper-o:before{content:"\f1ea"}.fa.fa-cc-amex,.fa.fa-cc-discover,.fa.fa-cc-mastercard,.fa.fa-cc-paypal,.fa.fa-cc-stripe,.fa.fa-cc-visa,.fa.fa-google-wallet,.fa.fa-paypal{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-bell-slash-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-bell-slash-o:before{content:"\f1f6"}.fa.fa-trash:before{content:"\f2ed"}.fa.fa-copyright{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-eyedropper:before{content:"\f1fb"}.fa.fa-area-chart:before{content:"\f1fe"}.fa.fa-pie-chart:before{content:"\f200"}.fa.fa-line-chart:before{content:"\f201"}.fa.fa-lastfm,.fa.fa-lastfm-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-lastfm-square:before{content:"\f203"}.fa.fa-angellist,.fa.fa-ioxhost{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-cc{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-cc:before{content:"\f20a"}.fa.fa-ils:before,.fa.fa-shekel:before,.fa.fa-sheqel:before{content:"\f20b"}.fa.fa-buysellads,.fa.fa-connectdevelop,.fa.fa-dashcube,.fa.fa-forumbee,.fa.fa-leanpub,.fa.fa-sellsy,.fa.fa-shirtsinbulk,.fa.fa-simplybuilt,.fa.fa-skyatlas{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-diamond{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-diamond:before{content:"\f3a5"}.fa.fa-intersex:before,.fa.fa-transgender:before{content:"\f224"}.fa.fa-transgender-alt:before{content:"\f225"}.fa.fa-facebook-official{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-facebook-official:before{content:"\f09a"}.fa.fa-pinterest-p,.fa.fa-whatsapp{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-hotel:before{content:"\f236"}.fa.fa-medium,.fa.fa-viacoin,.fa.fa-y-combinator,.fa.fa-yc{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-yc:before{content:"\f23b"}.fa.fa-expeditedssl,.fa.fa-opencart,.fa.fa-optin-monster{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-battery-4:before,.fa.fa-battery:before{content:"\f240"}.fa.fa-battery-3:before{content:"\f241"}.fa.fa-battery-2:before{content:"\f242"}.fa.fa-battery-1:before{content:"\f243"}.fa.fa-battery-0:before{content:"\f244"}.fa.fa-object-group,.fa.fa-object-ungroup,.fa.fa-sticky-note-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-sticky-note-o:before{content:"\f249"}.fa.fa-cc-diners-club,.fa.fa-cc-jcb{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-clone{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hourglass-o:before{content:"\f254"}.fa.fa-hourglass-1:before{content:"\f251"}.fa.fa-hourglass-2:before{content:"\f252"}.fa.fa-hourglass-3:before{content:"\f253"}.fa.fa-hand-rock-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-rock-o:before{content:"\f255"}.fa.fa-hand-grab-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-grab-o:before{content:"\f255"}.fa.fa-hand-paper-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-paper-o:before{content:"\f256"}.fa.fa-hand-stop-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-stop-o:before{content:"\f256"}.fa.fa-hand-scissors-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-scissors-o:before{content:"\f257"}.fa.fa-hand-lizard-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-lizard-o:before{content:"\f258"}.fa.fa-hand-spock-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-spock-o:before{content:"\f259"}.fa.fa-hand-pointer-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-pointer-o:before{content:"\f25a"}.fa.fa-hand-peace-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-hand-peace-o:before{content:"\f25b"}.fa.fa-registered{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-creative-commons,.fa.fa-gg,.fa.fa-gg-circle,.fa.fa-odnoklassniki,.fa.fa-odnoklassniki-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-odnoklassniki-square:before{content:"\f264"}.fa.fa-chrome,.fa.fa-firefox,.fa.fa-get-pocket,.fa.fa-internet-explorer,.fa.fa-opera,.fa.fa-safari,.fa.fa-wikipedia-w{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-television:before{content:"\f26c"}.fa.fa-500px,.fa.fa-amazon,.fa.fa-contao{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-calendar-plus-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-calendar-plus-o:before{content:"\f271"}.fa.fa-calendar-minus-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-calendar-minus-o:before{content:"\f272"}.fa.fa-calendar-times-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-calendar-times-o:before{content:"\f273"}.fa.fa-calendar-check-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-calendar-check-o:before{content:"\f274"}.fa.fa-map-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-map-o:before{content:"\f279"}.fa.fa-commenting:before{content:"\f4ad"}.fa.fa-commenting-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-commenting-o:before{content:"\f4ad"}.fa.fa-houzz,.fa.fa-vimeo{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-vimeo:before{content:"\f27d"}.fa.fa-black-tie,.fa.fa-edge,.fa.fa-fonticons,.fa.fa-reddit-alien{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-credit-card-alt:before{content:"\f09d"}.fa.fa-codiepie,.fa.fa-fort-awesome,.fa.fa-mixcloud,.fa.fa-modx,.fa.fa-product-hunt,.fa.fa-scribd,.fa.fa-usb{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-pause-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-pause-circle-o:before{content:"\f28b"}.fa.fa-stop-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-stop-circle-o:before{content:"\f28d"}.fa.fa-bluetooth,.fa.fa-bluetooth-b,.fa.fa-envira,.fa.fa-gitlab,.fa.fa-wheelchair-alt,.fa.fa-wpbeginner,.fa.fa-wpforms{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-wheelchair-alt:before{content:"\f368"}.fa.fa-question-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-question-circle-o:before{content:"\f059"}.fa.fa-volume-control-phone:before{content:"\f2a0"}.fa.fa-asl-interpreting:before{content:"\f2a3"}.fa.fa-deafness:before,.fa.fa-hard-of-hearing:before{content:"\f2a4"}.fa.fa-glide,.fa.fa-glide-g{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-signing:before{content:"\f2a7"}.fa.fa-viadeo,.fa.fa-viadeo-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-viadeo-square:before{content:"\f2aa"}.fa.fa-snapchat,.fa.fa-snapchat-ghost{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-snapchat-ghost:before{content:"\f2ab"}.fa.fa-snapchat-square{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-snapchat-square:before{content:"\f2ad"}.fa.fa-first-order,.fa.fa-google-plus-official,.fa.fa-pied-piper,.fa.fa-themeisle,.fa.fa-yoast{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-google-plus-official:before{content:"\f2b3"}.fa.fa-google-plus-circle{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-google-plus-circle:before{content:"\f2b3"}.fa.fa-fa,.fa.fa-font-awesome{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-fa:before{content:"\f2b4"}.fa.fa-handshake-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-handshake-o:before{content:"\f2b5"}.fa.fa-envelope-open-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-envelope-open-o:before{content:"\f2b6"}.fa.fa-linode{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-address-book-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-address-book-o:before{content:"\f2b9"}.fa.fa-vcard:before{content:"\f2bb"}.fa.fa-address-card-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-address-card-o:before{content:"\f2bb"}.fa.fa-vcard-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-vcard-o:before{content:"\f2bb"}.fa.fa-user-circle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-user-circle-o:before{content:"\f2bd"}.fa.fa-user-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-user-o:before{content:"\f007"}.fa.fa-id-badge{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-drivers-license:before{content:"\f2c2"}.fa.fa-id-card-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-id-card-o:before{content:"\f2c2"}.fa.fa-drivers-license-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-drivers-license-o:before{content:"\f2c2"}.fa.fa-free-code-camp,.fa.fa-quora,.fa.fa-telegram{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-thermometer-4:before,.fa.fa-thermometer:before{content:"\f2c7"}.fa.fa-thermometer-3:before{content:"\f2c8"}.fa.fa-thermometer-2:before{content:"\f2c9"}.fa.fa-thermometer-1:before{content:"\f2ca"}.fa.fa-thermometer-0:before{content:"\f2cb"}.fa.fa-bathtub:before,.fa.fa-s15:before{content:"\f2cd"}.fa.fa-window-maximize,.fa.fa-window-restore{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-times-rectangle:before{content:"\f410"}.fa.fa-window-close-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-window-close-o:before{content:"\f410"}.fa.fa-times-rectangle-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-times-rectangle-o:before{content:"\f410"}.fa.fa-bandcamp,.fa.fa-eercast,.fa.fa-etsy,.fa.fa-grav,.fa.fa-imdb,.fa.fa-ravelry{font-family:"Font Awesome 6 Brands";font-weight:400}.fa.fa-eercast:before{content:"\f2da"}.fa.fa-snowflake-o{font-family:"Font Awesome 6 Free";font-weight:400}.fa.fa-snowflake-o:before{content:"\f2dc"}.fa.fa-meetup,.fa.fa-superpowers,.fa.fa-wpexplorer{font-family:"Font Awesome 6 Brands";font-weight:400} \ No newline at end of file diff --git a/shared/static/fontawesome/webfonts/fa-brands-400.ttf b/shared/static/fontawesome/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5efb1d4f96407d7019631b361b571ea454f6ce09 GIT binary patch literal 207972 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


    Fu2c+838Y&)^E+3-Bm4W3Quvmd7ZA;T~L2aTdA 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 vFzIcoJNWQXizs4K*~~5y2xeCrAWi~}vOq{e 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 JEQs3W`hO%59=-qo literal 0 HcmV?d00001 diff --git a/shared/static/fontawesome/webfonts/fa-regular-400.woff2 b/shared/static/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b6cabbacb67f4ac88248ef235c5d7a5361f7003b GIT binary patch literal 25452 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 literal 0 HcmV?d00001 diff --git a/shared/static/fontawesome/webfonts/fa-solid-900.ttf b/shared/static/fontawesome/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ec24749db906da77229dcecd61d37b6489d02140 GIT binary patch literal 419720 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