Move release notes views to rely on database

* No longer rely on release notes JSON files on the file system.
* Switch everything to new update_release_notes_data command
* Move to using django-memoize for releasenotes caching
This commit is contained in:
Paul McLanahan 2017-10-06 16:57:06 -04:00
Родитель d93c193b28
Коммит ebd6a7c438
22 изменённых файлов: 264 добавлений и 491 удалений

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

@ -20,11 +20,11 @@ class SimpleDictCache(LocMemCache):
def get(self, key, default=None, version=None):
key = self.make_key(key, version=version)
self.validate_key(key)
value = None
value = default
with self._lock.reader():
if not self._has_expired(key):
value = self._cache[key]
if value is not None:
if value is not default:
return value
with self._lock.writer():

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

@ -94,6 +94,11 @@ class SimpleDictCacheTests(TestCase):
self.assertEqual(cache.get("does_not_exist"), None)
self.assertEqual(cache.get("does_not_exist", "bang!"), "bang!")
def test_non_none_default(self):
# Should cache None values if default is not None
cache.set('is_none', None)
self.assertIsNone(cache.get('is_none', 'bang!'))
def test_get_many(self):
# Multiple cache keys can be returned using get_many
cache.set('a', 'a')

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

@ -3,6 +3,7 @@ from __future__ import print_function
from django.conf import settings
from django.core.management.base import BaseCommand
from bedrock.releasenotes.models import ProductRelease
from bedrock.utils.git import GitRepo
@ -10,10 +11,29 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', default=False,
help='If no error occurs, swallow all output.'),
parser.add_argument('-f', '--force', action='store_true', dest='force', default=False,
help='Load the data even if nothing new from git.'),
def output(self, msg):
if not self.quiet:
print(msg)
def handle(self, *args, **options):
self.quiet = options['quiet']
repo = GitRepo(settings.RELEASE_NOTES_PATH, settings.RELEASE_NOTES_REPO,
branch_name=settings.RELEASE_NOTES_BRANCH)
self.output('Updating git repo')
repo.update()
if not options['quiet']:
print('Release Notes Successfully Updated')
if not (options['force'] or repo.has_changes()):
self.output('No release note updates')
return
self.output('Loading releases into database')
count = ProductRelease.objects.refresh()
self.output('%s release notes successfully loaded' % count)
repo.set_db_latest()
self.output('Saved latest git repo state to database')
self.output('Done!')

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

@ -1,29 +0,0 @@
from __future__ import print_function
from django.conf import settings
from django.core.management.base import BaseCommand
from bedrock.releasenotes.models import ProductRelease
from bedrock.utils.git import GitRepo
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', default=False,
help='If no error occurs, swallow all output.'),
def handle(self, *args, **options):
repo = GitRepo(settings.RELEASE_NOTES_PATH, settings.RELEASE_NOTES_REPO,
branch_name=settings.RELEASE_NOTES_BRANCH)
repo.update()
if not repo.has_changes():
if not options['quiet']:
print('No Release Note Updates')
return
count = ProductRelease.objects.refresh()
if not options['quiet']:
print('%s Release Notes Successfully Updated' % count)
repo.set_db_latest()

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

