Django Threaded Discussions
A standalone Django app for threaded discussions with HTMX support. Attach discussions to any model using a simple UUID identifier pattern.
Features
- Zero Coupling: Attach discussions to any model without modifying it
- UUID-Based: Use any unique identifier (UUID, slug, ShortUUID, etc.)
- HTMX Ready: Built-in HTMX templates for dynamic interactions
- SEO Friendly: Server-side rendering support with structured data helpers
- Threaded Comments: Nested comment replies with parent/child relationships
- User Permissions: Built-in authentication and ownership checks
- Spam Protection: Flags for spam detection and moderation
- Lightweight: Minimal dependencies, maximum flexibility
Installation
From PyPI (when published)
pip install django-threaded-discussions
From Source (Development)
# In your workspace directory
git clone https://github.com/directory-platform/django-threaded-discussions.git
cd django-threaded-discussions
pip install -e .
UV Workspace (Recommended for development)
Add to your workspace pyproject.toml:
[tool.uv.workspace]
members = [
"your-project",
"django-threaded-discussions",
]
Then run:
uv sync
Quick Start
1. Add to Django Settings
# settings.py
INSTALLED_APPS = [
# ...
"django_discussions",
]
2. Run Migrations
python manage.py migrate django_discussions
3. Include URLs
# urls.py
urlpatterns = [
# ...
path("discussions/", include("django_discussions.urls")),
]
4. Add to Your Templates
<!-- blog_post_detail.html -->
<div class="discussion-section">
<!-- HTMX lazy-load discussions -->
<div hx-get="/discussions/{{ blog_post.uuid }}/"
hx-trigger="load">
Loading discussions...
</div>
</div>
<!-- Or server-render for SEO -->
{% load discussion_tags %}
{% render_discussions_seo discussion_id=blog_post.uuid %}
That's it! No changes to your models required.
How It Works
Django Threaded Discussions uses a simple discussion_id string field to link threads to your content. This means:
- ✅ No ForeignKeys to your models
- ✅ No GenericForeignKey complexity
- ✅ No migrations when adding new model types
- ✅ Works with any unique identifier you already have
Usage Examples
With UUID Field
class BlogPost(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
# ... other fields
<div hx-get="/discussions/{{ blog_post.uuid }}/">...</div>
With Slug Field
class Article(models.Model):
slug = models.SlugField(unique=True)
# ... other fields
<div hx-get="/discussions/{{ article.slug }}/">...</div>
With ShortUUID
from shortuuid.django_fields import ShortUUIDField
class Product(models.Model):
short_id = ShortUUIDField(unique=True)
# ... other fields
<div hx-get="/discussions/{{ product.short_id }}/">...</div>
With Custom Identifier
class Entity(models.Model):
id = models.AutoField(primary_key=True)
@property
def discussion_id(self):
return f"entity-{self.id}"
<div hx-get="/discussions/{{ entity.discussion_id }}/">...</div>
Authentication & Permissions
Discussions integrate seamlessly with Django's auth system:
Public Views (No Login Required):
- View discussion threads
- View comments
- See thread counts
Authenticated Views (@login_required):
- Create new threads
- Post comments
- Reply to comments
Ownership Checks:
- Delete own threads/comments
- Superusers can delete any content
# Automatically enforced
@login_required
def create_thread(request, discussion_id):
thread = Thread.objects.create(
discussion_id=discussion_id,
author=request.user, # ← Django's auth
# ...
)
SEO Optimization
Server-Side Rendering
Render discussions on first page load for SEO:
# views.py
from django_discussions.models import Thread
def blog_post_detail(request, slug):
post = get_object_or_404(BlogPost, slug=slug)
# Get SEO-worthy threads
seo_threads = Thread.get_seo_threads(
discussion_id=str(post.uuid),
max_count=5
)
context = {
'post': post,
'seo_threads': seo_threads,
}
return render(request, 'blog_detail.html', context)
<!-- blog_detail.html -->
<div id="discussions">
<!-- Server-rendered for SEO -->
{% for thread in seo_threads %}
{% include "django_discussions/_thread.html" %}
{% endfor %}
<!-- HTMX load more -->
<div hx-get="/discussions/{{ post.uuid }}/?offset=5">
Load more...
</div>
</div>
Structured Data
Add schema.org markup for rich snippets:
{% load discussion_tags %}
{% discussion_structured_data discussion_id=post.uuid %}
Generates:
{
"@context": "https://schema.org",
"@type": "DiscussionForumPosting",
"headline": "Thread title",
"commentCount": 42,
// ... etc
}
Template Tags
{% load discussion_tags %}
<!-- Render SEO-optimized discussions -->
{% render_discussions_seo discussion_id="abc-123" max_threads=10 %}
<!-- Get thread count -->
{% thread_count discussion_id="abc-123" %}
<!-- Structured data for SEO -->
{% discussion_structured_data discussion_id="abc-123" %}
Models
Thread
class Thread(models.Model):
discussion_id = CharField(max_length=255) # Your unique identifier
title = CharField(max_length=255)
content = TextField()
author = ForeignKey(User)
short_uuid = ShortUUIDField(unique=True)
# Moderation flags
is_active = BooleanField(default=True)
is_spam = BooleanField(default=False)
is_deleted = BooleanField(default=False)
is_locked = BooleanField(default=False)
# Timestamps
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
Comment
class Comment(models.Model):
thread = ForeignKey(Thread)
content = TextField()
author = ForeignKey(User)
parent = ForeignKey('self', null=True) # Threaded replies
short_uuid = ShortUUIDField(unique=True)
# Moderation flags
is_active = BooleanField(default=True)
is_pinned = BooleanField(default=False)
is_spam = BooleanField(default=False)
# Timestamps
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
Customization
Settings
# settings.py
DISCUSSIONS_SETTINGS = {
'MAX_THREADS_PER_PAGE': 20,
'MAX_COMMENTS_PER_THREAD': 100,
'ALLOW_ANONYMOUS_VIEWING': True,
'MIN_TITLE_LENGTH': 3,
'MAX_TITLE_LENGTH': 100,
}
Custom Templates
Override any template by creating templates/django_discussions/ in your project:
your_project/
templates/
django_discussions/
index.html # Override thread list
_thread_header.html # Override thread display
_comment.html # Override comment display
Admin Interface
Discussions are registered in Django admin with search and filtering:
# Automatic admin registration
# Access at /admin/django_discussions/thread/
# Access at /admin/django_discussions/comment/
Development
Setup
git clone https://github.com/directory-platform/django-threaded-discussions.git
cd django-threaded-discussions
uv sync --extra dev
Running Tests
make test
# or
PYTHONPATH=. uv run python tests/manage.py test
Code Quality
make lint # Ruff linting
make format # Auto-format
make typecheck # Mypy type checking
make check # All checks + tests
Requirements
- Python 3.12+
- Django 4.2+
- shortuuid 1.0+
Contributing
Contributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - see LICENSE file for details.
Credits
Built by the Directory Platform team as part of the directory ecosystem.
Links
- Documentation: [Coming soon]
- GitHub: https://github.com/directory-platform/django-threaded-discussions
- Issues: https://github.com/directory-platform/django-threaded-discussions/issues
- PyPI: https://pypi.org/project/django-threaded-discussions/ (after first release)
Metadata
Release files for django-threaded-discussions 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_threaded_discussions-0.1.0.tar.gz | 21.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_threaded_discussions-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.4 kB
Release files / django_threaded_discussions-0.1.0.tar.gz
| Download URL | django_threaded_discussions-0.1.0.tar.gz |
|---|---|
| Size | 21.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1a4ffbdb41d7384369d6ebe4877e842effbae8410090e70e7825a7662ff8c31e
|
|
BLAKE2b-256 checksum How to use checksums |
7334b8f1d5b08c13541d0b57004993beac499bdb069e2f6f38d590225805dfa1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2025.
Transparency logRelease files / django_threaded_discussions-0.1.0-py3-none-any.whl
| Download URL | django_threaded_discussions-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
15e36a8c1ea7e79117199f00543e41aa2a551c5457ac349028cba4f9203028f0
|
|
BLAKE2b-256 checksum How to use checksums |
27146b67b59c842cd424701b5d1971dbce601f892f59d3d42b69f777fbc890fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2025.
Transparency log