diff --git a/apps/actioncounters/fields.py b/apps/actioncounters/fields.py
index d8cc78e0cdb..c3a718cac8f 100644
--- a/apps/actioncounters/fields.py
+++ b/apps/actioncounters/fields.py
@@ -122,3 +122,9 @@ def _change_total(self, delta):
# race condition. A subsequent save() could clobber concurrent counter
# changes.
self.total = self.total + delta
+
+ # HACK: Invalidate this object in cache-machine, if the method is available.
+ if hasattr(m_cls.objects, 'invalidate'):
+ m_cls.objects.invalidate(self.instance)
+
+
diff --git a/apps/demos/helpers.py b/apps/demos/helpers.py
index 4ea9f58f535..1407a3fe2b7 100644
--- a/apps/demos/helpers.py
+++ b/apps/demos/helpers.py
@@ -1,4 +1,5 @@
import datetime
+import functools
import hashlib
import random
@@ -21,18 +22,54 @@
from .models import Submission, TAG_DESCRIPTIONS, DEMO_LICENSES
from . import DEMOS_CACHE_NS_KEY
-from devmo.helpers import register_cached_inclusion_tag
# Monkeypatch threadedcomments URL reverse() to use devmo's
from devmo.urlresolvers import reverse
threadedcommentstags.reverse = reverse
+TEMPLATE_INCLUDE_CACHE_EXPIRES = getattr(settings,
+ 'TEMPLATE_INCLUDE_CACHE_EXPIRES', 300)
+
+
def new_context(context, **kw):
c = dict(context.items())
c.update(kw)
return c
+# TODO:liberate ?
+def register_cached_inclusion_tag(template, key_fn=None,
+ expires=TEMPLATE_INCLUDE_CACHE_EXPIRES):
+ """Decorator for inclusion tags with output caching.
+
+ Accepts a string or function to generate a cache key based on the incoming
+ parameters, along with an expiration time configurable as
+ INCLUDE_CACHE_EXPIRES or an explicit parameter"""
+
+ if key_fn is None:
+ key_fn = template
+
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapper(*args, **kw):
+
+ if type(key_fn) is str:
+ cache_key = key_fn
+ else:
+ cache_key = key_fn(*args, **kw)
+
+ out = cache.get(cache_key)
+ if out is None:
+ context = f(*args, **kw)
+ t = jingo.env.get_template(template).render(context)
+ out = jinja2.Markup(t)
+ cache.set(cache_key, out, expires)
+ return out
+
+ return register.function(wrapper)
+ return decorator
+
+
def submission_key(prefix):
"""Produce a cache key function with a prefix, which generates the rest of
the key based on a submission ID and last-modified timestamp."""
diff --git a/apps/devmo/helpers.py b/apps/devmo/helpers.py
index 656709471aa..b40d52a34b9 100644
--- a/apps/devmo/helpers.py
+++ b/apps/devmo/helpers.py
@@ -1,10 +1,9 @@
import datetime
-import functools
-import httplib
import re
-import socket
+import httplib
import urllib
import urlparse
+import socket
from django.conf import settings
from django.core.cache import cache
@@ -12,7 +11,7 @@
from django.utils.html import strip_tags
import bleach
-from jingo import register, env
+from jingo import register
import jinja2
import pytz
from soapbox.models import Message
@@ -30,10 +29,6 @@
register.filter(utils.entity_decode)
-TEMPLATE_INCLUDE_CACHE_EXPIRES = getattr(settings,
- 'TEMPLATE_INCLUDE_CACHE_EXPIRES', 300)
-
-
@register.function
def page_title(title):
return u'%s | MDN' % title
@@ -151,35 +146,3 @@ def get_soapbox_messages(url):
@register.inclusion_tag('devmo/elements/soapbox_messages.html')
def soapbox_messages(soapbox_messages):
return {'soapbox_messages': soapbox_messages}
-
-
-def register_cached_inclusion_tag(template, key_fn=None,
- expires=TEMPLATE_INCLUDE_CACHE_EXPIRES):
- """Decorator for inclusion tags with output caching.
-
- Accepts a string or function to generate a cache key based on the incoming
- parameters, along with an expiration time configurable as
- INCLUDE_CACHE_EXPIRES or an explicit parameter"""
-
- if key_fn is None:
- key_fn = template
-
- def decorator(f):
- @functools.wraps(f)
- def wrapper(*args, **kw):
-
- if isinstance(key_fn, basestring):
- cache_key = key_fn
- else:
- cache_key = key_fn(*args, **kw)
-
- out = cache.get(cache_key)
- if out is None:
- context = f(*args, **kw)
- t = env.get_template(template).render(context)
- out = jinja2.Markup(t)
- cache.set(cache_key, out, expires)
- return out
-
- return register.function(wrapper)
- return decorator
diff --git a/apps/devmo/models.py b/apps/devmo/models.py
index 4913ec53271..ab86995a921 100644
--- a/apps/devmo/models.py
+++ b/apps/devmo/models.py
@@ -14,6 +14,7 @@
from django.db import models
from django.utils.functional import cached_property
+import caching.base
import constance.config
import xml.sax
from xml.sax.handler import ContentHandler
@@ -37,7 +38,16 @@
'DEFAULT_AVATAR', settings.MEDIA_URL + 'img/avatar-default.png')
-class UserProfile(models.Model):
+class ModelBase(caching.base.CachingMixin, models.Model):
+ """Common base model for all MDN models: Implements caching."""
+
+ objects = caching.base.CachingManager()
+
+ class Meta:
+ abstract = True
+
+
+class UserProfile(ModelBase):
"""
The UserProfile *must* exist for each
django.contrib.auth.models.User object. This may be relaxed
@@ -271,7 +281,7 @@ def parse_header_line(header_line):
FIELD_MAP[field_name][1] = ''
-class Calendar(models.Model):
+class Calendar(ModelBase):
"""The Calendar spreadsheet"""
shortname = models.CharField(max_length=255)
@@ -357,7 +367,7 @@ def __unicode__(self):
return self.shortname
-class Event(models.Model):
+class Event(ModelBase):
"""An event"""
date = models.DateField()
diff --git a/apps/devmo/tests/test_views.py b/apps/devmo/tests/test_views.py
index 7e6c8095417..9bce0fb9d7f 100644
--- a/apps/devmo/tests/test_views.py
+++ b/apps/devmo/tests/test_views.py
@@ -511,7 +511,7 @@ class LoggingTests(test_utils.TestCase):
urls = 'devmo.tests.logging_urls'
def setUp(self):
- self.old_logging = settings.LOGGING.copy()
+ self.old_logging = settings.LOGGING
def tearDown(self):
settings.LOGGING = self.old_logging
diff --git a/apps/docs/views.py b/apps/docs/views.py
index db89f9d85cb..e57f28f652b 100644
--- a/apps/docs/views.py
+++ b/apps/docs/views.py
@@ -3,9 +3,10 @@
import random
from django.conf import settings
-from django.http import HttpResponseRedirect
+from django.http import (HttpResponseRedirect)
from django.shortcuts import render
+from caching.base import cached
import commonware
from dateutil.parser import parse as date_parse
from tower import ugettext as _
@@ -29,6 +30,9 @@ def docs(request):
if next.startswith('/'):
return HttpResponseRedirect(next)
+ # Doc of the day
+ dotd = cached(_get_popular_item, 'kuma_docs_dotd', 24*60*60)
+
# Recent updates
active_docs = []
entries = Entry.objects.filter(feed__shortname='mdc-latest')
@@ -58,3 +62,10 @@ def docs(request):
'review_flag_docs': review_flag_docs,
'dotd': dotd}
return render(request, 'docs/docs.html', data)
+
+
+def _get_popular_item():
+ """Get a single, random item off the popular pages list."""
+ # MindTouch is gone, and so is popular.json. Returning None for
+ # historical compatibility.
+ return None
diff --git a/apps/feeder/models.py b/apps/feeder/models.py
index 3ad0d34b67b..caa4a99ab4d 100644
--- a/apps/feeder/models.py
+++ b/apps/feeder/models.py
@@ -1,12 +1,14 @@
from django.db import models
-from django.utils.functional import cached_property
+import caching.base
import jsonpickle
from devmo import SECTIONS_TWITTER, SECTIONS_UPDATES
+from devmo.models import ModelBase
+import utils
-class BundleManager(models.Manager):
+class BundleManager(caching.base.CachingManager):
"""Custom manager for bundles."""
def recent_entries(self, bundles):
@@ -20,7 +22,7 @@ def recent_entries(self, bundles):
feed__bundles__shortname__in=bundles)
-class Bundle(models.Model):
+class Bundle(ModelBase):
"""A bundle of several feeds. A feed can be in several (or no) bundles."""
shortname = models.SlugField(
@@ -34,7 +36,7 @@ def __unicode__(self):
return self.shortname
-class Feed(models.Model):
+class Feed(ModelBase):
"""A feed holds the metadata of an RSS feed."""
shortname = models.SlugField(
@@ -76,7 +78,7 @@ def delete_old_entries(self):
item.delete()
-class Entry(models.Model):
+class Entry(ModelBase):
"""An entry is an item representing feed content."""
feed = models.ForeignKey(Feed, related_name='entries')
@@ -102,12 +104,12 @@ class Meta:
def __unicode__(self):
return '%s: %s' % (self.feed.shortname, self.guid)
- @cached_property
+ @utils.cached_property
def parsed(self):
"""Unpickled feed data."""
return jsonpickle.decode(self.raw)
- @cached_property
+ @utils.cached_property
def section(self):
"""The section this entry is associated with."""
try:
diff --git a/apps/kpi/models.py b/apps/kpi/models.py
index 0521c3166b2..7ed22c0fddb 100644
--- a/apps/kpi/models.py
+++ b/apps/kpi/models.py
@@ -1,13 +1,14 @@
-from django.db import models
from django.db.models import (CharField, DateField, ForeignKey,
PositiveIntegerField)
+from sumo.models import ModelBase
+
L10N_METRIC_CODE = 'general wiki:l10n:coverage'
KB_L10N_CONTRIBUTORS_METRIC_CODE = 'general wiki:l10n:contributors'
-class MetricKind(models.Model):
+class MetricKind(ModelBase):
"""A programmer-readable identifier of a metric, like 'clicks: search'"""
code = CharField(max_length=255, unique=True)
@@ -15,7 +16,7 @@ def __unicode__(self):
return self.code
-class Metric(models.Model):
+class Metric(ModelBase):
"""A single numeric measurement aggregated over a span of time.
For example, the number of hits to a page during a specific week.
diff --git a/apps/landing/helpers.py b/apps/landing/helpers.py
index d483110215b..003a4c26b9e 100644
--- a/apps/landing/helpers.py
+++ b/apps/landing/helpers.py
@@ -10,17 +10,17 @@
from decimal import Decimal
from django.utils.formats import number_format
+
from devmo import SECTIONS, SECTION_USAGE
-from devmo.helpers import register_cached_inclusion_tag
-@register_cached_inclusion_tag('landing/newsfeed.html')
+@register.inclusion_tag('landing/newsfeed.html')
def newsfeed(entries, section_headers=False):
"""Landing page news feed."""
return {'updates': entries, 'section_headers': section_headers}
-@register_cached_inclusion_tag('landing/discussions.html')
+@register.inclusion_tag('landing/discussions.html')
def discussions_feed(entries):
"""Landing page news feed."""
return {'updates': entries}
diff --git a/apps/landing/templates/landing/newsfeed.html b/apps/landing/templates/landing/newsfeed.html
index aa47271ec78..8003b60d803 100644
--- a/apps/landing/templates/landing/newsfeed.html
+++ b/apps/landing/templates/landing/newsfeed.html
@@ -1,3 +1,4 @@
+{% cache updates %}
{% for entry in updates %}
{% with e = entry.parsed %}
@@ -18,3 +19,4 @@
+{% endcache %}
diff --git a/apps/landing/templates/sidebar/twitter.html b/apps/landing/templates/sidebar/twitter.html
index 6c18a327fd5..a4a78208ad5 100644
--- a/apps/landing/templates/sidebar/twitter.html
+++ b/apps/landing/templates/sidebar/twitter.html
@@ -1,3 +1,4 @@
+{% cache tweet_qs %}
+{% endcache %}
diff --git a/apps/notifications/events.py b/apps/notifications/events.py
index 289215e6fd6..f01aedbc77b 100644
--- a/apps/notifications/events.py
+++ b/apps/notifications/events.py
@@ -246,7 +246,7 @@ def _watches_belonging_to_user(cls, user_or_email, object_id=None,
return Watch.objects.none()
# Filter by stuff in the Watch row:
- watches = Watch.objects.filter(
+ watches = Watch.uncached.filter(
user_condition,
Q(content_type=ContentType.objects.get_for_model(cls.content_type))
if cls.content_type
diff --git a/apps/notifications/models.py b/apps/notifications/models.py
index 2da7a639b5e..4fb0e7ce6b1 100644
--- a/apps/notifications/models.py
+++ b/apps/notifications/models.py
@@ -5,7 +5,7 @@
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
-from sumo.models import LocaleField
+from sumo.models import ModelBase, LocaleField
from sumo.urlresolvers import reverse
@@ -31,7 +31,7 @@ def multi_raw(query, params, models):
for model_class in models]
-class EventWatch(models.Model):
+class EventWatch(ModelBase):
"""
Allows anyone to watch a specific item for changes. Uses email instead of
user ID so anonymous visitors can also watch things eventually.
@@ -74,7 +74,7 @@ def get_remove_url(self):
return urlparams(url_, email=self.email)
-class Watch(models.Model):
+class Watch(ModelBase):
"""Watch events."""
# Key used by an Event to find watches it manages:
event_type = models.CharField(max_length=30, db_index=True)
@@ -108,7 +108,7 @@ def activate(self):
return self
-class WatchFilter(models.Model):
+class WatchFilter(ModelBase):
"""Additional key/value pairs that pare down the scope of a watch"""
watch = models.ForeignKey(Watch, related_name='filters')
name = models.CharField(max_length=20)
diff --git a/apps/sumo/models.py b/apps/sumo/models.py
index 1fe672a5966..645de468421 100644
--- a/apps/sumo/models.py
+++ b/apps/sumo/models.py
@@ -1,8 +1,51 @@
from django.conf import settings
from django.db import models
+import caching.base
from south.modelsinspector import add_introspection_rules
+# Our apps should subclass ManagerBase instead of models.Manager or
+# caching.base.CachingManager directly.
+ManagerBase = caching.base.CachingManager
+
+
+class ModelBase(caching.base.CachingMixin, models.Model):
+ """
+ Base class for SUMO models to abstract some common features.
+
+ * Caching.
+ """
+
+ objects = ManagerBase()
+ uncached = models.Manager()
+
+ class Meta:
+ abstract = True
+
+ def update(self, **kw):
+ """
+ Shortcut for doing an UPDATE on this object.
+
+ If _signal=False is in ``kw`` the post_save signal won't be sent.
+ """
+ signal = kw.pop('_signal', True)
+ cls = self.__class__
+ for k, v in kw.items():
+ setattr(self, k, v)
+ if signal:
+ # Detect any attribute changes during pre_save and add those to the
+ # update kwargs.
+ attrs = dict(self.__dict__)
+ models.signals.pre_save.send(sender=cls, instance=self)
+ for k, v in self.__dict__.items():
+ if attrs[k] != v:
+ kw[k] = v
+ setattr(self, k, v)
+ cls.objects.filter(pk=self.pk).update(**kw)
+ if signal:
+ models.signals.post_save.send(sender=cls, instance=self,
+ created=False)
+
class LocaleField(models.CharField):
"""CharField with locale settings specific to SUMO defaults."""
diff --git a/apps/users/models.py b/apps/users/models.py
index 3d3b7c420bb..1ef34052baa 100644
--- a/apps/users/models.py
+++ b/apps/users/models.py
@@ -15,6 +15,7 @@
from tower import ugettext_lazy as _lazy
from countries import COUNTRIES
+from sumo.models import ModelBase
from sumo.urlresolvers import reverse
from devmo.models import UserProfile
@@ -22,7 +23,7 @@
SHA1_RE = re.compile('^[a-f0-9]{40}$')
-class Profile(models.Model):
+class Profile(ModelBase):
"""Profile model for django users, get it with user.get_profile()."""
user = models.OneToOneField(User, primary_key=True,
diff --git a/apps/users/views.py b/apps/users/views.py
index 68c106c06b5..4e8a8b1b66f 100644
--- a/apps/users/views.py
+++ b/apps/users/views.py
@@ -267,7 +267,7 @@ def activate(request, activation_key):
# Claim anonymous watches belonging to this email
claim_watches.delay(account)
- # my_questions = Question.objects.filter(creator=account)
+ # my_questions = Question.uncached.filter(creator=account)
# TODO: remove this after dropping unconfirmed questions.
# my_questions.update(status=CONFIRMED)
return render(request, 'users/activate.html',
diff --git a/apps/wiki/fixtures/wiki/documents.json b/apps/wiki/fixtures/wiki/documents.json
index a92c51a28c6..4f8d29fb0eb 100644
--- a/apps/wiki/fixtures/wiki/documents.json
+++ b/apps/wiki/fixtures/wiki/documents.json
@@ -10,8 +10,7 @@
"modified": "2013-06-06 05:54:18",
"is_template": false,
"html": "the keyword video only appears in the body of this article\n
",
- "slug": "article-title",
- "render_max_age": null
+ "slug": "article-title"
}
},
{
@@ -25,8 +24,7 @@
"modified": "2013-06-06 05:54:18",
"is_template": false,
"html": "the keyword video only appears in the body of this article\n
",
- "slug": "article-title-2",
- "render_max_age": null
+ "slug": "article-title-2"
}
},
{
@@ -39,8 +37,7 @@
"is_template": false,
"modified": "2013-06-06 05:54:18",
"html": "",
- "slug": "article-title-3",
- "render_max_age": null
+ "slug": "article-title-3"
}
},
{
@@ -55,8 +52,7 @@
"modified": "2013-06-06 05:54:18",
"is_template": false,
"html": "ceci n'est pas une pipe\n
mon dieu!\n
",
- "slug": "le-title",
- "render_max_age": null
+ "slug": "le-title"
}
},
{
@@ -71,8 +67,7 @@
"current_revision": 22,
"is_template": false,
"rendered_html": "audio is in this but the word for tough things will be ignored\n
",
- "slug": "lorem-ipsum",
- "render_max_age": null
+ "slug": "lorem-ipsum"
}
},
{
@@ -85,8 +80,7 @@
"is_template": false,
"modified": "2013-06-06 05:54:18",
"html": "",
- "slug": "article-with-revisions",
- "render_max_age": null
+ "slug": "article-with-revisions"
}
},
{
diff --git a/apps/wiki/management/commands/refresh_wiki_caches.py b/apps/wiki/management/commands/refresh_wiki_caches.py
index dfacbf5eeb2..e028b186daa 100644
--- a/apps/wiki/management/commands/refresh_wiki_caches.py
+++ b/apps/wiki/management/commands/refresh_wiki_caches.py
@@ -21,7 +21,7 @@
from django.core.management.base import (BaseCommand, NoArgsCommand,
CommandError)
-from wiki.models import Document, Revision
+from wiki.models import (Document, Revision)
PAGE_EXISTS_KEY_TMPL = getattr(settings, 'wiki_page_exists_key_tmpl',
@@ -40,6 +40,7 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
+
base_url = options['baseurl']
if not base_url:
from django.contrib.sites.models import Site
diff --git a/apps/wiki/models.py b/apps/wiki/models.py
index 7fa200ecc08..8a86ed5c662 100644
--- a/apps/wiki/models.py
+++ b/apps/wiki/models.py
@@ -15,7 +15,7 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core import serializers
-from django.core.cache import cache
+from django.core.cache import get_cache, cache
from django.core.exceptions import ValidationError
from django.core.urlresolvers import resolve
from django.db import models
@@ -353,6 +353,9 @@
DEKI_FILE_URL = re.compile(r'@api/deki/files/(?P\d+)/=')
KUMA_FILE_URL = re.compile(r'/files/(?P\d+)/.+\..+')
+SECONDARY_CACHE_ALIAS = getattr(settings,
+ 'SECONDARY_CACHE_ALIAS',
+ 'secondary')
URL_REMAPS_CACHE_KEY_TMPL = 'DocumentZoneUrlRemaps:%s'
@@ -1842,7 +1845,8 @@ class DocumentZoneManager(models.Manager):
def get_url_remaps(self, locale):
cache_key = URL_REMAPS_CACHE_KEY_TMPL % locale
- remaps = cache.get(cache_key)
+ s_cache = get_cache(SECONDARY_CACHE_ALIAS)
+ remaps = s_cache.get(cache_key)
if not remaps:
qs = (self.filter(document__locale=locale,
@@ -1852,7 +1856,7 @@ def get_url_remaps(self, locale):
'original_path': '/docs/%s' % zone.document.slug,
'new_path': '/%s' % zone.url_root
} for zone in qs]
- cache.set(cache_key, remaps)
+ s_cache.set(cache_key, remaps)
return remaps
@@ -1878,7 +1882,8 @@ def save(self, *args, **kwargs):
# Invalidate URL remap cache for this zone
locale = self.document.locale
cache_key = URL_REMAPS_CACHE_KEY_TMPL % locale
- cache.delete(cache_key)
+ s_cache = get_cache(SECONDARY_CACHE_ALIAS)
+ s_cache.delete(cache_key)
class ReviewTag(TagBase):
diff --git a/apps/wiki/tests/test_middleware.py b/apps/wiki/tests/test_middleware.py
index b3de5e7f576..16e8f861ffb 100644
--- a/apps/wiki/tests/test_middleware.py
+++ b/apps/wiki/tests/test_middleware.py
@@ -8,7 +8,7 @@
from django.test.client import Client
from django.http import Http404
from django.utils.encoding import smart_str
-from django.core.cache import cache
+from django.core.cache import get_cache
import mock
from nose import SkipTest
@@ -22,7 +22,7 @@
from . import TestCaseBase, FakeResponse
-from wiki.models import (Document, Attachment, DocumentZone)
+from wiki.models import (Document, Attachment, DocumentZone, SECONDARY_CACHE_ALIAS)
from wiki.tests import (doc_rev, document, new_document_data, revision,
normalize_html, create_template_test_users)
from wiki.views import _version_groups, DOCUMENT_LAST_MODIFIED_CACHE_KEY_TMPL
@@ -33,7 +33,9 @@ class DocumentZoneMiddlewareTestCase(TestCaseBase):
def setUp(self):
super(DocumentZoneMiddlewareTestCase, self).setUp()
- cache.clear()
+
+ s_cache = get_cache(SECONDARY_CACHE_ALIAS)
+ s_cache.clear()
self.zone_root = 'ExtraWiki'
self.zone_root_content = 'This is the Zone Root'
diff --git a/apps/wiki/tests/test_templates.py b/apps/wiki/tests/test_templates.py
index 16cf10678d1..e777e8e5965 100644
--- a/apps/wiki/tests/test_templates.py
+++ b/apps/wiki/tests/test_templates.py
@@ -836,7 +836,7 @@ def test_approve_revision(self, get_current, reviewed_delay):
args=[self.document.slug, self.revision.id])
eq_(200, response.status_code)
- r = Revision.objects.get(pk=self.revision.id)
+ r = Revision.uncached.get(pk=self.revision.id)
eq_(significance, r.significance)
assert r.reviewed
assert r.is_approved
@@ -863,7 +863,7 @@ def test_reject_revision(self, get_current, delay):
'comment': comment},
args=[self.document.slug, self.revision.id])
eq_(200, response.status_code)
- r = Revision.objects.get(pk=self.revision.id)
+ r = Revision.uncached.get(pk=self.revision.id)
assert r.reviewed
assert not r.is_approved
delay.assert_called_with(r, r.document, comment)
@@ -931,7 +931,7 @@ def test_review_translation(self):
follow=True)
eq_(200, response.status_code)
d = Document.objects.get(pk=doc_es.id)
- r = Revision.objects.get(pk=rev_es2.id)
+ r = Revision.uncached.get(pk=rev_es2.id)
eq_(d.current_revision, r)
assert r.reviewed
assert r.is_approved
diff --git a/lib/utils.py b/lib/utils.py
index 0f15ddcfe85..604f13f393e 100644
--- a/lib/utils.py
+++ b/lib/utils.py
@@ -41,6 +41,53 @@ def wrapper(self, *args, **kwargs):
return decorator
+def cached_property(*args, **kw):
+ # Handles invocation as a direct decorator or
+ # with intermediate keyword arguments.
+ if args: # @cached_property
+ return CachedProperty(args[0])
+ else: # @cached_property(name=..., writable=...)
+ return lambda f: CachedProperty(f, **kw)
+
+
+class CachedProperty(object):
+ """A decorator that converts a function into a lazy property. The
+function wrapped is called the first time to retrieve the result
+and than that calculated result is used the next time you access
+the value::
+
+class Foo(object):
+
+@cached_property
+def foo(self):
+# calculate something important here
+return 42
+
+Lifted from werkzeug.
+"""
+
+ def __init__(self, func, name=None, doc=None, writable=False):
+ self.func = func
+ self.writable = writable
+ self.__name__ = name or func.__name__
+ self.__doc__ = doc or func.__doc__
+
+ def __get__(self, obj, type=None):
+ if obj is None:
+ return self
+ _missing = object()
+ value = obj.__dict__.get(self.__name__, _missing)
+ if value is _missing:
+ value = self.func(obj)
+ obj.__dict__[self.__name__] = value
+ return value
+
+ def __set__(self, obj, value):
+ if not self.writable:
+ raise TypeError('read only attribute')
+ obj.__dict__[self.__name__] = value
+
+
def entity_decode(str):
"""Turn HTML entities in a string into unicode."""
return htmlparser.unescape(str)
diff --git a/puppet/files/vagrant/settings_local.py b/puppet/files/vagrant/settings_local.py
index fea917c548c..127c33c0d1b 100644
--- a/puppet/files/vagrant/settings_local.py
+++ b/puppet/files/vagrant/settings_local.py
@@ -98,7 +98,18 @@
CACHES = {
'default': {
- 'BACKEND': 'memcached_hashring.backend.MemcachedHashRingCache',
+ # HACK: We currently have 'default' memcache disabled in production.
+ # This reflects that in local dev.
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ #'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ #'LOCATION': [
+ # '127.0.0.1:11211',
+ #],
+ 'TIMEOUT': 3600,
+ 'KEY_PREFIX': 'kuma',
+ },
+ 'secondary': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'127.0.0.1:11211',
],
@@ -107,7 +118,12 @@
}
}
-CONSTANCE_DATABASE_CACHE_BACKEND = 'default'
+# TODO: Switch this to 'default' when main cache issues are resolved
+SECONDARY_CACHE_ALIAS = 'secondary'
+
+# Use IP:PORT pairs separated by semicolons.
+CACHE_BACKEND = 'memcached://localhost:11211?timeout=60'
+CONSTANCE_DATABASE_CACHE_BACKEND = CACHE_BACKEND
# This is used to hash some things in Django.
SECRET_KEY = 'jenny8675309'
diff --git a/requirements/prod.txt b/requirements/prod.txt
index b12388359fa..3cac867fc91 100644
--- a/requirements/prod.txt
+++ b/requirements/prod.txt
@@ -16,6 +16,7 @@ jsonpickle==0.3.1
lockfile==0.8
django-dbgettext==0.1
+-e git://github.com/jbalogh/django-cache-machine.git#egg=django-cache-machine
-e git://github.com/django-extensions/django-extensions.git#egg=django_extensions
-e git://github.com/jbalogh/jingo.git#egg=jingo
-e git://github.com/jsocol/jingo-minify.git#egg=jingo-minify
diff --git a/scripts/build.sh b/scripts/build.sh
index fe64411fbd1..1324f93fecc 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -59,6 +59,7 @@ DATABASES['default']['USER'] = 'hudson'
DATABASES['default']['TEST_NAME'] = '$DB'
DATABASES['default']['TEST_CHARSET'] = 'utf8'
DATABASES['default']['TEST_COLLATION'] = 'utf8_general_ci'
+CACHE_BACKEND = 'caching.backends.locmem://'
ASYNC_SIGNALS = False
diff --git a/settings.py b/settings.py
index a8da769f706..cc74b0b9308 100644
--- a/settings.py
+++ b/settings.py
@@ -21,7 +21,6 @@
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
-MANAGERS = ADMINS
PROTOCOL = 'https://'
DOMAIN = 'developer.mozilla.org'
@@ -29,6 +28,8 @@
PRODUCTION_URL = SITE_URL
USE_X_FORWARDED_HOST = True
+MANAGERS = ADMINS
+
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
@@ -57,17 +58,30 @@
DEKIWIKI_APIKEY = 'SET IN LOCAL SETTINGS'
DEKIWIKI_MOCK = True
+# Cache Settings
+CACHE_BACKEND = 'locmem://?timeout=86400'
+CACHE_PREFIX = 'kuma:'
+CACHE_COUNT_TIMEOUT = 60 # seconds
+
CACHES = {
'default': {
- 'BACKEND': 'memcached_hashring.backend.MemcachedHashRingCache',
- 'LOCATION': [
- '127.0.0.1:11211',
- ],
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 60,
'KEY_PREFIX': 'kuma',
},
+ # NOTE: The 'secondary' cache should be the same as 'default' in
+ # settings_local. The only reason it exists is because we had some issues
+ # with caching, disabled 'default', and wanted to selectively re-enable
+ # caching on a case-by-case basis to resolve the issue.
+ 'secondary': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'TIMEOUT': 60,
+ 'KEY_PREFIX': 'kuma',
+ }
}
+SECONDARY_CACHE_ALIAS = 'secondary'
+
# Addresses email comes from
DEFAULT_FROM_EMAIL = 'notifications@developer.mozilla.org'
SERVER_EMAIL = 'server-error@developer.mozilla.org'
@@ -470,15 +484,23 @@ def lazy_language_deki_map():
FEEDER_TIMEOUT = 6 # in seconds
def JINJA_CONFIG():
- config = {
- 'extensions': [
- 'tower.template.i18n',
- 'jinja2.ext.with_',
- 'jinja2.ext.loopcontrols',
- 'jinja2.ext.autoescape',
- ],
- 'finalize': lambda x: x if x is not None else ''
- }
+ import jinja2
+ from django.conf import settings
+ from django.core.cache.backends.memcached import CacheClass as MemcachedCacheClass
+ from caching.base import cache
+ config = {'extensions': ['tower.template.i18n', 'caching.ext.cache',
+ 'jinja2.ext.with_', 'jinja2.ext.loopcontrols',
+ 'jinja2.ext.autoescape'],
+ 'finalize': lambda x: x if x is not None else ''}
+ if isinstance(cache, MemcachedCacheClass) and not settings.DEBUG:
+ # We're passing the _cache object directly to jinja because
+ # Django can't store binary directly; it enforces unicode on it.
+ # Details: http://jinja.pocoo.org/2/documentation/api#bytecode-cache
+ # and in the errors you get when you try it the other way.
+ bc = jinja2.MemcachedBytecodeCache(cache._cache,
+ "%sj2:" % settings.CACHE_PREFIX)
+ config['cache_size'] = -1 # Never clear the cache
+ config['bytecode_cache'] = bc
return config
# Let Tower know about our additional keywords.
diff --git a/vendor b/vendor
index ada44dd5b7f..8f91262c89e 160000
--- a/vendor
+++ b/vendor
@@ -1 +1 @@
-Subproject commit ada44dd5b7f43bd99a266f8306f6f95f7612093a
+Subproject commit 8f91262c89e41d8c8d9187fd10bda610d8efb46f