From 60bc97227cf407e7bf12c5f249c6d7f73cef5276 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Fri, 3 Dec 2010 01:52:36 -0500 Subject: added several more badges --- askbot/conf/badges.py | 67 ++++++++++++- askbot/models/__init__.py | 6 +- askbot/models/badges.py | 239 +++++++++++++++++++++++++++++++++++++++----- askbot/tests/badge_tests.py | 159 ++++++++++++++++++++++++++--- askbot/views/readers.py | 12 ++- 5 files changed, 441 insertions(+), 42 deletions(-) diff --git a/askbot/conf/badges.py b/askbot/conf/badges.py index b23f35e2..d5f8313a 100644 --- a/askbot/conf/badges.py +++ b/askbot/conf/badges.py @@ -54,7 +54,7 @@ settings.register( BADGES, 'GOOD_ANSWER_BADGE_MIN_UPVOTES', default=3, - description=_('Good answer: minimum upvotes for the answer') + description=_('Good Answer: minimum upvotes for the answer') ) ) @@ -62,7 +62,70 @@ settings.register( IntegerValue( BADGES, 'GREAT_ANSWER_BADGE_MIN_UPVOTES', + default=5, + description=_('Great Answer: minimum upvotes for the answer') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'NICE_QUESTION_BADGE_MIN_UPVOTES', + default=2, + description=_('Nice Question: minimum upvotes for the question') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'GOOD_QUESTION_BADGE_MIN_UPVOTES', default=3, - description=_('Great answer: minimum upvotes for the answer') + description=_('Good Question: minimum upvotes for the question') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'GREAT_QUESTION_BADGE_MIN_UPVOTES', + default=5, + description=_('Great Question: minimum upvotes for the question') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'POPULAR_QUESTION_BADGE_MIN_VIEWS', + default=5, + description=_('Popular Question: minimum views') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'NOTABLE_QUESTION_BADGE_MIN_VIEWS', + default=150, + description=_('Notable Question: minimum views') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'FAMOUS_QUESTION_BADGE_MIN_VIEWS', + default=500, + description=_('Famous Question: minimum views') + ) +) + +settings.register( + IntegerValue( + BADGES, + 'SELF_LEARNER_BADGE_MIN_UPVOTES', + default=500, + description=_('Self-Learner: minimum answer upvotes') ) ) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index e494c592..43982e37 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -1322,9 +1322,9 @@ def toggle_favorite_question(self, question, timestamp=None, cancel=False): VOTES_TO_EVENTS = { (Vote.VOTE_UP, 'answer'): 'upvote_answer', -# (Vote.VOTE_UP, 'question'): 'upvote_question', -# (Vote.VOTE_DOWN, 'answer'): 'downvote_answer', -# (Vote.VOTE_DOWN, 'question'): 'downvote_question' + (Vote.VOTE_UP, 'question'): 'upvote_question', + (Vote.VOTE_DOWN, 'question'): 'downvote', + (Vote.VOTE_DOWN, 'answer'): 'downvote' } @auto_now_timestamp def _process_vote(user, post, timestamp=None, cancel=False, vote_type=None): diff --git a/askbot/models/badges.py b/askbot/models/badges.py index cc02045e..99a10ef2 100644 --- a/askbot/models/badges.py +++ b/askbot/models/badges.py @@ -179,10 +179,72 @@ class Teacher(Badge): return self.award(context_object.author, context_object, timestamp) return False +class FirstVote(Badge): + """this badge is not awarded directly, but through + Supporter and Critic, which must provide + * key, name and description properties through __new__ call + """ + def __init__(self): + super(FirstVote, self).__init__( + key = self.key, + name = self.name, + description = self.description, + level = const.BRONZE_BADGE, + multiple = False + ) + + def consider_award(self, actor = None, + context_object = None, timestamp = None): + if context_object.post_type not in ('question', 'answer'): + return False + return self.award(actor, context_object, timestamp) + +class Supporter(FirstVote): + """first upvote""" + def __new__(cls): + self = super(Supporter, cls).__new__(cls) + self.key = 'supporter' + self.name = _('Supporter') + self.description = _('First upvote') + return self + +class Critic(FirstVote): + """like supporter, but for downvote""" + def __new__(cls): + self = super(Critic, cls).__new__(cls) + self.key = 'critic' + self.name = _('Critic') + self.description = _('First downvote') + return self + +class SelfLearner(Badge): + def __init__(self): + description = _('Answered own question with at least %(num)s up votes') + min_votes = askbot_settings.SELF_LEARNER_BADGE_MIN_UPVOTES + super(SelfLearner, self).__init__( + key = 'self-learner', + name = _('Self-Learner'), + description = description % {'num': min_votes}, + level = const.BRONZE_BADGE, + multiple = True + ) + + def consider_award(self, actor = None, + context_object = None, timestamp = None): + if context_object.post_type != 'answer': + return False + + min_upvotes = askbot_settings.SELF_LEARNER_BADGE_MIN_UPVOTES + question = context_object.question + answer = context_object + + if question.author == answer.author and answer.score >= min_upvotes: + self.award(context_object.author, context_object, timestamp) + class QualityPost(Badge): """Generic Badge for Nice/Good/Great Question or Answer this badge is not used directly but is instantiated - via subclasses + via subclasses created via __new__() method definitions The subclass has a responsibility to specify properties: * min_votes - a value from live settings @@ -200,7 +262,7 @@ class QualityPost(Badge): def consider_award(self, actor = None, context_object = None, timestamp = None): - if context_object.post_type != 'answer': + if context_object.post_type not in ('answer', 'question'): return False if context_object.score >= self.min_votes: return self.award(context_object.author, context_object, timestamp) @@ -242,52 +304,181 @@ class GreatAnswer(QualityPost): self.post_type = 'answer' return self +class NiceQuestion(QualityPost): + def __new__(cls): + self = super(NiceQuestion, cls).__new__(cls) + self.name = _('Nice Question') + self.key = 'nice-question' + self.level = const.BRONZE_BADGE + self.multiple = True + self.min_votes = askbot_settings.NICE_QUESTION_BADGE_MIN_UPVOTES + self.description = _('Question voted up %(num)s times') % {'num': self.min_votes} + self.post_type = 'question' + return self + +class GoodQuestion(QualityPost): + def __new__(cls): + self = super(GoodQuestion, cls).__new__(cls) + self.name = _('Good Question') + self.key = 'good-question' + self.level = const.SILVER_BADGE + self.multiple = True + self.min_votes = askbot_settings.GOOD_QUESTION_BADGE_MIN_UPVOTES + self.description = _('Question voted up %(num)s times') % {'num': self.min_votes} + self.post_type = 'question' + return self + +class GreatQuestion(QualityPost): + def __new__(cls): + self = super(GreatQuestion, cls).__new__(cls) + self.name = _('Great Question') + self.key = 'great-question' + self.level = const.GOLD_BADGE + self.multiple = True + self.min_votes = askbot_settings.GREAT_QUESTION_BADGE_MIN_UPVOTES + self.description = _('Question voted up %(num)s times') % {'num': self.min_votes} + self.post_type = 'question' + return self + +class Student(QualityPost): + def __new__(cls): + self = super(Student , cls).__new__(cls) + self.name = _('Student') + self.key = 'student' + self.level = const.BRONZE_BADGE + self.multiple = False + self.min_votes = 1 + self.description = _('Asked first question with at least one up vote') + self.post_type = 'question' + return self + +class FrequentedQuestion(Badge): + """this badge is not awarded directly + but must be subclassed by Popular, Notable and Famous Question + badges via __new__() method definitions + + The subclass has a responsibility to specify properties: + * min_views - a value from live settings + * key, name, description and level and multiple - as intended in the Badge + """ + def __init__(self): + super(FrequentedQuestion, self).__init__( + key = self.key, + name = self.name, + description = self.description, + level = self.level, + multiple = True + ) + + def consider_award(self, actor = None, + context_object = None, timestamp = None): + if context_object.post_type != 'question': + return False + if context_object.view_count >= self.min_views: + return self.award(context_object.author, context_object, timestamp) + return False + +class PopularQuestion(FrequentedQuestion): + def __new__(cls): + self = super(PopularQuestion, cls).__new__(cls) + self.name = _('Popular Question') + self.key = 'popular-question' + self.level = const.BRONZE_BADGE + self.min_views = askbot_settings.POPULAR_QUESTION_BADGE_MIN_VIEWS + self.description = _('Asked a question with %(views)s views') \ + % {'views' : self.min_views} + return self + +class NotableQuestion(FrequentedQuestion): + def __new__(cls): + self = super(NotableQuestion, cls).__new__(cls) + self.name = _('Notable Question') + self.key = 'notable-question' + self.level = const.SILVER_BADGE + self.min_views = askbot_settings.NOTABLE_QUESTION_BADGE_MIN_VIEWS + self.description = _('Asked a question with %(views)s views') \ + % {'views' : self.min_views} + return self + +class FamousQuestion(FrequentedQuestion): + def __new__(cls): + self = super(FamousQuestion, cls).__new__(cls) + self.name = _('Famous Question') + self.key = 'famous-question' + self.level = const.GOLD_BADGE + self.multiple = True + self.min_views = askbot_settings.FAMOUS_QUESTION_BADGE_MIN_VIEWS + self.description = _('Asked a question with %(views)s views') \ + % {'views' : self.min_views} + return self ORIGINAL_DATA = """ - (_('Nice Question'), 3, _('nice-question'), _('Question voted up 10 times'), True, 0), + (_('Civic duty'), 2, _('civic-duty'), _('Voted 300 times'), False, 0), + + (_('Enlightened'), 2, _('enlightened'), _('First answer was accepted with at least 10 up votes'), False, 0), + (_('Guru'), 2, _('guru'), _('Accepted answer and voted up 40 times'), True, 0), + + (_('Necromancer'), 2, _('necromancer'), _('Answered a question more than 60 days later with at least 5 votes'), True, 0), + + (_('Scholar'), 3, _('scholar'), _('First accepted answer on your own question'), False, 0), (_('Pundit'), 3, _('pundit'), _('Left 10 comments with score of 10 or more'), False, 0), - (_('Popular Question'), 3, _('popular-question'), _('Asked a question with 1,000 views'), True, 0), (_('Citizen patrol'), 3, _('citizen-patrol'), _('First flagged post'), False, 0), + (_('Cleanup'), 3, _('cleanup'), _('First rollback'), False, 0), - (_('Critic'), 3, _('critic'), _('First down vote'), False, 0), + (_('Editor'), 3, _('editor'), _('First edit'), False, 0), + (_('Strunk & White'), 2, _('strunk-and-white'), _('Edited 100 entries'), False, 0), (_('Organizer'), 3, _('organizer'), _('First retag'), False, 0), - (_('Scholar'), 3, _('scholar'), _('First accepted answer on your own question'), False, 0), - (_('Student'), 3, _('student'), _('Asked first question with at least one up vote'), False, 0), - (_('Supporter'), 3, _('supporter'), _('First up vote'), False, 0), + (_('Autobiographer'), 3, _('autobiographer'), _('Completed all user profile fields'), False, 0), - (_('Self-Learner'), 3, _('self-learner'), _('Answered your own question with at least 3 up votes'), True, 0), - (_('Great Question'), 1, _('great-question'), _('Question voted up 100 times'), True, 0), + (_('Stellar Question'), 1, _('stellar-question'), _('Question favorited by 100 users'), True, 0), - (_('Famous question'), 1, _('famous-question'), _('Asked a question with 10,000 views'), True, 0), - (_('Alpha'), 2, _('alpha'), _('Actively participated in the private alpha'), False, 0), - (_('Good Question'), 2, _('good-question'), _('Question voted up 25 times'), True, 0), (_('Favorite Question'), 2, _('favorite-question'), _('Question favorited by 25 users'), True, 0), - (_('Civic duty'), 2, _('civic-duty'), _('Voted 300 times'), False, 0), - (_('Strunk & White'), 2, _('strunk-and-white'), _('Edited 100 entries'), False, 0), + + (_('Alpha'), 2, _('alpha'), _('Actively participated in the private alpha'), False, 0), + (_('Generalist'), 2, _('generalist'), _('Active in many different tags'), False, 0), (_('Expert'), 2, _('expert'), _('Very active in one tag'), False, 0), + (_('Taxonomist'), 2, _('taxonomist'), _('Created a tag used by 50 questions'), True, 0) + (_('Yearling'), 2, _('yearling'), _('Active member for a year'), False, 0), - (_('Notable Question'), 2, _('notable-question'), _('Asked a question with 2,500 views'), True, 0), - (_('Enlightened'), 2, _('enlightened'), _('First answer was accepted with at least 10 up votes'), False, 0), (_('Beta'), 2, _('beta'), _('Actively participated in the private beta'), False, 0), - (_('Guru'), 2, _('guru'), _('Accepted answer and voted up 40 times'), True, 0), - (_('Necromancer'), 2, _('necromancer'), _('Answered a question more than 60 days later with at least 5 votes'), True, 0), - (_('Taxonomist'), 2, _('taxonomist'), _('Created a tag used by 50 questions'), True, 0) """ BADGES = { 'disciplined': Disciplined, 'peer-pressure': PeerPressure, 'teacher': Teacher, + 'student': Student, + 'supporter': Supporter, + 'self-learner': SelfLearner, 'nice-answer': NiceAnswer, 'good-answer': GoodAnswer, 'great-answer': GreatAnswer, + 'nice-question': NiceQuestion, + 'good-question': GoodQuestion, + 'great-question': GreatQuestion, + 'popular-question': PopularQuestion, + 'notable-question': NotableQuestion, + 'famous-question': FamousQuestion, + 'critic': Critic, } +#events are sent as a parameter via signal award_badges_signal +#from appropriate locations in the code of askbot application +#most likely - from manipulator functions that are added to the User objects EVENTS_TO_BADGES = { - 'upvote_answer': (Teacher, NiceAnswer, GoodAnswer, GreatAnswer), + 'upvote_answer': ( + Teacher, NiceAnswer, GoodAnswer, + GreatAnswer, Supporter, SelfLearner + ), + 'upvote_question': ( + NiceQuestion, GoodQuestion, + GreatQuestion, Student, Supporter + ), + 'downvote': (Critic,),#no regard for question or answer for now 'delete_post': (Disciplined, PeerPressure,), + 'view_question': (PopularQuestion, NotableQuestion, FamousQuestion,), } def get_badge(name = None): @@ -303,6 +494,8 @@ def init_badges(): `BADGES` dictionary """ #todo: maybe better to redo individual badges + #so that get_stored_data() is called implicitly + #from the __init__ function? for key in BADGES.keys(): get_badge(key).get_stored_data() @@ -320,13 +513,13 @@ def award_badges(event = None, actor = None, context_object = None, timestamp = None, **kwargs): """function that is called when signal `award_badges_signal` is sent """ - try: consider_badges = EVENTS_TO_BADGES[event] except KeyError: raise NotImplementedError('event "%s" is not implemented' % event) for badge in consider_badges: - badge().consider_award(actor, context_object, timestamp) + badge_instance = badge() + badge_instance.consider_award(actor, context_object, timestamp) award_badges_signal.connect(award_badges) diff --git a/askbot/tests/badge_tests.py b/askbot/tests/badge_tests.py index c74ea461..be7e604f 100644 --- a/askbot/tests/badge_tests.py +++ b/askbot/tests/badge_tests.py @@ -1,3 +1,4 @@ +from django.test.client import Client from askbot.tests.utils import AskbotTestCase from askbot.conf import settings from askbot import models @@ -8,17 +9,19 @@ class BadgeTests(AskbotTestCase): self.u1 = self.create_user(username = 'user1') self.u2 = self.create_user(username = 'user2') self.u3 = self.create_user(username = 'user3') + self.client = Client() - def assert_have_badge(self, badge_key, expected_count = 1): - count = models.Award.objects.filter(badge__slug = badge_key).count() + def assert_have_badge(self, badge_key, recipient = None, expected_count = 1): + filters = {'badge__slug': badge_key, 'user': recipient} + count = models.Award.objects.filter(**filters).count() self.assertEquals(count, expected_count) - def assert_voted_answer_badge_works(self, + def assert_upvoted_answer_badge_works(self, badge_key = None, min_score = None, multiple = False ): - """test answer badge where answer author + """test answer badge where answer author is the recipient where badge award is triggered by upvotes * min_score - minimum # of upvotes required * multiple - multiple award or not @@ -26,16 +29,16 @@ class BadgeTests(AskbotTestCase): """ question = self.post_question(user = self.u1) answer = self.post_answer(user = self.u2, question = question) - answer.score = min_score + answer.score = min_score - 1 answer.save() self.u1.upvote(answer) - self.assert_have_badge(badge_key) + self.assert_have_badge(badge_key, recipient = self.u2) self.u3.upvote(answer) - self.assert_have_badge(badge_key, expected_count = 1) + self.assert_have_badge(badge_key, recipient = self.u2, expected_count = 1) #post another question and check that there are no new badges answer2 = self.post_answer(user = self.u2, question = question) - answer2.score = min_score + answer2.score = min_score - 1 answer2.save() self.u1.upvote(answer2) @@ -44,14 +47,60 @@ class BadgeTests(AskbotTestCase): else: expected_count = 1 - self.assert_have_badge(badge_key, expected_count = expected_count) + self.assert_have_badge( + badge_key, + recipient = self.u2, + expected_count = expected_count + ) + + def assert_upvoted_question_badge_works(self, + badge_key = None, + min_score = None, + multiple = False + ): + """test question badge where question author is the recipient + where badge award is triggered by upvotes + * min_score - minimum # of upvotes required + * multiple - multiple award or not + * badge_key - key on askbot.models.badges.Badge object + """ + question = self.post_question(user = self.u1) + question.score = min_score - 1 + question.save() + self.u2.upvote(question) + self.assert_have_badge(badge_key, recipient = self.u1) + self.u3.upvote(question) + self.assert_have_badge(badge_key, recipient = self.u1, expected_count = 1) + + #post another question and check that there are no new badges + question2 = self.post_question(user = self.u1) + question2.score = min_score - 1 + question2.save() + self.u2.upvote(question2) + + if multiple == True: + expected_count = 2 + else: + expected_count = 1 + + self.assert_have_badge( + badge_key, + recipient = self.u1, + expected_count = expected_count + ) def test_disciplined_badge(self): question = self.post_question(user = self.u1) question.score = settings.DISCIPLINED_BADGE_MIN_UPVOTES question.save() self.u1.delete_question(question) - self.assert_have_badge('disciplined') + self.assert_have_badge('disciplined', recipient = self.u1) + + question2 = self.post_question(user = self.u1) + question2.score = settings.DISCIPLINED_BADGE_MIN_UPVOTES + question2.save() + self.u1.delete_question(question2) + self.assert_have_badge('disciplined', recipient = self.u1, expected_count = 2) def test_peer_pressure_badge(self): question = self.post_question(user = self.u1) @@ -59,18 +108,102 @@ class BadgeTests(AskbotTestCase): answer.score = -1*settings.PEER_PRESSURE_BADGE_MIN_DOWNVOTES answer.save() self.u1.delete_answer(answer) - self.assert_have_badge('peer-pressure') + self.assert_have_badge('peer-pressure', recipient = self.u1) def test_teacher_badge(self): - self.assert_voted_answer_badge_works( + self.assert_upvoted_answer_badge_works( badge_key = 'teacher', min_score = settings.TEACHER_BADGE_MIN_UPVOTES, multiple = False ) def test_nice_answer_badge(self): - self.assert_voted_answer_badge_works( + self.assert_upvoted_answer_badge_works( badge_key = 'nice-answer', min_score = settings.NICE_ANSWER_BADGE_MIN_UPVOTES, multiple = True ) + + def test_nice_question_badge(self): + self.assert_upvoted_question_badge_works( + badge_key = 'nice-question', + min_score = settings.NICE_QUESTION_BADGE_MIN_UPVOTES, + multiple = True + ) + + def test_popular_question_badge(self): + question = self.post_question(user = self.u1) + min_views = settings.POPULAR_QUESTION_BADGE_MIN_VIEWS + question.view_count = min_views - 1 + question.save() + + #patch not_a_robot_request to return True + from askbot.utils import functions + functions.not_a_robot_request = lambda v: True + + url = question.get_absolute_url() + + self.client.login(method='force', user_id = self.u2.id) + self.client.get(url) + self.assert_have_badge('popular-question', recipient = self.u1) + + self.client.login(method='force', user_id = self.u3.id) + self.client.get(url) + self.assert_have_badge('popular-question', recipient = self.u1, expected_count = 1) + + question2 = self.post_question(user = self.u1) + question2.view_count = min_views - 1 + question2.save() + self.client.login(method='force', user_id = self.u2.id) + self.client.get(question2.get_absolute_url()) + self.assert_have_badge('popular-question', recipient = self.u1, expected_count = 2) + + def test_student_badge(self): + question = self.post_question(user = self.u1) + self.u2.upvote(question) + self.assert_have_badge('student', recipient = self.u1) + self.u3.upvote(question) + self.assert_have_badge('student', recipient = self.u1, expected_count = 1) + + question2 = self.post_question(user = self.u1) + self.u2.upvote(question) + self.assert_have_badge('student', recipient = self.u1, expected_count = 1) + + def test_supporter_badge(self): + question = self.post_question(user = self.u1) + self.u2.upvote(question) + self.assert_have_badge('supporter', recipient = self.u2) + + answer = self.post_answer(user = self.u1, question = question) + self.u3.upvote(answer) + self.assert_have_badge('supporter', recipient = self.u3) + self.u2.upvote(answer) + self.assert_have_badge('supporter', recipient = self.u2, expected_count = 1) + + def test_critic_badge(self): + question = self.post_question(user = self.u1) + self.u2.downvote(question) + self.assert_have_badge('critic', recipient = self.u2) + + answer = self.post_answer(user = self.u1, question = question) + self.u3.downvote(answer) + self.assert_have_badge('critic', recipient = self.u3) + self.u2.downvote(answer) + self.assert_have_badge('critic', recipient = self.u2, expected_count = 1) + + def test_self_learner_badge(self): + question = self.post_question(user = self.u1) + answer = self.post_answer(user = self.u1, question = question) + min_votes = settings.SELF_LEARNER_BADGE_MIN_UPVOTES + answer.score = min_votes - 1 + answer.save() + self.u2.upvote(answer) + self.assert_have_badge('self-learner', recipient = self.u1) + + #copy-paste of the first question, except expect second badge + question = self.post_question(user = self.u1) + answer = self.post_answer(user = self.u1, question = question) + answer.score = min_votes - 1 + answer.save() + self.u2.upvote(answer) + self.assert_have_badge('self-learner', recipient = self.u1, expected_count = 2) diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 92228912..9301ba60 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -32,6 +32,7 @@ from askbot.utils.html import sanitize_html from askbot.utils.diff import textDiff as htmldiff from askbot.forms import AdvancedSearchForm, AnswerForm from askbot import models +from askbot.models.badges import award_badges_signal from askbot import const from askbot import auth from askbot.utils import markup @@ -481,7 +482,7 @@ def question(request, id):#refactor - long subroutine. display question body, an if 'question_view_times' not in request.session: request.session['question_view_times'] = {} - last_seen = request.session['question_view_times'].get(question.id,None) + last_seen = request.session['question_view_times'].get(question.id, None) updated_when, updated_who = question.get_last_update_info() if updated_who != request.user: @@ -493,6 +494,7 @@ def question(request, id):#refactor - long subroutine. display question body, an request.session['question_view_times'][question.id] = \ datetime.datetime.now() + if update_view_count: question.view_count += 1 question.save() @@ -502,6 +504,14 @@ def question(request, id):#refactor - long subroutine. display question body, an #get response notifications request.user.visit_question(question) + #3) send award badges signal for any badges + #that are awarded for question views + award_badges_signal.send(None, + event = 'view_question', + actor = request.user, + context_object = question, + ) + paginator_data = { 'is_paginated' : (objects_list.count > ANSWERS_PAGE_SIZE), 'pages': objects_list.num_pages, -- cgit v1.2.3-1-g7c22