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]) update_question_vote_chunk.apply_async(args=[chunk])
# TODO: remove this and use the karma top list.
@cronjobs.register @cronjobs.register
def cache_top_contributors(): def cache_top_contributors():
"""Compute the top contributors and store in cache.""" """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 activity.models import Action
from questions import ANSWERS_PER_PAGE from questions import ANSWERS_PER_PAGE
from questions.karma_actions import AnswerAction, FirstAnswerAction
log = logging.getLogger('k.task') log = logging.getLogger('k.task')
@ -79,3 +80,8 @@ def log_answer(answer):
transaction.commit_unless_managed() transaction.commit_unless_managed()
unpin_this_thread() 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.feeds import QuestionsFeed, AnswersFeed, TaggedQuestionsFeed
from questions.forms import (NewQuestionForm, EditQuestionForm, AnswerForm, from questions.forms import (NewQuestionForm, EditQuestionForm, AnswerForm,
WatchQuestionForm, FREQUENCY_CHOICES) WatchQuestionForm, FREQUENCY_CHOICES)
from questions.karma_actions import SolutionAction, AnswerMarkedHelpfulAction
from questions.models import Question, Answer, QuestionVote, AnswerVote from questions.models import Question, Answer, QuestionVote, AnswerVote
from questions.question_config import products from questions.question_config import products
from search.clients import WikiClient, QuestionsClient, SearchError from search.clients import WikiClient, QuestionsClient, SearchError
@ -389,7 +390,7 @@ def solve(request, question_id, answer_id):
question.save() question.save()
statsd.incr('questions.solution') statsd.incr('questions.solution')
QuestionSolvedEvent(answer).fire(exclude=question.creator) QuestionSolvedEvent(answer).fire(exclude=question.creator)
SolutionAction(answer.creator).save()
messages.add_message(request, messages.SUCCESS, messages.add_message(request, messages.SUCCESS,
_('Thank you for choosing a solution!')) _('Thank you for choosing a solution!'))
@ -460,6 +461,7 @@ def answer_vote(request, question_id, answer_id):
if 'helpful' in request.POST: if 'helpful' in request.POST:
vote.helpful = True vote.helpful = True
AnswerMarkedHelpfulAction(answer.creator).save()
message = _('Glad to hear it!') message = _('Glad to hear it!')
else: else:
message = _('Sorry to hear that.') message = _('Sorry to hear that.')

Просмотреть файл

@ -8,7 +8,7 @@ from django.conf import settings
from django.test.client import Client from django.test.client import Client
from nose.tools import eq_ from nose.tools import eq_
from test_utils import TestCase # So others can import it from here import test_utils
import sumo import sumo
from sumo.urlresolvers import reverse, split_path 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. # 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): class MigrationTests(TestCase):
"""Sanity checks for the SQL migration scripts""" """Sanity checks for the SQL migration scripts"""
@ -114,6 +120,7 @@ class MigrationTests(TestCase):
class MobileTestCase(TestCase): class MobileTestCase(TestCase):
def setUp(self): def setUp(self):
super(MobileTestCase, self).setUp()
self.client.cookies[settings.MOBILE_COOKIE] = 'on' 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 { #settings tr:hover th a {
visibility: visible; visibility: visible;
} }
#content section {
border-bottom: solid 2px #f0f0f0;
padding: 10px 0;
}

Просмотреть файл

@ -69,6 +69,7 @@ HOME = /tmp
# Once per day. # Once per day.
0 16 * * * $CRON reload_wiki_traffic_stats 0 16 * * * $CRON reload_wiki_traffic_stats
40 1 * * * $CRON update_weekly_votes 40 1 * * * $CRON update_weekly_votes
0 42 * * * $CRON update_top_contributors
# Twice per week. # Twice per week.
#05 01 * * 1,4 $CRON update_weekly_votes #05 01 * * 1,4 $CRON update_weekly_votes

Просмотреть файл

@ -26,6 +26,7 @@ HOME = /tmp
# Once per day. # Once per day.
0 16 * * * cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron reload_wiki_traffic_stats 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 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. # Twice per week.
#05 01 * * 1,4 cd /data/www/support.mozilla.com/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes #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. # Once per day.
0 16 * * * cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron reload_wiki_traffic_stats 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 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. # Twice per week.
#05 01 * * 1,4 cd /data/www/support.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes #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. # Once per day.
0 16 * * * cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron reload_wiki_traffic_stats 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 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. # Twice per week.
#05 01 * * 1,4 cd /data/www/support-release.allizom.org/kitsune; /usr/bin/python26 manage.py cron update_weekly_votes #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', 'messages',
'commonware.response.cookies', 'commonware.response.cookies',
'groups', 'groups',
'karma',
# Extra apps for testing. # Extra apps for testing.
'django_nose', 'django_nose',
@ -690,3 +691,9 @@ REDIS_BACKENDS = {
#'default': 'redis://localhost:6379?socket_timeout=0.5&db=0', #'default': 'redis://localhost:6379?socket_timeout=0.5&db=0',
#'karma': 'redis://localhost:6381?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