My projects

Comments on products and other Django models

Connect a Product model, protected view, stateless adapter and offline guest-mail policy.

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:

python
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:

python
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:

python
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:

python
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:

python
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:

django
{% 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:

django
{% 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.

python
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

Related documentation

Was this article useful?

Comments 0

Name: up to 40 characters.

After submitting, we will email you a link to confirm your email address.

Maximum 5000 characters.

No published comments yet.