From 50568a43725c632e744b81b4085781c01f80804c Mon Sep 17 00:00:00 2001 From: Dejan Noveski Date: Sun, 4 Dec 2011 15:48:58 +0100 Subject: Added - ability to remove own flags to questions and answers --- askbot/auth.py | 89 ++++++++- askbot/models/__init__.py | 216 +++++++++++++------- askbot/models/signals.py | 6 +- askbot/skins/common/media/js/post.js | 217 +++++++++++++++------ .../common/templates/question/answer_controls.html | 12 +- .../templates/question/question_controls.html | 10 +- askbot/templatetags/extra_filters_jinja.py | 11 +- askbot/views/commands.py | 29 ++- 8 files changed, 446 insertions(+), 144 deletions(-) diff --git a/askbot/auth.py b/askbot/auth.py index e9a93ec5..15044452 100644 --- a/askbot/auth.py +++ b/askbot/auth.py @@ -48,8 +48,8 @@ def onFlaggedItem(post, user, timestamp=None): reputation.save() signals.flag_offensive.send( - sender=post.__class__, - instance=post, + sender=post.__class__, + instance=post, mark_by=user ) @@ -102,6 +102,91 @@ def onFlaggedItem(post, user, timestamp=None): #post.deleted_by = Admin post.save() + +@transaction.commit_on_success +def onUnFlaggedItem(post, user, timestamp=None): + if timestamp is None: + timestamp = datetime.datetime.now() + + post.offensive_flag_count = post.offensive_flag_count - 1 + post.save() + + if post.post_type == 'comment':#todo: fix this + flagged_user = post.user + else: + flagged_user = post.author + + flagged_user.receive_reputation( + - askbot_settings.REP_LOSS_FOR_RECEIVING_FLAG + ) + flagged_user.save() + + question = post.get_origin_post() + + reputation = Repute( + user=flagged_user, + positive=askbot_settings.REP_LOSS_FOR_RECEIVING_FLAG, + question=question, + reputed_at=timestamp, + reputation_type=-4,#todo: clean up magic number + reputation=flagged_user.reputation + ) + reputation.save() + + signals.remove_flag_offensive.send( + sender=post.__class__, + instance=post, + mark_by=user + ) + + if post.post_type == 'comment': + #do not hide or delete comments automatically yet, + #because there is no .deleted field in the comment model + return + + #todo: These should be updated to work on same revisions. + # The post fell below HIDE treshold - unhide it. + if post.offensive_flag_count == askbot_settings.MIN_FLAGS_TO_HIDE_POST - 1: + #todo: strange - are we supposed to hide the post here or the name of + #setting is incorrect? + flagged_user.receive_reputation( + - askbot_settings.REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION + ) + + flagged_user.save() + + reputation = Repute( + user=flagged_user, + positive=\ + askbot_settings.REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION, + question=question, + reputed_at=timestamp, + reputation_type=-6, + reputation=flagged_user.reputation + ) + reputation.save() + # The post fell below DELETE treshold, undelete it + elif post.offensive_flag_count == askbot_settings.MIN_FLAGS_TO_DELETE_POST-1 : + flagged_user.receive_reputation( + - askbot_settings.REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION + ) + + flagged_user.save() + + reputation = Repute( + user=flagged_user, + positive =\ + askbot_settings.REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION, + question=question, + reputed_at=timestamp, + reputation_type=-7, + reputation=flagged_user.reputation + ) + reputation.save() + + post.deleted = False + post.save() + @transaction.commit_on_success def onAnswerAccept(answer, user, timestamp=None): answer.accepted = True diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index d63acc61..4c5cfdc5 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -44,7 +44,7 @@ def get_model(model_name): return models.get_model('askbot', model_name) User.add_to_class( - 'status', + 'status', models.CharField( max_length = 2, default = const.DEFAULT_USER_STATUS, @@ -55,14 +55,14 @@ User.add_to_class( User.add_to_class('email_isvalid', models.BooleanField(default=False)) User.add_to_class('email_key', models.CharField(max_length=32, null=True)) #hardcoded initial reputaion of 1, no setting for this one -User.add_to_class('reputation', +User.add_to_class('reputation', models.PositiveIntegerField(default=const.MIN_REPUTATION) ) User.add_to_class('gravatar', models.CharField(max_length=32)) #User.add_to_class('has_custom_avatar', models.BooleanField(default=False)) User.add_to_class( - 'avatar_type', - models.CharField(max_length=1, + 'avatar_type', + models.CharField(max_length=1, choices=const.AVATAR_STATUS_CHOICE, default='n') ) @@ -91,7 +91,7 @@ User.add_to_class('about', models.TextField(blank=True)) User.add_to_class('interesting_tags', models.TextField(blank = True)) User.add_to_class('ignored_tags', models.TextField(blank = True)) User.add_to_class( - 'email_tag_filter_strategy', + 'email_tag_filter_strategy', models.SmallIntegerField( choices=const.TAG_FILTER_STRATEGY_CHOICES, default=const.EXCLUDE_IGNORED @@ -158,7 +158,7 @@ def user_update_avatar_type(self): if 'avatar' in django_settings.INSTALLED_APPS: if self.avatar_set.count() > 0: - self.avatar_type = 'a' + self.avatar_type = 'a' else: self.avatar_type = _check_gravatar(self.gravatar) else: @@ -195,7 +195,7 @@ def user_get_old_vote_for_post(self, post): def user_has_affinity_to_question(self, question = None, affinity_type = None): - """returns True if number of tag overlap of the user tag + """returns True if number of tag overlap of the user tag selection with the question is 0 and False otherwise affinity_type can be either "like" or "dislike" """ @@ -228,7 +228,7 @@ def user_has_affinity_to_question(self, question = None, affinity_type = None): def user_has_ignored_wildcard_tags(self): - """True if wildcard tags are on and + """True if wildcard tags are on and user has some""" return ( askbot_settings.USE_WILDCARD_TAGS \ @@ -247,7 +247,7 @@ def user_has_interesting_wildcard_tags(self): def user_can_have_strong_url(self): - """True if user's homepage url can be + """True if user's homepage url can be followed by the search engine crawlers""" return (self.reputation >= askbot_settings.MIN_REP_TO_HAVE_STRONG_URL) @@ -327,14 +327,14 @@ def user_assert_can_unaccept_best_answer(self, answer = None): error_message = suspended_error_message elif self == answer.question.get_owner(): if self == answer.get_owner(): - if not self.is_administrator(): + if not self.is_administrator(): #check rep min_rep_setting = askbot_settings.MIN_REP_TO_ACCEPT_OWN_ANSWER low_rep_error_message = _( ">%(points)s points required to accept or unaccept " " your own answer to your own question" ) % {'points': min_rep_setting} - + _assert_user_can( user = self, blocked_error_message = blocked_error_message, @@ -346,7 +346,7 @@ def user_assert_can_unaccept_best_answer(self, answer = None): elif self.is_administrator() or self.is_moderator(): will_be_able_at = ( - answer.added_at + + answer.added_at + datetime.timedelta( days=askbot_settings.MIN_DAYS_FOR_STAFF_TO_ACCEPT_ANSWER) ) @@ -372,7 +372,7 @@ def user_assert_can_accept_best_answer(self, answer = None): self.assert_can_unaccept_best_answer(answer) def user_assert_can_vote_for_post( - self, + self, post = None, direction = None, ): @@ -657,7 +657,7 @@ def user_assert_can_delete_question(self, question = None): def user_assert_can_delete_answer(self, answer = None): """intentionally use "post" word in the messages - instead of "answer", because this logic also applies to + instead of "answer", because this logic also applies to assert on deleting question (in addition to some special rules) """ blocked_error_message = _( @@ -791,6 +791,54 @@ def user_assert_can_flag_offensive(self, post = None): } raise django_exceptions.PermissionDenied(flags_exceeded_error_message) +def user_assert_can_remove_flag_offensive(self, post = None): + + assert(post is not None) + + non_existing_flagging_error_message = _('cannot remove non-existing flag') + + if self.get_flags_for_post(post).count() < 1: + raise django_exceptions.PermissionDenied(non_existing_flagging_error_message) + + blocked_error_message = _('blocked users cannot remove flags') + + suspended_error_message = _('suspended users cannot remove flags') + + low_rep_error_message = _('need > %(min_rep)s points to remove flag') % \ + {'min_rep': askbot_settings.MIN_REP_TO_FLAG_OFFENSIVE} + min_rep_setting = askbot_settings.MIN_REP_TO_FLAG_OFFENSIVE + + _assert_user_can( + user = self, + post = post, + blocked_error_message = blocked_error_message, + suspended_error_message = suspended_error_message, + low_rep_error_message = low_rep_error_message, + min_rep_setting = min_rep_setting + ) + #one extra assertion + if self.is_administrator() or self.is_moderator(): + return + +def user_assert_can_remove_all_flags_offensive(self, post = None): + assert(post is not None) + permission_denied_message = _("you don't have the permission to remove all flags") + non_existing_flagging_error_message = _('no flags for this entry') + + # Check if the post is flagged by anyone + post_content_type = ContentType.objects.get_for_model(post) + all_flags = Activity.objects.filter( + activity_type = const.TYPE_ACTIVITY_MARK_OFFENSIVE, + content_type = post_content_type, object_id=post.id + ) + if all_flags.count() < 1: + raise django_exceptions.PermissionDenied(non_existing_flagging_error_message) + #one extra assertion + if self.is_administrator() or self.is_moderator(): + return + else: + raise django_exceptions.PermissionDenied(permission_denied_message) + def user_assert_can_retag_question(self, question = None): @@ -859,7 +907,7 @@ def user_assert_can_delete_comment(self, comment = None): def user_assert_can_revoke_old_vote(self, vote): - """raises exceptions.PermissionDenied if old vote + """raises exceptions.PermissionDenied if old vote cannot be revoked due to age of the vote """ if (datetime.datetime.now().day - vote.voted_at.day) \ @@ -874,7 +922,7 @@ def user_get_unused_votes_today(self): one_day_interval = (today, today + datetime.timedelta(1)) used_votes = Vote.objects.filter( - user = self, + user = self, voted_at__range = one_day_interval ).count() @@ -914,7 +962,7 @@ def user_post_comment( return comment def user_post_anonymous_askbot_content(user, session_key): - """posts any posts added just before logging in + """posts any posts added just before logging in the posts are identified by the session key, thus the second argument this function is used by the signal handler with a similar name @@ -953,7 +1001,7 @@ def user_mark_tags( ): """subscribe for or ignore a list of tags - * ``tagnames`` and ``wildcards`` are lists of + * ``tagnames`` and ``wildcards`` are lists of pure tags and wildcard tags, respectively * ``reason`` - either "good" or "bad" * ``action`` - eitrer "add" or "remove" @@ -1083,7 +1131,7 @@ def user_delete_answer( ): self.assert_can_delete_answer(answer = answer) answer.deleted = True - answer.deleted_by = self + answer.deleted_by = self answer.deleted_at = timestamp answer.save() @@ -1112,17 +1160,17 @@ def user_delete_question( self.assert_can_delete_question(question = question) question.deleted = True - question.deleted_by = self + question.deleted_by = self question.deleted_at = timestamp question.save() for tag in list(question.tags.all()): if tag.used_count == 1: tag.deleted = True - tag.deleted_by = self + tag.deleted_by = self tag.deleted_at = timestamp else: - tag.used_count = tag.used_count - 1 + tag.used_count = tag.used_count - 1 tag.save() signals.delete_question_or_answer.send( @@ -1192,20 +1240,20 @@ def user_restore_post( self.assert_can_restore_post(post) if isinstance(post, Question) or isinstance(post, Answer): post.deleted = False - post.deleted_by = None - post.deleted_at = None + post.deleted_by = None + post.deleted_at = None post.save() if isinstance(post, Answer): post.question.update_answer_count() elif isinstance(post, Question): #todo: make sure that these tags actually exist - #some may have since been deleted for good + #some may have since been deleted for good #or merged into others for tag in list(post.tags.all()): if tag.used_count == 1 and tag.deleted: tag.deleted = False tag.deleted_by = None - tag.deleted_at = None + tag.deleted_at = None tag.save() else: raise NotImplementedError() @@ -1323,9 +1371,9 @@ def user_post_answer( if self == question.author and not self.is_administrator(): # check date and rep required to post answer to own question - + delta = datetime.timedelta(askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION) - + now = datetime.datetime.now() asked = question.added_at if (now - asked < delta and self.reputation < askbot_settings.MIN_REP_TO_ANSWER_OWN_QUESTION): @@ -1348,7 +1396,7 @@ def user_post_answer( left = ungettext('in %(hr)d hour','in %(hr)d hours',hours) % {'hr':hours} else: left = ungettext('in %(min)d min','in %(min)d mins',minutes) % {'min':minutes} - day = ungettext('%(days)d day','%(days)d days',askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION) % {'days':askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION} + day = ungettext('%(days)d day','%(days)d days',askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION) % {'days':askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION} error_message = _( 'New users must wait %(days)s before answering their own question. ' ' You can post an answer %(left)s' @@ -1384,7 +1432,7 @@ def user_visit_question(self, question = None, timestamp = None): on behalf of the user represented by the self object and mark it as taking place at timestamp time - and remove pending on-screen notifications about anything in + and remove pending on-screen notifications about anything in the post - question, answer or comments """ if not isinstance(question, Question): @@ -1399,7 +1447,7 @@ def user_visit_question(self, question = None, timestamp = None): ) except QuestionView.DoesNotExist: question_view = QuestionView( - who = self, + who = self, question = question ) question_view.when = timestamp @@ -1520,7 +1568,7 @@ def get_name_of_anonymous_user(): def user_get_anonymous_name(self): """Returns name of anonymous user - - convinience method for use in the template + - convinience method for use in the template macros that accept user as parameter """ return get_name_of_anonymous_user() @@ -1535,10 +1583,10 @@ def user_set_status(self, new_status): there is a slight aberration - administrator status can be removed, but not added yet - if new status is applied to user, then the record is + if new status is applied to user, then the record is committed to the database """ - #d - administrator + #d - administrator #m - moderator #s - suspended #b - blocked @@ -1556,8 +1604,8 @@ def user_set_status(self, new_status): self.set_admin_status() else: #This was the old method, kept in the else clause when changing - #to admin, so if you change the status to another thing that - #is not Administrator it will simply remove admin if the user have + #to admin, so if you change the status to another thing that + #is not Administrator it will simply remove admin if the user have #that permission, it will mostly be false. if self.is_administrator(): self.remove_admin_status() @@ -1589,7 +1637,7 @@ def user_moderate_user_reputation( user.save() #any question. This is necessary because reputes are read in the - #user_reputation view with select_related('question__title') and it fails if + #user_reputation view with select_related('question__title') and it fails if #ForeignKey is nullable even though it should work (according to the manual) #probably a bug in the Django ORM #fake_question = Question.objects.all()[:1][0] @@ -1717,7 +1765,7 @@ def delete_messages(self): def get_profile_url(self): """Returns the URL for this User's profile.""" return reverse( - 'user_profile', + 'user_profile', kwargs={'id':self.id, 'slug':slugify(self.username)} ) @@ -1790,7 +1838,7 @@ def toggle_favorite_question( ): """cancel has no effect here, but is important for the SE loader it is hoped that toggle will work and data will be consistent - but there is no guarantee, maybe it's better to be more strict + but there is no guarantee, maybe it's better to be more strict about processing the "cancel" option another strange thing is that this function unlike others below returns a value @@ -1931,17 +1979,25 @@ def downvote(self, post, timestamp=None, cancel=False, force = False): @auto_now_timestamp def flag_post(user, post, timestamp=None, cancel=False, force = False): if cancel:#todo: can't unflag? - return - - if force == False: - user.assert_can_flag_offensive(post = post) - auth.onFlaggedItem(post, user, timestamp=timestamp) - award_badges_signal.send(None, - event = 'flag_post', - actor = user, - context_object = post, - timestamp = timestamp - ) + if force == False: + user.assert_can_remove_flag_offensive(post = post) + auth.onUnFlaggedItem(post, user, timestamp=timestamp) + award_badges_signal.send(None, + event = 'flag_post', + actor = user, + context_object = post, + timestamp = timestamp + ) + else: + if force == False: + user.assert_can_flag_offensive(post = post) + auth.onFlaggedItem(post, user, timestamp=timestamp) + award_badges_signal.send(None, + event = 'flag_post', + actor = user, + context_object = post, + timestamp = timestamp + ) def user_get_flags(self): """return flag Activity query set @@ -2042,7 +2098,7 @@ User.add_to_class( user_get_followed_question_alert_frequency ) User.add_to_class( - 'subscribe_for_followed_question_alerts', + 'subscribe_for_followed_question_alerts', user_subscribe_for_followed_question_alerts ) User.add_to_class('get_absolute_url', user_get_absolute_url) @@ -2132,6 +2188,8 @@ User.add_to_class('assert_can_edit_answer', user_assert_can_edit_answer) User.add_to_class('assert_can_close_question', user_assert_can_close_question) User.add_to_class('assert_can_reopen_question', user_assert_can_reopen_question) User.add_to_class('assert_can_flag_offensive', user_assert_can_flag_offensive) +User.add_to_class('assert_can_remove_flag_offensive', user_assert_can_remove_flag_offensive) +User.add_to_class('assert_can_remove_all_flags_offensive', user_assert_can_remove_all_flags_offensive) User.add_to_class('assert_can_retag_question', user_assert_can_retag_question) #todo: do we need assert_can_delete_post User.add_to_class('assert_can_delete_post', user_assert_can_delete_post) @@ -2294,7 +2352,7 @@ def calculate_gravatar_hash(instance, **kwargs): def record_post_update_activity( post, - newly_mentioned_users = None, + newly_mentioned_users = None, updated_by = None, timestamp = None, created = False, @@ -2332,7 +2390,7 @@ def record_post_update_activity( def record_award_event(instance, created, **kwargs): """ - After we awarded a badge to user, we need to + After we awarded a badge to user, we need to record this activity and notify user. We also recaculate awarded_count of this badge and user information. """ @@ -2372,15 +2430,15 @@ def notify_award_message(instance, created, **kwargs): msg = _(u"Congratulations, you have received a badge '%(badge_name)s'. " u"Check out your profile.") \ % { - 'badge_name':badge.name, + 'badge_name':badge.name, 'user_profile':user.get_profile_url() - } + } user.message_set.create(message=msg) def record_answer_accepted(instance, created, **kwargs): """ - when answer is accepted, we record this for question author + when answer is accepted, we record this for question author - who accepted it. """ if not created and instance.accepted: @@ -2440,9 +2498,9 @@ def record_cancel_vote(instance, **kwargs): when user canceled vote, the vote will be deleted. """ activity = Activity( - user=instance.user, - active_at=datetime.datetime.now(), - content_object=instance, + user=instance.user, + active_at=datetime.datetime.now(), + content_object=instance, activity_type=const.TYPE_ACTIVITY_CANCEL_VOTE ) #todo: same problem - cannot access receiving user here @@ -2461,9 +2519,9 @@ def record_delete_question(instance, delete_by, **kwargs): activity_type = const.TYPE_ACTIVITY_DELETE_ANSWER activity = Activity( - user=delete_by, - active_at=datetime.datetime.now(), - content_object=instance, + user=delete_by, + active_at=datetime.datetime.now(), + content_object=instance, activity_type=activity_type, question = instance.get_origin_post() ) @@ -2472,9 +2530,9 @@ def record_delete_question(instance, delete_by, **kwargs): def record_flag_offensive(instance, mark_by, **kwargs): activity = Activity( - user=mark_by, - active_at=datetime.datetime.now(), - content_object=instance, + user=mark_by, + active_at=datetime.datetime.now(), + content_object=instance, activity_type=const.TYPE_ACTIVITY_MARK_OFFENSIVE, question=instance.get_origin_post() ) @@ -2488,6 +2546,20 @@ def record_flag_offensive(instance, mark_by, **kwargs): ) activity.add_recipients(recipients) +def remove_flag_offensive(instance, mark_by, **kwargs): + "Remove flagging activity" + content_type = ContentType.objects.get_for_model(instance) + + activity = Activity.objects.filter( + user=mark_by, + content_type = content_type, + object_id = instance.id, + activity_type=const.TYPE_ACTIVITY_MARK_OFFENSIVE, + question=instance.get_origin_post() + ) + activity.delete() + + def record_update_tags(question, tags, user, timestamp, **kwargs): """ This function sends award badges signal on each updated tag @@ -2516,9 +2588,9 @@ def record_favorite_question(instance, created, **kwargs): """ if created: activity = Activity( - user=instance.user, - active_at=datetime.datetime.now(), - content_object=instance, + user=instance.user, + active_at=datetime.datetime.now(), + content_object=instance, activity_type=const.TYPE_ACTIVITY_FAVORITE, question=instance.question ) @@ -2530,9 +2602,9 @@ def record_favorite_question(instance, created, **kwargs): def record_user_full_updated(instance, **kwargs): activity = Activity( - user=instance, - active_at=datetime.datetime.now(), - content_object=instance, + user=instance, + active_at=datetime.datetime.now(), + content_object=instance, activity_type=const.TYPE_ACTIVITY_USER_FULL_UPDATED ) activity.save() @@ -2614,6 +2686,8 @@ signals.delete_question_or_answer.connect(record_delete_question, sender=Questio signals.delete_question_or_answer.connect(record_delete_question, sender=Answer) signals.flag_offensive.connect(record_flag_offensive, sender=Question) signals.flag_offensive.connect(record_flag_offensive, sender=Answer) +signals.remove_flag_offensive.connect(remove_flag_offensive, sender=Question) +signals.remove_flag_offensive.connect(remove_flag_offensive, sender=Answer) signals.tags_updated.connect(record_update_tags) signals.user_updated.connect(record_user_full_updated, sender=User) signals.user_logged_in.connect(complete_pending_tag_subscriptions)#todo: add this to fake onlogin middleware diff --git a/askbot/models/signals.py b/askbot/models/signals.py index 8802fcf7..baa4c149 100644 --- a/askbot/models/signals.py +++ b/askbot/models/signals.py @@ -19,13 +19,14 @@ delete_question_or_answer = django.dispatch.Signal( providing_args=['instance', 'deleted_by'] ) flag_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by']) +remove_flag_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by']) user_updated = django.dispatch.Signal(providing_args=['instance', 'updated_by']) #todo: move this to authentication app user_logged_in = django.dispatch.Signal(providing_args=['session']) post_updated = django.dispatch.Signal( providing_args=[ - 'post', + 'post', 'updated_by', 'newly_mentioned_users' ] @@ -61,6 +62,7 @@ def pop_all_db_signal_receivers(): edit_question_or_answer, delete_question_or_answer, flag_offensive, + remove_flag_offensive, user_updated, user_logged_in, post_updated, @@ -83,7 +85,7 @@ def pop_all_db_signal_receivers(): def set_all_db_signal_receivers(receiver_data): """takes receiver data as an argument - where the argument is as returned by the + where the argument is as returned by the pop_all_db_signal_receivers() call and sets the receivers back to the signals """ diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js index e35608fa..09c6c74d 100644 --- a/askbot/skins/common/media/js/post.js +++ b/askbot/skins/common/media/js/post.js @@ -38,8 +38,8 @@ function removeLoader() { $("img.ajax-loader").remove(); } -function setSubmitButtonDisabled(form, isDisabled) { - form.find("input[type='submit']").attr("disabled", isDisabled ? "true" : ""); +function setSubmitButtonDisabled(form, isDisabled) { + form.find("input[type='submit']").attr("disabled", isDisabled ? "true" : ""); } function enableSubmitButton(form) { @@ -72,10 +72,10 @@ function setupFormValidation(form, validationRules, validationMessages, onSubmit }, submitHandler: function(form_dom) { disableSubmitButton($(form_dom)); - + if (onSubmitCallback){ onSubmitCallback(); - } + } else{ form_dom.submit(); } @@ -110,7 +110,7 @@ var CPValidator = function(){ maxlength: 105, limit_tag_count: true, limit_tag_length: true - }, + }, text: { required: true, minlength: 10 @@ -132,11 +132,11 @@ var CPValidator = function(){ }, text: { required: " " + gettext('content cannot be empty'), - minlength: interpolate(gettext('%s content minchars'), [chars]) + minlength: interpolate(gettext('%s content minchars'), [chars]) }, title: { required: " " + gettext('please enter title'), - minlength: interpolate(gettext('%s title minchars'), [chars]) + minlength: interpolate(gettext('%s title minchars'), [chars]) } }; } @@ -146,7 +146,7 @@ var CPValidator = function(){ /** * @constructor * @extends {SimpleControl} - * @param {Comment} comment to upvote + * @param {Comment} comment to upvote */ var CommentVoteButton = function(comment){ SimpleControl.call(this); @@ -271,14 +271,18 @@ var Vote = function(){ var commentLinkIdPrefix = 'comment-'; var voteNumberClass = "vote-number"; var offensiveIdPrefixQuestionFlag = 'question-offensive-flag-'; + var removeOffensiveIdPrefixQuestionFlag = 'question-offensive-flag-remove-'; + var removeAllOffensiveIdPrefixQuestionFlag = 'question-offensive-flag-remove-all-'; var offensiveIdPrefixAnswerFlag = 'answer-offensive-flag-'; + var removeOffensiveIdPrefixAnswerFlag = 'answer-offensive-flag-remove-'; + var removeAllOffensiveIdPrefixAnswerFlag = 'answer-offensive-flag-remove-all'; var offensiveClassFlag = 'offensive-flag'; var questionControlsId = 'question-controls'; var removeQuestionLinkIdPrefix = 'question-delete-link-'; var removeAnswerLinkIdPrefix = 'answer-delete-link-'; var questionSubscribeUpdates = 'question-subscribe-updates'; var questionSubscribeSidebar= 'question-subscribe-sidebar'; - + var acceptAnonymousMessage = gettext('insufficient privilege'); var acceptOwnAnswerMessage = gettext('cannot pick own answer as best'); @@ -292,12 +296,13 @@ var Vote = function(){ var voteAnonymousMessage = gettext('anonymous users cannot vote') + pleaseLogin; //there were a couple of more messages... var offensiveConfirmation = gettext('please confirm offensive'); + var removeOffensiveConfirmation = gettext('please confirm removal of offensive flag'); var offensiveAnonymousMessage = gettext('anonymous users cannot flag offensive posts') + pleaseLogin; var removeConfirmation = gettext('confirm delete'); var removeAnonymousMessage = gettext('anonymous users cannot delete/undelete') + pleaseLogin; var recoveredMessage = gettext('post recovered'); var deletedMessage = gettext('post deleted'); - + var VoteType = { acceptAnswer : 0, questionUpVote : 1, @@ -306,7 +311,11 @@ var Vote = function(){ answerUpVote: 5, answerDownVote:6, offensiveQuestion : 7, + removeOffensiveQuestion : 7.5, + removeAllOffensiveQuestion : 7.6, offensiveAnswer:8, + removeOffensiveAnswer:8.5, + removeAllOffensiveAnswer:8.6, removeQuestion: 9, removeAnswer:10, questionSubscribeUpdates:11, @@ -346,17 +355,37 @@ var Vote = function(){ var answerVoteDownButton = 'div.'+ voteContainerId +' div[id='+ imgIdPrefixAnswerVotedown + id + ']'; return $(answerVoteDownButton); }; - + var getOffensiveQuestionFlag = function(){ - var offensiveQuestionFlag = '#question-table span[class='+ offensiveClassFlag +']'; + var offensiveQuestionFlag = '#question-table span[id^="'+ offensiveIdPrefixQuestionFlag +'"]'; return $(offensiveQuestionFlag); }; - + + var getRemoveOffensiveQuestionFlag = function(){ + var removeOffensiveQuestionFlag = '#question-table span[id^="'+ removeOffensiveIdPrefixQuestionFlag +'"]'; + return $(removeOffensiveQuestionFlag); + }; + + var getRemoveAllOffensiveQuestionFlag = function(){ + var removeAllOffensiveQuestionFlag = '#question-table span[id^="'+ removeAllOffensiveIdPrefixQuestionFlag +'"]'; + return $(removeAllOffensiveQuestionFlag); + }; + var getOffensiveAnswerFlags = function(){ - var offensiveQuestionFlag = 'div.answer span[class='+ offensiveClassFlag +']'; + var offensiveQuestionFlag = 'div.answer span[id^="'+ offensiveIdPrefixAnswerFlag +'"]'; return $(offensiveQuestionFlag); }; - + + var getRemoveOffensiveAnswerFlag = function(){ + var removeOffensiveAnswerFlag = 'div.answer span[id^="'+ removeOffensiveIdPrefixAnswerFlag +'"]'; + return $(removeOffensiveAnswerFlag); + }; + + var getRemoveAllOffensiveAnswerFlag = function(){ + var removeAllOffensiveAnswerFlag = 'div.answer span[id^="'+ removeAllOffensiveIdPrefixAnswerFlag +'"]'; + return $(removeAllOffensiveAnswerFlag); + }; + var getremoveQuestionLink = function(){ var removeQuestionLink = 'div#question-controls a[id^='+ removeQuestionLinkIdPrefix +']'; return $(removeQuestionLink); @@ -369,12 +398,12 @@ var Vote = function(){ var getquestionSubscribeSidebarCheckbox= function(){ return $('#' + questionSubscribeSidebar); }; - + var getremoveAnswersLinks = function(){ var removeAnswerLinks = 'div.answer-controls a[id^='+ removeAnswerLinkIdPrefix +']'; return $(removeAnswerLinks); }; - + var setVoteImage = function(voteType, undo, object){ var flag = undo ? false : true; if (object.hasClass("on")) { @@ -382,7 +411,7 @@ var Vote = function(){ }else{ object.addClass("on"); } - + if(undo){ if(voteType == VoteType.questionUpVote || voteType == VoteType.questionDownVote){ $(getQuestionVoteUpButton()).removeClass("on"); @@ -394,12 +423,12 @@ var Vote = function(){ } } }; - + var setVoteNumber = function(object, number){ var voteNumber = object.parent('div.'+ voteContainerId).find('div.'+ voteNumberClass); $(voteNumber).text(number); }; - + var bindEvents = function(){ // accept answers var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; @@ -412,36 +441,52 @@ var Vote = function(){ //Vote.favorite($(event.target)); Vote.favorite(favoriteButton); }); - + // question vote up var questionVoteUpButton = getQuestionVoteUpButton(); questionVoteUpButton.unbind('click').click(function(event){ Vote.vote($(event.target), VoteType.questionUpVote); }); - + var questionVoteDownButton = getQuestionVoteDownButton(); questionVoteDownButton.unbind('click').click(function(event){ Vote.vote($(event.target), VoteType.questionDownVote); }); - + var answerVoteUpButton = getAnswerVoteUpButtons(); answerVoteUpButton.unbind('click').click(function(event){ Vote.vote($(event.target), VoteType.answerUpVote); }); - + var answerVoteDownButton = getAnswerVoteDownButtons(); answerVoteDownButton.unbind('click').click(function(event){ Vote.vote($(event.target), VoteType.answerDownVote); }); - + getOffensiveQuestionFlag().unbind('click').click(function(event){ Vote.offensive(this, VoteType.offensiveQuestion); }); - + + getRemoveOffensiveQuestionFlag().unbind('click').click(function(event){ + Vote.remove_offensive(this, VoteType.removeOffensiveQuestion); + }); + + getRemoveAllOffensiveQuestionFlag().unbind('click').click(function(event){ + Vote.remove_offensive(this, VoteType.removeAllOffensiveQuestion); + }); + getOffensiveAnswerFlags().unbind('click').click(function(event){ Vote.offensive(this, VoteType.offensiveAnswer); }); - + + getRemoveOffensiveAnswerFlag().unbind('click').click(function(event){ + Vote.remove_offensive(this, VoteType.removeOffensiveAnswer); + }); + + getRemoveAllOffensiveAnswerFlag().unbind('click').click(function(event){ + Vote.remove_offensive(this, VoteType.removeAllOffensiveAnswer); + }); + getremoveQuestionLink().unbind('click').click(function(event){ Vote.remove(this, VoteType.removeQuestion); }); @@ -473,7 +518,7 @@ var Vote = function(){ Vote.remove(this, VoteType.removeAnswer); }); }; - + var submit = function(object, voteType, callback) { //this function submits votes $.ajax({ @@ -486,7 +531,7 @@ var Vote = function(){ success: function(data){callback(object, voteType, data);} }); }; - + var handleFail = function(xhr, msg){ alert("Callback invoke error: " + msg); }; @@ -511,7 +556,7 @@ var Vote = function(){ $(answers).removeClass("accepted-answer"); var commentLinks = ("div[id^="+answerContainerIdPrefix +"] div[id^="+ commentLinkIdPrefix +"]"); $(commentLinks).removeClass("comment-link-accepted"); - + object.attr("src", mediaUrl("media/images/vote-accepted-on.png")); $("#"+answerContainerIdPrefix+postId).addClass("accepted-answer"); $("#"+commentLinkIdPrefix+postId).addClass("comment-link-accepted"); @@ -524,12 +569,12 @@ var Vote = function(){ var callback_favorite = function(object, voteType, data){ if(data.allowed == "0" && data.success == "0"){ showMessage( - object, + object, favoriteAnonymousMessage.replace( - '{{QuestionID}}', + '{{QuestionID}}', questionId).replace( '{{questionSlug}}', - '' + '' ) ); } @@ -560,7 +605,7 @@ var Vote = function(){ showMessage(object, data.message); } }; - + var callback_vote = function(object, voteType, data){ if (data.success == '0'){ showMessage(object, data.message); @@ -583,7 +628,7 @@ var Vote = function(){ if (data.status == "1"){ setVoteImage(voteType, true, object); setVoteNumber(object, data.count); - } + } else if (data.success == "1"){ setVoteImage(voteType, false, object); setVoteNumber(object, data.count); @@ -592,20 +637,58 @@ var Vote = function(){ } } }; - + var callback_offensive = function(object, voteType, data){ //todo: transfer proper translations of these from i18n.js //to django.po files //_('anonymous users cannot flag offensive posts') + pleaseLogin; if (data.success == "1"){ $(object).children('span[class=darkred]').text("("+ data.count +")"); + + // Change the link text and rebind events + $(object).find("a.question-flag").html(gettext("remove flag")); + var obj_id = $(object).attr("id"); + $(object).attr("id", obj_id.replace("flag-", "flag-remove-")); + + getRemoveOffensiveQuestionFlag().unbind('click').click(function(event){ + Vote.remove_offensive(this, VoteType.removeOffensiveQuestion); + }); + + getRemoveOffensiveAnswerFlag().unbind('click').click(function(event){ + Vote.remove_offensive(this, VoteType.removeOffensiveAnswer); + }); + } + else { + object = $(object); + showMessage(object, data.message) + } + }; + + var callback_remove_offensive = function(object, voteType, data){ + //todo: transfer proper translations of these from i18n.js + //to django.po files + //_('anonymous users cannot flag offensive posts') + pleaseLogin; + if (data.success == "1"){ + $(object).children('span[class=darkred]').text("("+ data.count +")"); + // Change the link text and rebind events + $(object).find("a.question-flag").html(gettext("flag offensive")); + var obj_id = $(object).attr("id"); + $(object).attr("id", obj_id.replace("flag-remove-", "flag-")); + + getOffensiveQuestionFlag().unbind('click').click(function(event){ + Vote.offensive(this, VoteType.offensiveQuestion); + }); + + getOffensiveAnswerFlags().unbind('click').click(function(event){ + Vote.offensive(this, VoteType.offensiveAnswer); + }); } else { object = $(object); showMessage(object, data.message) } }; - + var callback_remove = function(object, voteType, data){ if (data.success == "1"){ if (removeActionType == 'delete'){ @@ -623,7 +706,7 @@ var Vote = function(){ showMessage(object, data.message) } }; - + return { init : function(qId, qSlug, questionAuthor, userId){ questionId = qId; @@ -632,7 +715,7 @@ var Vote = function(){ currentUserId = userId; bindEvents(); }, - + //accept answer accept: function(object){ postId = object.attr("id").substring(imgIdPrefixAccept.length); @@ -642,7 +725,7 @@ var Vote = function(){ favorite: function(object){ if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ showMessage( - object, + object, favoriteAnonymousMessage.replace( "{{QuestionID}}", questionId @@ -655,7 +738,7 @@ var Vote = function(){ } submit(object, VoteType.favorite, callback_favorite); }, - + vote: function(object, voteType){ if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ if (voteType == VoteType.questionSubscribeUpdates || voteType == VoteType.questionUnsubscribeUpdates){ @@ -683,7 +766,7 @@ var Vote = function(){ else if (voteType == VoteType.answerDownVote){ postId = object.attr("id").substring(imgIdPrefixAnswerVotedown.length); } - + submit(object, voteType, callback_vote); }, //flag offensive @@ -699,13 +782,33 @@ var Vote = function(){ questionSlug ) ); - return false; + return false; } if (confirm(offensiveConfirmation)){ postId = object.id.substr(object.id.lastIndexOf('-') + 1); submit(object, voteType, callback_offensive); } }, + //remove flag offensive + remove_offensive: function(object, voteType){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ + showMessage( + $(object), + offensiveAnonymousMessage.replace( + "{{QuestionID}}", + questionId + ).replace( + '{{questionSlug}}', + questionSlug + ) + ); + return false; + } + if (confirm(removeOffensiveConfirmation)){ + postId = object.id.substr(object.id.lastIndexOf('-') + 1); + submit(object, voteType, callback_remove_offensive); + } + }, //delete question or answer (comments are deleted separately) remove: function(object, voteType){ if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ @@ -719,13 +822,13 @@ var Vote = function(){ questionSlug ) ); - return false; + return false; } bits = object.id.split('-'); postId = bits.pop();/* this seems to be used within submit! */ postType = bits.shift(); - var do_proceed = false; + var do_proceed = false; if (postType == 'answer'){ postNode = $('#answer-container-' + postId); } @@ -976,7 +1079,7 @@ EditCommentForm.prototype.getCounterUpdater = function(){ if (length2 < 0){ length2 = Math.round(0.9*maxCommentLength); } - + //todo: //1) use class instead of color - move color def to css var color = 'maroon'; @@ -1017,7 +1120,7 @@ EditCommentForm.prototype.getCancelHandler = function(){ return function(){ if (form.canCancel()){ form.detach(); - } + } return false; }; }; @@ -1047,7 +1150,7 @@ EditCommentForm.prototype.createDom = function(){ /* this._help_text = $('').attr('class', 'help-text'); - this._help_text.html(gettext('Markdown is allowed in the comments')); + this._help_text.html(gettext('Markdown is allowed in the comments')); div.append(this._help_text); this._help_text = $('
').attr('class', 'clearfix'); @@ -1257,7 +1360,7 @@ Comment.prototype.setContent = function(data){ var votes = this.makeElement('div'); votes.addClass('comment-votes'); - + var vote = new CommentVoteButton(this); if (this._data['upvoted_by_user']){ vote.setVoted(true); @@ -1274,7 +1377,7 @@ Comment.prototype.setContent = function(data){ this._comment_delete.append(this._delete_icon.getElement()); } this._element.append(this._comment_delete); - + this._comment_body = $('
'); this._comment_body.html(this._data['html']); //this._comment_body.append(' – '); @@ -1296,7 +1399,7 @@ Comment.prototype.setContent = function(data){ this._comment_body.append(this._edit_link.getElement()); } this._element.append(this._comment_body); - + this._blank = false; }; @@ -1306,7 +1409,7 @@ Comment.prototype.dispose = function(){ } if (this._comment_delete){ this._comment_delete.remove(); - } + } if (this._user_link){ this._user_link.remove(); } @@ -1386,11 +1489,11 @@ Comment.prototype.getDeleteHandler = function(){ comment.getElement().hide(); $.ajax({ type: 'POST', - url: askbot['urls']['deleteComment'], - data: { comment_id: comment.getId() }, + url: askbot['urls']['deleteComment'], + data: { comment_id: comment.getId() }, success: function(json, textStatus, xhr) { comment.dispose(); - }, + }, error: function(xhr, textStatus, exception) { comment.getElement().show() showMessage(del_icon.getElement(), xhr.responseText); @@ -1600,7 +1703,7 @@ var socialSharing = function(){ setupButtonEventHandlers(ica, function(){share_page("identica")}); } } -}(); +}(); /** * @constructor @@ -1635,7 +1738,7 @@ QASwapper.prototype.startSwapping = function(){ success: function(data){ var url_template = askbot['urls']['question_url_template']; new_question_url = url_template.replace( - '{{QuestionID}}', + '{{QuestionID}}', data['id'] ).replace( '{{questionSlug}}', diff --git a/askbot/skins/common/templates/question/answer_controls.html b/askbot/skins/common/templates/question/answer_controls.html index 4d26ffb9..0dddc72d 100644 --- a/askbot/skins/common/templates/question/answer_controls.html +++ b/askbot/skins/common/templates/question/answer_controls.html @@ -1,7 +1,7 @@ {% set pipe=joiner('|') %} {{ pipe() }} {% trans %}permanent link{% endtrans %} @@ -10,13 +10,21 @@ {% trans %}edit{% endtrans %} {% endif %} {% if request.user|can_flag_offensive(answer) %}{{ pipe() }} - {% trans %}flag offensive{% endtrans %} {% if request.user|can_see_offensive_flags(answer) %} {% if answer.offensive_flag_count > 0 %}({{ answer.offensive_flag_count }}){% endif %} {% endif %} +{% elif request.user|can_remove_flag_offensive(answer)%}{{ pipe() }} + + {% trans %}remove flag{% endtrans %} + {% if request.user|can_see_offensive_flags(answer) %} + {% if answer.offensive_flag_count > 0 %}({{ answer.offensive_flag_count }}){% endif %} + {% endif %} + {% endif %} {% if request.user|can_delete_post(answer) %}{{ pipe() }} {% spaceless %} diff --git a/askbot/skins/common/templates/question/question_controls.html b/askbot/skins/common/templates/question/question_controls.html index 9de54526..ea112aa6 100644 --- a/askbot/skins/common/templates/question/question_controls.html +++ b/askbot/skins/common/templates/question/question_controls.html @@ -18,13 +18,21 @@ {% endif %} {% endif %} {% if request.user|can_flag_offensive(question) %}{{ pipe() }} - {% trans %}flag offensive{% endtrans %} {% if request.user|can_see_offensive_flags(question) %} {% if question.offensive_flag_count > 0 %}({{ question.offensive_flag_count }}){% endif %} {% endif %} +{% elif request.user|can_remove_flag_offensive(question)%}{{ pipe() }} + + {% trans %}remove flag{% endtrans %} + {% if request.user|can_see_offensive_flags(question) %} + {% if question.offensive_flag_count > 0 %}({{ question.offensive_flag_count }}){% endif %} + {% endif %} + {% endif %} {% if request.user|can_delete_post(question) %}{{ pipe() }} {% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}{% trans %}delete{% endtrans %}{% endif %} diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py index 2228eed4..8486d934 100644 --- a/askbot/templatetags/extra_filters_jinja.py +++ b/askbot/templatetags/extra_filters_jinja.py @@ -163,7 +163,16 @@ def can_moderate_user(user, other_user): can_flag_offensive = make_template_filter_from_permission_assertion( assertion_name = 'assert_can_flag_offensive', filter_name = 'can_flag_offensive', - allowed_exception = askbot_exceptions.DuplicateCommand + ) + +can_remove_flag_offensive = make_template_filter_from_permission_assertion( + assertion_name = 'assert_can_remove_flag_offensive', + filter_name = 'can_remove_flag_offensive', + ) + +can_remove_all_flags_offensive = make_template_filter_from_permission_assertion( + assertion_name = 'assert_can_remove_all_flags_offensive', + filter_name = 'can_remove_all_flags_offensive', ) can_post_comment = make_template_filter_from_permission_assertion( diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 04d4ef1b..2eb562ed 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -1,7 +1,7 @@ """ :synopsis: most ajax processors for askbot -This module contains most (but not all) processors for Ajax requests. +This module contains most (but not all) processors for Ajax requests. Not so clear if this subdivision was necessary as separation of Ajax and non-ajax views is not always very clean. """ @@ -39,7 +39,7 @@ def process_vote(user = None, vote_direction = None, post = None): raise exceptions.PermissionDenied(_('anonymous users cannot vote')) user.assert_can_vote_for_post( - post = post, + post = post, direction = vote_direction ) @@ -71,7 +71,7 @@ def process_vote(user = None, vote_direction = None, post = None): else: vote = user.downvote(post = post) - response_data['count'] = post.score + response_data['count'] = post.score response_data['status'] = 0 #this means "not cancel", normal operation response_data['success'] = 1 @@ -217,7 +217,7 @@ def vote(request, id): vote_direction = 'down' if vote_type in ('5', '6'): - #todo: fix this weirdness - why postId here + #todo: fix this weirdness - why postId here #and not with question? id = request.POST.get('postId') post = get_object_or_404(models.Answer, id=id) @@ -245,9 +245,22 @@ def vote(request, id): response_data['count'] = post.offensive_flag_count response_data['success'] = 1 + elif vote_type in ['7.5', '8.5']: + #flag question or answer + if vote_type == '7.5': + post = get_object_or_404(models.Question, id=id) + if vote_type == '8.5': + id = request.POST.get('postId') + post = get_object_or_404(models.Answer, id=id) + + request.user.flag_post(post, cancel = True) + + response_data['count'] = post.offensive_flag_count + response_data['success'] = 1 + elif vote_type in ['9', '10']: #delete question or answer - post = get_object_or_404(models.Question, id = id) + post = get_object_or_404(models.Question, id = id) if vote_type == '10': id = request.POST.get('postId') post = get_object_or_404(models.Answer, id = id) @@ -460,7 +473,7 @@ def set_tag_filter_strategy(request): @login_required @csrf.csrf_protect def close(request, id):#close question - """view to initiate and process + """view to initiate and process question close """ question = get_object_or_404(models.Question, id=id) @@ -490,7 +503,7 @@ def close(request, id):#close question @login_required @csrf.csrf_protect def reopen(request, id):#re-open question - """view to initiate and process + """view to initiate and process question close this is not an ajax view @@ -512,7 +525,7 @@ def reopen(request, id):#re-open question 'closed_by_username': closed_by_username, } return render_into_skin('reopen.html', data, request) - + except exceptions.PermissionDenied, e: request.user.message_set.create(message = unicode(e)) return HttpResponseRedirect(question.get_absolute_url()) -- cgit v1.2.3-1-g7c22