Skip to main content

Standalone Django app for threaded discussions with HTMX support

Project description

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:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_threaded_discussions-0.1.0.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_threaded_discussions-0.1.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file django_threaded_discussions-0.1.0.tar.gz.

File metadata

File hashes

Hashes for django_threaded_discussions-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1a4ffbdb41d7384369d6ebe4877e842effbae8410090e70e7825a7662ff8c31e
MD5 ea7873b3334e6e4dc7c350a71c3ffee8
BLAKE2b-256 7334b8f1d5b08c13541d0b57004993beac499bdb069e2f6f38d590225805dfa1

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_threaded_discussions-0.1.0.tar.gz:

Publisher: publish.yml on heysamtexas/django-threaded-discussions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_threaded_discussions-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_threaded_discussions-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15e36a8c1ea7e79117199f00543e41aa2a551c5457ac349028cba4f9203028f0
MD5 6ef81fe4cf0c394cfadc073a1fecb98c
BLAKE2b-256 27146b67b59c842cd424701b5d1971dbce601f892f59d3d42b69f777fbc890fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_threaded_discussions-0.1.0-py3-none-any.whl:

Publisher: publish.yml on heysamtexas/django-threaded-discussions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page