fix: email images — propagate shop_cdn_endpoint + thumbnails in offers
Two coupled fixes the rendered sale email exposed:
<img src="None/<shop_id>/<product_id>/thumbnail1?ts=...">
1. ShopContextRequestWrapper (lib/crypto_watcher/__init__.py:1243)
wraps env_request for email-rendering inside the watcher loop. It
overrode domain / host_url / app and proxied everything else via
__getattr__. send_purchase_email + send_sale_email read
`request.shop_cdn_endpoint` — but that's a reified Pyramid request
method, not a static attr on env_request. __getattr__'s default
returned None, and the email's <img src> became `None/.../...`.
Fix: add explicit `shop_cdn_endpoint` (and `shop`) properties on
the wrapper, derived from the wrapper's `_shop`. Order:
- BYOB shop with primary_s3_cdn_endpoint set → that
- else `app["bucket.secure_uploads.get_endpoint"]` (MPS default)
- else None
The BYOB branch also defends against an enabled-but-blank
primary_s3_cdn_endpoint — drops to the default rather than
returning None.
2. Offer + auction-outbid emails carried no product thumbnail at
all — bummer, since the recipient can't visually identify which
item the negotiation is about. New _product_thumbnail_html(request,
product) helper renders a 184px-max <img> identical to the
purchase/sale shape (or empty string if the product has no
thumbnail1 extension / no CDN endpoint).
Wired into:
- send_offer_received_email
- send_offer_accepted_email
- send_offer_countered_email
- send_offer_declined_email
- send_offer_withdrawn_email
- send_offer_buyer_cancelled_email
- send_auction_outbid_email
Templates in lib/mail_messages.py gained a `{thumbnail}` slot
between the headline and the click-through link. Text variants are
unchanged (no inline images in text email).
Both fixes target the same surface: every transactional email now
renders the right image, regardless of whether it's sent from a
view (Pyramid request) or the crypto watcher loop (wrapped env_req).
This commit is contained in:
parent
da79b11cae
commit
9fc280fd7b
3 changed files with 71 additions and 0 deletions
|
|
@ -1275,6 +1275,36 @@ class ShopContextRequestWrapper:
|
|||
"""Proxy app attribute from original request."""
|
||||
return getattr(self._original_request, "app", {})
|
||||
|
||||
@property
|
||||
def shop(self):
|
||||
"""The shop this wrapped request is scoped to. Used by
|
||||
derivative properties (e.g. shop_cdn_endpoint) that would
|
||||
otherwise return None — env_request from the watcher loop
|
||||
doesn't carry a request.shop."""
|
||||
return self._shop
|
||||
|
||||
@property
|
||||
def shop_cdn_endpoint(self):
|
||||
"""Mirror of the regular `request.shop_cdn_endpoint` reified
|
||||
method (see request_methods.py:add_shop_cdn_endpoint). Used by
|
||||
send_purchase_email + send_sale_email to build product
|
||||
thumbnail URLs. Without this, the proxy fell through to None
|
||||
and the email's <img src> rendered as `None/<path>/...`."""
|
||||
shop = self._shop
|
||||
# BYOB shop with an explicit CDN endpoint wins.
|
||||
if shop is not None and getattr(shop, "has_primary_s3", False):
|
||||
byob = shop.primary_s3_cdn_endpoint
|
||||
if byob:
|
||||
return byob
|
||||
# Otherwise fall back to the MPS-default public CDN.
|
||||
app = getattr(self._original_request, "app", None)
|
||||
if app is None:
|
||||
return None
|
||||
try:
|
||||
return app["bucket.secure_uploads.get_endpoint"]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def create_shop_context_request(env_request, crypto_payment: CryptoPayment):
|
||||
"""Create a request wrapper with shop domain context for email generation."""
|
||||
|
|
|
|||
|
|
@ -632,10 +632,38 @@ def send_auction_outbid_email(request, to_email, auction):
|
|||
title=auction.product.title,
|
||||
high=f"{auction.current_high:.2f}",
|
||||
auction_url=auction_url,
|
||||
thumbnail=_product_thumbnail_html(request, auction.product),
|
||||
)
|
||||
send_pyramid_email(request, to_email, subject, text, html)
|
||||
|
||||
|
||||
def _product_thumbnail_html(request, product):
|
||||
"""Render a tokenized thumbnail <img> tag for inclusion in a
|
||||
transactional email body, or empty string if the product has no
|
||||
thumbnail1 extension uploaded.
|
||||
|
||||
Sized for 184px max — same dimensions as the purchase/sale emails
|
||||
use, so the recipient sees a consistent image card across the whole
|
||||
email surface. Returns "" (not None) so format calls can splice
|
||||
without conditional logic.
|
||||
"""
|
||||
if product is None or "thumbnail1" not in getattr(product, "extensions", []):
|
||||
return ""
|
||||
cdn = getattr(request, "shop_cdn_endpoint", None)
|
||||
if not cdn:
|
||||
return ""
|
||||
return (
|
||||
'<img src="{cdn}/{path}/thumbnail1?ts={ts}" '
|
||||
'style="border: 1px solid #ddd; border-radius: 4px; '
|
||||
'max-width: 184px; max-height: 184px; width: auto; '
|
||||
'height: auto;" alt="" />'
|
||||
).format(
|
||||
cdn=cdn,
|
||||
path=product.s3_path,
|
||||
ts=product.updated_timestamp,
|
||||
)
|
||||
|
||||
|
||||
def send_offer_received_email(request, to_email, offer):
|
||||
"""Notify a shop owner that a new offer arrived for review."""
|
||||
offer_url = f"{request.host_url}/o/{offer.uuid_str}"
|
||||
|
|
@ -649,6 +677,7 @@ def send_offer_received_email(request, to_email, offer):
|
|||
amount=f"{offer.current_amount:.2f}",
|
||||
title=offer.product.title,
|
||||
offer_url=offer_url,
|
||||
thumbnail=_product_thumbnail_html(request, offer.product),
|
||||
)
|
||||
send_pyramid_email(request, to_email, subject, text, html)
|
||||
|
||||
|
|
@ -666,6 +695,7 @@ def send_offer_accepted_email(request, to_email, offer):
|
|||
amount=f"{offer.current_amount:.2f}",
|
||||
title=offer.product.title,
|
||||
offer_url=offer_url,
|
||||
thumbnail=_product_thumbnail_html(request, offer.product),
|
||||
)
|
||||
send_pyramid_email(request, to_email, subject, text, html)
|
||||
|
||||
|
|
@ -680,6 +710,7 @@ def send_offer_countered_email(request, to_email, offer):
|
|||
amount=f"{offer.current_amount:.2f}",
|
||||
title=offer.product.title,
|
||||
offer_url=offer_url,
|
||||
thumbnail=_product_thumbnail_html(request, offer.product),
|
||||
)
|
||||
send_pyramid_email(
|
||||
request, to_email, subject,
|
||||
|
|
@ -698,6 +729,7 @@ def send_offer_declined_email(request, to_email, offer):
|
|||
amount=f"{offer.current_amount:.2f}",
|
||||
title=offer.product.title,
|
||||
offer_url=offer_url,
|
||||
thumbnail=_product_thumbnail_html(request, offer.product),
|
||||
)
|
||||
send_pyramid_email(
|
||||
request, to_email, subject,
|
||||
|
|
@ -716,6 +748,7 @@ def send_offer_withdrawn_email(request, to_email, offer):
|
|||
amount=f"{offer.current_amount:.2f}",
|
||||
title=offer.product.title,
|
||||
offer_url=offer_url,
|
||||
thumbnail=_product_thumbnail_html(request, offer.product),
|
||||
)
|
||||
send_pyramid_email(
|
||||
request, to_email, subject,
|
||||
|
|
@ -734,6 +767,7 @@ def send_offer_buyer_cancelled_email(request, to_email, offer):
|
|||
amount=f"{offer.current_amount:.2f}",
|
||||
title=offer.product.title,
|
||||
offer_url=offer_url,
|
||||
thumbnail=_product_thumbnail_html(request, offer.product),
|
||||
)
|
||||
send_pyramid_email(
|
||||
request, to_email, subject,
|
||||
|
|
|
|||
|
|
@ -360,6 +360,7 @@ AUCTION_OUTBID_HTML = """
|
|||
<body>
|
||||
<h2>You have been outbid</h2>
|
||||
<p>Someone outbid you on <strong>{title}</strong>.</p>
|
||||
{thumbnail}
|
||||
<p>Current high: <strong>${high}</strong>.</p>
|
||||
<p><a href="{auction_url}" style="font-weight: bold;">Place a new bid</a></p>
|
||||
</body>
|
||||
|
|
@ -381,6 +382,7 @@ OFFER_RECEIVED_HTML = """
|
|||
<body>
|
||||
<h2>New offer received</h2>
|
||||
<p>You received a new offer of <strong>${amount}</strong> for <strong>{title}</strong>.</p>
|
||||
{thumbnail}
|
||||
<p><a href="{offer_url}" style="font-weight: bold;">View offer</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -401,6 +403,7 @@ OFFER_ACCEPTED_HTML = """
|
|||
<body>
|
||||
<h2>Offer accepted</h2>
|
||||
<p>Your offer of <strong>${amount}</strong> for <strong>{title}</strong> was accepted.</p>
|
||||
{thumbnail}
|
||||
<p><a href="{offer_url}" style="font-weight: bold;">Pay now</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -421,6 +424,7 @@ OFFER_COUNTERED_HTML = """
|
|||
<body>
|
||||
<h2>Counter offer received</h2>
|
||||
<p>The other party countered with <strong>${amount}</strong> for <strong>{title}</strong>.</p>
|
||||
{thumbnail}
|
||||
<p><a href="{offer_url}" style="font-weight: bold;">Review and respond</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -441,6 +445,7 @@ OFFER_DECLINED_HTML = """
|
|||
<body>
|
||||
<h2>Offer declined</h2>
|
||||
<p>Your offer of <strong>${amount}</strong> for <strong>{title}</strong> was declined.</p>
|
||||
{thumbnail}
|
||||
<p><a href="{offer_url}" style="font-weight: bold;">View details</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -461,6 +466,7 @@ OFFER_WITHDRAWN_HTML = """
|
|||
<body>
|
||||
<h2>Offer withdrawn</h2>
|
||||
<p>The buyer withdrew their offer of <strong>${amount}</strong> for <strong>{title}</strong>.</p>
|
||||
{thumbnail}
|
||||
<p><a href="{offer_url}" style="font-weight: bold;">View details</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -482,6 +488,7 @@ OFFER_BUYER_CANCELLED_HTML = """
|
|||
<body>
|
||||
<h2>Buyer cancelled accepted offer</h2>
|
||||
<p>The buyer cancelled their accepted offer of <strong>${amount}</strong> for <strong>{title}</strong>.</p>
|
||||
{thumbnail}
|
||||
<p>You accepted this offer earlier but the buyer backed out before paying.</p>
|
||||
<p><a href="{offer_url}" style="font-weight: bold;">View details</a></p>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue