From 44e96f0de4728b13f845bb96c399445b3fbcc973 Mon Sep 17 00:00:00 2001 From: Ricky Rosario Date: Thu, 12 May 2011 16:03:32 -0400 Subject: [PATCH] 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 --- apps/karma/__init__.py | 0 apps/karma/actions.py | 189 ++++++++++++++++++++ apps/karma/admin.py | 71 ++++++++ apps/karma/cron.py | 15 ++ apps/karma/models.py | 1 + apps/karma/tasks.py | 63 +++++++ apps/karma/templates/karma/admin/karma.html | 94 ++++++++++ apps/karma/tests/__init__.py | 0 apps/karma/tests/test_actions.py | 66 +++++++ apps/questions/cron.py | 1 + apps/questions/karma_actions.py | 25 +++ apps/questions/tasks.py | 6 + apps/questions/tests/test_karma.py | 47 +++++ apps/questions/views.py | 4 +- apps/sumo/tests/__init__.py | 9 +- configs/redis/redis-test.conf | 23 +++ media/css/kadmin.css | 5 + scripts/crontab/make-crons.py | 1 + scripts/crontab/prod | 1 + scripts/crontab/support | 1 + scripts/crontab/support-release | 1 + settings.py | 7 + vendor | 2 +- 23 files changed, 629 insertions(+), 3 deletions(-) create mode 100644 apps/karma/__init__.py create mode 100644 apps/karma/actions.py create mode 100644 apps/karma/admin.py create mode 100644 apps/karma/cron.py create mode 100644 apps/karma/models.py create mode 100644 apps/karma/tasks.py create mode 100644 apps/karma/templates/karma/admin/karma.html create mode 100644 apps/karma/tests/__init__.py create mode 100644 apps/karma/tests/test_actions.py create mode 100644 apps/questions/karma_actions.py create mode 100644 apps/questions/tests/test_karma.py create mode 100644 configs/redis/redis-test.conf diff --git a/apps/karma/__init__.py b/apps/karma/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/karma/actions.py b/apps/karma/actions.py new file mode 100644 index 00000000000..0de3740d68f --- /dev/null +++ b/apps/karma/actions.py @@ -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) diff --git a/apps/karma/admin.py b/apps/karma/admin.py new file mode 100644 index 00000000000..f46428e080c --- /dev/null +++ b/apps/karma/admin.py @@ -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), + } diff --git a/apps/karma/cron.py b/apps/karma/cron.py new file mode 100644 index 00000000000..675cc1d258a --- /dev/null +++ b/apps/karma/cron.py @@ -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() diff --git a/apps/karma/models.py b/apps/karma/models.py new file mode 100644 index 00000000000..4b1e59ddac4 --- /dev/null +++ b/apps/karma/models.py @@ -0,0 +1 @@ +# Everything is in REDIS diff --git a/apps/karma/tasks.py b/apps/karma/tasks.py new file mode 100644 index 00000000000..ddad60e8f2d --- /dev/null +++ b/apps/karma/tasks.py @@ -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() diff --git a/apps/karma/templates/karma/admin/karma.html b/apps/karma/templates/karma/admin/karma.html new file mode 100644 index 00000000000..760c283f329 --- /dev/null +++ b/apps/karma/templates/karma/admin/karma.html @@ -0,0 +1,94 @@ +{% extends "kadmin/base.html" %} +{% load waffle_tags %} + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} + +{% block content %} + {% switch 'karma' %} + {% else %} +

Karma is currently disabled. Activate waffle switch 'karma' to enable.

+ {% endswitch %} +
+

Top Contributors - All Time

+
    + {% for user in top_alltime %} +
  1. + {{ user.user.username }}: + Points: {{ user.points }} | + Answers: {{ user.answers }} | + First Answers: {{ user.first_answers }} | + Solutions: {{ user.solutions }} | + Helpful Votes: {{ user.helpful_votes }} +
  2. + {% endfor %} + +
+
+

Top Contributors - Last 7 Days

+
    + {% for user in top_week %} +
  1. + {{ user.user.username }}: + Points: {{ user.points }} | + Answers: {{ user.answers }} | + First Answers: {{ user.first_answers }} | + Solutions: {{ user.solutions }} | + Helpful Votes: {{ user.helpful_votes }} +
  2. + {% endfor %} + +
+
+
+ {% csrf_token %} + + +
+
+
+

User Karma{% if username %}: {{ username }}{% endif %}

+
+ + +
+ {% if user_karma %} + + + {% for row in user_karma %} + + + + + {% endfor %} + +
{{ row.key }}{{ row.value }}
+ {% else %} + {% if username %} +

