Master switch for the entire comment system. Set to True to enable
comments. Defaults to False.
CAST_COMMENTS_EXCLUDE_FIELDS
Tuple of form field names to hide from the comment form. Useful for
removing fields like email, url, or title. Defaults to ().
Also accepts the legacy name FLUENT_COMMENTS_EXCLUDE_FIELDS.
CAST_COMMENTS_DEFAULT_MODERATOR
Dotted Python path to a moderator class. The class must provide allow()
and moderate() methods (see Comment Moderation). Set to
"none", "null", "default", or "" to use a built-in
NullModerator that allows all comments and never moderates. Defaults to
"cast.moderation.Moderator" (the built-in spam filter moderator).
Also accepts the legacy name FLUENT_COMMENTS_DEFAULT_MODERATOR.
CAST_COMMENTS_FORM_CSS_CLASS
CSS class applied to the comment form. Defaults to
"comments-formform-horizontal".
CAST_COMMENTS_LABEL_CSS_CLASS
CSS class for form labels (crispy-forms). Defaults to "col-sm-2".
CAST_COMMENTS_FIELD_CSS_CLASS
CSS class for form field wrappers (crispy-forms). Defaults to
"col-sm-10".
CRISPY_TEMPLATE_PACK
The crispy-forms template pack used for rendering form fields and AJAX
error messages. Not specific to django-cast but affects how comment form
errors are rendered in AJAX responses. Defaults to "bootstrap4".
Setting COMMENTS_APP="cast.comments" tells django-contrib-comments
to use the cast comments package. The package provides two hook functions
in its __init__.py:
get_model()
Returns CastComment, a proxy model (managed=False) that
adds a custom manager with select_related("user") for efficient
queryset loading. When threadedcomments is installed, CastComment
inherits from ThreadedComment instead of the plain Comment model.
get_form()
Returns CastCommentForm, which extends the appropriate base form
(ThreadedCommentForm or CommentForm). It removes fields listed in
CAST_COMMENTS_EXCLUDE_FIELDS and reorders the remaining fields so
security fields (content_type, object_pk, timestamp,
security_hash) appear first, the parent field follows in threaded
mode, visible fields come next, and the honeypot field is placed last.
If threadedcomments is in INSTALLED_APPS, threaded replies are
enabled automatically. The comment system detects the package at startup and
switches the base model from django_comments.models.Comment to
threadedcomments.models.ThreadedComment, which adds a parent foreign
key for nesting.
When threaded mode is active:
The comment form includes a hidden parent field.
Each rendered comment shows a “reply” link that sets the parent value via
the JavaScript layer (see AJAX Comment Posting).
The comment list template uses fill_tree and annotate_tree filters
from threadedcomments to produce nested <ul> markup.
The flat list template (flat_list.html) is used when threaded comments are
disabled.
Note
Install django-threadedcomments and add "threadedcomments" to
INSTALLED_APPS to enable threaded replies. No additional configuration
is required.
Comments are posted asynchronously via a dedicated AJAX endpoint. The
JavaScript client (ajaxcomments.ts, built as an IIFE by Vite) intercepts
the comment form submission and uses fetch to POST to
/comments/post/ajax/. The request includes an
X-Requested-With:XMLHttpRequest header so the server-side view can
distinguish it from a regular form submission.
Authentication check – if the user is logged in, name and
email are auto-filled from the user profile when not provided.
Target resolution – the content_type and object_pk fields
identify the target object (typically a Post page).
Form validation – the standard django_comments form is
instantiated. Security hash and honeypot checks run first.
Preview mode – if the preview button was clicked and the form is
valid, the view renders the comment HTML and returns it without saving. If
the form has errors, no comment object is produced and the response
contains only the error details.
Signal dispatch – comment_will_be_posted fires, giving the
moderator (see Comment Moderation) a chance to mark the comment
as spam. If any receiver returns False, the comment is rejected.
Save and respond – the comment is saved, comment_was_posted
fires, and a JSON response is returned.
On success or form-validation errors, the AJAX endpoint returns a JSON object
with the following fields. Early failures (missing fields, invalid content
type, security hash mismatch) return a plain-text HTTP 400 response instead.
JSON fields:
success
Boolean indicating whether the comment was accepted.
action
Either "post" or "preview".
errors
A dictionary of field-name to rendered error HTML (empty on success).
html
The rendered comment HTML fragment, ready to insert into the page. Only
present when a comment object was successfully created or previewed (absent
on validation errors).
comment_id
The database ID of the saved comment (absent on preview or error).
parent_id
The parent comment ID when threaded replies are active (null for
top-level comments).
is_moderated
Present only for staff users. true when the comment was auto-moderated
(marked not public).
use_threadedcomments
Boolean indicating whether threaded comment mode is active.
The AJAX comment posting uses bundled assets under:
fluent_comments/js/ajaxcomments.js
fluent_comments/css/ajaxcomments.css
If these are not included in your templates, the form falls back to the
default django-contrib-comments flow (redirecting to comments/posted/ and
comments/preview/).
The form template at comments/form.html sets
data-ajax-action="{%url'comments-post-comment-ajax'%}" on the
<form> element. The JavaScript reads this attribute to determine the
AJAX endpoint URL.
Hint
The {%ajax_comment_tagsforobject%} template tag renders the
cancel-reply link, a loading spinner, and success/moderation messages
used by the JavaScript layer.
The fluent_comments_tags template tag library provides several tags and
filters for rendering comments in templates.
{%ajax_comment_tagsobject%}
Renders the AJAX helper markup (cancel-reply link, loading spinner,
success message, moderation notice for staff). Accepts both
{%ajax_comment_tagsobject%} and {%ajax_comment_tagsforobject%}
syntax.
{%render_commentcomment%}
Renders a single comment using the appropriate template from the
lookup chain: comments/<app>/<model>/comment.html,
comments/<app>/comment.html, comments/comment.html.
{%fluent_comments_list%}
Renders the full comment list. Uses threaded_list.html when threaded
comments are active, flat_list.html otherwise.
{{object|comments_are_open}}
Filter that returns True if comments are enabled for the object.
Checks the object’s comments_are_enabled attribute.
{{object|comments_are_moderated}}
Filter that returns whether comments require pre-approval for the object.
Currently always returns False (all moderation is handled
post-submission by the spam filter).
{{object|comments_count}}
Filter that returns the number of comments for the object.
The moderator is declared as a module-level SimpleLazyObject. It is not
resolved at import time – the CAST_COMMENTS_DEFAULT_MODERATOR setting is
read and the moderator class instantiated on first attribute access (i.e.,
when the first comment is submitted). The resolved instance is then reused
for all subsequent comments.
When a comment is submitted:
The on_comment_will_be_posted signal receiver calls the moderator’s
allow() method. If it returns False, the comment is rejected
entirely (the signal receiver returns False, causing the view to
return an error).
The moderator’s moderate() method is called. It can mark the comment as
spam by setting is_removed=True and is_public=False.
The default cast.moderation.Moderator class:
Always allows comments (allow() returns True) – even spam
comments are kept as training data for the classifier.
Auto-classifies comments using the spam filter. If the filter predicts
"spam", the comment is saved with is_removed=True and
is_public=False. Otherwise, the comment is published immediately.
Note
Staff users see a “(moderated)” flag next to auto-moderated comments, and
the AJAX response includes is_moderated:true so the JavaScript can
display a notice.
Django Cast includes a
Naive Bayes
spam classifier implemented in pure Python (src/cast/models/moderation.py).
It is fast, easy to train, and effective at filtering most spam.
The spam filter consists of three main components:
NaiveBayes
The classifier itself. Stores prior probabilities per label and
per-word label counts. Supports fit(), predict(), and
predict_label() methods.
SpamFilter (Django model)
Persists a trained NaiveBayes instance in a JSONField along with
performance metrics. The model field uses custom ModelEncoder /
ModelDecoder classes for JSON serialization of the classifier.
Evaluation
Cross-validation harness that measures precision, recall, and F1 score
using stratified k-fold splits (default: 3 folds).
Training data is derived directly from existing comments:
A comment is labeled ham if is_public=True and
is_removed=False.
All other comments are labeled spam.
Each comment is converted to a message string by concatenating its name,
email, title, and comment fields. The classifier tokenizes this
string into lowercase words (using the regex pattern \b\w\w+\b) and
builds per-word label frequency counts.
# How a comment becomes a training messagemessage=f"{comment.name}{comment.email}{comment.title}{comment.comment}"
When a new comment arrives, the Moderator.moderate() method:
Converts the comment to a message string.
Uses the SpamFilter instance that was loaded during Moderator
initialization (SpamFilter.get_default() is called once in
__init__, not on every comment).
Calls predict_label(message) on the stored NaiveBayes model.
If the predicted label is "spam", the comment is marked as removed and
not public.
The classifier computes posterior probabilities for each label by multiplying
the prior probability by each word’s conditional probability, normalizing
after each word. The label with the highest final probability wins.
The Evaluation class performs stratified k-fold cross-validation:
Comments are split by label so each fold has a proportional mix of ham and
spam.
For each fold, a fresh NaiveBayes model is trained on the remaining
folds and evaluated on the held-out fold.
A confusion matrix (true positives, false positives, false negatives) is
built per label.
Precision, recall, and F1 are computed from the final fold’s confusion
matrix.
The resulting metrics are stored in the SpamFilter.performance JSON field
and displayed as read-only spam and ham columns in the admin list view.
Hint
If classification quality degrades, check the balance of ham vs. spam in
your comment dataset. The classifier works best when both classes have a
reasonable number of examples.
Comments¶
Django Cast provides a full commenting system built on top of django-contrib-comments. Comments can be enabled or disabled at three levels:
The global CAST_COMMENTS_ENABLED setting (defaults to
False).Each
Blogmodel has acomments_enabledfield (defaults toTrue).Each
Postmodel also has acomments_enabledfield (defaults toTrue).A comment form is only rendered when all three levels evaluate to enabled.
Configuration¶
To enable the built-in comments integration, set:
Settings¶
CAST_COMMENTS_ENABLEDMaster switch for the entire comment system. Set to
Trueto enable comments. Defaults toFalse.CAST_COMMENTS_EXCLUDE_FIELDSTuple of form field names to hide from the comment form. Useful for removing fields like
email,url, ortitle. Defaults to(). Also accepts the legacy nameFLUENT_COMMENTS_EXCLUDE_FIELDS.CAST_COMMENTS_DEFAULT_MODERATORDotted Python path to a moderator class. The class must provide
allow()andmoderate()methods (see Comment Moderation). Set to"none","null","default", or""to use a built-inNullModeratorthat allows all comments and never moderates. Defaults to"cast.moderation.Moderator"(the built-in spam filter moderator). Also accepts the legacy nameFLUENT_COMMENTS_DEFAULT_MODERATOR.CAST_COMMENTS_FORM_CSS_CLASSCSS class applied to the comment form. Defaults to
"comments-form form-horizontal".CAST_COMMENTS_LABEL_CSS_CLASSCSS class for form labels (crispy-forms). Defaults to
"col-sm-2".CAST_COMMENTS_FIELD_CSS_CLASSCSS class for form field wrappers (crispy-forms). Defaults to
"col-sm-10".CRISPY_TEMPLATE_PACKThe crispy-forms template pack used for rendering form fields and AJAX error messages. Not specific to django-cast but affects how comment form errors are rendered in AJAX responses. Defaults to
"bootstrap4".Example configuration:
How
COMMENTS_APPIntegration Works¶Setting
COMMENTS_APP = "cast.comments"tellsdjango-contrib-commentsto use the cast comments package. The package provides two hook functions in its__init__.py:get_model()Returns
CastComment, a proxy model (managed = False) that adds a custom manager withselect_related("user")for efficient queryset loading. Whenthreadedcommentsis installed,CastCommentinherits fromThreadedCommentinstead of the plainCommentmodel.get_form()Returns
CastCommentForm, which extends the appropriate base form (ThreadedCommentFormorCommentForm). It removes fields listed inCAST_COMMENTS_EXCLUDE_FIELDSand reorders the remaining fields so security fields (content_type,object_pk,timestamp,security_hash) appear first, theparentfield follows in threaded mode, visible fields come next, and thehoneypotfield is placed last.Threaded Replies¶
If
threadedcommentsis inINSTALLED_APPS, threaded replies are enabled automatically. The comment system detects the package at startup and switches the base model fromdjango_comments.models.Commenttothreadedcomments.models.ThreadedComment, which adds aparentforeign key for nesting.When threaded mode is active:
The comment form includes a hidden
parentfield.Each rendered comment shows a “reply” link that sets the
parentvalue via the JavaScript layer (see AJAX Comment Posting).The comment list template uses
fill_treeandannotate_treefilters fromthreadedcommentsto produce nested<ul>markup.The flat list template (
flat_list.html) is used when threaded comments are disabled.Note
Install
django-threadedcommentsand add"threadedcomments"toINSTALLED_APPSto enable threaded replies. No additional configuration is required.AJAX Comment Posting¶
Comments are posted asynchronously via a dedicated AJAX endpoint. The JavaScript client (
ajaxcomments.ts, built as an IIFE by Vite) intercepts the comment form submission and usesfetchto POST to/comments/post/ajax/. The request includes anX-Requested-With: XMLHttpRequestheader so the server-side view can distinguish it from a regular form submission.Server-Side Flow¶
The
post_comment_ajaxview handles the request:Authentication check – if the user is logged in,
nameandemailare auto-filled from the user profile when not provided.Target resolution – the
content_typeandobject_pkfields identify the target object (typically aPostpage).Form validation – the standard
django_commentsform is instantiated. Security hash and honeypot checks run first.Preview mode – if the
previewbutton was clicked and the form is valid, the view renders the comment HTML and returns it without saving. If the form has errors, no comment object is produced and the response contains only the error details.Signal dispatch –
comment_will_be_postedfires, giving the moderator (see Comment Moderation) a chance to mark the comment as spam. If any receiver returnsFalse, the comment is rejected.Save and respond – the comment is saved,
comment_was_postedfires, and a JSON response is returned.JSON Response Format¶
On success or form-validation errors, the AJAX endpoint returns a JSON object with the following fields. Early failures (missing fields, invalid content type, security hash mismatch) return a plain-text HTTP 400 response instead.
JSON fields:
successBoolean indicating whether the comment was accepted.
actionEither
"post"or"preview".errorsA dictionary of field-name to rendered error HTML (empty on success).
htmlThe rendered comment HTML fragment, ready to insert into the page. Only present when a comment object was successfully created or previewed (absent on validation errors).
comment_idThe database ID of the saved comment (absent on preview or error).
parent_idThe parent comment ID when threaded replies are active (
nullfor top-level comments).is_moderatedPresent only for staff users.
truewhen the comment was auto-moderated (marked not public).use_threadedcommentsBoolean indicating whether threaded comment mode is active.
object_idThe primary key of the commented-on object.
Client-Side Assets¶
The AJAX comment posting uses bundled assets under:
fluent_comments/js/ajaxcomments.jsfluent_comments/css/ajaxcomments.cssIf these are not included in your templates, the form falls back to the default django-contrib-comments flow (redirecting to
comments/posted/andcomments/preview/).The form template at
comments/form.htmlsetsdata-ajax-action="{% url 'comments-post-comment-ajax' %}"on the<form>element. The JavaScript reads this attribute to determine the AJAX endpoint URL.Hint
The
{% ajax_comment_tags for object %}template tag renders the cancel-reply link, a loading spinner, and success/moderation messages used by the JavaScript layer.Template Tags¶
The
fluent_comments_tagstemplate tag library provides several tags and filters for rendering comments in templates.{% ajax_comment_tags object %}Renders the AJAX helper markup (cancel-reply link, loading spinner, success message, moderation notice for staff). Accepts both
{% ajax_comment_tags object %}and{% ajax_comment_tags for object %}syntax.{% render_comment comment %}Renders a single comment using the appropriate template from the lookup chain:
comments/<app>/<model>/comment.html,comments/<app>/comment.html,comments/comment.html.{% fluent_comments_list %}Renders the full comment list. Uses
threaded_list.htmlwhen threaded comments are active,flat_list.htmlotherwise.{{ object|comments_are_open }}Filter that returns
Trueif comments are enabled for the object. Checks the object’scomments_are_enabledattribute.{{ object|comments_are_moderated }}Filter that returns whether comments require pre-approval for the object. Currently always returns
False(all moderation is handled post-submission by the spam filter).{{ object|comments_count }}Filter that returns the number of comments for the object.
Comment Moderation¶
Django Cast provides an automatic moderation workflow that integrates with the
comment_will_be_postedsignal fromdjango-contrib-comments.Moderation Flow¶
The moderator is declared as a module-level
SimpleLazyObject. It is not resolved at import time – theCAST_COMMENTS_DEFAULT_MODERATORsetting is read and the moderator class instantiated on first attribute access (i.e., when the first comment is submitted). The resolved instance is then reused for all subsequent comments.When a comment is submitted:
The
on_comment_will_be_postedsignal receiver calls the moderator’sallow()method. If it returnsFalse, the comment is rejected entirely (the signal receiver returnsFalse, causing the view to return an error).The moderator’s
moderate()method is called. It can mark the comment as spam by settingis_removed = Trueandis_public = False.The default
cast.moderation.Moderatorclass:Always allows comments (
allow()returnsTrue) – even spam comments are kept as training data for the classifier.Auto-classifies comments using the spam filter. If the filter predicts
"spam", the comment is saved withis_removed = Trueandis_public = False. Otherwise, the comment is published immediately.Note
Staff users see a “(moderated)” flag next to auto-moderated comments, and the AJAX response includes
is_moderated: trueso the JavaScript can display a notice.Manual Moderation¶
Comments that were auto-moderated (or manually flagged) can be managed through the Django admin:
In the Comments admin, toggle
is_publicandis_removedto approve or reject individual comments.Correcting mis-classified comments (marking spam as public, or ham as removed) improves future classifier accuracy after retraining.
Comment Spam Filter¶
Django Cast includes a Naive Bayes spam classifier implemented in pure Python (
src/cast/models/moderation.py). It is fast, easy to train, and effective at filtering most spam.Architecture¶
The spam filter consists of three main components:
NaiveBayesThe classifier itself. Stores prior probabilities per label and per-word label counts. Supports
fit(),predict(), andpredict_label()methods.SpamFilter(Django model)Persists a trained
NaiveBayesinstance in aJSONFieldalong with performance metrics. Themodelfield uses customModelEncoder/ModelDecoderclasses for JSON serialization of the classifier.EvaluationCross-validation harness that measures precision, recall, and F1 score using stratified k-fold splits (default: 3 folds).
Training¶
Training data is derived directly from existing comments:
A comment is labeled ham if
is_public = Trueandis_removed = False.All other comments are labeled spam.
Each comment is converted to a message string by concatenating its
name,email,title, andcommentfields. The classifier tokenizes this string into lowercase words (using the regex pattern\b\w\w+\b) and builds per-word label frequency counts.Classification¶
When a new comment arrives, the
Moderator.moderate()method:Converts the comment to a message string.
Uses the
SpamFilterinstance that was loaded duringModeratorinitialization (SpamFilter.get_default()is called once in__init__, not on every comment).Calls
predict_label(message)on the storedNaiveBayesmodel.If the predicted label is
"spam", the comment is marked as removed and not public.The classifier computes posterior probabilities for each label by multiplying the prior probability by each word’s conditional probability, normalizing after each word. The label with the highest final probability wins.
Retraining¶
After moderating a batch of comments (approving legitimate ones, leaving spam as removed), retrain the filter via the Django admin:
Navigate to the Spam filters admin page.
Select the spam filter instance.
Choose the “Retrain model from scratch using marked comments” action.
The retrain action:
Collects all comments and labels them as ham or spam based on their current
is_public/is_removedstatus.Fits a new
NaiveBayesmodel on the full dataset.Runs a 3-fold stratified cross-validation to compute precision, recall, and F1 for both the “ham” and “spam” classes.
Saves the updated model and performance metrics to the database.
Evaluation Metrics¶
The
Evaluationclass performs stratified k-fold cross-validation:Comments are split by label so each fold has a proportional mix of ham and spam.
For each fold, a fresh
NaiveBayesmodel is trained on the remaining folds and evaluated on the held-out fold.A confusion matrix (true positives, false positives, false negatives) is built per label.
Precision, recall, and F1 are computed from the final fold’s confusion matrix.
The resulting metrics are stored in the
SpamFilter.performanceJSON field and displayed as read-onlyspamandhamcolumns in the admin list view.Hint
If classification quality degrades, check the balance of ham vs. spam in your comment dataset. The classifier works best when both classes have a reasonable number of examples.
JSON Serialization¶
The trained
NaiveBayesmodel is stored in aJSONFieldusing custom encoder/decoder classes:ModelEncoderSerializes a
NaiveBayesinstance to a JSON dictionary containingprior_probabilitiesandword_label_counts.ModelDecoderDeserializes the JSON dictionary back into a
NaiveBayesinstance, keyed by the"class": "NaiveBayes"marker.This allows the trained model to survive database migrations and backup/restore cycles without any external file dependencies.