Lesson 17 / 22 · Commentail 1.3.2
Use this integration for targets outside Wagtail Page, such as products. Complete lesson 2 first. The example uses a host app named shop; adapt an existing Product rather than creating a duplicate model.
Model and protected view
In a new practice project run python manage.py startapp shop and add shop to INSTALLED_APPS. Put this in shop/models.py:
from django.db import models
from django.urls import reverse
class Product(models.Model):
site = models.ForeignKey("wagtailcore.Site", on_delete=models.CASCADE, related_name="shop_products")
title = models.CharField(max_length=200)
is_public = models.BooleanField(default=False)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("product-detail", args=[self.pk])
In shop/views.py:
from django.http import Http404
from django.shortcuts import get_object_or_404, render
from wagtail.models import Site
from shop.models import Product
def product_detail(request, pk):
site = Site.find_for_request(request)
if site is None:
raise Http404
product = get_object_or_404(Product, pk=pk, site=site)
if not product.is_public and not request.user.is_staff:
raise Http404
return render(request, "shop/product_detail.html", {"product": product})
The request Site must match the product's Site. Protect the host view as well as Commentail. In the root urls.py, import path if necessary and insert before the Wagtail catch-all:
from shop.views import product_detail
# Insert before Wagtail's catch-all in the existing urlpatterns.
path("products/<int:pk>/", product_detail, name="product-detail"),
Adapter and mail entitlement
In shop/comments.py:
from shop.models import Product
class ProductAdapter:
def get_site(self, obj, request):
return obj.site
def can_view(self, obj, request):
return obj.is_public or request.user.is_staff
def get_url(self, obj, site):
return obj.get_absolute_url()
def can_send_product_email(recipient, target, site):
"""Allow guest mail only for this Site's public products."""
return (
isinstance(target, Product)
and target.site_id == site.pk
and target.is_public
)
Merge into the existing COMMENTAIL default:
COMMENTAIL = {
"default": {
"OBJECT_ADAPTERS": {"shop.Product": "shop.comments.ProductAdapter"},
"CAN_SEND_GUEST_EMAIL": "shop.comments.can_send_product_email",
},
}
The guest-mail callback authorizes only public products on the exact Site. For multiple object types or restricted Pages, extend the single callback with separate entitlement checks. Never use unconditional True. For account mail the worker checks the adapter with the recipient account and an empty session; browser-only proof is unavailable.
For a working example even when the practice site has no base.html, create templates/course_base.html:
{% load i18n commentail_tags %}
{% get_current_language as current_language %}
<!doctype html>
<html lang="{{ current_language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ product.title }}</title>
{% commentail_css %}
{% commentail_js %}
</head>
<body>
<main>{% block content %}{% endblock %}</main>
</body>
</html>
Create templates/shop/product_detail.html:
{% extends "course_base.html" %}
{% load commentail_tags %}
{% block content %}
<h1>{{ product.title }}</h1>
{% commentail product %}
{% endblock %}
On your own site you can instead extend its existing base template with Commentail assets, adapting the content block name. Run python manage.py makemigrations shop, python manage.py migrate and python manage.py check.
Create a practice product
Open python manage.py shell and paste the following, substituting the exact hostname and port from your Wagtail Site. The unchanged practice example uses 127.0.0.1 and 8000.
from wagtail.models import Site
from shop.models import Product
site = Site.objects.get(hostname="YOUR_TEST_HOST", port=8000)
product = Product.objects.create(site=site, title="Practice product", is_public=True)
print(product.get_absolute_url())
Open the printed address on that site. A Site DoesNotExist error means hostname/port did not match; correct them rather than creating an unrelated Site to bypass the error.
The first authorized render creates an entry under Comments → Object discussions, where object-specific settings are managed. Test public and private products as guest and staff, a different Site, and the complete mail flow. Supported targets are concrete non-proxy models with integer, UUID or string primary keys up to 255 characters.
Ordinary Django ORM deletion removes related discussions and service records. Raw SQL and historical-model data migrations bypass runtime cleanup and require explicit handling. Confirm retention requirements before deleting a live product.
Related lessons
- 2. Add Commentail to an existing Wagtail site
- 15. Avatars, display names and dynamic templates
- 19. Production nginx, caching and user data
Choose a sign-in method or comment as a guest below.