diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index 2b23ddbb06..e59b2636e3 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -93,6 +93,7 @@ source, template_path = Loader(engine).load_template_source(path) <% from django.template import Template, Context + from webpack_loader.exceptions import WebpackLoaderBadStatsError try: return Template(""" {% load render_bundle from webpack_loader %} @@ -105,9 +106,9 @@ source, template_path = Loader(engine).load_template_source(path) 'entry': entry, 'body': capture(caller.body) })) - except IOError as e: + except (IOError, WebpackLoaderBadStatsError) as e: # Don't break Mako template rendering if the bundle or webpack-stats can't be found, but log it - logger.error(e) + logger.error('[LEARNER-1938] {error}'.format(error=e)) %> diff --git a/lms/djangoapps/bulk_enroll/tests/test_views.py b/lms/djangoapps/bulk_enroll/tests/test_views.py index 99eaac5234..f805a73bbc 100644 --- a/lms/djangoapps/bulk_enroll/tests/test_views.py +++ b/lms/djangoapps/bulk_enroll/tests/test_views.py @@ -1,6 +1,7 @@ """ Tests for the Bulk Enrollment views. """ +import ddt import json from django.conf import settings from django.contrib.auth.models import User @@ -25,6 +26,7 @@ from xmodule.modulestore.tests.factories import CourseFactory @override_settings(ENABLE_BULK_ENROLLMENT_VIEW=True) +@ddt.ddt class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCase): """ Test the bulk enrollment endpoint @@ -67,9 +69,13 @@ class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCa self.about_path = '/courses/{}/about'.format(self.course.id) self.course_path = '/courses/{}/'.format(self.course.id) - def request_bulk_enroll(self, data=None, **extra): + def request_bulk_enroll(self, data=None, use_json=False, **extra): """ Make an authenticated request to the bulk enrollment API. """ - request = self.request_factory.post(self.url, data=data, **extra) + content_type = None + if use_json: + content_type = 'application/json' + data = json.dumps(data) + request = self.request_factory.post(self.url, data=data, content_type=content_type, **extra) force_authenticate(request, user=self.staff) response = self.view(request) response.render() @@ -221,14 +227,15 @@ class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCa res_json = json.loads(response.content) self.assertEqual(res_json, expected) - def test_enroll_with_email(self): + @ddt.data(False, True) + def test_enroll_with_email(self, use_json): """ Test enrolling using a username as the identifier. """ response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, - }) + }, use_json=use_json) self.assertEqual(response.status_code, 200) # test that the user is now enrolled @@ -274,10 +281,15 @@ class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCa # Check the outbox self.assertEqual(len(mail.outbox), 0) - def test_unenroll(self): + @ddt.data(False, True) + def test_unenroll(self, use_json): """ Test unenrolling a user. """ - response = self.request_bulk_enroll({'identifiers': self.enrolled_student.email, 'action': 'unenroll', - 'email_students': False, 'courses': self.course_key, }) + response = self.request_bulk_enroll({ + 'identifiers': self.enrolled_student.email, + 'action': 'unenroll', + 'email_students': False, + 'courses': self.course_key, + }, use_json=use_json) self.assertEqual(response.status_code, 200) # test that the user is now unenrolled diff --git a/lms/djangoapps/bulk_enroll/views.py b/lms/djangoapps/bulk_enroll/views.py index fe768853aa..2a2520cad8 100644 --- a/lms/djangoapps/bulk_enroll/views.py +++ b/lms/djangoapps/bulk_enroll/views.py @@ -60,6 +60,12 @@ class BulkEnrollView(APIView): def post(self, request): serializer = BulkEnrollmentSerializer(data=request.data) if serializer.is_valid(): + # Setting the content type to be form data makes Django Rest Framework v3.6.3 treat all passed JSON data as + # POST parameters. This is necessary because this request is forwarded on to the student_update_enrollment + # view, which requires all of the parameters to be passed in via POST parameters. + metadata = request._request.META # pylint: disable=protected-access + metadata['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' + response_dict = { 'auto_enroll': serializer.data.get('auto_enroll'), 'email_students': serializer.data.get('email_students'), diff --git a/lms/djangoapps/grades/new/course_grade.py b/lms/djangoapps/grades/new/course_grade.py index 745c6434b4..b438001d07 100644 --- a/lms/djangoapps/grades/new/course_grade.py +++ b/lms/djangoapps/grades/new/course_grade.py @@ -21,7 +21,7 @@ class CourseGradeBase(object): """ Base class for Course Grades. """ - def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False): + def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False, force_update_subsections=False): self.user = user self.course_data = course_data @@ -30,6 +30,7 @@ class CourseGradeBase(object): # Convert empty strings to None when reading from the table self.letter_grade = letter_grade or None + self.force_update_subsections = force_update_subsections def __unicode__(self): return u'Course Grade: percent: {}, letter_grade: {}, passed: {}'.format( @@ -203,7 +204,9 @@ class CourseGrade(CourseGradeBase): def update(self): """ - Updates the grade for the course. + Updates the grade for the course. Also updates subsection grades + if self.force_update_subsections is true, via the lazy call + to self.grader_result. """ grade_cutoffs = self.course_data.course.grade_cutoffs self.percent = self._compute_percent(self.grader_result) @@ -224,7 +227,10 @@ class CourseGrade(CourseGradeBase): def _get_subsection_grade(self, subsection): # Pass read_only here so the subsection grades can be persisted in bulk at the end. - return self._subsection_grade_factory.create(subsection, read_only=True) + if self.force_update_subsections: + return self._subsection_grade_factory.update(subsection) + else: + return self._subsection_grade_factory.create(subsection, read_only=True) @staticmethod def _compute_percent(grader_result): diff --git a/lms/djangoapps/grades/new/course_grade_factory.py b/lms/djangoapps/grades/new/course_grade_factory.py index 9085436068..4ffe091b79 100644 --- a/lms/djangoapps/grades/new/course_grade_factory.py +++ b/lms/djangoapps/grades/new/course_grade_factory.py @@ -66,7 +66,15 @@ class CourseGradeFactory(object): else: return None - def update(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None): + def update( + self, + user, + course=None, + collected_block_structure=None, + course_structure=None, + course_key=None, + force_update_subsections=False, + ): """ Computes, updates, and returns the CourseGrade for the given user in the course. @@ -75,7 +83,7 @@ class CourseGradeFactory(object): or course_key should be provided. """ course_data = CourseData(user, course, collected_block_structure, course_structure, course_key) - return self._update(user, course_data, read_only=False) + return self._update(user, course_data, read_only=False, force_update_subsections=force_update_subsections) @contextmanager def _course_transaction(self, course_key): @@ -118,10 +126,17 @@ class CourseGradeFactory(object): def _iter_grade_result(self, user, course_data, force_update): try: + kwargs = { + 'user': user, + 'course': course_data.course, + 'collected_block_structure': course_data.collected_structure, + 'course_key': course_data.course_key + } + if force_update: + kwargs['force_update_subsections'] = True + method = CourseGradeFactory().update if force_update else CourseGradeFactory().create - course_grade = method( - user, course_data.course, course_data.collected_structure, course_key=course_data.course_key, - ) + course_grade = method(**kwargs) return self.GradeResult(user, course_grade, None) except Exception as exc: # pylint: disable=broad-except # Keep marching on even if this student couldn't be graded for @@ -165,14 +180,14 @@ class CourseGradeFactory(object): return course_grade, persistent_grade.grading_policy_hash @staticmethod - def _update(user, course_data, read_only): + def _update(user, course_data, read_only, force_update_subsections=False): """ Computes, saves, and returns a CourseGrade object for the given user and course. Sends a COURSE_GRADE_CHANGED signal to listeners and a COURSE_GRADE_NOW_PASSED if learner has passed course. """ - course_grade = CourseGrade(user, course_data) + course_grade = CourseGrade(user, course_data, force_update_subsections=force_update_subsections) course_grade.update() should_persist = ( diff --git a/lms/djangoapps/grades/tasks.py b/lms/djangoapps/grades/tasks.py index 3bb8d9e3f9..3e4df7605e 100644 --- a/lms/djangoapps/grades/tasks.py +++ b/lms/djangoapps/grades/tasks.py @@ -106,7 +106,7 @@ def compute_grades_for_course_v2(self, **kwargs): @task(base=_BaseTask) def compute_grades_for_course(course_key, offset, batch_size, **kwargs): # pylint: disable=unused-argument """ - Compute grades for a set of students in the specified course. + Compute and save grades for a set of students in the specified course. The set of students will be determined by the order of enrollment date, and limited to at most students, starting from the specified diff --git a/lms/djangoapps/grades/tests/test_new.py b/lms/djangoapps/grades/tests/test_new.py index d5a942df0c..fb7feccbf0 100644 --- a/lms/djangoapps/grades/tests/test_new.py +++ b/lms/djangoapps/grades/tests/test_new.py @@ -246,6 +246,20 @@ class TestCourseGradeFactory(GradeTestBase): else: self.assertIsNone(course_grade) + @ddt.data(True, False) + def test_iter_force_update(self, force_update): + base_string = 'lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory.{}' + desired_method_name = base_string.format('update' if force_update else 'create') + undesired_method_name = base_string.format('create' if force_update else 'update') + with patch(desired_method_name) as desired_call: + with patch(undesired_method_name) as undesired_call: + set(CourseGradeFactory().iter( + users=[self.request.user], course=self.course, force_update=force_update + )) + + self.assertTrue(desired_call.called) + self.assertFalse(undesired_call.called) + @ddt.ddt class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase): diff --git a/lms/static/sass/_experiments.scss b/lms/static/sass/_experiments.scss index fee87f3911..cb1e952aea 100644 --- a/lms/static/sass/_experiments.scss +++ b/lms/static/sass/_experiments.scss @@ -4,22 +4,26 @@ // Please list the ticket number of the experiment // -------------------- -// LEARNER-1312 Track Selection V2 -/* This css was added as part of the LEARNER-1312 experiment */ +// LEARNER-1726 Track Selection V3 +/* This css was added as part of the LEARNER-1726 experiment */ .v2.register-choice { margin: 0 2% 20px 0 !important } + .v2.register-choice-certificate .list-actions { text-align: left !important; } + .v2.register-choice-donate .list-actions { margin-bottom: 0 !important; } + .v2.register-choice-donate .action-select { display: inline-block !important; list-style-type: none !important; width: 100% !important; } + .v2.register-choice-donate .donation-link { display: inline-block !important; padding: 10px 15px !important; @@ -30,219 +34,361 @@ text-align: center !important; color: #D7548E !important; float: left !important; + font-size: 15px; + font-weight: 500 !important; } + +@media (min-width: 375px) { + .donation-link { + font-size: 16px; + } +} + .v2.register-choice-v2-audit { - height: 250px !important; + height: 300px; background: none !important; border-top-color: grey !important; border-top-width: 1px !important; } + +@media screen and (min-width: 375px) { + .v2.register-choice-v2-audit { + height: 250px; + } +} + .v2.register-choice-v2-audit .list-actions { margin-bottom: 0 !important; } + .v2.register-choice-v2-audit .list-actions input { background: transparent !important; color: #0075b4 !important; box-shadow: none !important; text-decoration: underline !important; border: none !important; + white-space: normal; } + .v2.register-choice-v2-audit .wrapper-copy-inline { height: 70px !important; width: 100% !important; display: flex !important; } + .v2.register-choice-v2-audit .wrapper-copy { width: 70% !important; height: auto !important; } .v2.page-header { - padding-bottom: 0; + padding: 0; } + .v2 img { margin-top: 20px; margin-left: 5px; } + .v2 .donation-link { font-weight: bold !important; } -@media (min-width: 320px) { - .v2.register-choice-certificate, - .v2.register-choice-donate, - .v2.register-choice-view { - width: 100%; - } - .v2 .wrapper-copy-inline { - max-height: 115px; - } - .v2.register-choice-v2-audit .wrapper-copy-inline { - display: block !important; - } - .v2.register-choice-v2-audit .copy-inline { - width: 100% !important; - } - .v2.register-choice-v2-audit .list-actions { - width: 100% !important; - margin-top: 20px !important; - text-align: center !important; - } - .v2 .wrapper-copy-inline .wrapper-copy { - width: 100% !important; - } - .v2 .donation-link, .v2 input { - width: 100% !important; - font-size: 15px !important; - } - .v2 img { - display: none; + +.v2.register-choice-certificate, +.v2.register-choice-donate, +.v2.register-choice-view { + width: 100%; +} + +.v2.register-choice-donate { + border-color: #D7548E !important; +} + +.v2 .wrapper-copy-inline { + max-height: 115px; +} + +.v2.register-choice-v2-audit .wrapper-copy-inline { + display: block !important; +} + +.v2.register-choice-v2-audit .copy-inline { + width: 100% !important; +} + +.v2.register-choice-v2-audit .list-actions { + width: 100% !important; + margin-top: 20px !important; + text-align: center !important; +} + +.v2 .wrapper-copy-inline .wrapper-copy { + width: 100% !important; +} + +.v2 input{ + font-size: 15px !important; +} + +.v2 button { + background-color: rgb(0, 103, 0); + border-color: rgb(0, 103, 0); + border-radius: 2px; + box-shadow: rgb(0, 77, 0) 0px 2px 1px 0px; + cursor: pointer; + font-family: "Open Sans"; + height: auto; + margin-right: 4px; + margin-top: 0px; + padding: 10px 15px; + width: initial; + background-image: none !important; + font-size: 14px !important; + font-weight: 500 !important; + + &:hover, &:focus { + background-color: #009b00 !important; + border-color: #009b00; + box-shadow: #004d00 0px 2px 1px 0px; } } + +.savings-message { + margin-top: 10px; + font-size: 11px; +} +@media screen and (min-width: 375px) { + .savings-message { + font-size: 13px; + margin-left: 16px; + } +} + +.v2 .donation-link, .v2 input, .v2 button { + width: 100%; +} + +.v2 img { + display: none; +} + +.v2 .deco-divider { + display: none; +} + +.v2 .visual-reference { + width: 38%; +} + +@media (min-width: 420px) { + .v2 button { + height: 45px; + font-size: 16px !important; + } +} + @media (min-width: 768px) { .v2.register-choice-certificate, .v2.register-choice-donate { - width: 48% !important; + width: 46.5% !important; display: inline-block; - min-height: 250px; + min-height: 270px; } + .v2.register-choice-v2-audit .wrapper-copy-inline { display: flex !important; } + .v2.register-choice-v2-audit .copy-inline { width: 40% !important; } + .v2.register-choice-v2-audit .list-actions { margin-top: 0 !important; text-align: right !important; } + .v2 .wrapper-copy-inline .wrapper-copy { - width: 58% !important; + width: 100% !important; } - .v2 .donation-link, .v2 input { + + .v2 input { font-size: 15px !important; - width: 55% !important; } + + .v2 .donation-link, .v2.register-choice-certificate button { + margin-top: 20px; + width: initial; + } + + .v2.register-choice-v2-audit input { + width: 100% !important; + } + .v2.register-choice-view { height: 250px; } + .v2 img { display: initial; } + .v2.register-choice { margin: 0 2% 20px 0; } + .v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy { + width: 60%; + } + + .v2.register-choice-view .wrapper-copy-inline .wrapper-copy { + width: 100%; + } + + .v2.register-choice { + padding: 15px !important; + } + + .v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy { + width: 60%; + } + + .v2.register-choice { + padding: 20px !important; + } + + .v2.register-choice.register-choice-view { + margin-right: 0; + } + + .v2.register-choice .list-actions:last-child { + float: left; + width: 100%; + margin-top: 0px; + } + + .v2.register-choice .action-select { + width: 100% !important; + } + + .v2 .donation-link:hover, + .v2 .donation-link:focus { + background-color: #D7548E !important; + color: white !important; + text-decoration: none; + } + + .v2 .donation-link:hover { + cursor: pointer; + } + + .v2 .copy li { + margin-bottom: 5px; + } + + .v2.register-choice .copy-inline { + width: 100%; + } + + .v2 .register-choice-view { + border-color: #2991c3 !important; + } + + .v2 .visual-reference { + vertical-align: top; + } + + .v2 .wrapper-copy-inline .wrapper-copy ul { + margin-top: 0px; + padding-left: 30px; + } + + .v2 .img-certificate { + border: 2px solid #009b00 !important; + float: right; + height: 120px; + width: auto; + margin-top: 0 !important; + display: none; + } + + .v2 .img-donate { + margin-top: 0; + float: right; + border: 2px solid #D7548E !important; + display: none; + } + + .v2 .img-view { + border: 2px solid #2991c3 !important; + } + + .v2.register-choice .title { + width: 100%; + margin-bottom: 20px; + } + + .v2.register-choice.register-choice-view .action-select { + border: 1px solid transparent !important; + border-radius: 3px; + } + + .v2.register-choice.register-choice-view .action-select button { + border: 1px solid transparent !important; + } + + .v2.register-choice.register-choice-view .action-select:hover { + border: 1px solid #0075b4 !important; + } + .v2.deco-divider { width: 3% !important; box-sizing: border-box; float: left; display: inline-block; - height: 400px; + height: 250px; margin: 0px 0 40px 0 !important; - border-left: 4px solid #f5f5f5 !important; - border-top: none !important; + border-left: 4px solid #f5f5f5 !important; border-top:none !important; + + .copy { + position: absolute; + top: 110px !important; + left: calc(50% - 40px) !important; + margin-left: 20px; + background: white; + text-align: center; + color: #474747; + width: 10px; + padding: 0 !important; + } } } -@media (min-width: 320px) { - .v2 .visual-reference { - width: 38%; - } -} -@media (min-width: 768px) { - - @media (min-width: 320px) { - .v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy { - width: 60%; - } - } - @media (min-width: 768px) { - .v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy { - width: 60%; - } - } - @media (min-width: 320px) { - .v2.register-choice-view .wrapper-copy-inline .wrapper-copy { - width: 100%; - } - } - @media (min-width: 320px) { - .v2.register-choice { - padding: 15px !important; - } - } - @media (min-width: 768px) { - .v2.register-choice { - padding: 20px !important; - } - .v2.register-choice.register-choice-view { - margin-right: 0; - } - } - @media screen and (min-width: 769px) { - .v2.register-choice .list-actions:last-child { - float: left; - width: 100%; - margin-top: 0px; - } - } - @media screen and (min-width: 769px) { - .v2.register-choice .action-select { - width: 100% !important; - } - } - .v2 .donation-link:hover, - .v2 .donation-link:focus { - background-color: #D7548E !important; - color: white !important; - text-decoration: none; - } - .v2 .donation-link:hover { - cursor: pointer; - } - .v2 .copy li { - margin-bottom: 5px; - } - .v2.register-choice .copy-inline { - width: 100%; - } +@media (min-width: 835px) { + .v2.register-choice-certificate, .v2.register-choice-donate { - border-color: #D7548E !important; - } - .v2 .register-choice-view { - border-color: #2991c3 !important; - } - .v2 .visual-reference { - vertical-align: top; - } - .v2 .wrapper-copy-inline .wrapper-copy ul { - margin-top: 0px; - padding-left: 30px; - } - .v2 .img-certificate { - border: 2px solid #009b00 !important; - } - .v2 .img-donate { - border: 2px solid #D7548E !important; - } - .v2 .img-view { - border: 2px solid #2991c3 !important; - } - .v2.register-choice .title { - width: 100%; - margin-bottom: 20px; - } - .v2.register-choice.register-choice-view .action-select { - border: 1px solid transparent !important; - border-radius: 3px; - } - .v2.register-choice.register-choice-view .action-select input { - border: 1px solid transparent !important; - } - .v2.register-choice.register-choice-view .action-select:hover { - border: 1px solid #0075b4 !important; - } - .v2.deco-divider { - display: none !important; + min-height: 250px; } } + +@media (min-width: 1024px) { + .v2 .donation-link { + width: 55%; + } + .v2.deco-divider .copy { + margin-left: 15px; + } +} + +@media (min-width: 1064px) { + .v2.register-choice-certificate, + .v2.register-choice-donate { + min-height: 260px; + } + .v2 .img-certificate, .v2 .img-donate { + display: initial; + } + .v2 .donation-link, .v2.register-choice-certificate button { + margin-top: -22px !important; + } +} \ No newline at end of file diff --git a/openedx/core/djangoapps/catalog/management/commands/cache_programs.py b/openedx/core/djangoapps/catalog/management/commands/cache_programs.py index ec4f521d40..6037acbeb6 100644 --- a/openedx/core/djangoapps/catalog/management/commands/cache_programs.py +++ b/openedx/core/djangoapps/catalog/management/commands/cache_programs.py @@ -50,6 +50,7 @@ class Command(BaseCommand): site_config = getattr(site, 'configuration', None) if site_config is None or not site_config.get_value('COURSE_CATALOG_API_URL'): logger.info('Skipping site {domain}. No configuration.'.format(domain=site.domain)) + cache.set(SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=site.domain), [], None) continue client = create_catalog_api_client(user, site=site) diff --git a/requirements/edx/edx-private.txt b/requirements/edx/edx-private.txt index e6903ec81d..272479d471 100644 --- a/requirements/edx/edx-private.txt +++ b/requirements/edx/edx-private.txt @@ -30,7 +30,7 @@ git+https://github.com/open-craft/problem-builder.git@v2.6.5#egg=xblock-problem- -e git+https://github.com/edx/AnimationXBlock.git@d2b551bb8f49a138088e10298576102164145b87#egg=animation-xblock # Peer instruction XBlock -ubcpi-xblock==0.6.2 +ubcpi-xblock==0.6.3 # Vector Drawing and ActiveTable XBlocks (Davidson) -e git+https://github.com/open-craft/xblock-vectordraw.git@v0.2.1#egg=xblock-vectordraw==0.2.1 diff --git a/themes/edx.org/lms/templates/course_modes/choose.html b/themes/edx.org/lms/templates/course_modes/choose.html index dd57a3f62e..a8ab708436 100644 --- a/themes/edx.org/lms/templates/course_modes/choose.html +++ b/themes/edx.org/lms/templates/course_modes/choose.html @@ -75,7 +75,7 @@ from openedx.core.djangolib.markup import HTML, Text

${title_content}

- + @@ -86,7 +86,7 @@ from openedx.core.djangolib.markup import HTML, Text b_tag_kwargs = {'b_start': HTML(''), 'b_end': HTML('')} %> % if "verified" in modes: - +