diff --git a/AUTHORS b/AUTHORS index 59ef640fbc..944d143cba 100644 --- a/AUTHORS +++ b/AUTHORS @@ -236,3 +236,6 @@ Brian Beggs Bill DeRusha Kevin Falcone Mirjam Škarica +Saleem Latif +Julien Paillé +Michael Frey diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py b/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py new file mode 100644 index 0000000000..bd519a4167 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py @@ -0,0 +1,47 @@ +""" +Tests for the fix_not_found management command +""" + +from django.core.management import call_command +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + + +class TestFixNotFound(ModuleStoreTestCase): + """ + Tests for the fix_not_found management command + """ + def test_fix_not_found_non_split(self): + """ + The management command doesn't work on non split courses + """ + course = CourseFactory(default_store=ModuleStoreEnum.Type.mongo) + with self.assertRaises(SystemExit): + call_command("fix_not_found", unicode(course.id)) + + def test_fix_not_found(self): + course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) + ItemFactory.create(category='chapter', parent_location=course.location) + + # get course again in order to update its children list + course = self.store.get_course(course.id) + + # create a dangling usage key that we'll add to the course's children list + dangling_pointer = course.id.make_usage_key('chapter', 'DanglingPointer') + + course.children.append(dangling_pointer) + self.store.update_item(course, self.user.id) + + # the course block should now point to two children, one of which + # doesn't actually exist + self.assertEqual(len(course.children), 2) + self.assertIn(dangling_pointer, course.children) + + call_command("fix_not_found", unicode(course.id)) + + # make sure the dangling pointer was removed from + # the course block's children + course = self.store.get_course(course.id) + self.assertEqual(len(course.children), 1) + self.assertNotIn(dangling_pointer, course.children) diff --git a/cms/djangoapps/contentstore/views/certificates.py b/cms/djangoapps/contentstore/views/certificates.py index 91bdf80a36..caab0b8a80 100644 --- a/cms/djangoapps/contentstore/views/certificates.py +++ b/cms/djangoapps/contentstore/views/certificates.py @@ -218,7 +218,7 @@ class CertificateManager(object): # including the actual 'certificates' list that we're working with in this context certificates = course.certificates.get('certificates', []) if only_active: - certificates = [certificate for certificate in certificates if certificate['is_active']] + certificates = [certificate for certificate in certificates if certificate.get('is_active', False)] return certificates @staticmethod diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py index fc728333b4..d7bcd1b1be 100644 --- a/cms/djangoapps/contentstore/views/tests/test_certificates.py +++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py @@ -446,6 +446,45 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific self.assertEqual(course_certificates[1].get('name'), u'New test certificate') self.assertEqual(course_certificates[1].get('description'), 'New test description') + def test_can_edit_certificate_without_is_active(self): + """ + Tests user should be able to edit certificate, if is_active attribute is not present + for given certificate. Old courses might not have is_active attribute in certificate data. + """ + certificates = [ + { + 'id': 1, + 'name': 'certificate with is_active', + 'description': 'Description ', + 'signatories': [], + 'version': CERTIFICATE_SCHEMA_VERSION, + } + ] + self.course.certificates = {'certificates': certificates} + self.save_course() + + expected = { + u'id': 1, + u'version': CERTIFICATE_SCHEMA_VERSION, + u'name': u'New test certificate', + u'description': u'New test description', + u'is_active': True, + u'course_title': u'Course Title Override', + u'signatories': [] + + } + + response = self.client.post( + self._url(cid=1), + data=json.dumps(expected), + content_type="application/json", + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 201) + content = json.loads(response.content) + self.assertEqual(content, expected) + def test_can_delete_certificate_with_signatories(self): """ Delete certificate diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index a97c04792f..242463aa07 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -98,7 +98,9 @@ FEATURES['ENABLE_DISCUSSION_SERVICE'] = False USE_I18N = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command -INSTALLED_APPS += ('lettuce.django',) +# django.contrib.staticfiles used to be loaded by lettuce, now we must add it ourselves +# django.contrib.staticfiles is not added to lms as there is a ^/static$ route built in to the app +INSTALLED_APPS += ('lettuce.django', 'django.contrib.staticfiles') LETTUCE_APPS = ('contentstore',) LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') diff --git a/cms/envs/common.py b/cms/envs/common.py index fd73ab64d8..f1d34953a1 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -170,9 +170,6 @@ FEATURES = { # Teams feature 'ENABLE_TEAMS': True, - # Teams search feature - 'ENABLE_TEAMS_SEARCH': False, - # Show video bumper in Studio 'ENABLE_VIDEO_BUMPER': False, @@ -280,13 +277,6 @@ XQUEUE_INTERFACE = { simplefilter('ignore') ################################# Middleware ################################### -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'staticfiles.finders.FileSystemFinder', - 'staticfiles.finders.AppDirectoriesFinder', - 'pipeline.finders.PipelineFinder', -) # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( @@ -463,9 +453,23 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' ##### EMBARGO ##### EMBARGO_SITE_REDIRECT_URL = None -############################### Pipeline ####################################### +############################### PIPELINE ####################################### + +# Process static files using RequireJS Optimizer STATICFILES_STORAGE = 'openedx.core.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage' +# List of finder classes that know how to find static files in various locations. +# Note: the pipeline finder is included to be able to discover optimized files +STATICFILES_FINDERS = [ + 'staticfiles.finders.FileSystemFinder', + 'staticfiles.finders.AppDirectoriesFinder', + 'pipeline.finders.PipelineFinder', +] + +# Don't use compression by default +PIPELINE_CSS_COMPRESSOR = None +PIPELINE_JS_COMPRESSOR = None + from openedx.core.lib.rooted_paths import rooted_glob PIPELINE_CSS = { @@ -553,7 +557,9 @@ PIPELINE_JS_COMPRESSOR = None STATICFILES_IGNORE_PATTERNS = ( "*.py", "*.pyc", - # it would be nice if we could do, for example, "**/*.scss", + "*.html", + + # It would be nice if we could do, for example, "**/*.scss", # but these strings get passed down to the `fnmatch` module, # which doesn't support that. :( # http://docs.python.org/2/library/fnmatch.html @@ -566,6 +572,10 @@ STATICFILES_IGNORE_PATTERNS = ( "coffee/*/*/*.coffee", "coffee/*/*/*/*.coffee", + # Ignore tests + "spec", + "spec_helpers", + # Symlinks used by js-test-tool "xmodule_js", "common_static", diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 70e25475ca..bd00323668 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -33,8 +33,14 @@ FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE ########################### PIPELINE ################################# -# Skip RequireJS optimizer in development -STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' +# Skip packaging and optimization in development +STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage' + +# Revert to the default set of finders as we don't want the production pipeline +STATICFILES_FINDERS = [ + 'staticfiles.finders.FileSystemFinder', + 'staticfiles.finders.AppDirectoriesFinder', +] ############################# ADVANCED COMPONENTS ############################# diff --git a/cms/envs/test.py b/cms/envs/test.py index 1539c28fb7..16133ac8ef 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -281,8 +281,5 @@ SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" # teams feature FEATURES['ENABLE_TEAMS'] = True -# teams search -FEATURES['ENABLE_TEAMS_SEARCH'] = True - # Dummy secret key for dev/test SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' diff --git a/cms/envs/test_static_optimized.py b/cms/envs/test_static_optimized.py index 61cca130c1..af030f1633 100644 --- a/cms/envs/test_static_optimized.py +++ b/cms/envs/test_static_optimized.py @@ -20,7 +20,17 @@ DATABASES = { }, } -######################### Static file overrides #################################### + +######################### PIPELINE #################################### + +# Use RequireJS optimized storage +STATICFILES_STORAGE = 'openedx.core.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage' + +# Revert to the default set of finders as we don't want to dynamically pick up files from the pipeline +STATICFILES_FINDERS = [ + 'staticfiles.finders.FileSystemFinder', + 'staticfiles.finders.AppDirectoriesFinder', +] # Redirect to the test_root folder within the repo TEST_ROOT = REPO_ROOT / "test_root" @@ -33,4 +43,3 @@ STATIC_ROOT = (TEST_ROOT / "staticfiles" / "cms").abspath() # 1. Uglify is by far the slowest part of the build process # 2. Having full source code makes debugging tests easier for developers os.environ['REQUIRE_BUILD_PROFILE_OPTIMIZE'] = 'none' -PIPELINE_JS_COMPRESSOR = None diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index 580f005698..c64ac7f9c1 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -27,8 +27,9 @@ require.config({ "jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents", "datepair": "js/vendor/timepicker/datepair", "date": "js/vendor/date", - "text": 'js/vendor/requirejs/text', "moment": "js/vendor/moment.min", + "moment-with-locales": "js/vendor/moment-with-locales.min", + "text": 'js/vendor/requirejs/text', "underscore": "js/vendor/underscore-min", "underscore.string": "js/vendor/underscore.string.min", "backbone": "js/vendor/backbone-min", diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index b211eb3fdf..0fcf7f6e2e 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -23,6 +23,8 @@ requirejs.config({ "jquery.simulate": "xmodule_js/common_static/js/vendor/jquery.simulate", "datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair", "date": "xmodule_js/common_static/js/vendor/date", + "moment": "xmodule_js/common_static/js/vendor/moment.min", + "moment-with-locales": "xmodule_js/common_static/js/vendor/moment-with-locales.min", "text": "xmodule_js/common_static/js/vendor/requirejs/text", "underscore": "xmodule_js/common_static/js/vendor/underscore-min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", diff --git a/cms/static/js/certificates/models/signatory.js b/cms/static/js/certificates/models/signatory.js index ba478d5ece..416607d222 100644 --- a/cms/static/js/certificates/models/signatory.js +++ b/cms/static/js/certificates/models/signatory.js @@ -46,8 +46,8 @@ function(_, str, Backbone, BackboneRelational, gettext) { 'title': gettext('Signatory title should span over maximum of 2 lines.') }, errors); } - else if ((lines.length > 1 && (lines[0].length > 40 || lines[1].length > 40)) || - (lines.length === 1 && title.length > 40)) { + else if ((lines.length > 1 && (lines[0].length > 53 && lines[1].length > 53)) || + (lines.length === 1 && title.length > 106)) { errors = _.extend({ 'title': gettext('Signatory title should have maximum of 40 characters per line.') }, errors); diff --git a/cms/static/js/certificates/spec/views/certificate_details_spec.js b/cms/static/js/certificates/spec/views/certificate_details_spec.js index 75c3f84d51..b4044182fa 100644 --- a/cms/static/js/certificates/spec/views/certificate_details_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_details_spec.js @@ -246,7 +246,7 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails }); setValuesToInputs(this.view, { - inputSignatoryTitle: 'New Signatory Test Title longer than 40 characters in length' + inputSignatoryTitle: 'This is a certificate signatory title that has waaaaaaay more than 106 characters, in order to cause an exception.' }); setValuesToInputs(this.view, { diff --git a/cms/static/js/certificates/spec/views/certificate_editor_spec.js b/cms/static/js/certificates/spec/views/certificate_editor_spec.js index af3681d2de..5ccd3794f2 100644 --- a/cms/static/js/certificates/spec/views/certificate_editor_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_editor_spec.js @@ -228,7 +228,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce } ); - it('signatories should not save when title has more than 40 characters per line', function() { + it('signatories should not save when fields have too many characters per line', function() { this.view.$(SELECTORS.addSignatoryButton).click(); setValuesToInputs(this.view, { inputCertificateName: 'New Certificate Name' @@ -239,7 +239,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce }); setValuesToInputs(this.view, { - inputSignatoryTitle: 'New Signatory title longer than 40 characters on one line' + inputSignatoryTitle: 'This is a certificate signatory title that has waaaaaaay more than 106 characters, in order to cause an exception.' }); setValuesToInputs(this.view, { diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index cb738ac959..c9eb7c270d 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -574,6 +574,25 @@ define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/aja }); }); + it('also works for older-style add component links', function () { + // Some third party xblocks (problem-builder in particular) expect add + // event handlers on custom add buttons which is what the platform + // used to use instead of - + diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index db6ff974f3..d76823e9bb 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -100,6 +100,9 @@ class CourseMode(models.Model): # Modes that allow a student to pursue a verified certificate VERIFIED_MODES = [VERIFIED, PROFESSIONAL] + # Modes that allow a student to pursue a non-verified certificate + NON_VERIFIED_MODES = [HONOR, AUDIT, NO_ID_PROFESSIONAL_MODE] + # Modes that allow a student to earn credit with a university partner CREDIT_MODES = [CREDIT_MODE] diff --git a/common/djangoapps/monkey_patch/tests/test_django_utils_translation.py b/common/djangoapps/monkey_patch/tests/test_django_utils_translation.py index 2e9db80bdf..109d2a40f0 100644 --- a/common/djangoapps/monkey_patch/tests/test_django_utils_translation.py +++ b/common/djangoapps/monkey_patch/tests/test_django_utils_translation.py @@ -29,6 +29,9 @@ while patched. # All major functions are documented, the rest are self-evident shells. # pylint: disable=no-member # Pylint doesn't see our decorator `translate_with` add the `_` method. +# pylint: disable=test-inherits-tests +# This test file intentionally defines one base test class (UgettextTest) and +# patches the gettext function under test for all subsequent inheriting classes. from unittest import TestCase from ddt import data diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6f62e9e13b..cf6c9abbfb 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -29,7 +29,7 @@ from django.utils import timezone from django.contrib.auth.models import User from django.contrib.auth.hashers import make_password from django.contrib.auth.signals import user_logged_in, user_logged_out -from django.db import models, IntegrityError +from django.db import models, IntegrityError, transaction from django.db.models import Count from django.db.models.signals import pre_save, post_save from django.dispatch import receiver, Signal @@ -894,16 +894,32 @@ class CourseEnrollment(models.Model): if user.id is None: user.save() - enrollment, created = CourseEnrollment.objects.get_or_create( - user=user, - course_id=course_key, - ) + try: + enrollment, created = CourseEnrollment.objects.get_or_create( + user=user, + course_id=course_key, + ) - # If we *did* just create a new enrollment, set some defaults - if created: - enrollment.mode = "honor" - enrollment.is_active = False - enrollment.save() + # If we *did* just create a new enrollment, set some defaults + if created: + enrollment.mode = "honor" + enrollment.is_active = False + enrollment.save() + except IntegrityError: + log.info( + ( + "An integrity error occurred while getting-or-creating the enrollment" + "for course key %s and student %s. This can occur if two processes try to get-or-create " + "the enrollment at the same time and the database is set to REPEATABLE READ. We will try " + "committing the transaction and retrying." + ), + course_key, user + ) + transaction.commit() + enrollment = CourseEnrollment.objects.get( + user=user, + course_id=course_key, + ) return enrollment diff --git a/common/djangoapps/student/tests/test_certificates.py b/common/djangoapps/student/tests/test_certificates.py index 539dc456b9..af4a6db8f1 100644 --- a/common/djangoapps/student/tests/test_certificates.py +++ b/common/djangoapps/student/tests/test_certificates.py @@ -13,6 +13,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error from certificates.api import get_certificate_url # pylint: disable=import-error +from course_modes.models import CourseMode # pylint: disable=no-member @@ -42,6 +43,15 @@ class CertificateDisplayTest(ModuleStoreTestCase): self._create_certificate(enrollment_mode) self._check_can_download_certificate() + @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False}) + def test_display_verified_certificate_no_id(self): + """ + Confirm that if we get a certificate with a no-id-professional mode + we still can download our certificate + """ + self._create_certificate(CourseMode.NO_ID_PROFESSIONAL_MODE) + self._check_can_download_certificate_no_id() + @ddt.data('verified', 'honor') @override_settings(CERT_NAME_SHORT='Test_Certificate') @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) @@ -105,6 +115,16 @@ class CertificateDisplayTest(ModuleStoreTestCase): self.assertContains(response, u'Download Your ID Verified') self.assertContains(response, self.DOWNLOAD_URL) + def _check_can_download_certificate_no_id(self): + """ + Inspects the dashboard to see if a certificate for a non verified course enrollment + is present + """ + response = self.client.get(reverse('dashboard')) + self.assertContains(response, u'Download') + self.assertContains(response, u'(PDF)') + self.assertContains(response, self.DOWNLOAD_URL) + def _check_can_not_download_certificate(self): """ Make sure response does not have any of the download certificate buttons diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index e603c90b02..2e9da01e9b 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -120,6 +120,7 @@ def initial_setup(server): world.visit('/') except WebDriverException: + LOGGER.warn("Error acquiring %s browser, retrying", browser_driver, exc_info=True) if hasattr(world, 'browser'): world.browser.quit() num_attempts += 1 diff --git a/common/djangoapps/third_party_auth/tests/specs/test_lti.py b/common/djangoapps/third_party_auth/tests/specs/test_lti.py index fa9e2398e4..d6622def7e 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_lti.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_lti.py @@ -69,8 +69,8 @@ class IntegrationTestLTI(testutil.TestCase): self.assertTrue(login_response['Location'].endswith(reverse('signin_user'))) register_response = self.client.get(login_response['Location']) self.assertEqual(register_response.status_code, 200) - self.assertIn('currentProvider": "LTI Test Tool Consumer"', register_response.content) - self.assertIn('"errorMessage": null', register_response.content) + self.assertIn('"currentProvider": "LTI Test Tool Consumer"', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) # Now complete the form: ajax_register_response = self.client.post( @@ -153,7 +153,7 @@ class IntegrationTestLTI(testutil.TestCase): register_response = self.client.get(login_response['Location']) self.assertEqual(register_response.status_code, 200) self.assertIn( - 'currentProvider": "Tool Consumer with Secret in Settings"', + '"currentProvider": "Tool Consumer with Secret in Settings"', register_response.content ) - self.assertIn('"errorMessage": null', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index dc4ca99880..b833efab27 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -1,13 +1,20 @@ """ Third_party_auth integration tests using a mock version of the TestShib provider """ -from django.core.urlresolvers import reverse + +import json +import unittest import httpretty from mock import patch + +from django.core.urlresolvers import reverse + +from openedx.core.lib.json_utils import EscapedEdxJSONEncoder + from student.tests.factories import UserFactory from third_party_auth.tasks import fetch_saml_metadata from third_party_auth.tests import testutil -import unittest + TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth' TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml' @@ -81,11 +88,11 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): # We'd now like to see if the "You've successfully signed into TestShib" message is # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', register_response.content) - self.assertIn('"errorMessage": null', register_response.content) + self.assertIn('"currentProvider": "TestShib"', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) # Now do a crude check that the data (e.g. email) from the provider is displayed in the form: - self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) - self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) + self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) + self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) # Now complete the form: ajax_register_response = self.client.post( reverse('user_api_registration'), @@ -128,8 +135,8 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): # We'd now like to see if the "You've successfully signed into TestShib" message is # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', login_response.content) - self.assertIn('"errorMessage": null', login_response.content) + self.assertIn('"currentProvider": "TestShib"', login_response.content) + self.assertIn('"errorMessage": null', login_response.content) # Now the user enters their username and password. # The AJAX on the page will log them in: ajax_login_response = self.client.post( @@ -183,7 +190,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): response = self.client.get(self.login_page_url) self.assertEqual(response.status_code, 200) self.assertIn("TestShib", response.content) - self.assertIn(TPA_TESTSHIB_LOGIN_URL.replace('&', '&'), response.content) + self.assertIn(json.dumps(TPA_TESTSHIB_LOGIN_URL, cls=EscapedEdxJSONEncoder), response.content) return response def _check_register_page(self): @@ -191,7 +198,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): response = self.client.get(self.register_page_url) self.assertEqual(response.status_code, 200) self.assertIn("TestShib", response.content) - self.assertIn(TPA_TESTSHIB_REGISTER_URL.replace('&', '&'), response.content) + self.assertIn(json.dumps(TPA_TESTSHIB_REGISTER_URL, cls=EscapedEdxJSONEncoder), response.content) return response def _configure_testshib_provider(self, **kwargs): diff --git a/common/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py index 7f26b05494..f8715bcc0e 100644 --- a/common/djangoapps/track/middleware.py +++ b/common/djangoapps/track/middleware.py @@ -145,7 +145,7 @@ class TrackMiddleware(object): # this: _ga=GA1.2.1033501218.1368477899. The clientId is this part: 1033501218.1368477899. google_analytics_cookie = request.COOKIES.get('_ga') if google_analytics_cookie is None: - context['client_id'] = None + context['client_id'] = request.META.get('HTTP_X_EDX_GA_CLIENT_ID') else: context['client_id'] = '.'.join(google_analytics_cookie.split('.')[2:]) diff --git a/common/djangoapps/track/tests/test_middleware.py b/common/djangoapps/track/tests/test_middleware.py index 587e4e310d..93be67d911 100644 --- a/common/djangoapps/track/tests/test_middleware.py +++ b/common/djangoapps/track/tests/test_middleware.py @@ -136,12 +136,16 @@ class TrackMiddlewareTestCase(TestCase): def test_request_headers(self): ip_address = '10.0.0.0' user_agent = 'UnitTest/1.0' + client_id_header = '123.123' - factory = RequestFactory(REMOTE_ADDR=ip_address, HTTP_USER_AGENT=user_agent) + factory = RequestFactory( + REMOTE_ADDR=ip_address, HTTP_USER_AGENT=user_agent, HTTP_X_EDX_GA_CLIENT_ID=client_id_header + ) request = factory.get('/some-path') context = self.get_context_for_request(request) self.assert_dict_subset(context, { 'ip': ip_address, 'agent': user_agent, + 'client_id': client_id_header }) diff --git a/common/djangoapps/util/model_utils.py b/common/djangoapps/util/model_utils.py index 9ba07f04b1..e0a03bc690 100644 --- a/common/djangoapps/util/model_utils.py +++ b/common/djangoapps/util/model_utils.py @@ -99,6 +99,35 @@ def emit_field_changed_events(instance, user, db_table, excluded_fields=None, hi del instance._changed_fields +def truncate_fields(old_value, new_value): + """ + Truncates old_value and new_value for analytics event emission if necessary. + + Args: + old_value(obj): the value before the change + new_value(obj): the new value being saved + + Returns: + a dictionary with the following fields: + 'old': the truncated old value + 'new': the truncated new value + 'truncated': the list of fields that have been truncated + """ + # Compute the maximum value length so that two copies can fit into the maximum event size + # in addition to all the other fields recorded. + max_value_length = settings.TRACK_MAX_EVENT / 4 + + serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length) + serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length) + truncated_values = [] + if old_was_truncated: + truncated_values.append("old") + if new_was_truncated: + truncated_values.append("new") + + return {'old': serialized_old_value, 'new': serialized_new_value, 'truncated': truncated_values} + + def emit_setting_changed_event(user, db_table, setting_name, old_value, new_value): """Emits an event for a change in a setting. @@ -112,27 +141,15 @@ def emit_setting_changed_event(user, db_table, setting_name, old_value, new_valu Returns: None """ - # Compute the maximum value length so that two copies can fit into the maximum event size - # in addition to all the other fields recorded. - max_value_length = settings.TRACK_MAX_EVENT / 4 + truncated_fields = truncate_fields(old_value, new_value) + + truncated_fields['setting'] = setting_name + truncated_fields['user_id'] = user.id + truncated_fields['table'] = db_table - serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length) - serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length) - truncated_values = [] - if old_was_truncated: - truncated_values.append("old") - if new_was_truncated: - truncated_values.append("new") tracker.emit( USER_SETTINGS_CHANGED_EVENT_NAME, - { - "setting": setting_name, - "old": serialized_old_value, - "new": serialized_new_value, - "truncated": truncated_values, - "user_id": user.id, - "table": db_table, - } + truncated_fields ) diff --git a/common/lib/capa/capa/templates/formulaequationinput.html b/common/lib/capa/capa/templates/formulaequationinput.html index 206a8bc21c..d2ea463a9f 100644 --- a/common/lib/capa/capa/templates/formulaequationinput.html +++ b/common/lib/capa/capa/templates/formulaequationinput.html @@ -16,11 +16,12 @@ +