User or karma data not found.

+ {% endif %} + {% endif %} +
+
+

Initialize Karma

+

Warning: This will launch a task to delete all existing karma data from redis and recalculate from the database.

+
+ {% csrf_token %} + + +
+
+{% endblock %} + +{% block footer %} + {{ block.super }} + + + + +{% endblock %} diff --git a/apps/karma/tests/__init__.py b/apps/karma/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/karma/tests/test_actions.py b/apps/karma/tests/test_actions.py new file mode 100644 index 00000000000..afe11805a61 --- /dev/null +++ b/apps/karma/tests/test_actions.py @@ -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)) diff --git a/apps/questions/cron.py b/apps/questions/cron.py index a649c49e47b..f343c8f5515 100644 --- a/apps/questions/cron.py +++ b/apps/questions/cron.py @@ -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.""" diff --git a/apps/questions/karma_actions.py b/apps/questions/karma_actions.py new file mode 100644 index 00000000000..5b4ea2946a5 --- /dev/null +++ b/apps/questions/karma_actions.py @@ -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 diff --git a/apps/questions/tasks.py b/apps/questions/tasks.py index 963abe6205a..7ddd7f90370 100644 --- a/apps/questions/tasks.py +++ b/apps/questions/tasks.py @@ -8,6 +8,7 @@ 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() diff --git a/apps/questions/tests/test_karma.py b/apps/questions/tests/test_karma.py new file mode 100644 index 00000000000..b6812cdaa3e --- /dev/null +++ b/apps/questions/tests/test_karma.py @@ -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 diff --git a/apps/questions/views.py b/apps/questions/views.py index e93041b22f6..4b37b588ed0 100644 --- a/apps/questions/views.py +++ b/apps/questions/views.py @@ -35,6 +35,7 @@ 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.') diff --git a/apps/sumo/tests/__init__.py b/apps/sumo/tests/__init__.py index 65959999abd..32a242f7b4a 100644 --- a/apps/sumo/tests/__init__.py +++ b/apps/sumo/tests/__init__.py @@ -8,7 +8,7 @@ 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 @@ def request(self, **request): # 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 @@ def test_innodb_and_utf8(self): class MobileTestCase(TestCase): def setUp(self): + super(MobileTestCase, self).setUp() self.client.cookies[settings.MOBILE_COOKIE] = 'on' diff --git a/configs/redis/redis-test.conf b/configs/redis/redis-test.conf new file mode 100644 index 00000000000..35048a28b8a --- /dev/null +++ b/configs/redis/redis-test.conf @@ -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 diff --git a/media/css/kadmin.css b/media/css/kadmin.css index 66360274efe..0b2757f22a4 100644 --- a/media/css/kadmin.css +++ b/media/css/kadmin.css @@ -22,3 +22,8 @@ h2 { #settings tr:hover th a { visibility: visible; } + +#content section { + border-bottom: solid 2px #f0f0f0; + padding: 10px 0; +} \ No newline at end of file diff --git a/scripts/crontab/make-crons.py b/scripts/crontab/make-crons.py index e5cc9c1798a..b522d145b07 100755 --- a/scripts/crontab/make-crons.py +++ b/scripts/crontab/make-crons.py @@ -69,6 +69,7 @@ # 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 diff --git a/scripts/crontab/prod b/scripts/crontab/prod index cd2136ff5d8..227096b3572 100644 --- a/scripts/crontab/prod +++ b/scripts/crontab/prod @@ -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 diff --git a/scripts/crontab/support b/scripts/crontab/support index d7fad42e001..4cbeccae9a4 100644 --- a/scripts/crontab/support +++ b/scripts/crontab/support @@ -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 diff --git a/scripts/crontab/support-release b/scripts/crontab/support-release index 108a7c8089c..d0a98bec19a 100644 --- a/scripts/crontab/support-release +++ b/scripts/crontab/support-release @@ -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 diff --git a/settings.py b/settings.py index 435421f6dae..9e2aba8c9e8 100644 --- a/settings.py +++ b/settings.py @@ -251,6 +251,7 @@ 'messages', 'commonware.response.cookies', 'groups', + 'karma', # Extra apps for testing. 'django_nose', @@ -690,3 +691,9 @@ def read_only_mode(env): #'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', +} diff --git a/vendor b/vendor index 461132622f8..2718b74d3e6 160000 --- a/vendor +++ b/vendor @@ -1 +1 @@ -Subproject commit 461132622f8a5b324bab499089c1d253e915b124 +Subproject commit 2718b74d3e6483e50dda68ccadcc875ae572afb6