зеркало из https://github.com/mozilla/kitsune.git
A start to karma implementation with redis backend.
* Karma actions defined for answer, first answer, solution, helpful vote * Behind 'karma' waffle switch * /admin/karma page shows top contributors, allows user key lookup, and initializing karma. * REDIS_TEST_BACKENDS settings used for tests * SkipTest tests that depend on redis when the test backend(s) arent defined * Added redis-test.conf
This commit is contained in:
Родитель
03d14c012d
Коммит
44e96f0de4
|
@ -0,0 +1,189 @@
|
|||
from datetime import date, datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from celery.decorators import task
|
||||
import waffle
|
||||
|
||||
from sumo.utils import redis_client
|
||||
|
||||
|
||||
KEY_PREFIX = 'karma' # Prefix for the Redis keys used.
|
||||
|
||||
|
||||
class KarmaAction(object):
|
||||
"""Abstract base class for karma actions."""
|
||||
action_type = None # For example 'first-answer'.
|
||||
points = 0 # Number of points the action is worth.
|
||||
|
||||
def __init__(self, user, day=date.today(), redis=None):
|
||||
if not waffle.switch_is_active('karma'):
|
||||
return
|
||||
if isinstance(user, User):
|
||||
self.userid = user.id
|
||||
else:
|
||||
self.userid = user
|
||||
if isinstance(day, datetime): # Gracefully handle a datetime.
|
||||
self.date = day.date()
|
||||
else:
|
||||
self.date = day
|
||||
if not redis:
|
||||
self.redis = redis_client(name='karma')
|
||||
else:
|
||||
self.redis = redis
|
||||
|
||||
def save(self):
|
||||
"""Save the action information to redis."""
|
||||
if waffle.switch_is_active('karma'):
|
||||
self._save.delay(self)
|
||||
|
||||
@task
|
||||
def _save(self):
|
||||
key = hash_key(self.userid)
|
||||
|
||||
# Point counters:
|
||||
# Increment total points
|
||||
self.redis.hincrby(key, 'points:total', self.points)
|
||||
# Increment points daily count
|
||||
self.redis.hincrby(key, 'points:{d}'.format(
|
||||
d=self.date), self.points)
|
||||
# Increment points monthly count
|
||||
self.redis.hincrby(key, 'points:{y}-{m:02d}'.format(
|
||||
y=self.date.year, m=self.date.month), self.points)
|
||||
# Increment points yearly count
|
||||
self.redis.hincrby(key, 'points:{y}'.format(
|
||||
y=self.date.year), self.points)
|
||||
|
||||
# Action counters:
|
||||
# Increment action total count
|
||||
self.redis.hincrby(key, '{t}:total'.format(t=self.action_type), 1)
|
||||
# Increment action daily count
|
||||
self.redis.hincrby(key, '{t}:{d}'.format(
|
||||
t=self.action_type, d=self.date), 1)
|
||||
# Increment action monthly count
|
||||
self.redis.hincrby(key, '{t}:{y}-{m:02d}'.format(
|
||||
t=self.action_type, y=self.date.year, m=self.date.month), 1)
|
||||
# Increment action yearly count
|
||||
self.redis.hincrby(key, '{t}:{y}'.format(
|
||||
t=self.action_type, y=self.date.year), 1)
|
||||
|
||||
|
||||
# TODO: move this to it's own file?
|
||||
class KarmaManager(object):
|
||||
"""Manager for querying karma data in Redis."""
|
||||
def __init__(self):
|
||||
self.redis = redis_client(name='karma')
|
||||
|
||||
# Updaters:
|
||||
def update_top_alltime(self):
|
||||
"""Updated the top contributors alltime sorted set."""
|
||||
key = '{p}:points:total'.format(p=KEY_PREFIX)
|
||||
# TODO: Maintain a user id list in Redis?
|
||||
for userid in User.objects.values_list('id', flat=True):
|
||||
pts = self.total_points(userid)
|
||||
if pts:
|
||||
self.redis.zadd(key, userid, pts)
|
||||
|
||||
def update_top_week(self):
|
||||
"""Updated the top contributors past week sorted set."""
|
||||
key = '{p}:points:week'.format(p=KEY_PREFIX)
|
||||
for userid in User.objects.values_list('id', flat=True):
|
||||
pts = self.week_points(userid)
|
||||
if pts:
|
||||
self.redis.zadd(key, userid, pts)
|
||||
|
||||
# def update_trending...
|
||||
|
||||
# Getters:
|
||||
def top_alltime(self, count=10):
|
||||
"""Returns the top users based on alltime points."""
|
||||
return self._top_points(count, 'total')
|
||||
|
||||
def top_week(self, count=10):
|
||||
"""Returns the top users based on points in the last 7 days."""
|
||||
return self._top_points(count, 'week')
|
||||
|
||||
def _top_points(self, count, suffix):
|
||||
ids = self.redis.zrevrange('{p}:points:{s}'.format(
|
||||
p=KEY_PREFIX, s=suffix), 0, count - 1)
|
||||
users = list(User.objects.filter(id__in=ids))
|
||||
users.sort(key=lambda user: ids.index(str(user.id)))
|
||||
return users
|
||||
|
||||
def total_points(self, user):
|
||||
"""Returns the total points for a given user."""
|
||||
count = self.redis.hget(hash_key(user), 'points:total')
|
||||
return int(count) if count else 0
|
||||
|
||||
def week_points(self, user):
|
||||
"""Returns total points from the last 7 days for a given user."""
|
||||
today = date.today()
|
||||
days = [today - timedelta(days=d + 1) for d in range(7)]
|
||||
counts = self.redis.hmget(hash_key(user),
|
||||
['points:{d}'.format(d=d) for d in days])
|
||||
fn = lambda x: int(x) if x else 0
|
||||
count = sum([fn(c) for c in counts])
|
||||
return count
|
||||
|
||||
def daily_points(self, user, days_back=30):
|
||||
"""Returns a list of points from the past `days_back` days."""
|
||||
today = date.today()
|
||||
days = [today - timedelta(days=d) for d in range(days_back)]
|
||||
counts = self.redis.hmget(hash_key(user),
|
||||
['points:{d}'.format(d=d) for d in days])
|
||||
fn = lambda x: int(x) if x else 0
|
||||
return [fn(c) for c in counts]
|
||||
|
||||
def monthly_points(self, user, months_back=12):
|
||||
"""Returns a list of points from the past `months_back` months."""
|
||||
# TODO: Tricky?
|
||||
pass
|
||||
|
||||
def total_count(self, action, user):
|
||||
"""Returns the total count of an action for a given user."""
|
||||
count = self.redis.hget(
|
||||
hash_key(user), '{t}:total'.format(t=action.action_type))
|
||||
return int(count) if count else 0
|
||||
|
||||
def day_count(self, action, user, date=date.today()):
|
||||
"""Returns the total count of an action for a given user and day."""
|
||||
count = self.redis.hget(
|
||||
hash_key(user), '{t}:{d}'.format(d=date, t=action.action_type))
|
||||
return int(count) if count else 0
|
||||
|
||||
def week_count(self, action, user):
|
||||
"""Returns total count of an action for a given user (last 7 days)."""
|
||||
# TODO: DRY this up with week_points and daily_points.
|
||||
today = date.today()
|
||||
days = [today - timedelta(days=d + 1) for d in range(7)]
|
||||
counts = self.redis.hmget(hash_key(user), ['{t}:{d}'.format(
|
||||
t=action.action_type, d=d) for d in days])
|
||||
fn = lambda x: int(x) if x else 0
|
||||
count = sum([fn(c) for c in counts])
|
||||
return count
|
||||
|
||||
def month_count(self, action, user, year, month):
|
||||
"""Returns the total count of an action for a given user and month."""
|
||||
count = self.redis.hget(
|
||||
hash_key(user),
|
||||
'{t}:{y}-{m:02d}'.format(t=action.action_type, y=year, m=month))
|
||||
return int(count) if count else 0
|
||||
|
||||
def year_count(self, action, user, year):
|
||||
"""Returns the total count of an action for a given user and year."""
|
||||
count = self.redis.hget(
|
||||
hash_key(user), '{t}:{y}'.format(y=year, t=action.action_type))
|
||||
return int(count) if count else 0
|
||||
|
||||
def user_data(self, user):
|
||||
"""Returns all the data stored for the given user."""
|
||||
return self.redis.hgetall(hash_key(user))
|
||||
|
||||
|
||||
def hash_key(user):
|
||||
"""Returns the hash key for a given user."""
|
||||
if isinstance(user, User):
|
||||
userid = user.id
|
||||
else:
|
||||
userid = user
|
||||
return "{p}:{u}".format(p=KEY_PREFIX, u=userid)
|
|
@ -0,0 +1,71 @@
|
|||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
|
||||
from karma.actions import KarmaManager
|
||||
from karma.tasks import init_karma, update_top_contributors
|
||||
from questions.karma_actions import (AnswerAction, AnswerMarkedHelpfulAction,
|
||||
FirstAnswerAction, SolutionAction)
|
||||
|
||||
|
||||
def karma(request):
|
||||
"""Admin view that displays karma related data."""
|
||||
if request.POST.get('init'):
|
||||
init_karma.delay()
|
||||
messages.add_message(request, messages.SUCCESS,
|
||||
'init_karma task queued!')
|
||||
return HttpResponseRedirect(request.path)
|
||||
|
||||
if request.POST.get('update-top'):
|
||||
update_top_contributors.delay()
|
||||
messages.add_message(request, messages.SUCCESS,
|
||||
'update_top_contributors task queued!')
|
||||
return HttpResponseRedirect(request.path)
|
||||
|
||||
kmgr = KarmaManager()
|
||||
top_alltime = [_user_karma_alltime(u, kmgr) for u in kmgr.top_alltime()]
|
||||
top_week = [_user_karma_week(u, kmgr) for u in kmgr.top_week()]
|
||||
|
||||
username = request.GET.get('username')
|
||||
user_karma = None
|
||||
if username:
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
d = kmgr.user_data(user)
|
||||
user_karma = [{'key': k, 'value': d[k]} for k in sorted(d.keys())]
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
return render_to_response('karma/admin/karma.html',
|
||||
{'title': 'Karma',
|
||||
'top_alltime': top_alltime,
|
||||
'top_week': top_week,
|
||||
'username': username,
|
||||
'user_karma': user_karma},
|
||||
RequestContext(request, {}))
|
||||
|
||||
admin.site.register_view('karma', karma, 'Karma')
|
||||
|
||||
|
||||
def _user_karma_alltime(user, kmgr):
|
||||
return {
|
||||
'user': user,
|
||||
'points': kmgr.total_points(user),
|
||||
'answers': kmgr.total_count(AnswerAction, user),
|
||||
'first_answers': kmgr.total_count(FirstAnswerAction, user),
|
||||
'helpful_votes': kmgr.total_count(AnswerMarkedHelpfulAction, user),
|
||||
'solutions': kmgr.total_count(SolutionAction, user),
|
||||
}
|
||||
|
||||
|
||||
def _user_karma_week(user, kmgr):
|
||||
return {
|
||||
'user': user,
|
||||
'points': kmgr.week_points(user),
|
||||
'answers': kmgr.week_count(AnswerAction, user),
|
||||
'first_answers': kmgr.week_count(FirstAnswerAction, user),
|
||||
'helpful_votes': kmgr.week_count(AnswerMarkedHelpfulAction, user),
|
||||
'solutions': kmgr.week_count(SolutionAction, user),
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import cronjobs
|
||||
import waffle
|
||||
|
||||
from karma.actions import KarmaManager
|
||||
|
||||
|
||||
@cronjobs.register
|
||||
def update_top_contributors():
|
||||
""""Update the top contributor lists"""
|
||||
if not waffle.switch_is_active('karma'):
|
||||
return
|
||||
|
||||
kmgr = KarmaManager()
|
||||
kmgr.update_top_alltime()
|
||||
kmgr.update_top_week()
|
|
@ -0,0 +1 @@
|
|||
# Everything is in REDIS
|
|
@ -0,0 +1,63 @@
|
|||
from celery.decorators import task
|
||||
import waffle
|
||||
|
||||
from karma.actions import redis_client
|
||||
from karma.cron import update_top_contributors as _update_top_contributors
|
||||
from questions.karma_actions import (AnswerAction, AnswerMarkedHelpfulAction,
|
||||
FirstAnswerAction, SolutionAction)
|
||||
from questions.models import Question, AnswerVote
|
||||
from sumo.utils import chunked
|
||||
|
||||
|
||||
@task
|
||||
def init_karma():
|
||||
"""Flushes the karma redis backend and populates with fresh data.
|
||||
|
||||
Goes through all questions/answers/votes and save karma actions for them.
|
||||
"""
|
||||
if not waffle.switch_is_active('karma'):
|
||||
return
|
||||
|
||||
redis_client('karma').flushdb()
|
||||
|
||||
questions = Question.objects.all()
|
||||
for chunk in chunked(questions.values_list('pk', flat=True), 200):
|
||||
_process_question_chunk.apply_async(args=[chunk])
|
||||
|
||||
votes = AnswerVote.objects.filter(helpful=True)
|
||||
for chunk in chunked(votes.values_list('pk', flat=True), 1000):
|
||||
_process_answer_vote_chunk.apply_async(args=[chunk])
|
||||
|
||||
|
||||
@task
|
||||
def update_top_contributors():
|
||||
"""Updates the top contributor sorted sets."""
|
||||
_update_top_contributors()
|
||||
|
||||
|
||||
@task
|
||||
def _process_question_chunk(data, **kwargs):
|
||||
"""Save karma data for a chunk of questions."""
|
||||
redis = redis_client('karma')
|
||||
q_qs = Question.objects.select_related('solution').defer('content')
|
||||
for question in q_qs.filter(pk__in=data):
|
||||
first = True
|
||||
a_qs = question.answers.order_by('created').select_related('creator')
|
||||
for answer in a_qs.values_list('creator', 'created'):
|
||||
AnswerAction(answer[0], answer[1], redis).save()
|
||||
if first:
|
||||
FirstAnswerAction(answer[0], answer[1], redis).save()
|
||||
first = False
|
||||
soln = question.solution
|
||||
if soln:
|
||||
SolutionAction(soln.creator, soln.created, redis).save()
|
||||
|
||||
|
||||
@task
|
||||
def _process_answer_vote_chunk(data, **kwargs):
|
||||
"""Save karma data for a chunk of answer votes."""
|
||||
redis = redis_client('karma')
|
||||
v_qs = AnswerVote.objects.select_related('answer')
|
||||
for vote in v_qs.filter(pk__in=data):
|
||||
AnswerMarkedHelpfulAction(
|
||||
vote.answer.creator_id, vote.created, redis).save()
|
|
@ -0,0 +1,94 @@
|
|||
{% extends "kadmin/base.html" %}
|
||||
{% load waffle_tags %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" media="screen,projection,tv" href="{{ MEDIA_URL }}css/users.autocomplete.css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% switch 'karma' %}
|
||||
{% else %}
|
||||
<p>Karma is currently disabled. Activate waffle switch 'karma' to enable.</p>
|
||||
{% endswitch %}
|
||||
<section>
|
||||
<h1>Top Contributors - All Time</h1>
|
||||
<ol>
|
||||
{% for user in top_alltime %}
|
||||
<li>
|
||||
<a href="#">{{ user.user.username }}</a>:
|
||||
Points: {{ user.points }} |
|
||||
Answers: {{ user.answers }} |
|
||||
First Answers: {{ user.first_answers }} |
|
||||
Solutions: {{ user.solutions }} |
|
||||
Helpful Votes: {{ user.helpful_votes }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h1>Top Contributors - Last 7 Days</h1>
|
||||
<ol>
|
||||
{% for user in top_week %}
|
||||
<li>
|
||||
<a href="#">{{ user.user.username }}</a>:
|
||||
Points: {{ user.points }} |
|
||||
Answers: {{ user.answers }} |
|
||||
First Answers: {{ user.first_answers }} |
|
||||
Solutions: {{ user.solutions }} |
|
||||
Helpful Votes: {{ user.helpful_votes }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="update-top" value="1" />
|
||||
<input type="submit" value="Update Top Contributors" />
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
<h1>User Karma{% if username %}: {{ username }}{% endif %}</h1>
|
||||
<form action="" method="GET">
|
||||
<input type="text" placeholder="username" class="user-autocomplete" name="username" />
|
||||
<input type="submit" value="Show Karma Data" />
|
||||
</form>
|
||||
{% if user_karma %}
|
||||
<table class="redis-info">
|
||||
<tbody>
|
||||
{% for row in user_karma %}
|
||||
<tr>
|
||||
<th>{{ row.key }}</th>
|
||||
<td>{{ row.value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{% if username %}
|
||||
<p>User or karma data not found.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
<section>
|
||||
<h1>Initialize Karma</h1>
|
||||
<p>Warning: This will launch a task to delete all existing karma data from redis and recalculate from the database.</p>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="init" value="1" />
|
||||
<input type="submit" value="Init Karma" />
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/libs/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// Not happy about this... but it works!
|
||||
$('body').data('usernames-api', '/en-US/users/api/usernames');
|
||||
</script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/libs/jquery.autocomplete.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/users.autocomplete.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,66 @@
|
|||
from django.conf import settings
|
||||
from datetime import date
|
||||
import mock
|
||||
from nose import SkipTest
|
||||
from nose.tools import eq_
|
||||
import waffle
|
||||
|
||||
from karma.actions import KarmaAction, KarmaManager, redis_client
|
||||
from sumo.tests import TestCase
|
||||
from users.tests import user
|
||||
|
||||
|
||||
class TestAction1(KarmaAction):
|
||||
"""A test action for testing!"""
|
||||
action_type = 'test-action-1'
|
||||
points = 3
|
||||
|
||||
|
||||
class TestAction2(KarmaAction):
|
||||
"""Another test action for testing!"""
|
||||
action_type = 'test-action-2'
|
||||
points = 7
|
||||
|
||||
|
||||
class KarmaActionTests(TestCase):
|
||||
def setUp(self):
|
||||
super(KarmaActionTests, self).setUp()
|
||||
self.user = user(save=True)
|
||||
try:
|
||||
self.mgr = KarmaManager()
|
||||
redis_client('karma').flushdb()
|
||||
except (KeyError, AttributeError):
|
||||
raise SkipTest
|
||||
|
||||
@mock.patch.object(waffle, 'switch_is_active')
|
||||
def test_action(self, switch_is_active):
|
||||
"""Save an action and verify."""
|
||||
switch_is_active.return_value = True
|
||||
TestAction1(user=self.user).save()
|
||||
eq_(3, self.mgr.total_points(self.user))
|
||||
eq_(1, self.mgr.total_count(TestAction1, self.user))
|
||||
today = date.today()
|
||||
eq_(1, self.mgr.day_count(TestAction1, self.user, today))
|
||||
eq_(1, self.mgr.month_count(TestAction1, self.user, today.year,
|
||||
today.month))
|
||||
eq_(1, self.mgr.year_count(TestAction1, self.user, today.year))
|
||||
|
||||
@mock.patch.object(waffle, 'switch_is_active')
|
||||
def test_two_actions(self, switch_is_active):
|
||||
"""Save two actions, one twice, and verify."""
|
||||
switch_is_active.return_value = True
|
||||
TestAction1(user=self.user).save()
|
||||
TestAction2(user=self.user).save()
|
||||
TestAction2(user=self.user).save()
|
||||
eq_(17, self.mgr.total_points(self.user))
|
||||
eq_(1, self.mgr.total_count(TestAction1, self.user))
|
||||
eq_(2, self.mgr.total_count(TestAction2, self.user))
|
||||
today = date.today()
|
||||
eq_(1, self.mgr.day_count(TestAction1, self.user, today))
|
||||
eq_(1, self.mgr.month_count(TestAction1, self.user, today.year,
|
||||
today.month))
|
||||
eq_(1, self.mgr.year_count(TestAction1, self.user, today.year))
|
||||
eq_(2, self.mgr.day_count(TestAction2, self.user, today))
|
||||
eq_(2, self.mgr.month_count(TestAction2, self.user, today.year,
|
||||
today.month))
|
||||
eq_(2, self.mgr.year_count(TestAction2, self.user, today.year))
|
|
@ -25,6 +25,7 @@ def update_weekly_votes():
|
|||
update_question_vote_chunk.apply_async(args=[chunk])
|
||||
|
||||
|
||||
# TODO: remove this and use the karma top list.
|
||||
@cronjobs.register
|
||||
def cache_top_contributors():
|
||||
"""Compute the top contributors and store in cache."""
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
from karma.actions import KarmaAction
|
||||
|
||||
|
||||
class AnswerAction(KarmaAction):
|
||||
"""The user posted an answer."""
|
||||
action_type = 'answer'
|
||||
points = 1
|
||||
|
||||
|
||||
class FirstAnswerAction(KarmaAction):
|
||||
"""The user posted the first answer to a question."""
|
||||
action_type = 'first-answer'
|
||||
points = 5
|
||||
|
||||
|
||||
class AnswerMarkedHelpfulAction(KarmaAction):
|
||||
"""The user's answer was voted as helpful."""
|
||||
action_type = 'helpful-answer'
|
||||
points = 10
|
||||
|
||||
|
||||
class SolutionAction(KarmaAction):
|
||||
"""The user's answer was marked as the solution."""
|
||||
action_type = 'solution'
|
||||
points = 25
|
|
@ -8,6 +8,7 @@ from statsd import statsd
|
|||
|
||||
from activity.models import Action
|
||||
from questions import ANSWERS_PER_PAGE
|
||||
from questions.karma_actions import AnswerAction, FirstAnswerAction
|
||||
|
||||
|
||||
log = logging.getLogger('k.task')
|
||||
|
@ -79,3 +80,8 @@ def log_answer(answer):
|
|||
|
||||
transaction.commit_unless_managed()
|
||||
unpin_this_thread()
|
||||
|
||||
# Record karma actions
|
||||
AnswerAction(answer.creator, answer.created.date()).save()
|
||||
if answer == answer.question.answers.order_by('created')[0]:
|
||||
FirstAnswerAction(answer.creator, answer.created.date()).save()
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import mock
|
||||
|
||||
from questions.karma_actions import (AnswerAction, AnswerMarkedHelpfulAction,
|
||||
FirstAnswerAction, SolutionAction)
|
||||
from questions.models import Question, Answer
|
||||
from questions.tests import TestCaseBase
|
||||
from sumo.tests import post
|
||||
from users.tests import user
|
||||
|
||||
|
||||
class KarmaTests(TestCaseBase):
|
||||
"""Tests for karma actions."""
|
||||
def setUp(self):
|
||||
super(KarmaTests, self).setUp()
|
||||
self.user = user(save=True)
|
||||
|
||||
@mock.patch.object(AnswerAction, 'save')
|
||||
@mock.patch.object(FirstAnswerAction, 'save')
|
||||
def test_new_answer(self, first, answer):
|
||||
question = Question.objects.all()[0]
|
||||
Answer.objects.create(question=question, creator=self.user)
|
||||
assert answer.called
|
||||
assert not first.called
|
||||
|
||||
@mock.patch.object(AnswerAction, 'save')
|
||||
@mock.patch.object(FirstAnswerAction, 'save')
|
||||
def test_first_answer(self, first, answer):
|
||||
question = Question.objects.all()[1]
|
||||
Answer.objects.create(question=question, creator=self.user)
|
||||
assert answer.called
|
||||
assert first.called
|
||||
|
||||
@mock.patch.object(SolutionAction, 'save')
|
||||
def test_solution(self, save):
|
||||
answer = Answer.objects.get(pk=1)
|
||||
question = answer.question
|
||||
self.client.login(username='jsocol', password='testpass')
|
||||
post(self.client, 'questions.solve', args=[question.id, answer.id])
|
||||
assert save.called
|
||||
|
||||
@mock.patch.object(AnswerMarkedHelpfulAction, 'save')
|
||||
def test_helpful_vote(self, save):
|
||||
answer = Answer.objects.get(pk=1)
|
||||
question = answer.question
|
||||
post(self.client, 'questions.answer_vote', {'helpful': True},
|
||||
args=[question.id, answer.id])
|
||||
assert save.called
|
|
@ -35,6 +35,7 @@ from questions.events import QuestionReplyEvent, QuestionSolvedEvent
|
|||
from questions.feeds import QuestionsFeed, AnswersFeed, TaggedQuestionsFeed
|
||||
from questions.forms import (NewQuestionForm, EditQuestionForm, AnswerForm,
|
||||
WatchQuestionForm, FREQUENCY_CHOICES)
|
||||
from questions.karma_actions import SolutionAction, AnswerMarkedHelpfulAction
|
||||
from questions.models import Question, Answer, QuestionVote, AnswerVote
|
||||
from questions.question_config import products
|
||||
from search.clients import WikiClient, QuestionsClient, SearchError
|
||||
|
@ -389,7 +390,7 @@ def solve(request, question_id, answer_id):
|
|||
question.save()
|
||||
statsd.incr('questions.solution')
|
||||
QuestionSolvedEvent(answer).fire(exclude=question.creator)
|
||||
|
||||
SolutionAction(answer.creator).save()
|
||||
messages.add_message(request, messages.SUCCESS,
|
||||
_('Thank you for choosing a solution!'))
|
||||
|
||||
|
@ -460,6 +461,7 @@ def answer_vote(request, question_id, answer_id):
|
|||
|
||||
if 'helpful' in request.POST:
|
||||
vote.helpful = True
|
||||
AnswerMarkedHelpfulAction(answer.creator).save()
|
||||
message = _('Glad to hear it!')
|
||||
else:
|
||||
message = _('Sorry to hear that.')
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.conf import settings
|
|||
from django.test.client import Client
|
||||
|
||||
from nose.tools import eq_
|
||||
from test_utils import TestCase # So others can import it from here
|
||||
import test_utils
|
||||
|
||||
import sumo
|
||||
from sumo.urlresolvers import reverse, split_path
|
||||
|
@ -63,6 +63,12 @@ class LocalizingClient(Client):
|
|||
# prepending in a one-off case or do it outside a mock request.
|
||||
|
||||
|
||||
class TestCase(test_utils.TestCase):
|
||||
def setUp(self):
|
||||
super(TestCase, self).setUp()
|
||||
settings.REDIS_BACKENDS = settings.REDIS_TEST_BACKENDS
|
||||
|
||||
|
||||
class MigrationTests(TestCase):
|
||||
"""Sanity checks for the SQL migration scripts"""
|
||||
|
||||
|
@ -114,6 +120,7 @@ class MigrationTests(TestCase):
|
|||
class MobileTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(MobileTestCase, self).setUp()
|
||||
self.client.cookies[settings.MOBILE_COOKIE] = 'on'
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
daemonize yes
|
||||
pidfile /var/run/redis/redis-sumo-test.pid
|
||||
port 6383
|
||||
timeout 300
|
||||
loglevel verbose
|
||||
logfile stdout
|
||||
databases 4
|
||||
rdbcompression yes
|
||||
dbfilename /var/redis/sumo-test/dump.rdb
|
||||
dir /var/redis/sumo-test/
|
||||
maxmemory 15032385536
|
||||
maxmemory-policy allkeys-lru
|
||||
appendonly no
|
||||
appendfsync everysec
|
||||
vm-enabled no
|
||||
vm-swap-file /tmp/redis-sumo-test.swap
|
||||
vm-max-memory 0
|
||||
vm-page-size 32
|
||||
vm-pages 134217728
|
||||
vm-max-threads 4
|
||||
hash-max-zipmap-entries 64
|
||||
hash-max-zipmap-value 512
|
||||
activerehashing yes
|
|
@ -22,3 +22,8 @@ h2 {
|
|||
#settings tr:hover th a {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#content section {
|
||||
border-bottom: solid 2px #f0f0f0;
|
||||
padding: 10px 0;
|
||||
}
|
|
@ -69,6 +69,7 @@ HOME = /tmp
|
|||
# Once per day.
|
||||
0 16 * * * $CRON reload_wiki_traffic_stats
|
||||
40 1 * * * $CRON update_weekly_votes
|
||||
0 42 * * * $CRON update_top_contributors
|
||||
|
||||
# Twice per week.
|
||||
#05 01 * * 1,4 $CRON update_weekly_votes
|
||||
|
|
|
@ -26,6 +26,7 @@ HOME = /tmp
|
|||
# Once per day.
|
||||
0 16 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron reload_wiki_traffic_stats
|
||||
40 1 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||
0 42 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
||||
|
||||
# Twice per week.
|
||||
#05 01 * * 1,4 cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||
|
|
|
@ -26,6 +26,7 @@ HOME = /tmp
|
|||
# Once per day.
|
||||
0 16 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron reload_wiki_traffic_stats
|
||||
40 1 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||
0 42 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
||||
|
||||
# Twice per week.
|
||||
#05 01 * * 1,4 cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||
|
|
|
@ -26,6 +26,7 @@ HOME = /tmp
|
|||
# Once per day.
|
||||
0 16 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron reload_wiki_traffic_stats
|
||||
40 1 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||
0 42 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_top_contributors
|
||||
|
||||
# Twice per week.
|
||||
#05 01 * * 1,4 cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes
|
||||
|
|
|
@ -251,6 +251,7 @@ INSTALLED_APPS = (
|
|||
'messages',
|
||||
'commonware.response.cookies',
|
||||
'groups',
|
||||
'karma',
|
||||
|
||||
# Extra apps for testing.
|
||||
'django_nose',
|
||||
|
@ -690,3 +691,9 @@ REDIS_BACKENDS = {
|
|||
#'default': 'redis://localhost:6379?socket_timeout=0.5&db=0',
|
||||
#'karma': 'redis://localhost:6381?socket_timeout=0.5&db=0',
|
||||
}
|
||||
|
||||
# Redis backends used for testing.
|
||||
REDIS_TEST_BACKENDS = {
|
||||
#'default': 'redis://localhost:6383?socket_timeout=0.5&db=0',
|
||||
#'karma': 'redis://localhost:6383?socket_timeout=0.5&db=1',
|
||||
}
|
||||
|
|
2
vendor
2
vendor
|
@ -1 +1 @@
|
|||
Subproject commit 461132622f8a5b324bab499089c1d253e915b124
|
||||
Subproject commit 2718b74d3e6483e50dda68ccadcc875ae572afb6
|
Загрузка…
Ссылка в новой задаче