964 lines
34 KiB
Python
964 lines
34 KiB
Python
"""This file contains data on badges that is not stored in the database.
|
|
there are no django models in this file.
|
|
This data is static, so there is no point storing it in the db.
|
|
|
|
However, the database does have model BadgeData, that contains
|
|
additional mutable data pertaining to the badges - denormalized award counts
|
|
and lists of recipients.
|
|
|
|
BadgeData django model is located in askbot/models/repute.py
|
|
|
|
Badges in this file are connected with the contents of BadgeData
|
|
via key, determined as a slugified version of badge name.
|
|
|
|
To implement a new badge, one must create a subclass of Badge,
|
|
adde it to BADGES dictionary, register with event in EVENTS_TO_BADGES
|
|
and make sure that a signal `award_badges_signal` is sent with the
|
|
corresponding event name, actor (user object), context_object and optionally
|
|
- timestamp
|
|
"""
|
|
import datetime
|
|
from django.template.defaultfilters import slugify
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils.translation import ugettext as _
|
|
from django.utils.translation import ungettext
|
|
from django.dispatch import Signal
|
|
from askbot.models.post import Post
|
|
from askbot import const
|
|
from askbot.conf import settings as askbot_settings
|
|
from askbot.utils.decorators import auto_now_timestamp
|
|
|
|
class Badge(object):
|
|
"""base class for the badges
|
|
|
|
badges must implement method consider_award
|
|
which returns a boolean True if award succeds
|
|
and False otherwise
|
|
|
|
consider_award assumes that the function is called
|
|
upon correct event, i.e. it is the responsibility of
|
|
the caller to try awarding badges at appropriate times
|
|
"""
|
|
def __init__(self,
|
|
key = '',
|
|
name = '',
|
|
level = None,
|
|
description = None,
|
|
multiple = False):
|
|
|
|
#key - must be an ASCII only word
|
|
self.key = key
|
|
self.name = name
|
|
self.level = level
|
|
self.description = description
|
|
self.multiple = multiple
|
|
self.css_class = const.BADGE_CSS_CLASSES[self.level]
|
|
|
|
def get_stored_data(self):
|
|
from askbot.models.repute import BadgeData
|
|
data, created = BadgeData.objects.get_or_create(slug = self.key)
|
|
return data
|
|
|
|
@property
|
|
def awarded_count(self):
|
|
return self.get_stored_data().awarded_count
|
|
|
|
@property
|
|
def awarded_to(self):
|
|
return self.get_stored_data().awarded_to
|
|
|
|
@property
|
|
def award_badge(self):
|
|
"""related name from `askbot.models.Award`
|
|
the name of this property is confusing, but for now
|
|
left in sync with the name on the `Award` model
|
|
|
|
the goal is that any badge recalled from this
|
|
module would behave just like the instance of BadgeData
|
|
and vice versa
|
|
"""
|
|
return self.get_stored_data().award_badge
|
|
|
|
def get_level_display(self):
|
|
"""display name for the level of the badge"""
|
|
return dict(const.BADGE_TYPE_CHOICES).get(self.level)
|
|
|
|
def award(self, recipient = None, context_object = None, timestamp = None):
|
|
"""do award, the recipient was proven to deserve"""
|
|
from askbot.models.repute import Award
|
|
if self.multiple == False:
|
|
if recipient.badges.filter(slug = self.key).count() != 0:
|
|
return False
|
|
else:
|
|
content_type = ContentType.objects.get_for_model(context_object)
|
|
filters = {
|
|
'user': recipient,
|
|
'object_id': context_object.id,
|
|
'content_type': content_type,
|
|
'badge__slug': self.key,
|
|
}
|
|
#multiple badge is not re-awarded for the same post
|
|
if Award.objects.filter(**filters).count() != 0:
|
|
return False
|
|
|
|
badge = self.get_stored_data()
|
|
award = Award(
|
|
user = recipient,
|
|
badge = badge,
|
|
awarded_at = timestamp,
|
|
content_object = context_object
|
|
)
|
|
award.save()#note: there are signals that listen to saving the Award
|
|
return True
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
"""Normally this method should be reimplemented
|
|
in subclass, but some badges are awarded without
|
|
checks. Those do no need to override this method
|
|
|
|
actor - user who committed some action, context_object -
|
|
the object related to the award situation, e.g. answer
|
|
"""
|
|
return self.award(actor, context_object, timestamp)
|
|
|
|
class Disciplined(Badge):
|
|
def __init__(self):
|
|
description = _(
|
|
'Deleted own post with %(votes)s or more upvotes'
|
|
) % {'votes': askbot_settings.DISCIPLINED_BADGE_MIN_UPVOTES}
|
|
super(Disciplined, self).__init__(
|
|
key = 'disciplined',
|
|
name = _('Disciplined'),
|
|
description = description,
|
|
level = const.BRONZE_BADGE,
|
|
multiple = True
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
|
|
if context_object.author != actor:
|
|
return False
|
|
if context_object.points>= \
|
|
askbot_settings.DISCIPLINED_BADGE_MIN_UPVOTES:
|
|
return self.award(actor, context_object, timestamp)
|
|
|
|
class PeerPressure(Badge):
|
|
def __init__(self):
|
|
description = _(
|
|
u'Deleted own post with %(votes)s or more downvotes'
|
|
) % {'votes': askbot_settings.PEER_PRESSURE_BADGE_MIN_DOWNVOTES}
|
|
super(PeerPressure, self).__init__(
|
|
key = 'peer-pressure',
|
|
name = _('Peer Pressure'),
|
|
description = description,
|
|
level = const.BRONZE_BADGE,
|
|
multiple = True
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
|
|
if context_object.author != actor:
|
|
return False
|
|
if context_object.points<= \
|
|
-1 * askbot_settings.PEER_PRESSURE_BADGE_MIN_DOWNVOTES:
|
|
return self.award(actor, context_object, timestamp)
|
|
return False
|
|
|
|
class Teacher(Badge):
|
|
def __init__(self):
|
|
description = _(
|
|
'Gave an %(answer_voted_up)s at least %(votes)s times for the first time'
|
|
) % {
|
|
'votes': askbot_settings.TEACHER_BADGE_MIN_UPVOTES,
|
|
'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
|
|
}
|
|
super(Teacher, self).__init__(
|
|
key = 'teacher',
|
|
name = _('Teacher'),
|
|
description = description,
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
if context_object.post_type != 'answer':
|
|
return False
|
|
|
|
if context_object.points >= askbot_settings.TEACHER_BADGE_MIN_UPVOTES:
|
|
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 CivicDuty(Badge):
|
|
"""awarded once after a certain number of votes"""
|
|
def __init__(self):
|
|
min_votes = askbot_settings.CIVIC_DUTY_BADGE_MIN_VOTES
|
|
super(CivicDuty, self).__init__(
|
|
key = 'civic-duty',
|
|
name = _('Civic Duty'),
|
|
description = _('Voted %(num)s times') % {'num': min_votes},
|
|
level = const.SILVER_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
|
|
if actor.votes.count() == askbot_settings.CIVIC_DUTY_BADGE_MIN_VOTES:
|
|
return self.award(actor, context_object, timestamp)
|
|
|
|
class SelfLearner(Badge):
|
|
def __init__(self):
|
|
description = _('%(answered_own_question)s with at least %(num)s up votes') % {
|
|
'num': askbot_settings.SELF_LEARNER_BADGE_MIN_UPVOTES,
|
|
'answered_own_question': askbot_settings.WORDS_ANSWERED_OWN_QUESTION
|
|
}
|
|
super(SelfLearner, self).__init__(
|
|
key='self-learner',
|
|
name=_('Self-Learner'),
|
|
description=description,
|
|
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.thread._question_post()
|
|
answer = context_object
|
|
|
|
if question.author == answer.author and answer.points >= 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 created via __new__() method definitions
|
|
|
|
The subclass has a responsibility to specify properties:
|
|
* min_votes - a value from live settings
|
|
* post_type - string 'question' or 'answer'
|
|
* key, name, description, level and multiple - as intended in the Badge
|
|
"""
|
|
def __init__(self):
|
|
super(QualityPost, self).__init__(
|
|
key = self.key,
|
|
name = self.name,
|
|
description = self.description,
|
|
level = self.level,
|
|
multiple = self.multiple
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
if context_object.post_type not in ('answer', 'question'):
|
|
return False
|
|
if context_object.points >= self.min_votes:
|
|
return self.award(context_object.author, context_object, timestamp)
|
|
return False
|
|
|
|
class NiceAnswer(QualityPost):
|
|
def __new__(cls):
|
|
self = super(NiceAnswer, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_NICE_ANSWER
|
|
self.key = 'nice-answer'
|
|
self.level = const.BRONZE_BADGE
|
|
self.multiple = True
|
|
self.min_votes = askbot_settings.NICE_ANSWER_BADGE_MIN_UPVOTES
|
|
self.description = _('%(answer_voted_up)s %(num)s times') % {
|
|
'num': self.min_votes,
|
|
'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
|
|
}
|
|
self.post_type = 'answer'
|
|
return self
|
|
|
|
class GoodAnswer(QualityPost):
|
|
def __new__(cls):
|
|
self = super(GoodAnswer, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_GOOD_ANSWER
|
|
self.key = 'good-answer'
|
|
self.level = const.SILVER_BADGE
|
|
self.multiple = True
|
|
self.min_votes = askbot_settings.GOOD_ANSWER_BADGE_MIN_UPVOTES
|
|
self.description = _('%(answer_voted_up)s %(num)s times') % {
|
|
'num': self.min_votes,
|
|
'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
|
|
}
|
|
self.post_type = 'answer'
|
|
return self
|
|
|
|
class GreatAnswer(QualityPost):
|
|
def __new__(cls):
|
|
self = super(GreatAnswer, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_GREAT_ANSWER
|
|
self.key = 'great-answer'
|
|
self.level = const.GOLD_BADGE
|
|
self.multiple = True
|
|
self.min_votes = askbot_settings.GREAT_ANSWER_BADGE_MIN_UPVOTES
|
|
self.description = _('%(answer_voted_up)s %(num)s times') % {
|
|
'num': self.min_votes,
|
|
'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
|
|
}
|
|
self.post_type = 'answer'
|
|
return self
|
|
|
|
class NiceQuestion(QualityPost):
|
|
def __new__(cls):
|
|
self = super(NiceQuestion, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_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)s up %(num)s times') % {
|
|
'num': self.min_votes,
|
|
'question_voted_up': askbot_settings.WORDS_QUESTION_VOTED_UP
|
|
}
|
|
self.post_type = 'question'
|
|
return self
|
|
|
|
class GoodQuestion(QualityPost):
|
|
def __new__(cls):
|
|
self = super(GoodQuestion, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_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)s up %(num)s times') % {
|
|
'num': self.min_votes,
|
|
'question_voted_up': askbot_settings.WORDS_QUESTION_VOTED_UP
|
|
}
|
|
self.post_type = 'question'
|
|
return self
|
|
|
|
class GreatQuestion(QualityPost):
|
|
def __new__(cls):
|
|
self = super(GreatQuestion, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_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)s %(num)s times') % {
|
|
'num': self.min_votes,
|
|
'question_voted_up': askbot_settings.WORDS_QUESTION_VOTED_UP
|
|
}
|
|
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)s with at least one up vote') % {
|
|
'asked_first_question': askbot_settings.WORDS_ASKED_FIRST_QUESTION
|
|
}
|
|
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.thread.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 = askbot_settings.WORDS_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)s with %(views)s views') % {
|
|
'views' : self.min_views,
|
|
'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
|
|
}
|
|
return self
|
|
|
|
class NotableQuestion(FrequentedQuestion):
|
|
def __new__(cls):
|
|
self = super(NotableQuestion, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_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)s with %(views)s views') % {
|
|
'views' : self.min_views,
|
|
'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
|
|
}
|
|
return self
|
|
|
|
class FamousQuestion(FrequentedQuestion):
|
|
def __new__(cls):
|
|
self = super(FamousQuestion, cls).__new__(cls)
|
|
self.name = askbot_settings.WORDS_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)s with %(views)s views') % {
|
|
'views' : self.min_views,
|
|
'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
|
|
}
|
|
return self
|
|
|
|
class Scholar(Badge):
|
|
"""scholar badge is awarded to the asker when
|
|
he/she accepts an answer for the first time
|
|
"""
|
|
def __init__(self):
|
|
description = _('%(asked_a_question)s and %(accepted_an_answer)s') % {
|
|
'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION,
|
|
'accepted_an_answer': askbot_settings.WORDS_ACCEPTED_AN_ANSWER
|
|
}
|
|
super(Scholar, self).__init__(
|
|
key = 'scholar',
|
|
name = _('Scholar'),
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False,
|
|
description = description
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
if context_object.post_type != 'answer':
|
|
return False
|
|
answer = context_object
|
|
if answer.thread._question_post().author != actor:
|
|
return False
|
|
return self.award(actor, context_object, timestamp)
|
|
|
|
class VotedAcceptedAnswer(Badge):
|
|
"""superclass for Enlightened and Guru badges
|
|
not awarded directly
|
|
|
|
Subclasses must define __new__ function
|
|
"""
|
|
def __init__(self):
|
|
super(VotedAcceptedAnswer, self).__init__(
|
|
key = self.key,
|
|
name = self.name,
|
|
level = self.level,
|
|
multiple = self.multiple,
|
|
description = self.description
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
if context_object.post_type != 'answer':
|
|
return None
|
|
answer = context_object
|
|
if answer.points >= self.min_votes and answer.accepted():
|
|
return self.award(answer.author, answer, timestamp)
|
|
|
|
class Enlightened(VotedAcceptedAnswer):
|
|
def __new__(cls):
|
|
self = super(Enlightened, cls).__new__(cls)
|
|
self.key = 'enlightened'
|
|
self.name = _('Enlightened')
|
|
self.level = const.SILVER_BADGE
|
|
self.multiple = False
|
|
self.min_votes = askbot_settings.ENLIGHTENED_BADGE_MIN_UPVOTES
|
|
descr = _('%(gave_accepted_answer)s upvoted %(num)s or more times')
|
|
self.description = descr % {
|
|
'num': self.min_votes,
|
|
'gave_accepted_answer': askbot_settings.WORDS_GAVE_ACCEPTED_ANSWER
|
|
}
|
|
return self
|
|
|
|
class Guru(VotedAcceptedAnswer):
|
|
def __new__(cls):
|
|
self = super(Guru, cls).__new__(cls)
|
|
self.key = 'guru'
|
|
self.name = _('Guru')
|
|
self.level = const.GOLD_BADGE
|
|
self.multiple = True
|
|
self.min_votes = askbot_settings.GURU_BADGE_MIN_UPVOTES
|
|
descr = _('%(gave_accepted_answer)s upvoted %(num)s or more times')
|
|
self.description = descr % {
|
|
'num': self.min_votes,
|
|
'gave_accepted_answer': askbot_settings.WORDS_GAVE_ACCEPTED_ANSWER
|
|
}
|
|
return self
|
|
|
|
class Necromancer(Badge):
|
|
def __init__(self):
|
|
days = askbot_settings.NECROMANCER_BADGE_MIN_DELAY
|
|
votes = askbot_settings.NECROMANCER_BADGE_MIN_UPVOTES
|
|
description = _(
|
|
'%(answered_a_question)s more than %(days)s days '
|
|
'later with at least %(votes)s votes'
|
|
) % {
|
|
'days':days,
|
|
'votes':votes,
|
|
'answered_a_question': askbot_settings.WORDS_ANSWERED_A_QUESTION
|
|
}
|
|
super(Necromancer, self).__init__(
|
|
key = 'necromancer',
|
|
name = _('Necromancer'),
|
|
level = const.SILVER_BADGE,
|
|
description = description,
|
|
multiple = True
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
if context_object.post_type != 'answer':
|
|
return False
|
|
answer = context_object
|
|
question = answer.thread._question_post()
|
|
delta = datetime.timedelta(askbot_settings.NECROMANCER_BADGE_MIN_DELAY)
|
|
min_score = askbot_settings.NECROMANCER_BADGE_MIN_UPVOTES
|
|
if answer.added_at - question.added_at >= delta \
|
|
and answer.points >= min_score:
|
|
return self.award(answer.author, answer, timestamp)
|
|
return False
|
|
|
|
class CitizenPatrol(Badge):
|
|
def __init__(self):
|
|
super(CitizenPatrol, self).__init__(
|
|
key = 'citizen-patrol',
|
|
name = _('Citizen Patrol'),
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False,
|
|
description = _('First flagged post')
|
|
)
|
|
|
|
class Cleanup(Badge):
|
|
"""This badge is inactive right now.
|
|
to make it live we need to be able to either
|
|
detect "undo" actions or rewrite the view
|
|
correspondingly
|
|
"""
|
|
def __init__(self):
|
|
super(Cleanup, self).__init__(
|
|
key = 'cleanup',
|
|
name = _('Cleanup'),
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False,
|
|
description = _('First rollback')
|
|
)
|
|
|
|
class Pundit(Badge):
|
|
"""Inactive until it is possible to vote
|
|
for comments.
|
|
Pundit is someone who makes good comments.
|
|
"""
|
|
def __init__(self):
|
|
super(Pundit, self).__init__(
|
|
key = 'pundit',
|
|
name = _('Pundit'),
|
|
level = const.SILVER_BADGE,
|
|
multiple = False,
|
|
description = _('Left 10 comments with score of 10 or more')
|
|
)
|
|
|
|
class EditorTypeBadge(Badge):
|
|
"""subclassing badges are types of editors
|
|
must provide usual parameters + min_edits
|
|
via __new__ function
|
|
"""
|
|
def __init__(self):
|
|
super(EditorTypeBadge, self).__init__(
|
|
key = self.key,
|
|
name = self.name,
|
|
level = self.level,
|
|
multiple = False,
|
|
description = self.description
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
|
|
atypes = (
|
|
const.TYPE_ACTIVITY_UPDATE_QUESTION,
|
|
const.TYPE_ACTIVITY_UPDATE_ANSWER
|
|
)
|
|
filters = {'user': actor, 'activity_type__in': atypes}
|
|
from askbot.models.user import Activity
|
|
if Activity.objects.filter(**filters).count() == self.min_edits:
|
|
return self.award(actor, context_object, timestamp)
|
|
|
|
class Editor(EditorTypeBadge):
|
|
def __new__(cls):
|
|
self = super(Editor, cls).__new__(cls)
|
|
self.key = 'editor'
|
|
self.name = _('Editor')
|
|
self.level = const.BRONZE_BADGE
|
|
self.multiple = False
|
|
self.description = _('First edit')
|
|
self.min_edits = 1
|
|
return self
|
|
|
|
class AssociateEditor(EditorTypeBadge):
|
|
def __new__(cls):
|
|
self = super(AssociateEditor, cls).__new__(cls)
|
|
self.key = 'strunk-and-white'#legacy copycat name from stackoverflow
|
|
self.name = _('Associate Editor')
|
|
self.level = const.SILVER_BADGE
|
|
self.multiple = False
|
|
self.min_edits = askbot_settings.ASSOCIATE_EDITOR_BADGE_MIN_EDITS
|
|
self.description = _('Edited %(num)s entries') % {'num': self.min_edits}
|
|
return self
|
|
|
|
class Organizer(Badge):
|
|
def __init__(self):
|
|
super(Organizer, self).__init__(
|
|
key = 'organizer',
|
|
name = _('Organizer'),
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False,
|
|
description = _('First retag')
|
|
)
|
|
|
|
class Autobiographer(Badge):
|
|
def __init__(self):
|
|
super(Autobiographer, self).__init__(
|
|
key = 'autobiographer',
|
|
name = _('Autobiographer'),
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False,
|
|
description = _('Completed all user profile fields')
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
user = context_object
|
|
if user.email and user.real_name and user.website \
|
|
and user.location and user.about:
|
|
return self.award(user, user, timestamp)
|
|
return False
|
|
|
|
class FavoriteTypeBadge(Badge):
|
|
"""subclass must use __new__ and in addition
|
|
must provide min_stars property for the badge
|
|
"""
|
|
def __init__(self):
|
|
description = _(
|
|
'%(asked_a_question)s with %(num)s followers'
|
|
) % {
|
|
'num': self.min_stars,
|
|
'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
|
|
}
|
|
super(FavoriteTypeBadge, self).__init__(
|
|
key=self.key,
|
|
name=self.name,
|
|
level=self.level,
|
|
multiple=True,
|
|
description=description
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
question = context_object
|
|
#model FavoriteQuestion imported under alias of Fave
|
|
from askbot.models.question import FavoriteQuestion as Fave#name collision
|
|
count = Fave.objects.filter(
|
|
thread = question.thread
|
|
).exclude(
|
|
user = question.author
|
|
).count()
|
|
if count == self.min_stars:
|
|
return self.award(question.author, question, timestamp)
|
|
return False
|
|
|
|
class StellarQuestion(FavoriteTypeBadge):
|
|
def __new__(cls):
|
|
self = super(StellarQuestion, cls).__new__(cls)
|
|
self.key = 'stellar-question'
|
|
self.name = askbot_settings.WORDS_STELLAR_QUESTION
|
|
self.level = const.GOLD_BADGE
|
|
self.min_stars = askbot_settings.STELLAR_QUESTION_BADGE_MIN_STARS
|
|
return self
|
|
|
|
class FavoriteQuestion(FavoriteTypeBadge):
|
|
def __new__(cls):
|
|
self = super(FavoriteQuestion, cls).__new__(cls)
|
|
self.key = 'favorite-question'
|
|
self.name = askbot_settings.WORDS_FAVORITE_QUESTION
|
|
self.level = const.SILVER_BADGE
|
|
self.min_stars = askbot_settings.FAVORITE_QUESTION_BADGE_MIN_STARS
|
|
return self
|
|
|
|
class Enthusiast(Badge):
|
|
"""Awarded to a user who visits the site
|
|
for a certain number of days in a row
|
|
"""
|
|
def __init__(self):
|
|
super(Enthusiast, self).__init__(
|
|
key = 'enthusiast',
|
|
name = _('Enthusiast'),
|
|
level = const.SILVER_BADGE,
|
|
multiple = False,
|
|
description = _(
|
|
'Visited site every day for %(num)s days in a row'
|
|
) % {'num': askbot_settings.ENTHUSIAST_BADGE_MIN_DAYS}
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
min_days = askbot_settings.ENTHUSIAST_BADGE_MIN_DAYS
|
|
if actor.consecutive_days_visit_count == min_days:
|
|
return self.award(actor, context_object, timestamp)
|
|
return False
|
|
|
|
class Commentator(Badge):
|
|
"""Commentator is a bronze badge that is
|
|
awarded once when user posts a certain number of
|
|
comments"""
|
|
def __init__(self):
|
|
super(Commentator, self).__init__(
|
|
key = 'commentator',
|
|
name = _('Commentator'),
|
|
level = const.BRONZE_BADGE,
|
|
multiple = False,
|
|
description = _(
|
|
'Posted %(num_comments)s comments'
|
|
) % {'num_comments': askbot_settings.COMMENTATOR_BADGE_MIN_COMMENTS}
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
num_comments = Post.objects.get_comments().filter(author=actor).count()
|
|
if num_comments >= askbot_settings.COMMENTATOR_BADGE_MIN_COMMENTS:
|
|
return self.award(actor, context_object, timestamp)
|
|
return False
|
|
|
|
class Taxonomist(Badge):
|
|
"""Stub badge"""
|
|
def __init__(self):
|
|
super(Taxonomist, self).__init__(
|
|
key = 'taxonomist',
|
|
name = _('Taxonomist'),
|
|
level = const.SILVER_BADGE,
|
|
multiple = False,
|
|
description = ungettext(
|
|
'Created a tag used %(num)s time',
|
|
'Created a tag used %(num)s times',
|
|
askbot_settings.TAXONOMIST_BADGE_MIN_USE_COUNT
|
|
) % {'num': askbot_settings.TAXONOMIST_BADGE_MIN_USE_COUNT}
|
|
)
|
|
|
|
def consider_award(self, actor = None,
|
|
context_object = None, timestamp = None):
|
|
|
|
tag = context_object
|
|
taxonomist_threshold = askbot_settings.TAXONOMIST_BADGE_MIN_USE_COUNT
|
|
if tag.used_count == taxonomist_threshold:
|
|
return self.award(tag.created_by, tag, timestamp)
|
|
return False
|
|
|
|
class Expert(Badge):
|
|
"""Stub badge"""
|
|
def __init__(self):
|
|
super(Expert, self).__init__(
|
|
key = 'expert',
|
|
name = _('Expert'),
|
|
level = const.SILVER_BADGE,
|
|
multiple = False,
|
|
description = _('Very active in one tag')
|
|
)
|
|
|
|
ORIGINAL_DATA = """
|
|
|
|
extra badges from stackexchange
|
|
* commentator - left n comments (single)
|
|
* enthusiast, fanatic - visited site n days in a row (s)
|
|
* epic, legendary - hit daily reputation cap on n days (s)
|
|
* mortarboard - hit the daily reputation cap for the first time (s)
|
|
* populist - provided an answer that outscored an accepted answer two-fold or by n points, whichever is higher (m)
|
|
* reversal - provided an answer with +n points to a question of -m points
|
|
(_('Yearling'), 2, _('yearling'), _('Active member for a year'), False, 0),
|
|
|
|
|
|
(_('Generalist'), 2, _('generalist'), _('Active in many different tags'), False, 0),
|
|
(_('Beta'), 2, _('beta'), _('Actively participated in the private beta'), False, 0),
|
|
(_('Alpha'), 2, _('alpha'), _('Actively participated in the private alpha'), False, 0),
|
|
"""
|
|
|
|
BADGES = {
|
|
#'strunk-and-white': AssociateEditor,#legacy slug name
|
|
'autobiographer': Autobiographer,
|
|
'cleanup': Cleanup,
|
|
#'citizen-patrol': CitizenPatrol,
|
|
'civic-duty': CivicDuty,
|
|
#'commentator': Commentator,
|
|
'critic': Critic,
|
|
#'disciplined': Disciplined,
|
|
'editor': Editor,
|
|
#'enlightened': Enlightened,
|
|
#'enthusiast': Enthusiast,
|
|
'expert': Expert,
|
|
'famous-question': FamousQuestion,
|
|
'favorite-question': FavoriteQuestion,
|
|
'good-answer': GoodAnswer,
|
|
'good-question': GoodQuestion,
|
|
'great-answer': GreatAnswer,
|
|
'great-question': GreatQuestion,
|
|
'guru': Guru,
|
|
'necromancer': Necromancer,
|
|
#'nice-answer': NiceAnswer,
|
|
#'nice-question': NiceQuestion,
|
|
#'notable-question': NotableQuestion,
|
|
'organizer': Organizer,
|
|
#'peer-pressure': PeerPressure,
|
|
'popular-question': PopularQuestion,
|
|
'pundit': Pundit,
|
|
#'scholar': Scholar,
|
|
'self-learner': SelfLearner,
|
|
#'stellar-question': StellarQuestion,
|
|
#'student': Student,
|
|
'supporter': Supporter,
|
|
'teacher': Teacher,
|
|
'taxonomist': Taxonomist,
|
|
}
|
|
|
|
#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 = {
|
|
'accept_best_answer': (Guru,),
|
|
'delete_post': (),
|
|
'downvote': (Critic, CivicDuty),#no regard for question or answer for now
|
|
'edit_answer': (Editor, AssociateEditor),
|
|
'edit_question': (Editor, AssociateEditor),
|
|
'flag_post': (),
|
|
'post_answer': (Necromancer,),
|
|
'post_comment': (),
|
|
'retag_question': (Organizer,),
|
|
'select_favorite_question': (FavoriteQuestion, ),
|
|
'site_visit': (),
|
|
'update_tag': (Taxonomist,),
|
|
'update_user_profile': (Autobiographer,),
|
|
'upvote_answer': (
|
|
Teacher, GoodAnswer,
|
|
GreatAnswer, Supporter, SelfLearner, CivicDuty,
|
|
Guru, Necromancer
|
|
),
|
|
'upvote_question': (
|
|
GoodQuestion,
|
|
GreatQuestion, Supporter, CivicDuty
|
|
),
|
|
'upvote_comment':(),#todo - add some badges here
|
|
'view_question': (PopularQuestion, FamousQuestion,),
|
|
}
|
|
|
|
def get_badge(name = None):
|
|
"""Get badge object by name, if none matches the name
|
|
raise KeyError
|
|
"""
|
|
key = slugify(name)
|
|
return BADGES[key]()
|
|
|
|
def init_badges():
|
|
"""Calling this function will set up badge record
|
|
int the database for each badge enumerated in the
|
|
`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()
|
|
#remove any badges from the database
|
|
#that are no longer in the BADGES dictionary
|
|
from askbot.models.repute import BadgeData
|
|
BadgeData.objects.exclude(
|
|
slug__in = map(slugify, BADGES.keys())
|
|
).delete()
|
|
|
|
award_badges_signal = Signal(
|
|
providing_args=[
|
|
'actor', 'event', 'context_object', 'timestamp'
|
|
]
|
|
)
|
|
#actor - user who triggers the event
|
|
#event - string name of the event, e.g 'downvote'
|
|
#context_object - database object related to the event, e.g. question
|
|
|
|
@auto_now_timestamp
|
|
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_instance = badge()
|
|
badge_instance.consider_award(actor, context_object, timestamp)
|
|
|
|
award_badges_signal.connect(award_badges)
|