@ -1,25 +1,22 @@
import codecs
import json
import os
import re
from glob import glob
from hashlib import sha256
from operator import attrgetter
from django.conf import settings
from django.core.cache import caches
from django.db import models, transaction
from django.http import Http404
from django.utils.dateparse import parse_date, parse_datetime
from django.utils.dateparse import parse_datetime
from django.utils.functional import cached_property
from django.utils.text import slugify
import markdown
from django_extensions.db.fields.json import JSONField
from product_details.version_compare import Version
from raven.contrib.django.raven_compat.models import client as sentry_client
from bedrock.base.urlresolvers import reverse
from bedrock.releasenotes.utils import memoize
LONG_RN_CACHE_TIMEOUT = 7200 # 2 hours
@ -29,10 +26,6 @@ markdowner = markdown.Markdown(extensions=[
])
def release_notes_path():
return os.path.join(settings.RELEASE_NOTES_PATH, 'releases')
def process_markdown(value):
return markdowner.reset().convert(value)
@ -50,26 +43,18 @@ def process_is_public(is_public):
def process_note_release(rel_data):
return Release(rel_data)
return ProductRelease(**rel_data)
FIELD_PROCESSORS = {
'release_date': parse_date,
'created': parse_datetime,
'modified': parse_datetime,
'notes': process_notes,
'is_public': process_is_public,
'note': process_markdown,
'text': process_markdown,
'system_requirements': process_markdown,
'fixed_in_release': process_note_release,
}
class ReleaseNotFound(Exception):
pass
class RNModel(object):
def __init__(self, data):
for key, value in data.items():
@ -111,45 +96,50 @@ class NotesField(JSONField):
class ProductReleaseQuerySet(models.QuerySet):
def public(self):
if settings.DEV:
return self.all()
return self.filter(is_public=True)
def product(self, product_name, channel_name=None):
def product(self, product_name, channel_name=None, version=None):
if product_name.lower() == 'firefox extended support release':
product_name = 'firefox'
channel_name = 'esr'
q = self.filter(product__iexact=product_name)
if channel_name:
q = q.filter(channel__iexact=channel_name)
if version:
q = q.filter(version=version)
return q
class ProductReleaseManager(models.Manager):
def get_queryset(self):
return ProductReleaseQuerySet(self.model, using=self._db)
qs = ProductReleaseQuerySet(self.model, using=self._db)
if settings.DEV:
return qs
def public(self):
return self.get_queryset().public()
return qs.filter(is_public=True)
def product(self, product_name, channel_name=None):
return self.get_queryset().product(product_name, channel_name)
def product(self, product_name, channel_name=None, version=None):
return self.get_queryset().product(product_name, channel_name, version)
def refresh(self):
count = 0
release_objs = []
rn_path = os.path.join(settings.RELEASE_NOTES_PATH, 'releases')
with transaction.atomic(using=self.db):
self.all().delete()
releases = glob(os.path.join(release_notes_path(), '*.json'))
releases = glob(os.path.join(rn_path, '*.json'))
for release_file in releases:
with codecs.open(release_file, 'r', encoding='utf-8') as rel_fh:
data = json.load(rel_fh)
# doing this to simplify queries for Firefox since it is always
# looked up with product=Firefox and relies on the version number
# and channel to determine ESR.
if data['product'] == 'Firefox Extended Support Release':
data['product'] = 'Firefox'
data['channel'] = 'ESR'
self.create(**data)
count += 1
release_objs.append(ProductRelease(**data))
return count
self.bulk_create(release_objs)
return len(release_objs)
class ProductRelease(models.Model):
@ -175,27 +165,12 @@ class ProductRelease(models.Model):
objects = ProductReleaseManager()
class Meta:
ordering = ['-release_date']
def __unicode__(self):
return self.title
class Release(RNModel):
CHANNELS = ['release', 'esr', 'beta', 'aurora', 'nightly']
product = None
channel = None
version = None
slug = None
title = None
release_date = None
text = ''
is_public = True
bug_list = None
bug_search_url = None
system_requirements = None
created = None
modified = None
notes = None
@cached_property
def major_version(self):
return str(self.version_obj.major)
@ -258,13 +233,9 @@ class Release(RNModel):
channel and major version with the highest minor version,
or None if no such releases exist
"""
major_version_file_id = get_file_id(product, self.channel, self.major_version + '.*')
releases = glob(os.path.join(release_notes_path(), major_version_file_id + '.json'))
releases = ProductRelease.objects.product(product, self.channel).filter(version__startswith='%s.' % self.major_version)
if releases:
releases = [get_release_from_file(fn) for fn in releases]
releases = [r for r in releases if r.is_public]
if releases:
return sorted(releases, reverse=True, key=attrgetter('version_obj'))[0]
return sorted(releases, reverse=True, key=attrgetter('version_obj'))[0]
return None
@ -277,142 +248,47 @@ class Release(RNModel):
return self.equivalent_release_for_product('Firefox')
@memoize(LONG_RN_CACHE_TIMEOUT)
def get_release(product, version, channel=None):
channels = [channel] if channel else Release.CHANNELS
channels = [channel] if channel else ProductRelease.CHANNELS
if product.lower() == 'firefox extended support release':
product = 'firefox'
channels = ['esr']
for channel in channels:
file_name = get_release_file_name(product, channel, version)
if not file_name:
try:
return ProductRelease.objects.product(product, channel, version).get()
except ProductRelease.DoesNotExist:
continue
release = get_release_from_file(file_name)
if release is not None:
return release
raise ReleaseNotFound()
def get_data_version():
"""Add the etag from the repo to the cache keys.
This will ensure that the cache is invalidated when the repo is updated.
"""
etag_key = 'releasenotes:repo:etag'
etag = cache.get(etag_key)
if not etag:
etag_file = os.path.join(release_notes_path(), '.latest-update-etag')
if os.path.exists(etag_file):
try:
with codecs.open(etag_file) as fh:
etag = fh.read().strip()
cache.set(etag_key, etag, 60) # 1 min
except IOError:
etag = 'default'
else:
etag = 'default'
return etag
def get_cache_key(key):
"""Cache key returned will be a sha256 hash of the key and repo data version.
This ensures that we can use a long cache for the release files while still
getting fast invalidation when we check the small repo data version file
at most once per minute.
"""
return sha256('%s:%s' % (get_data_version(), key)).hexdigest()
def get_release_from_file(file_name):
cache_key = get_cache_key(file_name)
release = cache.get(cache_key)
if not release:
release = get_release_from_file_system(file_name)
if release:
cache.set(cache_key, release, LONG_RN_CACHE_TIMEOUT)
return release
def get_release_from_file_system(file_name):
try:
with codecs.open(file_name, 'r', encoding='utf-8') as rel_fh:
return Release(json.load(rel_fh))
except Exception:
sentry_client.captureException()
return None
def get_release_file_name(product, channel, version):
file_id = get_file_id(product, channel, version)
file_name = os.path.join(release_notes_path(), '{}.json'.format(file_id))
if os.path.exists(file_name):
return file_name
return None
def get_file_id(product, channel, version):
product = slugify(product)
channel = channel.lower()
if product == 'firefox-extended-support-release':
product = 'firefox'
channel = 'esr'
return '-'.join([product, version, channel])
def get_release_or_404(version, product):
try:
release = get_release(product, version)
except ReleaseNotFound:
raise Http404
if not release.is_public:
release = get_release(product, version)
if release is None:
raise Http404
return release
def get_all_releases(product, channel='release'):
file_prefix = get_file_id(product, channel, '*')
cache_key = get_cache_key('all:%s:%s' % (product, channel))
releases = cache.get(cache_key)
product_prefix = file_prefix.split('*')[0]
# ensure only files for the specific product are returned
# without this the file glob would match e.g. "firefox-for-android-56.0-release.json"
# when the glob was "firefox-*-release.json"
product_re = re.compile(r'%s\d' % product_prefix)
if not releases:
releases = glob(os.path.join(release_notes_path(), file_prefix + '.json'))
if releases:
releases = (get_release_from_file(r) for r in releases if product_re.search(r))
releases = sorted((r for r in releases if r.is_public),
key=attrgetter('release_date'), reverse=True)
if releases:
cache.set(cache_key, releases, LONG_RN_CACHE_TIMEOUT)
return releases
@memoize(LONG_RN_CACHE_TIMEOUT)
def get_releases(product, channel, num_results=10):
return ProductRelease.objects.product(product, channel)[:num_results]
def get_releases_or_404(product, channel):
releases = get_all_releases(product, channel)
def get_releases_or_404(product, channel, num_results=10):
releases = get_releases(product, channel, num_results)
if releases:
return releases
raise Http404
@memoize(LONG_RN_CACHE_TIMEOUT)
def get_latest_release(product, channel='release'):
cache_key = get_cache_key('latest:%s:%s' % (product, channel))
release = cache.get(cache_key)
if not release:
releases = get_all_releases(product, channel)
if releases:
release = releases[0]
cache.set(cache_key, release, LONG_RN_CACHE_TIMEOUT)
try:
release = ProductRelease.objects.product(product, channel)[0]
except IndexError:
release = None
return release

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

@ -2,5 +2,8 @@
"product": "Firefox",
"channel": "Release",
"version": "56.0",
"release_date": "2017-08-02",
"created": "2017-03-21T13:19:13.668000+00:00",
"modified": "2017-03-21T13:19:13.668000+00:00",
"is_public": true
}

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

@ -3,5 +3,7 @@
"channel": "Release",
"version": "56.0",
"release_date": "2017-08-02",
"created": "2017-03-21T13:19:13.668000+00:00",
"modified": "2017-03-21T13:19:13.668000+00:00",
"is_public": true
}

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

@ -3,5 +3,7 @@
"channel": "Release",
"version": "56.0.1",
"release_date": "2017-08-03",
"created": "2017-03-21T13:19:13.668000+00:00",
"modified": "2017-03-21T13:19:13.668000+00:00",
"is_public": true
}

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

@ -3,5 +3,7 @@
"channel": "Release",
"version": "56.0.2",
"release_date": "2017-08-04",
"created": "2017-03-21T13:19:13.668000+00:00",
"modified": "2017-03-21T13:19:13.668000+00:00",
"is_public": true
}

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

@ -3,5 +3,7 @@
"channel": "Release",
"version": "56.0.3",
"release_date": "2017-08-05",
"created": "2017-03-21T13:19:13.668000+00:00",
"modified": "2017-03-21T13:19:13.668000+00:00",
"is_public": false
}

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

@ -0,0 +1,71 @@
import time
from mock import Mock, patch
from django.core.cache import caches
from bedrock.mozorg.tests import TestCase
from bedrock.releasenotes import utils
release_cache = caches['release-notes']
@patch.object(utils, 'GitRepo')
class TestGetDataVersion(TestCase):
def test_get_data_version(self, git_mock):
git_mock().get_db_latest.return_value = 'El Dudarino'
assert utils.get_data_version() == 'El Dudarino'
def test_get_data_version_not_found(self, git_mock):
git_mock().get_db_latest.return_value = None
assert utils.get_data_version() == 'default'
@patch.object(utils, 'get_data_version')
class TestReleaseMemoizer(TestCase):
def setUp(self):
release_cache.clear()
def test_calls_version_after_cache_timeout(self, gdv_cache):
def mem_func():
pass
gdv_cache.return_value = 'dude'
memoizer = utils.ReleaseMemoizer(version_timeout=0.1)
memoizer._memoize_version(mem_func)
memoizer._memoize_version(mem_func)
time.sleep(0.2)
memoizer._memoize_version(mem_func)
assert gdv_cache.call_count == 2
def test_calls_function_when_version_changes(self, gdv_cache):
"""Memoized function should be called after timeout or version change.
Also demonstrates that even None return values are cached."""
counter = Mock()
memoizer = utils.ReleaseMemoizer(version_timeout=0.1)
gdv_cache.side_effect = ['thing1', 'thing1', 'thing2', 'thing2']
@memoizer.memoize(1)
def mem_func():
counter()
return None
mem_func()
# cached
mem_func()
time.sleep(0.2)
# cached, but checked the version
mem_func()
time.sleep(0.2)
# not cached because the version changed
mem_func()
time.sleep(1)
# not cached because timeout
mem_func()
# function should have been called 3 times
assert counter.call_count == 3
# version should have been called 4 times
assert gdv_cache.call_count == 4

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

@ -16,7 +16,7 @@ from pyquery import PyQuery as pq
from bedrock.firefox.firefox_details import FirefoxDesktop
from bedrock.mozorg.tests import TestCase
from bedrock.releasenotes import views
from bedrock.releasenotes.models import Release, ReleaseNotFound
from bedrock.releasenotes.models import ProductRelease
from bedrock.thunderbird.details import ThunderbirdDesktop
@ -30,6 +30,7 @@ RELEASES_PATH = str(TESTS_PATH)
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH)
class TestReleaseViews(TestCase):
def setUp(self):
ProductRelease.objects.refresh()
caches['release-notes'].clear()
self.activate('en-US')
self.factory = RequestFactory()
@ -55,7 +56,7 @@ class TestReleaseViews(TestCase):
eq_(views.get_release_or_404('version', 'product'),
get_release.return_value)
get_release.assert_called_with('product', 'version')
get_release.side_effect = ReleaseNotFound
get_release.return_value = None
with self.assertRaises(Http404):
views.get_release_or_404('version', 'product')
@ -82,8 +83,7 @@ class TestReleaseViews(TestCase):
"""
mock_release = get_release_or_404.return_value
mock_release.major_version = '34'
mock_release.notes.return_value = ([Release({}), Release({})],
[Release({}), Release({})])
mock_release.notes.return_value = []
views.release_notes(self.request, '27.0')
get_release_or_404.assert_called_with('27.0', 'Firefox')
@ -165,9 +165,9 @@ class TestReleaseViews(TestCase):
@patch('bedrock.releasenotes.models.get_release')
def test_non_public_release(self, get_release):
"""
Should raise 404 if not release.is_public and not settings.DEV
Should raise 404 if release is not public and not settings.DEV
"""
get_release.return_value = Release({'is_public': False})
get_release.return_value = None
with self.assertRaises(Http404):
views.get_release_or_404('42', 'Firefox')
@ -305,7 +305,7 @@ class TestNotesRedirects(TestCase):
eq_(response['Location'], 'http://testserver/en-US' + url_to)
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='22.0', channel='Release'))))
Mock(return_value=ProductRelease(product='Firefox', version='22.0', channel='Release')))
def test_desktop_release_version(self):
self._test('/firefox/notes/',
'/firefox/22.0/releasenotes/')
@ -313,49 +313,49 @@ class TestNotesRedirects(TestCase):
'/firefox/22.0/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='23.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Firefox', version='23.0beta', channel='Beta')))
def test_desktop_beta_version(self):
self._test('/firefox/beta/notes/',
'/firefox/23.0beta/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='23.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Firefox', version='23.0beta', channel='Beta')))
def test_desktop_developer_version(self):
self._test('/firefox/developer/notes/',
'/firefox/23.0beta/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='24.2.0', channel='ESR'))))
Mock(return_value=ProductRelease(product='Firefox', version='24.2.0', channel='ESR')))
def test_desktop_esr_version(self):
self._test('/firefox/organizations/notes/',
'/firefox/24.2.0/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox for Android', version='22.0', channel='Release'))))
Mock(return_value=ProductRelease(product='Firefox for Android', version='22.0', channel='Release')))
def test_android_release_version(self):
self._test('/firefox/android/notes/',
'/firefox/android/22.0/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox for Android', version='23.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Firefox for Android', version='23.0beta', channel='Beta')))
def test_android_beta_version(self):
self._test('/firefox/android/beta/notes/',
'/firefox/android/23.0beta/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox for Android', version='24.0a2', channel='Aurora'))))
Mock(return_value=ProductRelease(product='Firefox for Android', version='24.0a2', channel='Aurora')))
def test_android_aurora_version(self):
self._test('/firefox/android/aurora/notes/',
'/firefox/android/24.0a2/auroranotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox for iOS', version='1.4', channel='Release'))))
Mock(return_value=ProductRelease(product='Firefox for iOS', version='1.4', channel='Release')))
def test_ios_release_version(self):
self._test('/firefox/ios/notes/',
'/firefox/ios/1.4/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Thunderbird', version='22.0', channel='Release'))))
Mock(return_value=ProductRelease(product='Thunderbird', version='22.0', channel='Release')))
def test_thunderbird_release_version(self):
self._test('/thunderbird/notes/',
'/thunderbird/22.0/releasenotes/')
@ -363,13 +363,13 @@ class TestNotesRedirects(TestCase):
'/thunderbird/22.0/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Thunderbird', version='41.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Thunderbird', version='41.0beta', channel='Beta')))
def test_thunderbird_beta_version(self):
self._test('/thunderbird/beta/notes/',
'/thunderbird/41.0beta/releasenotes/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Thunderbird', version='41.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Thunderbird', version='41.0beta', channel='Beta')))
def test_thunderbird_earlybird_version(self):
self._test('/thunderbird/earlybird/notes/',
'/thunderbird/41.0beta/releasenotes/')
@ -384,31 +384,31 @@ class TestSysreqRedirect(TestCase):
eq_(response['Location'], 'http://testserver/en-US' + url_to)
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='22.0', channel='Release'))))
Mock(return_value=ProductRelease(product='Firefox', version='22.0', channel='Release')))
def test_desktop_release_version(self):
self._test('/firefox/system-requirements/',
'/firefox/22.0/system-requirements/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='23.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Firefox', version='23.0beta', channel='Beta')))
def test_desktop_beta_version(self):
self._test('/firefox/beta/system-requirements/',
'/firefox/23.0beta/system-requirements/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='23.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Firefox', version='23.0beta', channel='Beta')))
def test_desktop_developer_version(self):
self._test('/firefox/developer/system-requirements/',
'/firefox/23.0beta/system-requirements/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Firefox', version='24.2.0', channel='ESR'))))
Mock(return_value=ProductRelease(product='Firefox', version='24.2.0', channel='ESR')))
def test_desktop_esr_version(self):
self._test('/firefox/organizations/system-requirements/',
'/firefox/24.2.0/system-requirements/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Thunderbird', version='22.0', channel='Release'))))
Mock(return_value=ProductRelease(product='Thunderbird', version='22.0', channel='Release')))
def test_thunderbird_release_version(self):
self._test('/thunderbird/system-requirements/',
'/thunderbird/22.0/system-requirements/')
@ -416,7 +416,7 @@ class TestSysreqRedirect(TestCase):
'/thunderbird/22.0/system-requirements/')
@patch('bedrock.releasenotes.views.get_latest_release_or_404',
Mock(return_value=Release(dict(product='Thunderbird', version='41.0beta', channel='Beta'))))
Mock(return_value=ProductRelease(product='Thunderbird', version='41.0beta', channel='Beta')))
def test_thunderbird_beta_version(self):
self._test('/thunderbird/beta/system-requirements/',
'/thunderbird/41.0beta/system-requirements/')

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

