Commit 8b5e5c59 by Nimisha Asthagiri

Merge branch 'release'

parents 091dc4c3 4b643164
......@@ -308,9 +308,7 @@ MIDDLEWARE_CLASSES = (
'embargo.middleware.EmbargoMiddleware',
# Detects user-requested locale from 'accept-language' header in http request
# TODO: Re-import the Django version once we upgrade to Django 1.8 [PLAT-671]
# 'django.middleware.locale.LocaleMiddleware',
'django_locale.middleware.LocaleMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.transaction.TransactionMiddleware',
# needs to run after locale middleware (or anything that modifies the request context)
......
......@@ -115,7 +115,10 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_location_mem_cache',
},
'course_structure_cache': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_course_structure_mem_cache',
},
}
# Make the keyedcache startup warnings go away
......
......@@ -166,7 +166,9 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_location_mem_cache',
},
'course_structure_cache': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
},
}
# Add external_auth to Installed apps for testing
......
......@@ -43,6 +43,7 @@
'js/certificates/factories/certificates_page_factory',
'js/factories/import',
'js/factories/index',
'js/factories/library',
'js/factories/login',
'js/factories/manage_users',
'js/factories/outline',
......
......@@ -4,9 +4,6 @@
<a class="link-tab" href="#tab1"><%= gettext("Common Problem Types") %></a>
</li>
<li>
<a class="link-tab" href="#tab2"><%= gettext("Common Problems with Hints and Feedback") %></a>
</li>
<li>
<a class="link-tab" href="#tab3"><%= gettext("Advanced") %></a>
</li>
</ul>
......
......@@ -12,11 +12,9 @@ the SessionMiddleware.
"""
from django.conf import settings
from dark_lang.models import DarkLangConfig
from django.utils.translation.trans_real import parse_accept_lang_header
# TODO re-import this once we're on Django 1.5 or greater. [PLAT-671]
# from django.utils.translation.trans_real import parse_accept_lang_header
from django_locale.trans_real import parse_accept_lang_header
from dark_lang.models import DarkLangConfig
def dark_parse_accept_lang_header(accept):
......@@ -83,17 +81,11 @@ class DarkLangMiddleware(object):
self._clean_accept_headers(request)
self._activate_preview_language(request)
def _fuzzy_match(self, lang_code):
"""Returns a fuzzy match for lang_code"""
if lang_code in self.released_langs:
return lang_code
lang_prefix = lang_code.partition('-')[0]
for released_lang in self.released_langs:
released_prefix = released_lang.partition('-')[0]
if lang_prefix == released_prefix:
return released_lang
return None
def _is_released(self, lang_code):
"""
``True`` iff one of the values in ``self.released_langs`` is a prefix of ``lang_code``.
"""
return any(lang_code.lower().startswith(released_lang.lower()) for released_lang in self.released_langs)
def _format_accept_value(self, lang, priority=1.0):
"""
......@@ -110,13 +102,12 @@ class DarkLangMiddleware(object):
if accept is None or accept == '*':
return
new_accept = []
for lang, priority in dark_parse_accept_lang_header(accept):
fuzzy_code = self._fuzzy_match(lang.lower())
if fuzzy_code:
new_accept.append(self._format_accept_value(fuzzy_code, priority))
new_accept = ", ".join(new_accept)
new_accept = ", ".join(
self._format_accept_value(lang, priority)
for lang, priority
in dark_parse_accept_lang_header(accept)
if self._is_released(lang)
)
request.META['HTTP_ACCEPT_LANGUAGE'] = new_accept
......
......@@ -25,7 +25,7 @@ class DarkLangConfig(ConfigurationModel):
if not self.released_languages.strip(): # pylint: disable=no-member
return []
languages = [lang.lower().strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member
languages = [lang.strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member
# Put in alphabetical order
languages.sort()
return languages
......@@ -4,10 +4,8 @@ Tests of DarkLangMiddleware
from django.contrib.auth.models import User
from django.http import HttpRequest
import ddt
from django.test import TestCase
from mock import Mock
import unittest
from dark_lang.middleware import DarkLangMiddleware
from dark_lang.models import DarkLangConfig
......@@ -25,7 +23,6 @@ def set_if_set(dct, key, value):
dct[key] = value
@ddt.ddt
class DarkLangMiddlewareTests(TestCase):
"""
Tests of DarkLangMiddleware
......@@ -85,10 +82,6 @@ class DarkLangMiddlewareTests(TestCase):
def test_wildcard_accept(self):
self.assertAcceptEquals('*', self.process_request(accept='*'))
def test_malformed_accept(self):
self.assertAcceptEquals('', self.process_request(accept='xxxxxxxxxxxx'))
self.assertAcceptEquals('', self.process_request(accept='en;q=1.0, es-419:q-0.8'))
def test_released_accept(self):
self.assertAcceptEquals(
'rel;q=1.0',
......@@ -130,17 +123,14 @@ class DarkLangMiddlewareTests(TestCase):
)
def test_accept_released_territory(self):
# We will munge 'rel-ter' to be 'rel', so the 'rel-ter'
# user will actually receive the released language 'rel'
# (Otherwise, the user will actually end up getting the server default)
self.assertAcceptEquals(
'rel;q=1.0, rel;q=0.5',
'rel-ter;q=1.0, rel;q=0.5',
self.process_request(accept='rel-ter;q=1.0, rel;q=0.5')
)
def test_accept_mixed_case(self):
self.assertAcceptEquals(
'rel;q=1.0, rel;q=0.5',
'rel-TER;q=1.0, REL;q=0.5',
self.process_request(accept='rel-TER;q=1.0, REL;q=0.5')
)
......@@ -150,85 +140,11 @@ class DarkLangMiddlewareTests(TestCase):
enabled=True
).save()
# Since we have only released "rel-ter", the requested code "rel" will
# fuzzy match to "rel-ter", in addition to "rel-ter" exact matching "rel-ter"
self.assertAcceptEquals(
'rel-ter;q=1.0, rel-ter;q=0.5',
'rel-ter;q=1.0',
self.process_request(accept='rel-ter;q=1.0, rel;q=0.5')
)
@ddt.data(
('es;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es' should get 'es-419', not English
('es-AR;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es-AR' should get 'es-419', not English
)
@ddt.unpack
def test_partial_match_es419(self, accept_header, expected):
# Release es-419
DarkLangConfig(
released_languages=('es-419, en'),
changed_by=self.user,
enabled=True
).save()
self.assertAcceptEquals(
expected,
self.process_request(accept=accept_header)
)
def test_partial_match_esar_es(self):
# If I release 'es', 'es-AR' should get 'es', not English
DarkLangConfig(
released_languages=('es, en'),
changed_by=self.user,
enabled=True
).save()
self.assertAcceptEquals(
'es;q=1.0',
self.process_request(accept='es-AR;q=1.0, pt;q=0.5')
)
@ddt.data(
# Test condition: If I release 'es-419, es, es-es'...
('es;q=1.0, pt;q=0.5', 'es;q=1.0'), # 1. es should get es
('es-419;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 2. es-419 should get es-419
('es-es;q=1.0, pt;q=0.5', 'es-es;q=1.0'), # 3. es-es should get es-es
)
@ddt.unpack
def test_exact_match_gets_priority(self, accept_header, expected):
# Release 'es-419, es, es-es'
DarkLangConfig(
released_languages=('es-419, es, es-es'),
changed_by=self.user,
enabled=True
).save()
self.assertAcceptEquals(
expected,
self.process_request(accept=accept_header)
)
@unittest.skip("This won't work until fallback is implemented for LA country codes. See LOC-86")
@ddt.data(
'es-AR', # Argentina
'es-PY', # Paraguay
)
def test_partial_match_es_la(self, latin_america_code):
# We need to figure out the best way to implement this. There are a ton of LA country
# codes that ought to fall back to 'es-419' rather than 'es-es'.
# http://unstats.un.org/unsd/methods/m49/m49regin.htm#americas
# If I release 'es, es-419'
# Latin American codes should get es-419
DarkLangConfig(
released_languages=('es, es-419'),
changed_by=self.user,
enabled=True
).save()
self.assertAcceptEquals(
'es-419;q=1.0',
self.process_request(accept='{};q=1.0, pt;q=0.5'.format(latin_america_code))
)
def assertSessionLangEquals(self, value, request):
"""
Assert that the 'django_language' set in request.session is equal to value
......@@ -308,6 +224,6 @@ class DarkLangMiddlewareTests(TestCase):
).save()
self.assertAcceptEquals(
'zh-cn;q=1.0, zh-tw;q=0.5, zh-hk;q=0.3',
'zh-CN;q=1.0, zh-TW;q=0.5, zh-HK;q=0.3',
self.process_request(accept='zh-Hans;q=1.0, zh-Hant-TW;q=0.5, zh-HK;q=0.3')
)
"""
TODO: This module is imported from the stable Django 1.8 branch, as a
copy of https://github.com/django/django/blob/stable/1.8.x/django/middleware/locale.py.
Remove this file and re-import this middleware from Django once the
codebase is upgraded with a modern version of Django. [PLAT-671]
"""
# TODO: This file is imported from the stable Django 1.8 branch. Remove this file
# and re-import this middleware from Django once the codebase is upgraded. [PLAT-671]
# pylint: disable=invalid-name, missing-docstring
"This is the locale selecting middleware that will look at accept headers"
from django.conf import settings
from django.core.urlresolvers import (
LocaleRegexURLResolver, get_resolver, get_script_prefix, is_valid_path,
)
from django.http import HttpResponseRedirect
from django.utils import translation
from django.utils.cache import patch_vary_headers
# Override the Django 1.4 implementation with the 1.8 implementation
from django_locale.trans_real import get_language_from_request
class LocaleMiddleware(object):
"""
This is a very simple middleware that parses a request
and decides what translation object to install in the current
thread context. This allows pages to be dynamically
translated to the language the user desires (if the language
is available, of course).
"""
response_redirect_class = HttpResponseRedirect
def __init__(self):
self._is_language_prefix_patterns_used = False
for url_pattern in get_resolver(None).url_patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
self._is_language_prefix_patterns_used = True
break
def process_request(self, request):
check_path = self.is_language_prefix_patterns_used()
# This call is broken in Django 1.4:
# https://github.com/django/django/blob/stable/1.4.x/django/utils/translation/trans_real.py#L399
# (we override parse_accept_lang_header to a fixed version in dark_lang.middleware)
language = get_language_from_request(
request, check_path=check_path)
translation.activate(language)
request.LANGUAGE_CODE = translation.get_language()
def process_response(self, request, response):
language = translation.get_language()
language_from_path = translation.get_language_from_path(request.path_info)
if (response.status_code == 404 and not language_from_path
and self.is_language_prefix_patterns_used()):
urlconf = getattr(request, 'urlconf', None)
language_path = '/%s%s' % (language, request.path_info)
path_valid = is_valid_path(language_path, urlconf)
if (not path_valid and settings.APPEND_SLASH
and not language_path.endswith('/')):
path_valid = is_valid_path("%s/" % language_path, urlconf)
if path_valid:
script_prefix = get_script_prefix()
language_url = "%s://%s%s" % (
request.scheme,
request.get_host(),
# insert language after the script prefix and before the
# rest of the URL
request.get_full_path().replace(
script_prefix,
'%s%s/' % (script_prefix, language),
1
)
)
return self.response_redirect_class(language_url)
if not (self.is_language_prefix_patterns_used()
and language_from_path):
patch_vary_headers(response, ('Accept-Language',))
if 'Content-Language' not in response:
response['Content-Language'] = language
return response
def is_language_prefix_patterns_used(self):
"""
Returns `True` if the `LocaleRegexURLResolver` is used
at root level of the urlpatterns, else it returns `False`.
"""
return self._is_language_prefix_patterns_used
# pylint: disable=invalid-name, line-too-long, super-method-not-called
"""
Tests taken from Django upstream:
https://github.com/django/django/blob/e6b34193c5c7d117ededdab04bb16caf8864f07c/tests/regressiontests/i18n/tests.py
"""
from django.conf import settings
from django.test import TestCase, RequestFactory
from django_locale.trans_real import (
parse_accept_lang_header, get_language_from_request, LANGUAGE_SESSION_KEY
)
# Added to test middleware around dark lang
from django.contrib.auth.models import User
from django.test.utils import override_settings
from dark_lang.models import DarkLangConfig
# Adding to support test differences between Django and our own settings
@override_settings(LANGUAGES=[
('pt', 'Portuguese'),
('pt-br', 'Portuguese-Brasil'),
('es', 'Spanish'),
('es-ar', 'Spanish (Argentina)'),
('de', 'Deutch'),
('zh-cn', 'Chinese (China)'),
('ar-sa', 'Arabic (Saudi Arabia)'),
])
class MiscTests(TestCase):
"""
Tests taken from Django upstream:
https://github.com/django/django/blob/e6b34193c5c7d117ededdab04bb16caf8864f07c/tests/regressiontests/i18n/tests.py
"""
def setUp(self):
self.rf = RequestFactory()
# Added to test middleware around dark lang
user = User()
user.save()
DarkLangConfig(
released_languages='pt, pt-br, es, de, es-ar, zh-cn, ar-sa',
changed_by=user,
enabled=True
).save()
def test_parse_spec_http_header(self):
"""
Testing HTTP header parsing. First, we test that we can parse the
values according to the spec (and that we extract all the pieces in
the right order).
"""
p = parse_accept_lang_header
# Good headers.
self.assertEqual([('de', 1.0)], p('de'))
self.assertEqual([('en-AU', 1.0)], p('en-AU'))
self.assertEqual([('es-419', 1.0)], p('es-419'))
self.assertEqual([('*', 1.0)], p('*;q=1.00'))
self.assertEqual([('en-AU', 0.123)], p('en-AU;q=0.123'))
self.assertEqual([('en-au', 0.5)], p('en-au;q=0.5'))
self.assertEqual([('en-au', 1.0)], p('en-au;q=1.0'))
self.assertEqual([('da', 1.0), ('en', 0.5), ('en-gb', 0.25)], p('da, en-gb;q=0.25, en;q=0.5'))
self.assertEqual([('en-au-xx', 1.0)], p('en-au-xx'))
self.assertEqual([('de', 1.0), ('en-au', 0.75), ('en-us', 0.5), ('en', 0.25), ('es', 0.125), ('fa', 0.125)], p('de,en-au;q=0.75,en-us;q=0.5,en;q=0.25,es;q=0.125,fa;q=0.125'))
self.assertEqual([('*', 1.0)], p('*'))
self.assertEqual([('de', 1.0)], p('de;q=0.'))
self.assertEqual([('en', 1.0), ('*', 0.5)], p('en; q=1.0, * ; q=0.5'))
self.assertEqual([], p(''))
# Bad headers; should always return [].
self.assertEqual([], p('en-gb;q=1.0000'))
self.assertEqual([], p('en;q=0.1234'))
self.assertEqual([], p('en;q=.2'))
self.assertEqual([], p('abcdefghi-au'))
self.assertEqual([], p('**'))
self.assertEqual([], p('en,,gb'))
self.assertEqual([], p('en-au;q=0.1.0'))
self.assertEqual([], p('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ,en'))
self.assertEqual([], p('da, en-gb;q=0.8, en;q=0.7,#'))
self.assertEqual([], p('de;q=2.0'))
self.assertEqual([], p('de;q=0.a'))
self.assertEqual([], p('12-345'))
self.assertEqual([], p(''))
def test_parse_literal_http_header(self):
"""
Now test that we parse a literal HTTP header correctly.
"""
g = get_language_from_request
r = self.rf.get('/')
r.COOKIES = {}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'}
self.assertEqual('pt-br', g(r))
r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt'}
self.assertEqual('pt', g(r))
r.META = {'HTTP_ACCEPT_LANGUAGE': 'es,de'}
self.assertEqual('es', g(r))
r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-ar,de'}
self.assertEqual('es-ar', g(r))
# This test assumes there won't be a Django translation to a US
# variation of the Spanish language, a safe assumption. When the
# user sets it as the preferred language, the main 'es'
# translation should be selected instead.
r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-us'}
self.assertEqual(g(r), 'es')
# This tests the following scenario: there isn't a main language (zh)
# translation of Django but there is a translation to variation (zh_CN)
# the user sets zh-cn as the preferred language, it should be selected
# by Django without falling back nor ignoring it.
r.META = {'HTTP_ACCEPT_LANGUAGE': 'zh-cn,de'}
self.assertEqual(g(r), 'zh-cn')
def test_logic_masked_by_darklang(self):
g = get_language_from_request
r = self.rf.get('/')
r.COOKIES = {}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'ar-qa'}
self.assertEqual('ar-sa', g(r))
r.session = {LANGUAGE_SESSION_KEY: 'es'}
self.assertEqual('es', g(r))
def test_parse_language_cookie(self):
"""
Now test that we parse language preferences stored in a cookie correctly.
"""
g = get_language_from_request
r = self.rf.get('/')
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'}
r.META = {}
self.assertEqual('pt-br', g(r))
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt'}
r.META = {}
self.assertEqual('pt', g(r))
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es'}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'}
self.assertEqual('es', g(r))
# This test assumes there won't be a Django translation to a US
# variation of the Spanish language, a safe assumption. When the
# user sets it as the preferred language, the main 'es'
# translation should be selected instead.
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es-us'}
r.META = {}
self.assertEqual(g(r), 'es')
# This tests the following scenario: there isn't a main language (zh)
# translation of Django but there is a translation to variation (zh_CN)
# the user sets zh-cn as the preferred language, it should be selected
# by Django without falling back nor ignoring it.
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-cn'}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'}
self.assertEqual(g(r), 'zh-cn')
"""Translation helper functions."""
# Imported from Django 1.8
# pylint: disable=invalid-name
import re
from django.conf import settings
from django.conf.locale import LANG_INFO
from django.utils import translation
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
# and RFC 3066, section 2.1
accept_language_re = re.compile(r'''
([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z", "*"
(?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
(?:\s*,\s*|$) # Multiple accepts per header.
''', re.VERBOSE)
language_code_re = re.compile(r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*$', re.IGNORECASE)
LANGUAGE_SESSION_KEY = '_language'
def parse_accept_lang_header(lang_string):
"""
Parses the lang_string, which is the body of an HTTP Accept-Language
header, and returns a list of (lang, q-value), ordered by 'q' values.
Any format errors in lang_string results in an empty list being returned.
"""
# parse_accept_lang_header is broken until we are on Django 1.5 or greater
# See https://code.djangoproject.com/ticket/19381
result = []
pieces = accept_language_re.split(lang_string)
if pieces[-1]:
return []
for i in range(0, len(pieces) - 1, 3):
first, lang, priority = pieces[i: i + 3]
if first:
return []
priority = priority and float(priority) or 1.0
result.append((lang, priority))
result.sort(key=lambda k: k[1], reverse=True)
return result
def get_supported_language_variant(lang_code, strict=False):
"""
Returns the language-code that's listed in supported languages, possibly
selecting a more generic variant. Raises LookupError if nothing found.
If `strict` is False (the default), the function will look for an alternative
country-specific variant when the currently checked is not found.
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
as the provided language codes are taken from the HTTP request. See also
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
"""
if lang_code:
# If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
possible_lang_codes = [lang_code]
try:
# TODO skip this, or import updated LANG_INFO format from __future__
# (fallback option wasn't added until
# https://github.com/django/django/commit/5dcdbe95c749d36072f527e120a8cb463199ae0d)
possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
except KeyError:
pass
generic_lang_code = lang_code.split('-')[0]
possible_lang_codes.append(generic_lang_code)
supported_lang_codes = dict(settings.LANGUAGES)
for code in possible_lang_codes:
# Note: django 1.4 implementation of check_for_language is OK to use
if code in supported_lang_codes and translation.check_for_language(code):
return code
if not strict:
# if fr-fr is not supported, try fr-ca.
for supported_code in supported_lang_codes:
if supported_code.startswith(generic_lang_code + '-'):
return supported_code
raise LookupError(lang_code)
def get_language_from_request(request, check_path=False):
"""
Analyzes the request to find what language the user wants the system to
show. Only languages listed in settings.LANGUAGES are taken into account.
If the user requests a sublanguage where we have a main language, we send
out the main language.
If check_path is True, the URL path prefix will be checked for a language
code, otherwise this is skipped for backwards compatibility.
"""
if check_path:
# Note: django 1.4 implementation of get_language_from_path is OK to use
lang_code = translation.get_language_from_path(request.path_info)
if lang_code is not None:
return lang_code
supported_lang_codes = dict(settings.LANGUAGES)
if hasattr(request, 'session'):
lang_code = request.session.get(LANGUAGE_SESSION_KEY)
# Note: django 1.4 implementation of check_for_language is OK to use
if lang_code in supported_lang_codes and lang_code is not None and translation.check_for_language(lang_code):
return lang_code
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
try:
return get_supported_language_variant(lang_code)
except LookupError:
pass
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
# broken in 1.4, so defined above
for accept_lang, unused in parse_accept_lang_header(accept):
if accept_lang == '*':
break
if not language_code_re.search(accept_lang):
continue
try:
return get_supported_language_variant(accept_lang)
except LookupError:
continue
try:
return get_supported_language_variant(settings.LANGUAGE_CODE)
except LookupError:
return settings.LANGUAGE_CODE
......@@ -59,7 +59,8 @@ class CertificateDisplayTest(ModuleStoreTestCase):
def test_linked_student_to_web_view_credential(self, enrollment_mode):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
course_id=unicode(self.course.id),
verify_uuid='abcdefg12345678'
)
self._create_certificate(enrollment_mode)
......
......@@ -306,11 +306,14 @@ def _cert_info(user, course, cert_status, course_mode):
# showing the certificate web view button if certificate is ready state and feature flags are enabled.
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
if get_active_web_certificate(course) is not None:
certificate_url = get_certificate_url(
user_id=user.id,
course_id=unicode(course.id),
verify_uuid=None
)
status_dict.update({
'show_cert_web_view': True,
'cert_web_view_url': u'{url}'.format(
url=get_certificate_url(user_id=user.id, course_id=unicode(course.id))
)
'cert_web_view_url': u'{url}'.format(url=certificate_url)
})
else:
# don't show download certificate button if we don't have an active certificate for course
......
......@@ -377,6 +377,19 @@ class EditInfo(object):
source_version="UNSET" if self.source_version is None else self.source_version,
) # pylint: disable=bad-continuation
def __eq__(self, edit_info):
"""
Two EditInfo instances are equal iff their storable representations
are equal.
"""
return self.to_storable() == edit_info.to_storable()
def __neq__(self, edit_info):
"""
Two EditInfo instances are not equal if they're not equal.
"""
return not self == edit_info
class BlockData(object):
"""
......@@ -434,6 +447,19 @@ class BlockData(object):
classname=self.__class__.__name__,
) # pylint: disable=bad-continuation
def __eq__(self, block_data):
"""
Two BlockData objects are equal iff all their attributes are equal.
"""
attrs = ['fields', 'block_type', 'definition', 'defaults', 'edit_info']
return all(getattr(self, attr) == getattr(block_data, attr) for attr in attrs)
def __neq__(self, block_data):
"""
Just define this as not self.__eq__(block_data)
"""
return not self == block_data
new_contract('BlockData', BlockData)
......
......@@ -2,7 +2,9 @@
Segregation of pymongo functions from the data modeling mechanisms for split modulestore.
"""
import datetime
import cPickle as pickle
import math
import zlib
import pymongo
import pytz
import re
......@@ -11,6 +13,8 @@ from time import time
# Import this just to export it
from pymongo.errors import DuplicateKeyError # pylint: disable=unused-import
from django.core.cache import get_cache, InvalidCacheBackendError
import dogstats_wrapper as dog_stats_api
from contracts import check, new_contract
from mongodb_proxy import autoretry_read, MongoProxy
......@@ -203,6 +207,50 @@ def structure_to_mongo(structure, course_context=None):
return new_structure
class CourseStructureCache(object):
"""
Wrapper around django cache object to cache course structure objects.
The course structures are pickled and compressed when cached.
If the 'course_structure_cache' doesn't exist, then don't do anything for
for set and get.
"""
def __init__(self):
self.no_cache_found = False
try:
self.cache = get_cache('course_structure_cache')
except InvalidCacheBackendError:
self.no_cache_found = True
def get(self, key):
"""Pull the compressed, pickled struct data from cache and deserialize."""
if self.no_cache_found:
return None
compressed_pickled_data = self.cache.get(key)
if compressed_pickled_data is None:
return None
return pickle.loads(zlib.decompress(compressed_pickled_data))
def set(self, key, structure):
"""Given a structure, will pickle, compress, and write to cache."""
if self.no_cache_found:
return None
pickled_data = pickle.dumps(structure, pickle.HIGHEST_PROTOCOL)
# 1 = Fastest (slightly larger results)
compressed_pickled_data = zlib.compress(pickled_data, 1)
# record compressed course structure sizes
dog_stats_api.histogram(
'compressed_course_structure.size',
len(compressed_pickled_data),
tags=[key]
)
# Stuctures are immutable, so we set a timeout of "never"
self.cache.set(key, compressed_pickled_data, None)
class MongoConnection(object):
"""
Segregation of pymongo functions from the data modeling mechanisms for split modulestore.
......@@ -256,15 +304,23 @@ class MongoConnection(object):
def get_structure(self, key, course_context=None):
"""
Get the structure from the persistence mechanism whose id is the given key
Get the structure from the persistence mechanism whose id is the given key.
This method will use a cached version of the structure if it is availble.
"""
with TIMER.timer("get_structure", course_context) as tagger_get_structure:
with TIMER.timer("get_structure.find_one", course_context) as tagger_find_one:
doc = self.structures.find_one({'_id': key})
tagger_find_one.measure("blocks", len(doc['blocks']))
tagger_get_structure.measure("blocks", len(doc['blocks']))
return structure_from_mongo(doc, course_context)
cache = CourseStructureCache()
structure = cache.get(key)
tagger_get_structure.tag(from_cache='true' if structure else 'false')
if not structure:
with TIMER.timer("get_structure.find_one", course_context) as tagger_find_one:
doc = self.structures.find_one({'_id': key})
tagger_find_one.measure("blocks", len(doc['blocks']))
structure = structure_from_mongo(doc, course_context)
cache.set(key, structure)
return structure
@autoretry_read()
def find_structures_by_id(self, ids, course_context=None):
......
......@@ -794,7 +794,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# find: find parent (definition.children) 2x, find draft item, get inheritance items
# send: one delete query for specific item
# Split:
# find: active_version & structure
# find: active_version & structure (cached)
# send: update structure and active_versions
@ddt.data(('draft', 4, 1), ('split', 2, 2))
@ddt.unpack
......
......@@ -12,6 +12,7 @@ import uuid
from contracts import contract
from nose.plugins.attrib import attr
from django.core.cache import get_cache, InvalidCacheBackendError
from openedx.core.lib import tempdir
from xblock.fields import Reference, ReferenceList, ReferenceValueDict
......@@ -29,6 +30,7 @@ from xmodule.fields import Date, Timedelta
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from xmodule.modulestore.tests.test_modulestore import check_has_course_method
from xmodule.modulestore.split_mongo import BlockKey
from xmodule.modulestore.tests.factories import check_mongo_calls
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.tests.utils import mock_tab_from_json
from xmodule.modulestore.edit_info import EditInfoMixin
......@@ -771,6 +773,79 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(result.children[0].children[0].locator.version_guid, versions[0])
class TestCourseStructureCache(SplitModuleTest):
"""Tests for the CourseStructureCache"""
def setUp(self):
# use the default cache, since the `course_structure_cache`
# is a dummy cache during testing
self.cache = get_cache('default')
# make sure we clear the cache before every test...
self.cache.clear()
# ... and after
self.addCleanup(self.cache.clear)
# make a new course:
self.user = random.getrandbits(32)
self.new_course = modulestore().create_course(
'org', 'course', 'test_run', self.user, BRANCH_NAME_DRAFT,
)
super(TestCourseStructureCache, self).setUp()
@patch('xmodule.modulestore.split_mongo.mongo_connection.get_cache')
def test_course_structure_cache(self, mock_get_cache):
# force get_cache to return the default cache so we can test
# its caching behavior
mock_get_cache.return_value = self.cache
with check_mongo_calls(1):
not_cached_structure = self._get_structure(self.new_course)
# when cache is warmed, we should have one fewer mongo call
with check_mongo_calls(0):
cached_structure = self._get_structure(self.new_course)
# now make sure that you get the same structure
self.assertEqual(cached_structure, not_cached_structure)
@patch('xmodule.modulestore.split_mongo.mongo_connection.get_cache')
def test_course_structure_cache_no_cache_configured(self, mock_get_cache):
mock_get_cache.side_effect = InvalidCacheBackendError
with check_mongo_calls(1):
not_cached_structure = self._get_structure(self.new_course)
# if the cache isn't configured, we expect to have to make
# another mongo call here if we want the same course structure
with check_mongo_calls(1):
cached_structure = self._get_structure(self.new_course)
# now make sure that you get the same structure
self.assertEqual(cached_structure, not_cached_structure)
def test_dummy_cache(self):
with check_mongo_calls(1):
not_cached_structure = self._get_structure(self.new_course)
# Since the test is using the dummy cache, it's not actually caching
# anything
with check_mongo_calls(1):
cached_structure = self._get_structure(self.new_course)
# now make sure that you get the same structure
self.assertEqual(cached_structure, not_cached_structure)
def _get_structure(self, course):
"""
Helper function to get a structure from a course.
"""
return modulestore().db_connection.get_structure(
course.location.as_object_id(course.location.version_guid)
)
class SplitModuleItemTests(SplitModuleTest):
'''
Item read tests including inheritance
......
---
metadata:
display_name: Checkboxes with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.
You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>Which of the following is a fruit? Check all that apply.<<
[x] apple {{ selected: You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds. }, { unselected: Remember that an apple is also a fruit.}}
[x] pumpkin {{ selected: You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds. }, { unselected: Remember that a pumpkin is also a fruit.}}
[ ] potato {{ U: You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.}, { S: A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.}}
[x] tomato {{ S: You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds. }, { U: Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
{{ ((A B D)) An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. }}
{{ ((A B C D)) You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is a vegetable. }}
||A fruit is the fertilized ovary from a flower.||
||A fruit contains seeds of the plant.||
data: |
<problem>
<p>You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.</p>
<p>You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers.</p>
<p>You can also add hints for learners.</p>
<p>Use the following example problem as a model.</p>
<p>Which of the following is a fruit? Check all that apply.</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">apple
<choicehint selected="true">You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds.</choicehint>
<choicehint selected="false">Remember that an apple is also a fruit.</choicehint>
</choice>
<choice correct="true">pumpkin
<choicehint selected="true">You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds.</choicehint>
<choicehint selected="false">Remember that a pumpkin is also a fruit.</choicehint>
</choice>
<choice correct="false">potato
<choicehint selected="true">A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.</choicehint>
<choicehint selected="false">You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.</choicehint>
</choice>
<choice correct="true">tomato
<choicehint selected="true">You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds.</choicehint>
<choicehint selected="false">Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it a fruit.</choicehint>
</choice>
<compoundhint value="A B D">An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds.</compoundhint>
<compoundhint value="A B C D">You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is classified as a vegetable.</compoundhint>
</checkboxgroup>
</choiceresponse>
<demandhint>
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</problem>
---
metadata:
display_name: Multiple Choice with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for each option in a multiple choice problem.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>Which of the following is a vegetable?<<
( ) apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}}
( ) pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}}
(x) potato {{A potato is an edible part of a plant in tuber form and is a vegetable.}}
( ) tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
||A fruit is the fertilized ovary from a flower.||
||A fruit contains seeds of the plant.||
data: |
<problem>
<p>You can provide feedback for each option in a multiple choice problem.</p>
<p>You can also add hints for learners.</p>
<p>Use the following example problem as a model.</p>
<p>Which of the following is a vegetable?</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">apple <choicehint>An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.</choicehint></choice>
<choice correct="false">pumpkin <choicehint>A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.</choicehint></choice>
<choice correct="true">potato <choicehint>A potato is an edible part of a plant in tuber form and is a vegetable.</choicehint></choice>
<choice correct="false">tomato <choicehint>Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</choicehint></choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</problem>
---
metadata:
display_name: Numerical Input with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.
Use feedback for the correct answer to reinforce the process for arriving at the numerical value.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)<<
= 4 {{The mean for this set of numbers is 20 / 5, which equals 4.}}
||The mean is calculated by summing the set of numbers and dividing by n.||
||n is the count of items in the set.||
[explanation]
The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.
[explanation]
data: |
<problem>
<p>You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.</p>
<p>Use feedback for the correct answer to reinforce the process for arriving at the numerical value.</p>
<p>Use the following example problem as a model.</p>
<p>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)</p>
<numericalresponse answer="4">
<formulaequationinput label="What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)" />
<correcthint>The mean for this set of numbers is 20 / 5, which equals 4.</correcthint>
</numericalresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.</p>
</div>
</solution>
<demandhint>
<hint>The mean is calculated by summing the set of numbers and dividing by n.</hint>
<hint>n is the count of items in the set.</hint>
</demandhint>
</problem>
\ No newline at end of file
---
metadata:
display_name: Dropdown with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for each available option in a dropdown problem.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>> A/an ________ is a vegetable.<<
[[
apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}}
pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}}
(potato) {{A potato is an edible part of a plant in tuber form and is a vegetable.}}
tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
]]
||A fruit is the fertilized ovary from a flower.||
||A fruit contains seeds of the plant.||
data: |
<problem>
<p>You can provide feedback for each available option in a dropdown problem.</p>
<p>You can also add hints for learners.</p>
<p>Use the following example problem as a model.</p>
<p> A/an ________ is a vegetable.</p>
<br/>
<optionresponse>
<optioninput>
<option correct="False">apple <optionhint>An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.</optionhint></option>
<option correct="False">pumpkin <optionhint>A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.</optionhint></option>
<option correct="True">potato <optionhint>A potato is an edible part of a plant in tuber form and is a vegetable.</optionhint></option>
<option correct="False">tomato <optionhint>Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</optionhint></option>
</optioninput>
</optionresponse>
<demandhint>
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</problem>
---
metadata:
display_name: Text Input with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.
Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>Which U.S. state has the largest land area?<<
=Alaska {{Alaska is 576,400 square miles, more than double the land area
of the second largest state, Texas.}}
not=Texas {{While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.}}
not=California {{California is the third largest state, with 155,959 square miles.}}
||Consider the square miles, not population.||
||Consider all 50 states, not just the continental United States.||
data: |
<problem>
<p>You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.</p>
<p>Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.</p>
<p>Use the following example problem as a model.</p>
<p>Which U.S. state has the largest land area?</p>
<stringresponse answer="Alaska" type="ci" >
<correcthint>Alaska is 576,400 square miles, more than double the land area of the second largest state, Texas.</correcthint>
<stringequalhint answer="Texas">While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.</stringequalhint>
<stringequalhint answer="California">California is the third largest state, with 155,959 square miles.</stringequalhint>
<textline label="Which U.S. state has the largest land area?" size="20"/>
</stringresponse>
<demandhint>
<hint>Consider the square miles, not population.</hint>
<hint>Consider all 50 states, not just the continental United States.</hint>
</demandhint>
</problem>
......@@ -253,15 +253,18 @@ def example_certificates_status(course_key):
# pylint: disable=no-member
def get_certificate_url(user_id, course_id):
def get_certificate_url(user_id, course_id, verify_uuid):
"""
:return certificate url
"""
url = u'{url}'.format(url=reverse('cert_html_view',
kwargs=dict(
user_id=str(user_id),
course_id=unicode(course_id))))
return url
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return u'{url}'.format(
url=reverse(
'cert_html_view',
kwargs=dict(user_id=str(user_id), course_id=unicode(course_id))
)
)
return '{url}{uuid}'.format(url=settings.CERTIFICATES_STATIC_VERIFY_URL, uuid=verify_uuid)
def get_active_web_certificate(course, is_preview_mode=None):
......@@ -290,7 +293,7 @@ def emit_certificate_event(event_name, user, course_id, course=None, event_data=
data = {
'user_id': user.id,
'course_id': unicode(course_id),
'certificate_url': get_certificate_url(user.id, course_id)
'certificate_url': get_certificate_url(user.id, course_id, event_data['certificate_id'])
}
event_data = event_data or {}
event_data.update(data)
......
......@@ -148,7 +148,7 @@ class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase):
'edx.certificate.created',
user_id=self.student.id,
course_id=unicode(self.course.id),
certificate_url=certs_api.get_certificate_url(self.student.id, self.course.id),
certificate_url=certs_api.get_certificate_url(self.student.id, self.course.id, cert.verify_uuid),
certificate_id=cert.verify_uuid,
enrollment_mode=cert.mode,
generation_mode='batch'
......@@ -164,7 +164,7 @@ class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase):
self.assertEqual(cert.status, 'error')
self.assertIn(self.ERROR_REASON, cert.error_reason)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
def test_new_cert_requests_returns_generating_for_html_certificate(self):
"""
Test no message sent to Xqueue if HTML certificate view is enabled
......
......@@ -308,7 +308,8 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
self.assertEquals(config.configuration, test_configuration_string)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=2)
response = self.client.get(test_url)
......@@ -341,7 +342,8 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
self.assertEquals(config.configuration, test_configuration_string)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=2)
response = self.client.get(test_url)
......@@ -427,7 +429,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_render_html_view_valid_certificate(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id) # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=2)
response = self.client.get(test_url)
......@@ -449,7 +452,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_render_html_view_with_valid_signatories(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=2)
response = self.client.get(test_url)
......@@ -465,7 +469,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
# if certificate in descriptor has not course_title then course name should not be overridden with this title.
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
test_certificates = [
{
......@@ -488,7 +493,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_certificate_view_without_org_logo(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
test_certificates = [
{
......@@ -510,7 +516,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_render_html_view_without_signatories(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course),
verify_uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=0)
response = self.client.get(test_url)
......@@ -518,19 +525,20 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.assertNotIn('Signatory_Title 0', response.content)
@override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED)
def test_render_html_view_invalid_feature_flag(self):
def test_render_html_view_disabled_feature_flag_returns_static_url(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
self.assertIn('invalid', response.content)
self.assertIn(str(self.cert.verify_uuid), test_url)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_render_html_view_invalid_course_id(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id='az/23423/4vs'
course_id='az/23423/4vs',
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
......@@ -540,7 +548,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_render_html_view_invalid_course(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id='missing/course/key'
course_id='missing/course/key',
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
self.assertIn('invalid', response.content)
......@@ -549,7 +558,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_render_html_view_invalid_user(self):
test_url = get_certificate_url(
user_id=111,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
self.assertIn('invalid', response.content)
......@@ -560,7 +570,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.assertEqual(len(GeneratedCertificate.objects.all()), 0)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
self.assertIn('invalid', response.content)
......@@ -576,7 +587,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self._add_course_certificates(count=1, signatory_count=2)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course.id.to_deprecated_string() # pylint: disable=no-member
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url + '?preview=honor')
self.assertNotIn(self.course.display_name, response.content)
......@@ -594,7 +606,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
def test_render_html_view_invalid_certificate_configuration(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
self.assertIn("Invalid Certificate", response.content)
......@@ -606,7 +619,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.recreate_tracker()
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
course_id=unicode(self.course.id),
verify_uuid=self.cert.verify_uuid
)
response = self.client.get(test_url)
self.assertEqual(response.status_code, 200)
......@@ -626,7 +640,12 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_evidence_event_sent(self):
test_url = get_certificate_url(user_id=self.user.id, course_id=self.course_id) + '?evidence_visit=1'
cert_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course_id,
verify_uuid=self.cert.verify_uuid
)
test_url = '{}?evidence_visit=1'.format(cert_url)
self.recreate_tracker()
assertion = BadgeAssertion(
user=self.user, course_id=self.course_id, mode='honor',
......
......@@ -439,7 +439,8 @@ def _update_certificate_context(context, course, user, user_certificate):
user_certificate.mode,
get_certificate_url(
user_id=user.id,
course_id=course.id.to_deprecated_string()
course_id=unicode(course.id),
verify_uuid=user_certificate.verify_uuid
)
)
......
......@@ -4,59 +4,37 @@ Tests i18n in courseware
import re
from nose.plugins.attrib import attr
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.utils import override_settings
from dark_lang.models import DarkLangConfig
class BaseI18nTestCase(TestCase):
@attr('shard_1')
@override_settings(LANGUAGES=[('eo', 'Esperanto'), ('ar', 'Arabic')])
class I18nTestCase(TestCase):
"""
Base utilities for i18n test classes to derive from
Tests for i18n
"""
def assert_tag_has_attr(self, content, tag, attname, value):
"""Assert that a tag in `content` has a certain value in a certain attribute."""
regex = r"""<{tag} [^>]*\b{attname}=['"]([\w\d\- ]+)['"][^>]*>""".format(tag=tag, attname=attname)
regex = r"""<{tag} [^>]*\b{attname}=['"]([\w\d ]+)['"][^>]*>""".format(tag=tag, attname=attname)
match = re.search(regex, content)
self.assertTrue(match, "Couldn't find desired tag '%s' with attr '%s' in %r" % (tag, attname, content))
self.assertTrue(match, "Couldn't find desired tag in %r" % content)
attvalues = match.group(1).split()
self.assertIn(value, attvalues)
def release_languages(self, languages):
"""
Release a set of languages using the dark lang interface.
languages is a list of comma-separated lang codes, eg, 'ar, es-419'
"""
user = User()
user.save()
DarkLangConfig(
released_languages=languages,
changed_by=user,
enabled=True
).save()
@attr('shard_1')
class I18nTestCase(BaseI18nTestCase):
"""
Tests for i18n
"""
def test_default_is_en(self):
self.release_languages('fr')
response = self.client.get('/')
self.assert_tag_has_attr(response.content, "html", "lang", "en")
self.assertEqual(response['Content-Language'], 'en')
self.assert_tag_has_attr(response.content, "body", "class", "lang_en")
def test_esperanto(self):
self.release_languages('fr, eo')
response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='eo')
self.assert_tag_has_attr(response.content, "html", "lang", "eo")
self.assertEqual(response['Content-Language'], 'eo')
self.assert_tag_has_attr(response.content, "body", "class", "lang_eo")
def test_switching_languages_bidi(self):
self.release_languages('ar, eo')
response = self.client.get('/')
self.assert_tag_has_attr(response.content, "html", "lang", "en")
self.assertEqual(response['Content-Language'], 'en')
......@@ -68,26 +46,3 @@ class I18nTestCase(BaseI18nTestCase):
self.assertEqual(response['Content-Language'], 'ar')
self.assert_tag_has_attr(response.content, "body", "class", "lang_ar")
self.assert_tag_has_attr(response.content, "body", "class", "rtl")
@attr('shard_1')
class I18nRegressionTests(BaseI18nTestCase):
"""
Tests for i18n
"""
def test_es419_acceptance(self):
# Regression test; LOC-72, and an issue with Django
self.release_languages('es-419')
response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='es-419')
self.assert_tag_has_attr(response.content, "html", "lang", "es-419")
def test_unreleased_lang_resolution(self):
# Regression test; LOC-85
self.release_languages('fa')
# We've released 'fa', AND we have language files for 'fa-ir' but
# we want to keep 'fa-ir' as a dark language. Requesting 'fa-ir'
# in the http request (NOT with the ?preview-lang query param) should
# receive files for 'fa'
response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fa-ir')
self.assert_tag_has_attr(response.content, "html", "lang", "fa")
......@@ -824,7 +824,7 @@ class ProgressPageTests(ModuleStoreTestCase):
If certificate web view is enabled then certificate web view button should appear for user who certificate is
available/generated
"""
GeneratedCertificateFactory.create(
certificate = GeneratedCertificateFactory.create(
user=self.user,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
......@@ -859,7 +859,12 @@ class ProgressPageTests(ModuleStoreTestCase):
resp = views.progress(self.request, course_id=unicode(self.course.id))
self.assertContains(resp, u"View Certificate")
self.assertContains(resp, u"You can now view your certificate")
self.assertContains(resp, certs_api.get_certificate_url(user_id=self.user.id, course_id=self.course.id))
cert_url = certs_api.get_certificate_url(
user_id=self.user.id,
course_id=self.course.id,
verify_uuid=certificate.verify_uuid
)
self.assertContains(resp, cert_url)
# when course certificate is not active
certificates[0]['is_active'] = False
......
......@@ -1086,7 +1086,11 @@ def _progress(request, course_key, student_id):
context.update({
'show_cert_web_view': True,
'cert_web_view_url': u'{url}'.format(
url=certs_api.get_certificate_url(user_id=student.id, course_id=unicode(course.id))
url=certs_api.get_certificate_url(
user_id=student.id,
course_id=unicode(course.id),
verify_uuid=None
)
)
})
else:
......
......@@ -640,3 +640,7 @@ EDXNOTES_INTERNAL_API = ENV_TOKENS.get('EDXNOTES_INTERNAL_API', EDXNOTES_INTERNA
##### Credit Provider Integration #####
CREDIT_PROVIDER_SECRET_KEYS = AUTH_TOKENS.get("CREDIT_PROVIDER_SECRET_KEYS", {})
############ CERTIFICATE VERIFICATION URL (STATIC FILES) ###########
ENV_TOKENS.get('CERTIFICATES_STATIC_VERIFY_URL', CERTIFICATES_STATIC_VERIFY_URL)
......@@ -1152,9 +1152,7 @@ MIDDLEWARE_CLASSES = (
'lang_pref.middleware.LanguagePreferenceMiddleware',
# Detects user-requested locale from 'accept-language' header in http request
# TODO: Re-import the Django version once we upgrade to Django 1.8 [PLAT-671]
# 'django.middleware.locale.LocaleMiddleware',
'django_locale.middleware.LocaleMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.transaction.TransactionMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
......@@ -2095,6 +2093,9 @@ REGISTRATION_EXTRA_FIELDS = {
CERT_NAME_SHORT = "Certificate"
CERT_NAME_LONG = "Certificate of Achievement"
############ CERTIFICATE VERIFICATION URL (STATIC FILES) ###########
CERTIFICATES_STATIC_VERIFY_URL = "https://verify-test.edx.org/cert/"
#################### Badgr OpenBadges generation #######################
# Be sure to set up images for course modes using the BadgeImageConfiguration model in the certificates app.
BADGR_API_TOKEN = None
......
......@@ -94,6 +94,10 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_location_mem_cache',
},
'course_structure_cache': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_course_structure_mem_cache',
},
}
......
......@@ -207,7 +207,9 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_location_mem_cache',
},
'course_structure_cache': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
},
}
# Dummy secret key for dev
......@@ -472,9 +474,6 @@ FACEBOOK_APP_SECRET = "Test"
FACEBOOK_APP_ID = "Test"
FACEBOOK_API_VERSION = "v2.2"
# Certificates Views
FEATURES['CERTIFICATES_HTML_VIEW'] = True
######### custom courses #########
INSTALLED_APPS += ('ccx',)
FEATURES['CUSTOM_COURSES_EDX'] = True
......
......@@ -28,7 +28,7 @@ h2 {
<body>
<table width="650" border="0" cellspacing="5" cellpadding="5">
<tr>
<td align="left" valign="top" class="box-bg"><h2>${_("Executive Summary for {display_name}".format(display_name=display_name))}</h2>
<td align="left" valign="top" class="box-bg"><h2>${_("Executive Summary for {display_name}").format(display_name=display_name)}</h2>
<table width="100%">
<tr>
......
......@@ -105,13 +105,13 @@ from django.utils.translation import ungettext
<div class="numbers-row" aria-live="polite">
<label for="field_${item.id}">${_('Students:')}</label>
<div class="counter">
<input maxlength="3" title="${_('Input quantity and press enter.')}" max="999" type="text" name="students" value="${item.qty}" id="field_${item.id}" data-unit-cost="${item.unit_cost}" data-qty="${item.qty}" aria-describedby="students-${item.id}">
<input maxlength="3" class="spin-counter" title="${_('Input quantity and press enter.')}" max="999" type="text" name="students" value="${item.qty}" id="field_${item.id}" data-unit-cost="${item.unit_cost}" data-qty="${item.qty}" data-item-id="${item.id}" aria-describedby="students-${item.id}">
</div>
<button class="inc button">
<button class="inc button" data-operation="inc">
<i class="icon fa fa-caret-up" aria-hidden="true"><span>+</span></i>
<span class="sr">Increase</span>
</button>
<button class="dec button">
<button class="dec button" data-operation="dec">
<i class="icon fa fa-caret-down"></i>
<span class="sr">Decrease</span>
</button>
......@@ -310,15 +310,15 @@ from django.utils.translation import ungettext
var wasBusinessType = false;
var studentField = $(this).parent().find("input[type='text']");
var unit_cost = parseFloat(studentField.data('unit-cost'));
var ItemId = studentField.attr('id');
var ItemId = studentField.data('item-id');
var $button = $(this);
var oldValue = $("#"+ItemId).data('qty');
var oldValue = studentField.data('qty');
var newVal = 1; // initialize with 1.
oldValue = parseFloat(oldValue);
hideErrorMsg('students-'+ItemId);
if ($.isNumeric(oldValue)){
if ($button.text() == "+") {
if ($button.data("operation") == "inc") {
if(oldValue > 0){
newVal = oldValue + 1;
if(newVal > 1000){
......@@ -338,7 +338,7 @@ from django.utils.translation import ungettext
$button.parent().find("input").val(newVal);
isBusinessType = getBusinessType();
update_user_cart(ItemId, newVal, oldValue, unit_cost, wasBusinessType, isBusinessType);
$("#"+ItemId).data('qty', newVal);
studentField.data('qty', newVal);
}
});
......@@ -390,7 +390,7 @@ from django.utils.translation import ungettext
typeChanged = true;
$('html').css({'cursor':'wait'});
$(".button").css({'cursor':'wait'});
$('.col-2.relative').find("input[type='submit']").attr('disabled', true);
$('.col-2.relative').find("button[type='submit']").attr('disabled', true);
}
......@@ -406,11 +406,14 @@ from django.utils.translation import ungettext
$(".button").css({'cursor':'default'});
$("#processor_form").html(data['form_html']);
if(typeChanged){
var submit_button = $('.col-2.relative').find("input[type='submit']")
var submit_button = $('.col-2.relative').find("button[type='submit']");
submit_button.removeAttr('disabled');
for (var i = 0; i< data['oldToNewIdMap'].length; i++) {
$('#'+data['oldToNewIdMap'][i]['oldId']+'').attr('id',data['oldToNewIdMap'][i]['newId']);
$('a.btn-remove[data-item-id]=' +data['oldToNewIdMap'][i]['oldId']+'').data('item-id', data['oldToNewIdMap'][i]['newId']);
var oldId = data['oldToNewIdMap'][i]['oldId'];
var newId = data['oldToNewIdMap'][i]['newId'];
$('input.spin-counter[data-item-id]=' + oldId ).data('item-id', newId);
$('button.btn-remove[data-item-id]=' + oldId ).data('item-id', newId);
}
if(isbusinessType){
$( "div[name='payment']").addClass('hidden');
......@@ -447,8 +450,9 @@ from django.utils.translation import ungettext
function updateTextFieldQty(event){
if(isSpinnerBtnEnabled){
var itemId = event.currentTarget.id;
var prevQty = $("#"+itemId).data('qty');
var el_id = event.currentTarget.id;
var itemId = $("#"+el_id).data('item-id');
var prevQty = $("#"+el_id).data('qty');
var newQty = event.currentTarget.value;
var unitCost = event.currentTarget.dataset.unitCost;
var isBusinessType = getBusinessType();
......@@ -456,7 +460,7 @@ from django.utils.translation import ungettext
var wasBusinessType = !isBusinessType;
isSpinnerBtnEnabled = false;
update_user_cart(itemId, newQty, prevQty, unitCost, wasBusinessType, isBusinessType);
$("#"+itemId).data('qty', newQty);
$("#"+el_id).data('qty', newQty);
}
}
......
......@@ -662,11 +662,10 @@ if settings.FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
)
# Certificates Web/HTML View
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
urlpatterns += (
url(r'^certificates/user/(?P<user_id>[^/]*)/course/{course_id}'.format(course_id=settings.COURSE_ID_PATTERN),
'certificates.views.render_html_view', name='cert_html_view'),
)
urlpatterns += (
url(r'^certificates/user/(?P<user_id>[^/]*)/course/{course_id}'.format(course_id=settings.COURSE_ID_PATTERN),
'certificates.views.render_html_view', name='cert_html_view'),
)
BADGE_SHARE_TRACKER_URL = url(
r'^certificates/badge_share_tracker/{}/(?P<network>[^/]+)/(?P<student_username>[^/]+)/$'.format(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment