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:
Ricky Rosario 2011-05-12 16:03:32 -04:00
Родитель 03d14c012d
Коммит 44e96f0de4
23 изменённых файлов: 629 добавлений и 3 удалений

0
apps/karma/__init__.py Normal file
Просмотреть файл

189
apps/karma/actions.py Normal file
Просмотреть файл

@ -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)

71
apps/karma/admin.py Normal file
Просмотреть файл

@ -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),
}

15
apps/karma/cron.py Normal file
Просмотреть файл

@ -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()

1
apps/karma/models.py Normal file
Просмотреть файл

@ -0,0 +1 @@
# Everything is in REDIS

63
apps/karma/tasks.py Normal file
Просмотреть файл

@ -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

@ -1 +1 @@
Subproject commit 461132622f8a5b324bab499089c1d253e915b124
Subproject commit 2718b74d3e6483e50dda68ccadcc875ae572afb6