diff options
-rw-r--r-- | askbot/forms.py | 18 | ||||
-rw-r--r-- | askbot/importers/stackexchange/management/commands/load_stackexchange.py | 2 | ||||
-rw-r--r-- | askbot/models/__init__.py | 13 | ||||
-rw-r--r-- | askbot/models/content.py | 28 | ||||
-rw-r--r-- | askbot/models/meta.py | 9 | ||||
-rw-r--r-- | askbot/skins/default/media/images/go-up-grey.png | bin | 0 -> 563 bytes | |||
-rw-r--r-- | askbot/skins/default/media/images/go-up-orange.png | bin | 0 -> 586 bytes | |||
-rw-r--r-- | askbot/skins/default/media/js/post.js | 119 | ||||
-rwxr-xr-x | askbot/skins/default/media/style/style.css | 38 | ||||
-rw-r--r-- | askbot/skins/default/templates/macros.html | 20 | ||||
-rw-r--r-- | askbot/skins/default/templates/question.html | 1 | ||||
-rw-r--r-- | askbot/tests/db_api_tests.py | 32 | ||||
-rw-r--r-- | askbot/urls.py | 5 | ||||
-rw-r--r-- | askbot/views/commands.py | 20 | ||||
-rw-r--r-- | askbot/views/writers.py | 12 |
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 Binary files differnew file mode 100644 index 00000000..763bb799 --- /dev/null +++ b/askbot/skins/default/media/images/go-up-grey.png diff --git a/askbot/skins/default/media/images/go-up-orange.png b/askbot/skins/default/media/images/go-up-orange.png Binary files differnew file mode 100644 index 00000000..eca3579d --- /dev/null +++ b/askbot/skins/default/media/images/go-up-orange.png 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( |