[pirate-politics] r524 committed - threaded comments fully operational with reply, timedeltas, and more. ...

1 view
Skip to first unread message

pirate-...@googlecode.com

unread,
Jan 26, 2011, 6:30:24 PM1/26/11
to pirate-poli...@googlegroups.com
Revision: 524
Author: fragro
Date: Wed Jan 26 15:29:32 2011
Log: threaded comments fully operational with reply, timedeltas, and more.
Some ugly recursive code and dynamic generation of html by python needed to
pull it off, but it seems to be pretty fast
http://code.google.com/p/pirate-politics/source/detail?r=524

Modified:
/trunk/pirate-politics/new_templates/base.html
/trunk/pirate-politics/new_templates/issue_detail.html
/trunk/pirate-politics/pirate_comments/models.py
/trunk/pirate-politics/pirate_comments/templatetags/commenttags.py
/trunk/pirate-politics/pirate_issues/templatetags/issuetags.py
/trunk/pirate-politics/static/style.css

=======================================
--- /trunk/pirate-politics/new_templates/base.html Tue Jan 25 15:15:31 2011
+++ /trunk/pirate-politics/new_templates/base.html Wed Jan 26 15:29:32 2011
@@ -28,8 +28,8 @@

<script type="text/javascript">

- $(document).ready(function(){
- $('#comments').collapsible({xoffset:'-20',yoffset:'50',
defaulthide: false});
+ jQuery(document).ready(function(){
+ $('#collapsible_comments').collapsible();
});

function changeText(text,div_id)
=======================================
--- /trunk/pirate-politics/new_templates/issue_detail.html Tue Jan 25
15:15:31 2011
+++ /trunk/pirate-politics/new_templates/issue_detail.html Wed Jan 26
15:29:32 2011
@@ -83,15 +83,16 @@

{% include '_commentform.html' %}

- {% pp_load_comments object=pp_issue.issue %}
-
- {% for node, count in pp_comment.nodes %}
-
- {% include '_commentleaf.html' %}
-
- {% endfor %}
-
- {% endpp_load_comments%}
+ <div class="commentstructure">
+
+ {% pp_comment_list_get user=request.user
object=pp_issue.issue.id request=request %}
+
+ {% comment %} {{ pp_comment.debug_comments }} {%
endcomment %}
+ {{ pp_comment.comments|safe }}
+
+ {% endpp_comment_list_get%}
+
+ </div>


{% endpp_get_issue %}
=======================================
--- /trunk/pirate-politics/pirate_comments/models.py Tue Jan 25 15:15:31
2011
+++ /trunk/pirate-politics/pirate_comments/models.py Wed Jan 26 15:29:32
2011
@@ -12,38 +12,24 @@
from pirate_core.views import template_for_model


-class PirateComment(models.Model):
+class Comment(models.Model):
user = models.ForeignKey(User)
- submit_date = models.DateTimeField('date_published',auto_now_add=True)
+ submit_date = models.DateTimeField('date_published')
text = models.TextField(max_length=1200)
content_type = models.ForeignKey(ContentType,
verbose_name=_('content type'),

related_name="content_type_set_for_pirate_%(class)s")
object_pk = models.IntegerField(_('object ID'))
content_object = generic.GenericForeignKey(ct_field="content_type",
fk_field="object_pk")
+ reply_to = models.ForeignKey('self',
related_name="comment_parent",null=True)
is_leaf = models.BooleanField()
+ is_root = models.BooleanField()

class Meta:
verbose_name = _('comment')

def __unicode__(self):
return self.user.username + ":" + str(self.submit_date)
-
-
-class NSComment(NS_Node):
- user = models.ForeignKey(User)
- text = models.TextField()
-
- #created = models.DateTimeField(auto_now_add=True)
- # Exception Value: Cannot use None as a query value
- created = models.DateTimeField(editable=False)
-
- content_type = models.ForeignKey(ContentType,
- verbose_name=_('content type'),
-
related_name="content_type_set_for_%(class)s")
- object_pk = models.IntegerField(_('object ID'))
- content_object = generic.GenericForeignKey(ct_field="content_type",
fk_field="object_pk")
-

@models.permalink
#This must be added to all classes that can be tagged
@@ -51,16 +37,15 @@
content_type = self.content_type
path = template_for_model(str(content_type)) + "?_t=" +
str(content_type.pk) + "&_o=" + str(self.object_pk)
return path
-
- def __unicode__(self):
- return u'NS_Comment %d: %s' % (self.id, self.text)
-
- class Meta:
- verbose_name = _('nscomment')
-
- # when adding a custom Meta class to a NS model, the ordering must
be
- # set again
-
-
-admin.site.register(NSComment)
-
+
+ def save(self, *args, **kwargs):
+ new_comment = super(Comment, self).save(*args, **kwargs)
+ try:
+ new_comment.reply_to.is_leaf = False
+ new_comment.reply_to.save()
+ except: pass
+ return new_comment
+
+
+admin.site.register(Comment)
+
=======================================
--- /trunk/pirate-politics/pirate_comments/templatetags/commenttags.py Tue
Jan 25 15:15:31 2011
+++ /trunk/pirate-politics/pirate_comments/templatetags/commenttags.py Wed
Jan 26 15:29:32 2011
@@ -2,9 +2,11 @@
from django import forms
from django.http import HttpResponse, HttpResponseRedirect
from django.utils import simplejson
-from pirate_comments.models import PirateComment, NSComment
+from pirate_comments.models import Comment
from django.db import transaction
+from django.middleware import csrf
from django.contrib.contenttypes.models import ContentType
+from pirate_profile.models import Profile

import datetime

@@ -20,77 +22,100 @@
get_namespace = namespace_get('pp_comment')

@block
-def pp_load_comments(context, nodelist, *args, **kwargs):
-
+def pp_comment_get(context, nodelist, *args, **kwargs):
+ pass
+
+@block
+def pp_comment_list_get(context, nodelist, *args, **kwargs):
+
+ """we have to render the tree html here, because recursive includes
are not allowed in django templates
+ this could be more efficient with pre/post order tree traversal, but
for now this suffices.
+ mptt and treebeard both are not designed for GAE, need a tree
traversal library for non-rel"""
+
context.push()
namespace = get_namespace(context)

- obj = kwargs.pop('object', None)
- node_id = kwargs.pop('node_id',None)
-
- if obj is None:
+ object_pk = kwargs.get('object', None)
+ user = kwargs.get('user',None) #needs request.user for reply submission
+ if object_pk is None:
raise ValueError("pp_consensus_get tag requires that a consensus
object be passed "
"to it assigned to the 'object=' argument,
and that the str "
"be assigned the string value 'consensus.")
- user = kwargs.pop('user', None)
-
- if node_id:
- root = get_object_or_404(NSComment, id=node_id)
- if root.get_depth() != 1:
- # meh not really a root node, redirecting...
- raise
HttpRedirectException(HttpResponseRedirect(obj.get_absolute_url()))
- else:
- root = None
-
- if node_id:
- descendants = root.get_descendants()
- nodes = [(root, len(descendants))] + [
- (node, node.get_children_count())
- for node in descendants
- ]
- namespace['mainpage'] = False
- namespace['nodes'] = nodes
-
- else:
- namespace['mainpage'] = True
- nodes = NSComment.objects.all()
- nodes = nodes.filter(object_pk=obj.pk)
- namespace['nodes'] = [(node, node.get_descendant_count())
- for node in nodes if node.get_depth() == 1]
- namespace['total_comments'] = len(namespace['nodes']) + \
- sum([count for _, count in
namespace['nodes']])
- namespace['treetype'] = 'ns'
-
+
+ request = kwargs.get('request',None)
+
+ comment_tree = []
+
+ comments = Comment.objects.all()
+ comments = comments.filter(object_pk=object_pk,is_root=True)
+ comments = comments.order_by('-submit_date')
+
+ for c in comments:
+ if c.is_leaf:
+ comment_tree.append((c,0))
+ else:
+ comment_tree.append(get_children(object_pk,c))
+
+ tree_html = render_to_comment_tree_html(comment_tree, user, request)
+ tree_html = "<ul id='collapsible_comments'>" + tree_html + "</ul>"
+ namespace['comments'] = tree_html
+ namespace['debug_comments'] = comment_tree

output = nodelist.render(context)
context.pop()

return output

-@block
-def pp_comment_list_get(context, nodelist, *args, **kwargs):
-
- #load nested comments from treebeard
- context.push()
- namespace = get_namespace(context)
-
- object_pk = kwargs.pop('object', None)
- if object_pk is None:
- raise ValueError("pp_consensus_get tag requires that a consensus
object be passed "
- "to it assigned to the 'object=' argument,
and that the str "
- "be assigned the string value 'consensus.")
- user = kwargs.pop('user', None)
-
- comments = Comment.objects.all()
- comments = comments.filter(object_pk=object_pk)
-
-
- namespace['comments'] = comments
-
- output = nodelist.render(context)
- context.pop()
-
- return output
+def render_to_comment_tree_html(comment_tree, user, request):
+ """Comment tree in form:
+ c_tree = [[Comment1, [Comment1_2, Comment1_3, Comment1_4]],
Comment2, Comment 3]
+ must be rendered as a <ul>...<li><ul> ...
<li>render_comment()</li> ... </ul> </li> </ul>"""
+
+ ret_html = ""
+ for comment in comment_tree:
+ if isinstance(comment, tuple):
+ ret_html+="<li> "+ render_comment(comment[0], comment[1],
user, request) + "</li>"
+ elif isinstance(comment, list):
+ ret_html+= "<li> "+ render_comment(comment[0][0],
comment[0][1], user, request) + "<ul>" +
render_to_comment_tree_html(comment[1], user, request) + "</ul></li>"
+
+ return ret_html
+
+def generate_time_string(then, now):
+ time_to = abs(now - then)
+ hours = time_to.seconds / 360
+
+ if hours > 24:
+ ret = str(time_to.days) + " days ago"
+ elif hours == 0:
+ if time_to.seconds / 60 == 0:
+ ret = str(time_to.seconds) + " seconds ago"
+ else:
+ ret = str(time_to.seconds / 60) + " minutes ago"
+
+ else:
+ ret = str(time_to.seconds / 360 ) + " hours ago"
+ return " said " + ret
+
+
+#ok this is as ugly as it gets, but there's little other ways to generate
this html that I am aware of
+def render_comment(comment_obj, count, user, request):
+ content_type = ContentType.objects.get_for_model(comment_obj.user)
+ form =
ReplyForm(initial={'is_root':False,'is_leaf':True,'content_type':comment_obj.content_type,'object_pk':comment_obj.object_pk,'reply_to':comment_obj, 'submit_date':datetime.datetime.now(),'user':user})
+ if count == 0 or count > 1: plural = "s"
+ else: plural = ""
+ """returns the relevant html for a single atomic comment given the
comment object"""
+ return " <div class='comment_user'> <a href='/user_profile.html"
+ "?_t=" + str(content_type.pk) + "&_o=" + str(comment_obj.user.pk) + "'>"
+ str(comment_obj.user.username)+ "</a>" +
generate_time_string(comment_obj.submit_date, datetime.datetime.now())
+ ":</div><div>" + str(comment_obj.text) +"</div><div
class='comment_reply'>" + " <a href='javascript:;' onmousedown="
+ "'toggleSlide(" + '"add_reply' + str(comment_obj.id) + '"'
+ ");'>reply</a><div id='add_reply" + str(comment_obj.id) + "'
style='display:none; overflow:hidden; height:250px;'><form method='post'
action=''><div style='display:none'><input type='hidden'
name='csrfmiddlewaretoken' value='" + str(csrf.get_token(request)) + "'
/><input id='reply_to_object' type='hidden' name='reply_to_object' value='"
+ str(comment_obj.id)+ "'/></div>" + str(form.as_p()) + "<input
type='submit' class='button green' value='Submit
Comment'></form></div></div>"
+
+def get_children(object_pk,cur_comment):
+ get_list = []
+ comments = Comment.objects.all()
+ comments =
comments.filter(object_pk=object_pk,is_root=False,reply_to=cur_comment)
+ for c in comments:
+ if c.is_leaf:
+ get_list.append((c,0))
+ else:
+ get_list.append(get_children(object_pk,c))
+ return [(cur_comment, len(comments)), get_list]

@block
def pp_comment_form2(context, nodelist, *args, **kwargs):
@@ -103,14 +128,13 @@
obj = kwargs.get('object',None)
comment = kwargs.get('edit',None)
user = kwargs.get('user', None)
- tbmodel = NSComment

if obj == None:
raise ValueException("The designer must specify 'object' to the
form of type models.Model ")
if user == None:
raise ValueException("The designer must specify one of 'user',
generally request.user ")

- if isinstance(obj, NSComment):
+ if isinstance(obj, Comment):
root = obj
else:
root = None
@@ -168,24 +192,49 @@

POST = kwargs.get('POST', None)
path = kwargs.get('path', None)
- obj = kwargs.get('object',None)
+ reply_to = kwargs.get('object',None)
user = kwargs.get('user', None)
-
- if isinstance(obj, Comment):
- comment = obj
+ comment = kwargs.get('edit',None)
+
+ if isinstance(reply_to, Comment):
+ c_type = reply_to.content_type
+ object_pk = reply_to.object_pk
else:
- comment = None
+ c_type = ContentType.objects.get_for_model(reply_to.__class__)
+ object_pk = reply_to.id
+ reply_to = None
+
if POST and POST.get("form_id") == "pp_comment_form":
form = CommentForm(POST) if comment is None else CommentForm(POST,
instance=comment)
- comment = form.save(commit=False)
- comment.user = user
- c_type = ContentType.objects.get_for_model(obj.__class__)
- comment.content_type = c_type
- comment.object_pk = obj.pk
- comment.is_leaf = False
- comment.save()
-
- raise
HttpRedirectException(HttpResponseRedirect(obj.get_absolute_url()))
+ if form.is_valid():
+ newcomment = form.save(commit=False)
+ newcomment.user = user
+ newcomment.content_type = c_type
+ newcomment.object_pk = object_pk
+ newcomment.reply_to = reply_to
+ newcomment.is_leaf = True
+ newcomment.submit_date = datetime.datetime.now()
+ if reply_to == None: newcomment.is_root = True #non-root
comments will use auto-generated reply form
+ else: newcomment.is_root = False
+ newcomment.save()
+ raise
HttpRedirectException(HttpResponseRedirect(newcomment.content_object.get_absolute_url()))
+
+ if POST and POST.get("form_id") == "pp_reply_form":
+ form = ReplyForm(POST) if comment is None else ReplyForm(POST,
instance=comment)
+ if form.is_valid():
+ com_rep = int(POST.get("reply_to_object"))
+ newcomment = form.save(commit=False)
+ newcomment.user = user
+ newcomment.content_type = c_type
+ newcomment.object_pk = object_pk
+ newcomment.reply_to = Comment.objects.get(pk=com_rep)
+ newcomment.reply_to.is_leaf = False
+ newcomment.reply_to.save()
+ newcomment.is_leaf = True
+ newcomment.is_root = False
+ newcomment.submit_date = datetime.datetime.now()
+ newcomment.save()
+ raise
HttpRedirectException(HttpResponseRedirect(newcomment.content_object.get_absolute_url()))

else: form = CommentForm() if comment is None else
CommentForm(instance=comment)

@@ -196,7 +245,7 @@

return output

-class CommentForm(forms.Form):
+class CommentForm(forms.ModelForm):
'''
This form is used to allow creation and modification of comment
objects.
It extends FormMixin in order to provide a create() class method, which
@@ -208,11 +257,30 @@
new_comment = super(CommentForm, self).save(commit=commit)
return new_comment

+ class Meta:
+ model = Comment
+ exclude =
('user','object_pk','content_type','reply_to','submit_date', 'is_leaf','is_root', 'content_object')
#need to grab user from authenticatio
form_id = forms.CharField(widget=forms.HiddenInput(),
initial="pp_comment_form")
text = forms.CharField(widget=forms.Textarea)
- parent = forms.IntegerField(label='Parent',
- required=False,
- widget=forms.HiddenInput)
+
+class ReplyForm(forms.ModelForm):
+ '''
+ This form is used to allow creation and modification of comment
objects.
+ It extends FormMixin in order to provide a create() class method, which
+ is used to process POST, path, and object variables in a consistant
way,
+ and in order to automatically provide the form with a form_id.
+ '''
+
+ def save(self, commit=True):
+ new_comment = super(ReplyForm, self).save(commit=commit)
+ return new_comment
+
+ class Meta:
+ model = Comment
+ exclude =
('user','object_pk','content_type','reply_to','submit_date', 'is_leaf','is_root', 'content_object')
+ #need to grab user from authenticatio
+ form_id = forms.CharField(widget=forms.HiddenInput(),
initial="pp_reply_form")
+ text = forms.CharField(widget=forms.Textarea,label="")


=======================================
--- /trunk/pirate-politics/pirate_issues/templatetags/issuetags.py Tue Jan
25 15:15:31 2011
+++ /trunk/pirate-politics/pirate_issues/templatetags/issuetags.py Wed Jan
26 15:29:32 2011
@@ -155,10 +155,7 @@
#These don't work
###>>> issue1.delete(); issue2.delete(); issue3.delete();
###>>> topic1.delete(); topic2.delete()
-
-
"""
-
context.push()
namespace = get_namespace(context)

=======================================
--- /trunk/pirate-politics/static/style.css Tue Jan 25 19:05:51 2011
+++ /trunk/pirate-politics/static/style.css Wed Jan 26 15:29:32 2011
@@ -1,6 +1,6 @@
-html, body { margin: 0; padding: 0; }
+ohtml, body { margin: 0; padding: 0; }
body{
- font-family: Helvetica;
+ font-family: sans-serif;
background:#eee;
}
a { text-decoration: none; color: #91534D; }
@@ -92,7 +92,6 @@
border-right: 5px solid #ccc;
position:relative;
width:25%;
-
float:left;

}
@@ -383,6 +382,20 @@
left: 10px;
font-size: small;
}
+
+.comment_user, .comment_reply{
+ font-size:80%;
+
+}
+
+
+
+.commentstructure li {
+ padding:5px;
+}
+
+.collapsible_comments { padding: 0; margin: 0; }
+

.bullet h1, .bullet p {
color:white;
@@ -390,7 +403,7 @@
text-align:center;
border:0;
position:relative;
- left:0px;
+ left:-2px;
bottom:+2px;
overflow:visible;
}
@@ -608,8 +621,3 @@
.button.large { font-size: 125%; padding: 7px 12px; }
.button.large:active { padding: 8px 12px 6px; background-position: 0 top; }

-#comments { padding: 0; margin: 0; }
-#comments li { float: left; padding: 10px 0 0; }
-.comment { background: #fff; padding: 10px; display: block; overflow:
hidden; }
-.comment-author { float: left; width: 50px; font-size: 10px; text-align:
center; margin: 0 10px 10px 0; }
-.comment-body, .comment-body p { font-size: 12px; line-height: 18px;
margin: 0; }

Reply all
Reply to author
Forward
0 new messages