Merge pull request #8675 from edx/release-merge-test
Release merge 06-24-2015
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
<li class="current">
|
||||
<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>
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
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]
|
||||
"""
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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
|
||||
@@ -1,157 +0,0 @@
|
||||
# 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')
|
||||
@@ -1,131 +0,0 @@
|
||||
"""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']))
|
||||
cache = CourseStructureCache()
|
||||
|
||||
return structure_from_mongo(doc, course_context)
|
||||
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
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
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 dark_lang.models import DarkLangConfig
|
||||
|
||||
|
||||
class BaseI18nTestCase(TestCase):
|
||||
"""
|
||||
Base utilities for i18n test classes to derive from
|
||||
"""
|
||||
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)
|
||||
match = re.search(regex, content)
|
||||
self.assertTrue(match, "Couldn't find desired tag '%s' with attr '%s' in %r" % (tag, attname, 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()
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class I18nTestCase(BaseI18nTestCase):
|
||||
@override_settings(LANGUAGES=[('eo', 'Esperanto'), ('ar', 'Arabic')])
|
||||
class I18nTestCase(TestCase):
|
||||
"""
|
||||
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)
|
||||
match = re.search(regex, content)
|
||||
self.assertTrue(match, "Couldn't find desired tag in %r" % content)
|
||||
attvalues = match.group(1).split()
|
||||
self.assertIn(value, attvalues)
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user