+
\[\] Loading
-

diff --git a/common/lib/capa/capa/templates/optioninput.html b/common/lib/capa/capa/templates/optioninput.html index a4f25d801b..24b7afec9e 100644 --- a/common/lib/capa/capa/templates/optioninput.html +++ b/common/lib/capa/capa/templates/optioninput.html @@ -12,7 +12,6 @@ % endfor -
${value|h} - ${status.display_name}
+

% if msg: ${msg|n} % endif diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index cf441cb462..77d9225f3d 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -406,7 +406,6 @@ div.problem { margin-top: 3px; .MathJax_Display { - display: inline-block; width: auto; } @@ -429,6 +428,11 @@ div.problem { } } + // Fix for formulaequationinput, overriding MathJax_Display default style to allow "loading" image to sit next to it + section.formulaequationinput div.equation .MathJax_Display { + display: inline-block !important; + } + // Hides equation previews in symbolic response problems when printing [id^='display'].equation { @media print { @@ -713,13 +717,10 @@ div.problem { height: 46px; } - > .incorrect, .correct, .unanswered { - - .status { - display: inline-block; - margin-top: ($baseline/2); - background: none; - } + .status { + display: inline-block; + margin-top: ($baseline/2); + background: none; } // CASE: incorrect answer @@ -746,8 +747,8 @@ div.problem { } } - // CASE: unanswered - > .unanswered { + // CASE: unanswered and unsubmitted + > .unanswered, > .unsubmitted { input { border: 2px solid $gray-l4; diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index bee65b3bf6..d184499477 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -39,6 +39,7 @@ $sequence--border-color: #C8C8C8; margin: -4px 0 ($baseline*1.5); position: relative; border-bottom: none; + z-index: 0; @media print { display: none; diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py index 89bd27a9c7..8c134991bd 100644 --- a/common/lib/xmodule/xmodule/discussion_module.py +++ b/common/lib/xmodule/xmodule/discussion_module.py @@ -97,13 +97,10 @@ class DiscussionModule(DiscussionFields, XModule): def get_course(self): """ - Return the CourseDescriptor at the root of the tree we're in. + Return CourseDescriptor by course id. """ - block = self - while block.parent: - block = block.get_parent() - - return block + course = self.runtime.modulestore.get_course(self.course_id) + return course class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor): diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 95cf99e00c..379f5f04ba 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -312,7 +312,7 @@ class SplitBulkWriteMixin(BulkOperationsMixin): if bulk_write_record.active: bulk_write_record.index = updated_index_entry else: - self.db_connection.update_course_index(updated_index_entry, course_key) + self.db_connection.update_course_index(updated_index_entry, course_context=course_key) def get_structure(self, course_key, version_guid): bulk_write_record = self._get_bulk_ops_record(course_key) diff --git a/common/static/common/js/components/collections/paging_collection.js b/common/static/common/js/components/collections/paging_collection.js index 4bf4ff3066..a8af909897 100644 --- a/common/static/common/js/components/collections/paging_collection.js +++ b/common/static/common/js/components/collections/paging_collection.js @@ -90,16 +90,20 @@ */ setPage: function (page) { var oldPage = this.currentPage, - self = this; - return this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then( + self = this, + deferred = $.Deferred(); + this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then( function () { self.isStale = false; self.trigger('page_changed'); + deferred.resolve(); }, function () { self.currentPage = oldPage; + deferred.fail(); } ); + return deferred.promise(); }, diff --git a/common/static/common/templates/components/search-field.underscore b/common/static/common/templates/components/search-field.underscore index aac29f640d..c25d8640f6 100644 --- a/common/static/common/templates/components/search-field.underscore +++ b/common/static/common/templates/components/search-field.underscore @@ -1,7 +1,7 @@