@ -2,13 +2,12 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
from itertools import chain
from django.conf import settings
from django.core.cache import caches
from django.test.utils import override_settings
from mock import call, patch, Mock
from mock import call, patch
from pathlib2 import Path
from bedrock.mozorg.tests import TestCase
@ -25,8 +24,7 @@ class TestReleaseNotesURL(TestCase):
"""
Should return the results of reverse with the correct args
"""
release = models.Release(dict(
channel='Aurora', version='42.0a2', product='Firefox for Android'))
release = models.ProductRelease(channel='Aurora', version='42.0a2', product='Firefox for Android')
assert release.get_absolute_url() == mock_reverse.return_value
mock_reverse.assert_called_with('firefox.android.releasenotes', args=['42.0a2', 'aurora'])
@ -34,7 +32,7 @@ class TestReleaseNotesURL(TestCase):
"""
Should return the results of reverse with the correct args
"""
release = models.Release(dict(version='42.0', product='Firefox'))
release = models.ProductRelease(version='42.0', product='Firefox')
assert release.get_absolute_url() == mock_reverse.return_value
mock_reverse.assert_called_with('firefox.desktop.releasenotes', args=['42.0', 'release'])
@ -42,6 +40,7 @@ class TestReleaseNotesURL(TestCase):
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH, DEV=False)
class TestReleaseModel(TestCase):
def setUp(self):
models.ProductRelease.objects.refresh()
release_cache.clear()
def test_release_major_version(self):
@ -64,14 +63,7 @@ class TestReleaseModel(TestCase):
assert android.product == 'Firefox for Android'
def test_equivalent_release_for_product_none_match(self):
rel = models.get_release('firefox', '45.0esr', 'esr')
android = rel.equivalent_release_for_product('Firefox for Android')
assert android is None
@patch.object(models, 'get_release_from_file')
def test_equivalent_release_for_product_no_public_match(self, grff_mock):
rel = models.Release(dict(product='Firefox', version='56.0', is_public=True, channel='Release'))
grff_mock.return_value = models.Release(dict(product='Firefox for Android', is_public=False, version='56.0.2', channel='Release'))
rel = models.get_release('firefox', '45.0esr')
android = rel.equivalent_release_for_product('Firefox for Android')
assert android is None
@ -105,258 +97,52 @@ class TestReleaseModel(TestCase):
assert note.id == 787203
@override_settings(DEV=False)
def test_is_public_field_processor(self):
"""Should return the real value when DEV is false."""
rel = models.get_release('firefox for android', '56.0.3')
assert not rel.is_public
def test_is_public_query(self):
"""Should not return the release value when DEV is false.
Should also only include public notes."""
assert models.get_release('firefox for android', '56.0.3') is None
rel = models.get_release('firefox', '57.0a1')
assert len(rel.notes) == 4
@override_settings(DEV=True)
def test_is_public_field_processor_dev_true(self):
"""Should always be true when DEV is true."""
rel = models.get_release('firefox for android', '56.0.3')
assert rel.is_public
models.get_release('firefox for android', '56.0.3')
rel = models.get_release('firefox', '57.0a1')
assert len(rel.notes) == 6
@patch.object(models, 'get_release_file_name')
@patch.object(models, 'get_release_from_file')
@patch.object(models.ProductRelease, 'objects')
class TestGetRelease(TestCase):
def test_get_release(self, grff_mock, grfn_mock):
grfn_mock.return_value = 'dude'
grff_mock.return_value = 'dude is released'
ret = models.get_release('Firefox', '57.0')
grfn_mock.assert_called_with('Firefox', models.Release.CHANNELS[0], '57.0')
grff_mock.assert_called_with('dude')
assert ret == 'dude is released'
def setUp(self):
release_cache.clear()
def test_get_release_esr(self, grff_mock, grfn_mock):
grfn_mock.return_value = 'dude'
grff_mock.return_value = 'dude is released'
ret = models.get_release('Firefox Extended Support Release', '51.0')
grfn_mock.assert_called_with('firefox', 'esr', '51.0')
grff_mock.assert_called_with('dude')
assert ret == 'dude is released'
def test_get_release(self, manager_mock):
manager_mock.product().get.return_value = 'dude is released'
assert models.get_release('Firefox', '57.0') == 'dude is released'
manager_mock.product.assert_called_with('Firefox', models.ProductRelease.CHANNELS[0], '57.0')
def test_get_release_none_match(self, grff_mock, grfn_mock):
def test_get_release_esr(self, manager_mock):
manager_mock.product().get.return_value = 'dude is released'
assert models.get_release('Firefox Extended Support Release', '51.0') == 'dude is released'
manager_mock.product.assert_called_with('Firefox Extended Support Release', 'esr', '51.0')
def test_get_release_none_match(self, manager_mock):
"""Make sure the proper exception is raised if no file matches the query"""
grfn_mock.return_value = None
with self.assertRaises(models.ReleaseNotFound):
models.get_release('Firefox', '57.0')
manager_mock.product().get.side_effect = models.ProductRelease.DoesNotExist
assert models.get_release('Firefox', '57.0') is None
expected_calls = [call('Firefox', ch, '57.0') for ch in models.Release.CHANNELS]
grfn_mock.assert_has_calls(expected_calls)
def test_get_release_none_load(self, grff_mock, grfn_mock):
"""Make sure the proper exception is raised if no file successfully loads"""
grfn_mock.return_value = 'dude'
grff_mock.return_value = None
with self.assertRaises(models.ReleaseNotFound):
models.get_release('Firefox', '57.0')
expected_calls = [call('Firefox', ch, '57.0') for ch in models.Release.CHANNELS]
grfn_mock.assert_has_calls(expected_calls)
expected_calls = chain.from_iterable((call('Firefox', ch, '57.0'), call().get()) for ch in models.ProductRelease.CHANNELS)
manager_mock.product.assert_has_calls(expected_calls)
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH)
@patch.object(models, 'cache')
@patch.object(models, 'get_cache_key')
@patch.object(models, 'get_release_from_file_system')
class TestGetReleaseFromFile(TestCase):
def test_get_release_from_file(self, grffs_mock, cache_key_mock, cache_mock):
cache_mock.get.return_value = 'dude'
assert models.get_release_from_file('walter') == 'dude'
cache_key_mock.assert_called_with('walter')
grffs_mock.assert_not_called()
def test_get_release_from_file_no_cache(self, grffs_mock, cache_key_mock, cache_mock):
cache_mock.get.return_value = None
grffs_mock.return_value = 'donnie'
assert models.get_release_from_file('walter') == 'donnie'
cache_key_mock.assert_called_with('walter')
grffs_mock.assert_called_with('walter')
cache_mock.set.assert_called_with(cache_key_mock(), 'donnie', models.LONG_RN_CACHE_TIMEOUT)
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH)
class TestGetReleaseFromFileSystem(TestCase):
def test_get_release_from_file_system(self):
filename = models.get_release_file_name('firefox', 'nightly', '57.0a1')
rel = models.get_release_from_file_system(filename)
assert rel.product == 'Firefox'
assert rel.channel == 'Nightly'
assert rel.version == '57.0a1'
@patch.object(models, 'codecs')
def test_get_release_from_file_system_exception(self, codecs_mock):
codecs_mock.open.side_effect = IOError()
assert models.get_release_from_file_system('does-not-exist') is None
@patch('os.path.exists')
@patch.object(models, 'get_file_id')
class TestGetReleaseFileName(TestCase):
def test_get_release_file_name(self, gfi_mock, exists_mock):
gfi_mock.return_value = 'dude'
exists_mock.return_value = True
file_name = os.path.join(settings.RELEASE_NOTES_PATH, 'releases', 'dude.json')
assert models.get_release_file_name('firefox', 'nightly', '57.0a1') == file_name
gfi_mock.assert_called_with('firefox', 'nightly', '57.0a1')
exists_mock.assert_called_with(file_name)
def test_get_release_file_name_no_exists(self, gfi_mock, exists_mock):
gfi_mock.return_value = 'dude'
exists_mock.return_value = False
file_name = os.path.join(settings.RELEASE_NOTES_PATH, 'releases', 'dude.json')
assert models.get_release_file_name('firefox', 'nightly', '57.0a1') is None
gfi_mock.assert_called_with('firefox', 'nightly', '57.0a1')
exists_mock.assert_called_with(file_name)
class TestGetFileID(TestCase):
def test_get_file_id(self):
assert models.get_file_id('Firefox', 'Nightly', '57.0a1') == 'firefox-57.0a1-nightly'
assert models.get_file_id('Firefox', 'Release', '57.0') == 'firefox-57.0-release'
assert models.get_file_id('Firefox Extended Support Release', 'ESR', '52.0') == 'firefox-52.0-esr'
assert models.get_file_id('Firefox for Android', 'Beta', '57.0b2') == 'firefox-for-android-57.0b2-beta'
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH)
@patch.object(models, 'cache')
class TestGetDataVersion(TestCase):
def test_get_data_version(self, cache_mock):
cache_mock.get.return_value = None
# value from the test data in .latest-update-etag
assert models.get_data_version() == '"bae656422b8d046543540f42b1658938f"'
cache_mock.set.assert_called_with('releasenotes:repo:etag', '"bae656422b8d046543540f42b1658938f"', 60)
def test_get_data_version_cache_hit(self, cache_mock):
cache_mock.get.return_value = 'dude'
assert models.get_data_version() == 'dude'
cache_mock.set.assert_not_called()
@patch.object(models, 'os')
def test_get_data_version_file_not_found(self, os_mock, cache_mock):
cache_mock.get.return_value = None
os_mock.path.exists.return_value = False
assert models.get_data_version() == 'default'
cache_mock.set.assert_not_called()
@patch.object(models, 'codecs')
def test_get_data_version_io_error(self, open_mock, cache_mock):
cache_mock.get.return_value = None
open_mock.open.side_effect = IOError
assert models.get_data_version() == 'default'
cache_mock.set.assert_not_called()
@patch.object(models, 'get_data_version')
@patch.object(models, 'sha256')
class TestGetCacheKey(TestCase):
def test_get_cache_key(self, sha_mock, gdv_mock):
gdv_mock.return_value = 'dude'
assert models.get_cache_key('abide') == sha_mock.return_value.hexdigest()
sha_mock.assert_called_with('dude:abide')
@override_settings(DEV=False)
@patch.object(models, 'cache')
@patch.object(models, 'glob')
@patch.object(models, 'get_release_from_file')
@patch.object(models, 'get_cache_key', Mock(return_value='dude'))
class TestGetAllReleases(TestCase):
def test_get_all_releases(self, grff_mock, glob_mock, cache_mock):
releases = []
globs = []
calls = []
for i in range(5):
globs.append('firefox-%s.json' % (i + 1))
calls.append(call(globs[-1]))
releases.append(models.Release(dict(product='Firefox',
channel='Release',
is_public=True,
version='56.0.%s' % (i + 1),
release_date='2017-01-0%s' % (i + 1))))
grff_mock.side_effect = releases
glob_mock.return_value = globs
cache_mock.get.return_value = None
reversed_releases = list(reversed(releases))
assert models.get_all_releases('firefox', 'release') == reversed_releases
grff_mock.assert_has_calls(calls)
cache_mock.get.assert_called_with('dude')
cache_mock.set.assert_called_with('dude', reversed_releases, models.LONG_RN_CACHE_TIMEOUT)
def test_get_all_releases_only_product(self, grff_mock, glob_mock, cache_mock):
"""Should only return the specific product asked for"""
releases = []
globs = []
calls = []
for i in range(2):
globs.append('firefox-%s.json' % (i + 1))
calls.append(call(globs[-1]))
releases.append(models.Release(dict(product='Firefox',
channel='Release',
is_public=True,
version='56.0.%s' % (i + 1),
release_date='2017-01-0%s' % (i + 1))))
# android ones are newer
for i in range(2):
globs.append('firefox-for-android-%s.json' % (i + 1))
releases.append(models.Release(dict(product='Firefox For Android',
channel='Release',
is_public=True,
version='56.0.%s' % (i + 1),
release_date='2017-01-1%s' % (i + 1))))
grff_mock.side_effect = releases
glob_mock.return_value = globs
cache_mock.get.return_value = None
reversed_releases = list(reversed(releases[:2]))
assert models.get_all_releases('firefox', 'release') == reversed_releases
grff_mock.assert_has_calls(calls)
cache_mock.get.assert_called_with('dude')
cache_mock.set.assert_called_with('dude', reversed_releases, models.LONG_RN_CACHE_TIMEOUT)
def test_get_all_releases_no_files(self, grff_mock, glob_mock, cache_mock):
glob_mock.return_value = []
cache_mock.get.return_value = None
assert models.get_all_releases('firefox', 'release') == []
cache_mock.get.assert_called_with('dude')
cache_mock.set.assert_not_called()
def test_get_all_releases_none_public(self, grff_mock, glob_mock, cache_mock):
releases = []
globs = []
calls = []
for i in range(5):
globs.append('firefox-%s.json' % (i + 1))
calls.append(call(globs[-1]))
releases.append(models.Release(dict(product='Firefox',
channel='Release',
is_public=False,
version='56.0.%s' % (i + 1),
release_date='2017-01-0%s' % (i + 1))))
grff_mock.side_effect = releases
glob_mock.return_value = globs
cache_mock.get.return_value = None
assert models.get_all_releases('firefox', 'release') == []
grff_mock.assert_has_calls(calls)
cache_mock.get.assert_called_with('dude')
print cache_mock.set.mock_calls
cache_mock.set.assert_not_called()
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH)
@patch.object(models, 'cache')
@patch.object(models, 'get_all_releases')
@override_settings(RELEASE_NOTES_PATH=RELEASES_PATH, DEV=False)
class TestGetLatestRelease(TestCase):
def test_latest_release(self, gar_mock, cache_mock):
releases = [Mock(), Mock()]
gar_mock.return_value = releases
cache_mock.get.return_value = None
assert models.get_latest_release('firefox', 'release') == releases[0]
cache_mock.set.assert_called_with('cd1252c0b9e7db652d816a45c7c813696456bd124a64fc54452a73220831f081',
releases[0], models.LONG_RN_CACHE_TIMEOUT)
def setUp(self):
models.ProductRelease.objects.refresh()
release_cache.clear()
def test_latest_release(self):
correct_release = models.get_release('firefox for android', '56.0.2')
assert models.get_latest_release('firefox for android', 'release') == correct_release

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

