summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-06-22 22:48:02 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-06-22 22:48:02 -0400
commit4ee5266e7bf6df61eaff48a67db717b04aa2a90a (patch)
treea1f168ec4d14f018c259def97b50c8fdf174eba8
parentcc370d1adb7fd9ed6708a03890b191be0c2dab93 (diff)
parente29674c17e846b227d44d62b64f3f2c47c81c488 (diff)
downloadaskbot-4ee5266e7bf6df61eaff48a67db717b04aa2a90a.tar.gz
askbot-4ee5266e7bf6df61eaff48a67db717b04aa2a90a.tar.bz2
askbot-4ee5266e7bf6df61eaff48a67db717b04aa2a90a.zip
Merge branch 'comment-vote'
-rw-r--r--askbot/forms.py18
-rw-r--r--askbot/importers/stackexchange/management/commands/load_stackexchange.py2
-rw-r--r--askbot/models/__init__.py13
-rw-r--r--askbot/models/content.py28
-rw-r--r--askbot/models/meta.py9
-rw-r--r--askbot/skins/default/media/images/go-up-grey.pngbin0 -> 563 bytes
-rw-r--r--askbot/skins/default/media/images/go-up-orange.pngbin0 -> 586 bytes
-rw-r--r--askbot/skins/default/media/js/post.js119
-rwxr-xr-xaskbot/skins/default/media/style/style.css38
-rw-r--r--askbot/skins/default/templates/macros.html20
-rw-r--r--askbot/skins/default/templates/question.html1
-rw-r--r--askbot/tests/db_api_tests.py32
-rw-r--r--askbot/urls.py5
-rw-r--r--askbot/views/commands.py20
-rw-r--r--askbot/views/writers.py12
15 files changed, 300 insertions, 17 deletions
diff --git a/askbot/forms.py b/askbot/forms.py
index 9e4f4c14..2ab67213 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -647,6 +647,24 @@ class AnswerForm(forms.Form):
return
self.fields['email_notify'].initial = False
+class VoteForm(forms.Form):
+ """form used in ajax vote view (only comment_upvote so far)
+ """
+ post_id = forms.IntegerField()
+ cancel_vote = forms.CharField()#char because it is 'true' or 'false' as string
+
+ def clean_cancel_vote(self):
+ val = self.cleaned_data['cancel_vote']
+ if val == 'true':
+ result = True
+ elif val == 'false':
+ result = False
+ else:
+ del self.cleaned_data['cancel_vote']
+ raise forms.ValidationError('either "true" or "false" strings expected')
+ self.cleaned_data['cancel_vote'] = result
+ return self.cleaned_data['cancel_vote']
+
class CloseForm(forms.Form):
reason = forms.ChoiceField(choices=const.CLOSE_REASONS)
diff --git a/askbot/importers/stackexchange/management/commands/load_stackexchange.py b/askbot/importers/stackexchange/management/commands/load_stackexchange.py
index 47e920e2..ddac764e 100644
--- a/askbot/importers/stackexchange/management/commands/load_stackexchange.py
+++ b/askbot/importers/stackexchange/management/commands/load_stackexchange.py
@@ -6,7 +6,7 @@ import zipfile
from django.core.management.base import BaseCommand, CommandError
import askbot.importers.stackexchange.parse_models as se_parser
from xml.etree import ElementTree as et
-from django.db import models, transaction
+from django.db import models#, transaction
#from askbot.utils import dummy_transaction as transaction
import askbot.models as askbot
import askbot.deps.django_authopenid.models as askbot_openid
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 1e33ecb8..66992dad 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -336,7 +336,12 @@ def user_assert_can_vote_for_post(
:param:post can be instance of question or answer
"""
- if self == post.author:
+ #todo: after unifying models this if else will go away
+ if isinstance(post, Comment):
+ post_author = post.user
+ else:
+ post_author = post.author
+ if self == post_author:
raise django_exceptions.PermissionDenied(_('cannot vote for own posts'))
blocked_error_message = _(
@@ -1786,7 +1791,8 @@ def user_is_following_question(user, question):
def upvote(self, post, timestamp=None, cancel=False):
return _process_vote(
- self,post,
+ self,
+ post,
timestamp=timestamp,
cancel=cancel,
vote_type=Vote.VOTE_UP
@@ -1794,7 +1800,8 @@ def upvote(self, post, timestamp=None, cancel=False):
def downvote(self, post, timestamp=None, cancel=False):
return _process_vote(
- self,post,
+ self,
+ post,
timestamp=timestamp,
cancel=cancel,
vote_type=Vote.VOTE_DOWN
diff --git a/askbot/models/content.py b/askbot/models/content.py
index 84bd2421..c1760593 100644
--- a/askbot/models/content.py
+++ b/askbot/models/content.py
@@ -1,8 +1,10 @@
import datetime
from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import html as html_utils
+from django.utils.datastructures import SortedDict
from askbot import const
from askbot.models.meta import Comment, Vote
from askbot.models.user import EmailFeedSetting
@@ -46,9 +48,29 @@ class Content(models.Model):
abstract = True
app_label = 'askbot'
- def get_comments(self):
- comments = self.comments.all().order_by('id')
- return comments
+ def get_comments(self, visitor = None):
+ """returns comments for a post, annotated with
+ ``upvoted_by_user`` parameter, if visitor is logged in
+ otherwise, returns query set for all comments to a given post
+ """
+ if visitor.is_anonymous():
+ return self.comments.all().order_by('id')
+ else:
+ comment_content_type = ContentType.objects.get_for_model(Comment)
+ #a fancy query to annotate comments with the visitor votes
+ comments = self.comments.extra(
+ select = SortedDict([
+ (
+ 'upvoted_by_user',
+ 'SELECT COUNT(*) from vote, comment '
+ 'WHERE vote.user_id = %s AND '
+ 'vote.content_type_id = %s AND '
+ 'vote.object_id = comment.id',
+ )
+ ]),
+ select_params = (visitor.id, comment_content_type.id)
+ ).order_by('id')
+ return comments
#todo: maybe remove this wnen post models are unified
def get_text(self):
diff --git a/askbot/models/meta.py b/askbot/models/meta.py
index d7cf4b7e..0db36ab3 100644
--- a/askbot/models/meta.py
+++ b/askbot/models/meta.py
@@ -296,6 +296,15 @@ class Comment(base.MetaContent, base.UserContent):
def get_latest_revision_number(self):
return 1
+ def is_upvoted_by(self, user):
+ content_type = ContentType.objects.get_for_model(self)
+ what_to_count = {
+ 'user': user,
+ 'object_id': self.id,
+ 'content_type': self.content_type
+ }
+ return Vote.objects.count(**what_to_count) > 0
+
def is_last(self):
"""True if there are no newer comments on
the related parent object
diff --git a/askbot/skins/default/media/images/go-up-grey.png b/askbot/skins/default/media/images/go-up-grey.png
new file mode 100644
index 00000000..763bb799
--- /dev/null
+++ b/askbot/skins/default/media/images/go-up-grey.png
Binary files differ
diff --git a/askbot/skins/default/media/images/go-up-orange.png b/askbot/skins/default/media/images/go-up-orange.png
new file mode 100644
index 00000000..eca3579d
--- /dev/null
+++ b/askbot/skins/default/media/images/go-up-orange.png
Binary files differ
diff --git a/askbot/skins/default/media/js/post.js b/askbot/skins/default/media/js/post.js
index a228e9ce..5fbf0ad8 100644
--- a/askbot/skins/default/media/js/post.js
+++ b/askbot/skins/default/media/js/post.js
@@ -141,7 +141,111 @@ var CPValidator = function(){
};
}();
+/**
+ * @constructor
+ * @extends {SimpleControl}
+ * @param {Comment} comment to upvote
+ */
+var CommentVoteButton = function(comment){
+ SimpleControl.call(this);
+ /**
+ * @param {Comment}
+ */
+ this._comment = comment;
+ /**
+ * @type {boolean}
+ */
+ this._voted = false;
+ /**
+ * @type {number}
+ */
+ this._score = 0;
+};
+inherits(CommentVoteButton, SimpleControl);
+/**
+ * @param {number} score
+ */
+CommentVoteButton.prototype.setScore = function(score){
+ this._score = score;
+ if (this._element){
+ this._element.html(score);
+ }
+};
+/**
+ * @param {boolean} voted
+ */
+CommentVoteButton.prototype.setVoted = function(voted){
+ this._voted = voted;
+ if (this._element){
+ this._element.addClass('upvoted');
+ }
+};
+CommentVoteButton.prototype.getVoteHandler = function(){
+ var me = this;
+ var comment = this._comment;
+ return function(){
+ var voted = me._voted;
+ var post_id = me._comment.getId();
+ var data = {
+ cancel_vote: voted ? true:false,
+ post_id: post_id
+ };
+ $.ajax({
+ type: 'POST',
+ data: data,
+ dataType: 'json',
+ url: askbot['urls']['upvote_comment'],
+ cache: false,
+ success: function(data){
+ me.setScore(data['score']);
+ me.setVoted(true);
+ },
+ error: function(xhr, textStatus, exception) {
+ showMessage(comment.getElement(), xhr.responseText, 'after');
+ }
+ });
+ };
+};
+
+CommentVoteButton.prototype.decorate = function(element){
+ this._element = element;
+ this.setHandler(this.getVoteHandler());
+
+ var element = this._element;
+ var comment = this._comment;
+ /* can't call comment.getElement() here due
+ * an issue in the getElement() of comment
+ * so use an "illegal" access to comment._element here
+ */
+ comment._element.mouseenter(function(){
+ //outside height may not be known
+ var height = comment.getElement().height();
+ element.height(height);
+ element.addClass('hover');
+ });
+ comment._element.mouseleave(function(){
+ element.removeClass('hover');
+ });
+
+};
+
+CommentVoteButton.prototype.createDom = function(){
+ this._element = this.makeElement('div');
+ if (this._score > 0){
+ this._element.html(this._score);
+ }
+ this._element.addClass('upvote');
+ if (this._voted){
+ this._element.addClass('upvoted');
+ }
+ this.decorate(this._element);
+};
+
+/**
+ * legacy Vote class
+ * handles all sorts of vote-like operations
+ */
var Vote = function(){
// All actions are related to a question
var questionId;
@@ -1059,6 +1163,9 @@ Comment.prototype.decorate = function(element){
this._edit_link.decorate(edit_link);
}
+ var vote = new CommentVoteButton(this);
+ vote.decorate(this._element.find('.comment-votes .upvote'));
+
this._blank = false;
};
@@ -1097,6 +1204,18 @@ Comment.prototype.setContent = function(data){
this._element.attr('class', 'comment');
this._element.attr('id', 'comment-' + this._data['id']);
+ var votes = this.makeElement('div');
+ votes.addClass('comment-votes');
+
+ var vote = new CommentVoteButton(this);
+ if (this._data['upvoted_by_user']){
+ vote.setVoted(true);
+ }
+ vote.setScore(this._data['score']);
+ votes.append(vote.getElement());
+
+ this._element.append(votes);
+
this._element.append(this._data['html']);
this._element.append(' - ');
diff --git a/askbot/skins/default/media/style/style.css b/askbot/skins/default/media/style/style.css
index ffa15009..85bc8801 100755
--- a/askbot/skins/default/media/style/style.css
+++ b/askbot/skins/default/media/style/style.css
@@ -958,6 +958,41 @@ a:hover.medal {
clear: both;
}
+.comments .content {
+ width: 650px;
+ float: right;
+}
+
+.comments div.comment {
+ min-height: 25px;
+}
+
+div.comment .comment-votes {
+ position: absolute;
+ width: 20px;
+ margin: -2px 0 0 -20px;
+}
+
+div.comment .upvote {
+ width: 20px;
+ height: 20px;
+ padding: 3px 0 0 3px;
+ font-weight: bold;
+ color: #777;
+}
+
+div.comment .upvote.upvoted {
+ color: #d64000;
+}
+
+div.comment .upvote.hover {
+ background: url(../images/go-up-grey.png) no-repeat;
+}
+
+div.comment .upvote:hover {
+ background: url(../images/go-up-orange.png) no-repeat;
+}
+
.comments div.controls {
clear: both;
background: url(../images/gray-up-arrow-h18px.png) no-repeat;
@@ -1781,8 +1816,7 @@ button::-moz-focus-inner {
border-top: 1px dotted #ccccce;
margin: 0;
color: #444;
- padding: 2px 3px 5px 3px;
- width: 670px;
+ padding: 5px 3px 5px 3px;
overflow: auto;
}
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 969052ba..23177c0b 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -450,9 +450,23 @@ poor design of the data or methods on data objects #}
</div>
{%- endmacro -%}
+{%- macro comment_votes(comment = None) -%}
+ <div class="comment-votes">
+ {% if comment.score > 0 %}
+ <div class="upvote{% if comment.upvoted_by_user %} upvoted{% endif %}">
+ {{comment.score}}
+ </div>
+ {% else %}
+ <div class="upvote">
+ </div>
+ {% endif %}
+ </div>
+{%- endmacro -%}
+
{%- macro comment_list(comments = None, user = None) -%}
{% for comment in comments %}
<div class="comment" id="comment-{{comment.id}}">
+ {{ comment_votes(comment = comment) }}
{{comment.html}} -
<a
class="author"
@@ -501,14 +515,14 @@ poor design of the data or methods on data objects #}
<div class="content">
{% if show_post == post and show_comment %}
{% if comment_order_number > max_comments %}
- {% set comments = post.get_comments()[:comment_order_number] %}
+ {% set comments = post.get_comments(visitor = user)[:comment_order_number] %}
{{ comment_list(comments = comments, user = user) }}
{% else %}
- {% set comments = post.get_comments()[:max_comments] %}
+ {% set comments = post.get_comments(visitor = user)[:max_comments] %}
{{ comment_list(comments = comments, user = user) }}
{% endif %}
{% else %}
- {% set comments = post.get_comments()[:max_comments] %}
+ {% set comments = post.get_comments(visitor = user)[:max_comments] %}
{{ comment_list(comments = comments, user = user) }}
{% endif %}
</div>
diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html
index d0eadaef..343648ac 100644
--- a/askbot/skins/default/templates/question.html
+++ b/askbot/skins/default/templates/question.html
@@ -447,6 +447,7 @@
askbot['urls']['user_signin'] = '{{ settings.LOGIN_URL }}';
askbot['urls']['vote_url_template'] = scriptUrl + '{% trans %}questions/{% endtrans %}{{ "{{QuestionID}}/" }}{% trans %}vote/{% endtrans %}';
askbot['urls']['swap_question_with_answer'] = '{% url swap_question_with_answer %}';
+ askbot['urls']['upvote_comment'] = '{% url upvote_comment %}';
askbot['messages']['addComment'] = '{% trans %}add comment{% endtrans %}';
{% if settings.SAVE_COMMENT_ON_ENTER %}
askbot['settings']['saveCommentOnEnter'] = true;
diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py
index 6473845f..18fc174b 100644
--- a/askbot/tests/db_api_tests.py
+++ b/askbot/tests/db_api_tests.py
@@ -3,6 +3,7 @@ functions that happen on behalf of users
e.g. ``some_user.do_something(...)``
"""
+from django.core import exceptions
from askbot.tests.utils import AskbotTestCase
from askbot import models
from askbot import const
@@ -345,3 +346,34 @@ class GlobalTagSubscriberGetterTests(AskbotTestCase):
expected_subscribers = set([self.u2,]),
reason = 'bad'
)
+
+class CommentTests(AskbotTestCase):
+ """unfortunately, not very useful tests,
+ as assertions of type "user can" are not inside
+ the User.upvote() function
+ todo: refactor vote processing code
+ """
+ def setUp(self):
+ self.create_user()
+ self.create_user(username = 'other_user')
+ self.question = self.post_question()
+ self.now = datetime.datetime.now()
+ self.comment = self.user.post_comment(
+ parent_post = self.question,
+ body_text = 'lalalalalalalalal hahahah'
+ )
+
+ def test_other_user_can_upvote_comment(self):
+ self.other_user.upvote(self.comment)
+ comments = self.question.get_comments(visitor = self.other_user)
+ self.assertEquals(len(comments), 1)
+ self.assertEquals(comments[0].upvoted_by_user, True)
+
+
+ def test_other_user_can_cancel_upvote(self):
+ self.test_other_user_can_upvote_comment()
+ comment = models.Comment.objects.get(id = self.comment.id)
+ self.assertEquals(comment.score, 1)
+ self.other_user.upvote(comment, cancel = True)
+ comment = models.Comment.objects.get(id = self.comment.id)
+ self.assertEquals(comment.score, 0)
diff --git a/askbot/urls.py b/askbot/urls.py
index f6c5e937..05bb2db3 100644
--- a/askbot/urls.py
+++ b/askbot/urls.py
@@ -107,6 +107,11 @@ urlpatterns = patterns('',
name='question_revisions'
),
url(#ajax only
+ r'^comment/upvote/$',
+ views.commands.upvote_comment,
+ name = 'upvote_comment'
+ ),
+ url(#ajax only
r'^post_comments/$',
views.writers.post_comments,
name='post_comments'
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 5b7e8f18..04d4ef1b 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -35,7 +35,6 @@ def process_vote(user = None, vote_direction = None, post = None):
also in the future make keys in response data be more meaningful
right now they are kind of cryptic - "status", "count"
"""
-
if user.is_anonymous():
raise exceptions.PermissionDenied(_('anonymous users cannot vote'))
@@ -536,6 +535,25 @@ def swap_question_with_answer(request):
}
raise Http404
+@decorators.ajax_only
+@decorators.post_only
+def upvote_comment(request):
+ if request.user.is_anonymous():
+ raise exceptions.PermissionDenied(_('Please sign in to vote'))
+ form = forms.VoteForm(request.POST)
+ if form.is_valid():
+ comment_id = form.cleaned_data['post_id']
+ cancel_vote = form.cleaned_data['cancel_vote']
+ comment = models.Comment.objects.get(id = comment_id)
+ process_vote(
+ post = comment,
+ vote_direction = 'up',
+ user = request.user
+ )
+ else:
+ raise ValueError
+ return {'score': comment.score}
+
#askbot-user communication system
def read_message(request):#marks message a read
if request.method == "POST":
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index bd65fbcd..c5a69c1d 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -509,7 +509,7 @@ def answer(request, id):#process a new answer
def __generate_comments_json(obj, user):#non-view generates json data for the post comments
"""non-view generates json data for the post comments
"""
- comments = obj.comments.all().order_by('id')
+ comments = obj.get_comments(visitor = user)
# {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null}
json_comments = []
for comment in comments:
@@ -529,7 +529,7 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po
comment_owner = comment.get_owner()
- json_comments.append({'id' : comment.id,
+ comment_data = {'id' : comment.id,
'object_id': obj.id,
'comment_age': diff_date(comment.added_at),
'html': comment.html,
@@ -538,12 +538,15 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po
'user_id': comment_owner.id,
'is_deletable': is_deletable,
'is_editable': is_editable,
- })
+ 'score': comment.score,
+ 'upvoted_by_user': getattr(comment, 'upvoted_by_user', False)
+ }
+ json_comments.append(comment_data)
data = simplejson.dumps(json_comments)
return HttpResponse(data, mimetype="application/json")
-def post_comments(request):#non-view generic ajax handler to load comments to an object
+def post_comments(request):#generic ajax handler to load comments to an object
# only support get post comments by ajax now
user = request.user
if request.is_ajax():
@@ -604,6 +607,7 @@ def edit_comment(request):
'user_id': comment.user.id,
'is_deletable': is_deletable,
'is_editable': is_editable,
+ 'voted': comment.is_upvoted_by(request.user),
}
else:
raise exceptions.PermissionDenied(