Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Working Ajax comments

Based on django-ajaxcomments, but fully customized to be
translatable and thread safe.
  • Loading branch information...
commit 8d8f294014155932174820fe8f6d9a08b643e1c7 1 parent 67c8761
@vdboor vdboor authored
View
54 README.rst
@@ -0,0 +1,54 @@
+Introduction
+============
+
+The *django-fluent-comments* module enhances the default appearance
+of the *django.contrib.comments* application to be directly usable in web sites.
+
+Installation
+============
+
+First install the module, preferably in a virtual environment::
+
+ git clone https://github.com/edoburu/django-fluent-comments.git
+ cd django-fluent-comments
+ pip install .
+
+Configuration
+-------------
+
+To use comments, the following settings are required::
+
+ INSTALLED_APPS += (
+ 'fluent_contents',
+ 'django.contrib.comments',
+ )
+
+Optionally, `django-threadedcomments`_ can be included::
+
+ INSTALLED_APPS += (
+ 'threadedcomments',
+ )
+
+ COMMENTS_APP = 'threadedcomments'
+
+Add the following in ``urls.py``::
+
+ urlpatterns += patterns('',
+ url(r'^blog/comments/', include('fluent_comments.urls')),
+ )
+
+Provide a template that displays the comments for the ``object``::
+
+ {% load comments %}
+
+ {% render_comment_list for object %}
+ {% render_comment_form for object %}
+
+The database can be created afterwards::
+
+ ./manage.py syncdb
+ ./manage.py runserver
+
+
+.. _django-threadedcomments: https://github.com/HonzaKral/django-threadedcomments.git
+
View
9 fluent_comments/__init__.py
@@ -1,9 +0,0 @@
-# Make sure the monkey patching of django.contrib.comments is made,
-# even if ajaxcomments is not included in the installed apps
-# (only this feature is reused of it).
-#
-# it patches render_to_response() to:
-# - return JSON on Ajax requests
-# - makes request object accessable everywhere
-#
-import ajaxcomments.utils
View
13 fluent_comments/static/fluent_comments/css/ajaxcomments.css
@@ -0,0 +1,13 @@
+#comment-waiting {
+ line-height: 16px;
+}
+
+#comment-waiting img {
+ vertical-align: middle;
+ padding: 0 4px 0 10px;
+}
+
+#comment-added-message,
+#comment-thanks {
+ padding-left: 10px;
+}
View
BIN  fluent_comments/static/fluent_comments/img/ajax-wait.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
101 fluent_comments/static/fluent_comments/js/ajaxcomments-init.js
@@ -1,101 +0,0 @@
-(function($)
-{
- var scrollElement = 'html, body';
- var active_input = '';
-
- // Parse script parameters!
- var scripts = document.getElementsByTagName('script');
- var myscript = scripts[ scripts.length - 1 ];
- var cap = /[?&]STATIC_URL=([^&]+)/.exec(myscript.src);
-
- var STATIC_URL = cap[1] || window.STATIC_URL || '/static/';
- var ajaxmedia = STATIC_URL + 'fluent_comments/';
-
-
- $.fn.ready(function()
- {
- var commentform = $('#comment-form > form');
- if( commentform.length > 0 )
- {
- // Detect last active input.
- // Submit if return is hit, or any button other then preview is hit.
- commentform.find(':input').focus(setActiveInput).mousedown(setActiveInput);
- commentform.submit(onCommentFormSubmit);
- }
-
-
- // Find the element to use for scrolling.
- // This code is much shorter then jQuery.scrollTo()
- $('html, body').each(function () {
- var initScrollTop = $(this).attr('scrollTop');
- $(this).attr('scrollTop', initScrollTop + 1);
- if ($(this).attr('scrollTop') == initScrollTop + 1) {
- scrollElement = this.nodeName.toLowerCase();
- $(this).attr('scrollTop', initScrollTop); // Firefox 2 reset
- return false;
- }
- });
-
-
- // On load, scroll to proper comment.
- var hash = window.location.hash;
- if( hash.substring(0, 2) == "#c" )
- {
- var id = parseInt(hash.substring(2));
- scrollToComment(id, 1000);
- }
- });
-
-
- function setActiveInput()
- {
- active_input = this.name;
- }
-
- function onCommentFormSubmit(event)
- {
- if( active_input != 'preview' )
- {
- ajaxComment({
- 'media': ajaxmedia,
- 'complete': onCommentPosted
- });
- event.preventDefault(); // only after ajax call worked.
- return false;
- }
- return true;
- }
-
-
- function scrollToComment(id, speed)
- {
- // Allow initialisation for scrolling.
- if( window.on_scroll_to_comment && window.on_scroll_to_comment(id) === false )
- return;
-
- // Scroll to the comment.
- var $comment = $("#c" + id);
- if( $comment.length )
- $(scrollElement).animate( {scrollTop: $comment.offset().top }, speed || 1000 );
- }
-
-
- function onCommentPosted($comment)
- {
- var id = $comment.attr('id');
-
- if( id.substring(0, 1) == 'c' )
- {
- id = parseInt(id.substring(1));
- $("#comment-added-message").fadeIn(200);
-
- setTimeout(function(){
- scrollToComment(id, 1000);
- }, 1000);
-
- setTimeout(function(){
- $("#comment-added-message").fadeOut(500);
- }, 4000);
- }
- }
-})(window.jQuery);
View
183 fluent_comments/static/fluent_comments/js/ajaxcomments.js
@@ -0,0 +1,183 @@
+(function($)
+{
+ var scrollElement = 'html, body';
+ var active_input = '';
+
+ // Settings
+ var SCROLL_TOP_OFFSET = 60;
+
+
+ $.fn.ready(function()
+ {
+ var commentform = $('form.js-comments-form');
+ if( commentform.length > 0 )
+ {
+ // Detect last active input.
+ // Submit if return is hit, or any button other then preview is hit.
+ commentform.find(':input').focus(setActiveInput).mousedown(setActiveInput);
+ commentform.submit(onCommentFormSubmit);
+ }
+
+
+ // Find the element to use for scrolling.
+ // This code is much shorter then jQuery.scrollTo()
+ $('html, body').each(function()
+ {
+ // See which tag updates the scrollTop attribute
+ var initScrollTop = $(this).attr('scrollTop');
+ $(this).attr('scrollTop', initScrollTop + 1);
+ if( $(this).attr('scrollTop') == initScrollTop + 1 )
+ {
+ scrollElement = this.nodeName.toLowerCase();
+ $(this).attr('scrollTop', initScrollTop); // Firefox 2 reset
+ return false;
+ }
+ });
+
+
+ // On load, scroll to proper comment.
+ var hash = window.location.hash;
+ if( hash.substring(0, 2) == "#c" )
+ {
+ var id = parseInt(hash.substring(2));
+ scrollToComment(id, 1000);
+ }
+ });
+
+
+ function setActiveInput()
+ {
+ active_input = this.name;
+ }
+
+
+ function onCommentFormSubmit(event)
+ {
+ var form = event.target;
+ if( active_input != 'preview' )
+ {
+ event.preventDefault(); // only after ajax call worked.
+ ajaxComment(form, {
+ 'complete': onCommentPosted
+ });
+ return false;
+ }
+ return true;
+ }
+
+
+ function scrollToComment(id, speed)
+ {
+ // Allow initialisation before scrolling.
+ var $comment = $("#c" + id);
+ if( window.on_scroll_to_comment && window.on_scroll_to_comment({comment: $comment}) === false )
+ return;
+
+ // Scroll to the comment.
+ if( $comment.length )
+ $(scrollElement).animate( {scrollTop: $comment.offset().top - SCROLL_TOP_OFFSET }, speed || 1000 );
+ }
+
+
+ function onCommentPosted($comment)
+ {
+ var id = $comment.attr('id');
+
+ if( id.substring(0, 1) == 'c' )
+ {
+ id = parseInt(id.substring(1));
+ $("#comment-added-message").fadeIn(200);
+
+ setTimeout(function(){ scrollToComment(id, 1000); }, 1000);
+ setTimeout(function(){ $("#comment-added-message").fadeOut(500) }, 4000);
+ }
+ }
+
+
+ /*
+ Based on django-ajaxcomments, BSD licensed.
+ Copyright (c) 2009 Brandon Konkle and individual contributors.
+
+ Updated to be more generic, more fancy, and usable with different templates.
+ */
+ var commentBusy = false;
+
+ function ajaxComment(form, args)
+ {
+ var oncomplete = args.complete;
+
+ $('div.comment-error').remove();
+ if (commentBusy) {
+ return false;
+ }
+
+ commentBusy = true;
+ var $form = $(form);
+ var comment = $form.serialize();
+ var url = $form.attr('action') || './';
+ var ajaxurl = $form.attr('data-ajax-action');
+
+ // Add a wait animation
+ $('#comment-waiting').fadeIn(1000);
+
+ // Use AJAX to post the comment.
+ $.ajax({
+ type: 'POST',
+ url: ajaxurl || url,
+ data: comment,
+ dataType: 'json',
+ success: function(data) {
+ commentBusy = false;
+ removeWaitAnimation();
+
+ if (data.success) {
+ commentSuccess(data);
+ var added = $("#comments > :last-child");
+ if( oncomplete ) oncomplete(added);
+ }
+ else {
+ commentFailure(data);
+ }
+ },
+ error: function(data) {
+ commentBusy = false;
+ removeWaitAnimation();
+
+ // Submit as non-ajax instead
+ //$form.unbind('submit').submit();
+ }
+ });
+
+ return false;
+ }
+
+ function commentSuccess(data)
+ {
+ // Clean form
+ $('form.js-comments-form textarea')[0].value = "";
+ $('#id_comment').val('');
+
+ // Show comment
+ $('#comments').append(data['html']);
+ $('div.comment:last').show('slow');
+ }
+
+ function commentFailure(data)
+ {
+ // Show errors
+ $('form.js-comments-form ul.errorlist').each(function() {
+ this.parentNode.removeChild(this);
+ });
+
+ for (var error in data.errors) {
+ $('#id_' + error).parent().before(data.errors[error])
+ }
+ }
+
+ function removeWaitAnimation()
+ {
+ // Remove the wait animation and message
+ $('#comment-waiting').hide().stop();
+ }
+
+})(window.jQuery);
View
88 fluent_comments/static/fluent_comments/js/post-comment.js
@@ -1,88 +0,0 @@
-/*
- Based on django-ajaxcomments, BSD licensed.
- Copyright (c) 2009 Brandon Konkle and individual contributors.
-
- Updated to be more generic, more fancy, and usable with different templates.
- */
-
-var ajaxComment = (function()
-{
- var commentBusy = false;
-
- function ajaxComment(args)
- {
- var media = args.media;
- var oncomplete = args.complete;
-
- $('div.comment-error').remove();
- if (commentBusy) {
- return false;
- }
-
- commentBusy = true;
- var comment = $('div.comment-form form').serialize();
- var url = $('div.comment-form form').attr('action');
-
- // Add a wait animation
- $('#comment-waiting').fadeIn(1000);
-
- // Use AJAX to post the comment.
- $.ajax({
- type: 'POST',
- url: url + 'ajax/',
- data: comment,
- success: function(data) {
- commentBusy = false;
- removeWaitAnimation();
-
- if (data.success) {
- commentSuccess(data);
- var added = $("#comments > :last-child");
- if( oncomplete ) oncomplete(added);
- }
- else {
- commentFailure(data);
- }
- },
- error: function(data) {
- commentBusy = false;
- removeWaitAnimation();
-
- $('div.comment-form form').unbind('submit');
- $('div.comment-form form').submit();
- },
- dataType: 'json'
- });
-
- return false;
- }
-
- function commentSuccess(data)
- {
- $('div.comment-form form textarea')[0].value = "";
- $('#id_comment').val('');
- $('#comments').append(data['html']);
- $('div.comment:last').show('slow');
- $('#comment-thanks').show().fadeOut(4000);
- }
-
- function commentFailure(data)
- {
- $('div.comment-form ul.errorlist').each(function() {
- this.parentNode.removeChild(this);
- });
-
- for (var error in data.errors) {
- $('#id_' + error).parent().before(data.errors[error])
- }
- }
-
- function removeWaitAnimation()
- {
- // Remove the wait animation and message
- $('.ajax-loader').remove();
- $('div.comment-waiting').stop().remove();
- }
-
- return ajaxComment;
-})();
View
4 fluent_comments/templates/comments/comment.html
@@ -22,8 +22,8 @@
{% spaceless %}
<h4>
{% if comment.url %}<a href="{{ comment.url }}" rel="nofollow">{% endif %}
- {% if comment.name %}{{ comment.name }}{% else %}{% trans "Anonymous" %}{% endif %}
- {% if comment.url %}</a>{% endif %}
+ {% if comment.name %}{{ comment.name }}{% else %}{% trans "Anonymous" %}{% endif %}{% comment %}
+ {% endcomment %}{% if comment.url %}</a>{% endif %}<span>&nbsp;</span>
<span class="comment-date">{% blocktrans with submit_date=comment.submit_date %}on {{ submit_date }}{% endblocktrans %}</span>
</h4>
{% endspaceless %}
View
11 fluent_comments/templates/comments/form.html
@@ -1,6 +1,6 @@
-{% load comments i18n crispy_forms_tags %}
+{% load comments i18n crispy_forms_tags fluent_comments_tags %}{% load url from future %}
-<form action="{% comment_form_target %}" method="post" class="form-horizontal">{% csrf_token %}
+<form action="{% comment_form_target %}" data-ajax-action="{% url 'comments-post-comment-ajax' %}" method="post" class="js-comments-form comments-form form-horizontal">{% csrf_token %}
{% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
{{ form|crispy }}
@@ -8,11 +8,6 @@
<div class="form-actions">
<input type="submit" name="post" class="btn btn-submit" value="{% trans "Post" %}" />
<input type="submit" name="preview" class="btn btn-submit" value="{% trans "Preview" %}" />
-
- <span id="comment-waiting" style="display: none;">
- <img src="{{ STATIC_URL }}fluent_comments/img/ajax-wait.gif" alt="" class="ajax-loader" />{% trans "Please wait . . ." %}
- </span>
- <span id="comment-added-message" style="display: none;">{% trans "Your comment has been posted!" %}</span>
- <span id="comment-thanks" style="display: none;">{% trans "Thank you for your comment!" %}</span>
+ {% ajax_comment_tags %}
</div>
</form>
View
5 fluent_comments/templates/fluent_comments/templatetags/ajax_comment_tags.html
@@ -0,0 +1,5 @@
+{% load i18n %}
+<span id="comment-waiting" style="display: none;">
+ <img src="{{ STATIC_URL }}fluent_comments/img/ajax-wait.gif" alt="" class="ajax-loader" />{% trans "Please wait . . ." %}
+</span>
+<span id="comment-added-message" style="display: none;">{% trans "Your comment has been posted!" %}</span>
View
0  fluent_comments/templatetags/__init__.py
No changes.
View
12 fluent_comments/templatetags/fluent_comments_tags.py
@@ -0,0 +1,12 @@
+from django.template import Library
+from django.core import context_processors
+
+register = Library()
+
+@register.inclusion_tag("fluent_comments/templatetags/ajax_comment_tags.html", takes_context=True)
+def ajax_comment_tags(context):
+ request = context['request']
+
+ new_context = {}
+ new_context.update(context_processors.static(request))
+ return new_context
View
2  fluent_comments/urls.py
@@ -1,6 +1,6 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('fluent_comments.views',
- url(r'^post/ajax/$', 'ajax_post_comment', name='comments-post-comment-ajax'),
+ url(r'^post/ajax/$', 'post_comment_ajax', name='comments-post-comment-ajax'),
url(r'', include('django.contrib.comments.urls')),
)
View
133 fluent_comments/views.py
@@ -1,2 +1,131 @@
-def ajax_post_comment(request):
- pass
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.db import models
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.utils import simplejson
+from django.template.loader import render_to_string
+from django.template import RequestContext
+from django.contrib import comments
+from django.contrib.comments import signals
+from django.contrib.comments.views.comments import CommentPostBadRequest
+from django.utils.html import escape
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.http import require_POST
+
+
+@csrf_protect
+@require_POST
+def post_comment_ajax(request, using=None):
+ """
+ Post a comment, via an Ajax call.
+ """
+ if not request.is_ajax():
+ return HttpResponseBadRequest("Expecting Ajax call")
+
+ # This is copied from django.contrib.comments.
+ # Basically this view does too much, and doesn't offer a hook to change the rendering
+ # (request is not passed to next_redirect for example)
+
+ # Fill out some initial data fields from an authenticated user, if present
+ data = request.POST.copy()
+ if request.user.is_authenticated():
+ if not data.get('name', ''):
+ data["name"] = request.user.get_full_name() or request.user.username
+ if not data.get('email', ''):
+ data["email"] = request.user.email
+
+ # Look up the object we're trying to comment about
+ ctype = data.get("content_type")
+ object_pk = data.get("object_pk")
+ if ctype is None or object_pk is None:
+ return CommentPostBadRequest("Missing content_type or object_pk field.")
+ try:
+ model = models.get_model(*ctype.split(".", 1))
+ target = model._default_manager.using(using).get(pk=object_pk)
+ except TypeError:
+ return CommentPostBadRequest(
+ "Invalid content_type value: %r" % escape(ctype))
+ except AttributeError:
+ return CommentPostBadRequest(
+ "The given content-type %r does not resolve to a valid model." %\
+ escape(ctype))
+ except ObjectDoesNotExist:
+ return CommentPostBadRequest(
+ "No object matching content-type %r and object PK %r exists." %\
+ (escape(ctype), escape(object_pk)))
+ except (ValueError, ValidationError), e:
+ return CommentPostBadRequest(
+ "Attempting go get content-type %r and object PK %r exists raised %s" %\
+ (escape(ctype), escape(object_pk), e.__class__.__name__))
+
+ # Do we want to preview the comment?
+ preview = "preview" in data
+
+ # Construct the comment form
+ form = comments.get_form()(target, data=data)
+
+ # Check security information
+ if form.security_errors():
+ return CommentPostBadRequest(
+ "The comment form failed security verification: %s" %\
+ escape(str(form.security_errors())))
+
+ # If there are errors or if we requested a preview show the comment
+ if form.errors or preview:
+ return _ajax_result(request, form)
+
+ # Otherwise create the comment
+ comment = form.get_comment_object()
+ comment.ip_address = request.META.get("REMOTE_ADDR", None)
+ if request.user.is_authenticated():
+ comment.user = request.user
+
+ # Signal that the comment is about to be saved
+ responses = signals.comment_will_be_posted.send(
+ sender = comment.__class__,
+ comment = comment,
+ request = request
+ )
+
+ for (receiver, response) in responses:
+ if response == False:
+ return CommentPostBadRequest(
+ "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)
+
+ # Save the comment and signal that it was saved
+ comment.save()
+ signals.comment_was_posted.send(
+ sender = comment.__class__,
+ comment = comment,
+ request = request
+ )
+
+ return _ajax_result(request, form, comment)
+
+
+def _ajax_result(request, form, comment=None):
+ # Based on django-ajaxcomments, BSD licensed.
+ # Copyright (c) 2009 Brandon Konkle and individual contributors.
+ #
+ # This code was extracted out of django-ajaxcomments because
+ # django-ajaxcomments is not threadsafe, and it was refactored afterwards.
+
+ success = True
+ json_errors = {}
+
+ if form.errors:
+ for field in form.errors:
+ json_errors.update({field: str(form.errors[field])})
+ success = False
+
+ comment_html = None
+ if comment:
+ context = {'comment': comment}
+ comment_html = render_to_string('comments/comment.html', context, context_instance=RequestContext(request))
+
+ json_response = simplejson.dumps({
+ 'success': success,
+ 'errors': json_errors,
+ 'html': comment_html,
+ })
+
+ return HttpResponse(json_response, mimetype="application/json")
View
36 setup.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+from setuptools import setup, find_packages
+
+setup(
+ name='django-fluent-comments',
+ version='0.1.0',
+ license='Apache License, Version 2.0',
+
+ install_requires=[
+ 'Django>=1.2.0',
+ 'django-crispy-forms>=1.1.1',
+ 'django-ajaxcomments>=0.2',
+ ],
+ description='A modern plug&play commenting setup based on django.contrib.comments, crispy-forms and ajaxcomments',
+ long_description=open('README.rst').read(),
+
+ author='Diederik van der Boor',
+ author_email='opensource@edoburu.nl',
+
+ url='https://github.com/edoburu/django-fluent-comments',
+ download_url='https://github.com/edoburu/django-fluent-comments/zipball/master',
+
+ packages=find_packages(),
+ include_package_data=True,
+
+ zip_safe=False,
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Framework :: Django',
+ ]
+)
Please sign in to comment.
Something went wrong with that request. Please try again.