@ -0,0 +1,39 @@
from django.conf import settings
from django.core.cache import caches
from memoize import Memoizer
from bedrock.utils.git import GitRepo
def get_data_version():
"""Add the git ref from the repo to the cache keys.
This will ensure that the cache is invalidated when the repo is updated.
"""
repo = GitRepo(settings.RELEASE_NOTES_PATH,
settings.RELEASE_NOTES_REPO,
branch_name=settings.RELEASE_NOTES_BRANCH)
git_ref = repo.get_db_latest()
if git_ref is None:
git_ref = 'default'
return git_ref
class ReleaseMemoizer(Memoizer):
"""A memoizer class that uses the git hash as the version"""
def __init__(self, version_timeout=300):
self.version_timeout = version_timeout
return super(ReleaseMemoizer, self).__init__(cache=caches['release-notes'])
def _memoize_make_version_hash(self):
return get_data_version()
def _memoize_version(self, f, args=None, reset=False, delete=False, timeout=None):
"""Use a shorter timeout for the version so that we can refresh based on git hash"""
return super(ReleaseMemoizer, self)._memoize_version(f, args, reset, delete, self.version_timeout)
memoizer = ReleaseMemoizer()
memoize = memoizer.memoize

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

@ -180,7 +180,7 @@ def releases_index(request, product):
def nightly_feed(request):
"""Serve an Atom feed with the latest changes in Firefox Nightly"""
notes = {}
releases = get_releases_or_404('firefox', 'nightly')[0:5]
releases = get_releases_or_404('firefox', 'nightly', 5)
for release in releases:
link = reverse('firefox.desktop.releasenotes',
@ -191,7 +191,7 @@ def nightly_feed(request):
continue
if note.is_public and note.tag:
note.link = link + '#note-' + str(note.id)
note.link = '%s#note-%s' % (link, note.id)
note.version = release.version
notes[note.id] = note

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

@ -51,6 +51,7 @@ CACHES['product-details'] = {
CACHES['release-notes'] = {
'BACKEND': 'bedrock.base.cache.SimpleDictCache',
'LOCATION': 'release-notes',
'TIMEOUT': 5,
'OPTIONS': {
'MAX_ENTRIES': 300, # currently 564 json files but most are rarely accessed
'CULL_FREQUENCY': 4, # 1/4 entries deleted if max reached

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

@ -99,8 +99,8 @@ def schedule_database_jobs():
call_command('update_wordpress --database bedrock')
@scheduled_job('interval', minutes=5)
def update_release_notes_data():
call_command('update_release_notes_data --quiet')
def update_release_notes():
call_command('update_release_notes --quiet')
def schedul_l10n_jobs():
@ -108,10 +108,6 @@ def schedul_l10n_jobs():
def update_locales():
call_command('l10n_update')
@scheduled_job('interval', minutes=5)
def update_release_notes():
call_command('update_release_notes --quiet')
if __name__ == '__main__':
args = sys.argv[1:]

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

@ -13,7 +13,6 @@ fi
./manage.py update_security_advisories
./manage.py l10n_update
./manage.py update_release_notes
./manage.py update_release_notes_data
./manage.py update_sitemaps
#requires twitter api credentials not distributed publicly
./manage.py cron update_tweets

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

@ -88,7 +88,6 @@ if $PROD_MODE && ! imageExists "l10n"; then
ENVFILE="master";
fi
dockerRun $ENVFILE code "python manage.py l10n_update"
dockerRun $ENVFILE code "python manage.py update_release_notes"
dockerRun $ENVFILE code "python manage.py update_sitemaps"
docker/bin/docker_build.sh "l10n"
fi

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

@ -1,7 +1,6 @@
FROM mozorg/bedrock_code:${GIT_COMMIT}
COPY ./locale ./locale
COPY ./release_notes ./release_notes
COPY ./root_files/sitemap.xml ./root_files/
COPY ./root_files/default-urls.json ./root_files/
COPY ./bedrock.db ./

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

@ -1,13 +1,11 @@
FROM mozorg/bedrock_code:${GIT_COMMIT}
COPY ./locale ./locale
COPY ./release_notes ./release_notes
COPY ./root_files/sitemap.xml ./root_files/
COPY ./root_files/default-urls.json ./root_files/
# Change User
USER root
RUN chown webdev.webdev -R locale
RUN chown webdev.webdev -R release_notes
RUN chown webdev.webdev -R root_files
USER webdev

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

@ -139,3 +139,5 @@ pbr==1.10.0 \
mock==2.0.0 \
--hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \
--hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba
django-memoize==2.1.0 \
--hash=sha256:ad969e02f25dab6484626cc3b023c2ff17004b0e3bbd8386467865826bc2bc6c