Compare commits

8 Commits

Author SHA1 Message Date
giles
356d916e26 fix: correct endpoint name to market.browse.product.cart
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:40:55 +00:00
giles
a28424c07c fix: use url_for for cart/product URLs to include page/market slugs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:29:46 +00:00
giles
37ceb37b82 fix: add trailing slashes to cart URLs in product templates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:55:01 +00:00
giles
0c9b8d6aa2 feat: add page_cart_url helper and update shared cart templates for Phase 4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 21:46:50 +00:00
giles
123f752946 feat: per-page SumUp models and migration (Phase 3)
- Migration: add market_place_id to cart_items, page_config_id to orders
- Order model: add page_config_id FK and page_config relationship
- CartItem model: add market_place_id FK and market_place relationship

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:49:55 +00:00
giles
a420bfa7f0 fix: market URLs include post slug prefix
Nav entries template now links to /<post_slug>/<market_slug>/
matching the events app calendar URL pattern. market_url_for()
updated to accept post_slug parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:15:29 +00:00
giles
42f4a8b68f fix: top menu Market link goes to coop blog page, not market app
Removed 'market' from _app_slugs so the Market menu item links
to coop.rose-ash.com/market/ (the blog page) instead of directly
to the market app. Individual markets are linked from the post nav.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:07:52 +00:00
giles
e653acb921 feat: add market links to post nav entries template
Markets now appear alongside calendars in the post nav bar,
linking to the market app via market_url().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 18:36:57 +00:00
11 changed files with 106 additions and 19 deletions

View File

@@ -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')

View File

@@ -413,13 +413,23 @@ class CartItem(Base):
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",

View File

@@ -17,6 +17,12 @@ class Order(Base):
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,
@@ -68,6 +74,11 @@ class Order(Base):
back_populates="order",
lazy="selectin",
)
page_config: Mapped[Optional["PageConfig"]] = relationship(
"PageConfig",
foreign_keys=[page_config_id],
lazy="selectin",
)
class OrderItem(Base):

View File

@@ -13,7 +13,7 @@ from suma_browser.app.csrf import generate_csrf_token
from suma_browser.app.authz import has_access
from suma_browser.app.filters import register as register_filters
from .urls import coop_url, market_url, cart_url, events_url, login_url
from .urls import coop_url, market_url, cart_url, events_url, login_url, page_cart_url
def setup_jinja(app: Quart) -> None:
@@ -93,6 +93,7 @@ def setup_jinja(app: Quart) -> None:
app.jinja_env.globals["cart_url"] = cart_url
app.jinja_env.globals["events_url"] = events_url
app.jinja_env.globals["login_url"] = login_url
app.jinja_env.globals["page_cart_url"] = page_cart_url
# register jinja filters
register_filters(app)

View File

@@ -37,10 +37,10 @@ def events_url(path: str = "/") -> str:
return app_url("events", path)
def market_url_for(market_slug: str, path: str = "/") -> str:
def page_cart_url(page_slug: str, path: str = "/") -> str:
if not path.startswith("/"):
path = "/" + path
return market_url(f"/{market_slug}{path}")
return cart_url(f"/{page_slug}{path}")
def login_url(next_url: str = "") -> str:

View File

@@ -103,7 +103,7 @@
{% if g.user %}
<form
method="post"
action="{{ url_for('cart.checkout')|host }}"
action="{{ url_for('page_cart.page_checkout')|host if page_post is defined and page_post else url_for('cart_global.checkout')|host }}"
class="w-full"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

View File

@@ -27,7 +27,7 @@
<div>
<a
href="{{ url_for('cart.view_cart')|host }}"
href="{{ cart_url('/') }}"
class="inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
>
<i class="fa fa-shopping-cart mr-2" aria-hidden="true"></i>

View File

@@ -1,7 +1,7 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='cart-row', oob=oob) %}
{% call links.link(url_for('cart.view_cart'), hx_select_search ) %}
{% call links.link(cart_url('/'), hx_select_search ) %}
<i class="fa fa-shopping-cart"></i>
<h2 class="text-xl font-bold">cart</h2>
{% endcall %}

View File

@@ -37,6 +37,16 @@
<div>{{calendar.name}}</div>
</a>
{% endfor %}
{# Markets #}
{% for m in markets %}
<a
href="{{ market_url('/' + post.slug + '/' + m.slug + '/') }}"
class="{{styles.nav_button_less_pad}}">
<i class="fa fa-shopping-bag" aria-hidden="true"></i>
<div>{{m.name}}</div>
</a>
{% endfor %}
</div>
</div>

View File

@@ -7,9 +7,9 @@
{% if not quantity %}
<form
action="{{ market_url('/product/' + slug + '/cart') }}"
action="{{ url_for('market.browse.product.cart', slug=slug) }}"
method="post"
hx-post="{{ market_url('/product/' + slug + '/cart') }}"
hx-post="{{ url_for('market.browse.product.cart', slug=slug) }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
class="rounded flex items-center"
@@ -38,9 +38,9 @@
<div class="rounded flex items-center gap-2">
<!-- minus -->
<form
action="{{ market_url('/product/' + slug + '/cart') }}"
action="{{ url_for('market.browse.product.cart', slug=slug) }}"
method="post"
hx-post="{{ market_url('/product/' + slug + '/cart') }}"
hx-post="{{ url_for('market.browse.product.cart', slug=slug) }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
@@ -80,9 +80,9 @@
<!-- plus -->
<form
action="{{ market_url('/product/' + slug + '/cart') }}"
action="{{ url_for('market.browse.product.cart', slug=slug) }}"
method="post"
hx-post="{{ market_url('/product/' + slug + '/cart') }}"
hx-post="{{ url_for('market.browse.product.cart', slug=slug) }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
@@ -139,7 +139,7 @@
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3">
<div class="min-w-0">
<h2 class="text-sm sm:text-base md:text-lg font-semibold text-stone-900">
{% set href=market_url('/product/' + p.slug + '/') %}
{% set href=url_for('market.browse.product.product_detail', slug=p.slug) %}
<a
href="{{ href }}"
hx_get="{{href}}"
@@ -189,9 +189,9 @@
<div class="flex items-center gap-2 text-xs sm:text-sm text-stone-700">
<span class="text-[0.65rem] sm:text-xs uppercase tracking-wide text-stone-500">Quantity</span>
<form
action="{{ market_url('/product/' + p.slug + '/cart') }}"
action="{{ url_for('market.browse.product.cart', slug=p.slug) }}"
method="post"
hx-post="{{ market_url('/product/' + p.slug + '/cart') }}"
hx-post="{{ url_for('market.browse.product.cart', slug=p.slug) }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
@@ -212,9 +212,9 @@
{{ item.quantity }}
</span>
<form
action="{{ market_url('/product/' + p.slug + '/cart') }}"
action="{{ url_for('market.browse.product.cart', slug=p.slug) }}"
method="post"
hx-post="{{ market_url('/product/' + p.slug + '/cart') }}"
hx-post="{{ url_for('market.browse.product.cart', slug=p.slug) }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>

View File

@@ -1,4 +1,4 @@
{% set _app_slugs = {'market': market_url('/'), 'cart': cart_url('/')} %}
{% set _app_slugs = {'cart': cart_url('/')} %}
<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
id="menu-items-nav-wrapper">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}