From f7b7963193d0ae5f165c29054f0b26dd0c4b9d89 Mon Sep 17 00:00:00 2001 From: stv Date: Tue, 5 Mar 2019 02:20:15 -0800 Subject: [PATCH 001/106] Remove unused version from footer I'm not seeing this value ever initialized in `settings`, or referenced anywhere else in platform, for that matter.. --- lms/djangoapps/dashboard/sysadmin.py | 5 ----- lms/templates/sysadmin_dashboard.html | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lms/djangoapps/dashboard/sysadmin.py b/lms/djangoapps/dashboard/sysadmin.py index abf1a466fb..db7d845b07 100644 --- a/lms/djangoapps/dashboard/sysadmin.py +++ b/lms/djangoapps/dashboard/sysadmin.py @@ -203,7 +203,6 @@ class Users(SysadminDashboardView): 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': {'users': 'active-section'}, - 'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''), } return render_to_response(self.template_name, context) @@ -241,7 +240,6 @@ class Users(SysadminDashboardView): 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': {'users': 'active-section'}, - 'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''), } return render_to_response(self.template_name, context) @@ -370,7 +368,6 @@ class Courses(SysadminDashboardView): 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': {'courses': 'active-section'}, - 'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''), } return render_to_response(self.template_name, context) @@ -422,7 +419,6 @@ class Courses(SysadminDashboardView): 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': {'courses': 'active-section'}, - 'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''), } return render_to_response(self.template_name, context) @@ -459,7 +455,6 @@ class Staffing(SysadminDashboardView): 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': {'staffing': 'active-section'}, - 'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''), } return render_to_response(self.template_name, context) diff --git a/lms/templates/sysadmin_dashboard.html b/lms/templates/sysadmin_dashboard.html index 90d3a554e2..e48752a66e 100644 --- a/lms/templates/sysadmin_dashboard.html +++ b/lms/templates/sysadmin_dashboard.html @@ -216,7 +216,6 @@ textarea {
'Django PID': ${djangopid} - ## Translators: A version number appears after this string - | ${_('Platform Version')}: ${edx_platform_version}
+ From 2171ddb9df40ad18cc6a2af8f8b6ec6cc965ee9f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 22 Apr 2019 13:41:31 -0400 Subject: [PATCH 002/106] URL patterns should be anchored. SEC-367 Some notes: * The completion API was pulled out into a new repo, but left behind a url() entry. That entry used a different namespace than the pulled-out repo, so I had to fix the one place we used the namespace. * Two urls couldn't be anchored because they broke tests: url(r'users/(?P\w+)/followed$', views.followed_threads, name='followed_threads'), url(r'users/(?P\w+)$', views.user_profile, name='user_profile'), --- lms/djangoapps/certificates/urls.py | 6 +-- .../django_comment_client/base/urls.py | 48 +++++++++---------- lms/djangoapps/grades/apps.py | 2 +- lms/djangoapps/instructor/apps.py | 2 +- lms/djangoapps/instructor/views/api_urls.py | 24 +++++----- lms/urls.py | 13 ++--- openedx/core/djangoapps/bookmarks/apps.py | 2 +- openedx/core/djangoapps/theming/apps.py | 2 +- openedx/features/announcements/apps.py | 2 +- .../completion_integration/test_views.py | 2 +- 10 files changed, 50 insertions(+), 53 deletions(-) diff --git a/lms/djangoapps/certificates/urls.py b/lms/djangoapps/certificates/urls.py index 9954a2c39d..af306e3caa 100644 --- a/lms/djangoapps/certificates/urls.py +++ b/lms/djangoapps/certificates/urls.py @@ -25,7 +25,7 @@ urlpatterns = [ # End-points used by student support # The views in the lms/djangoapps/support use these end-points # to retrieve certificate information and regenerate certificates. - url(r'search', views.search_certificates, name="search"), - url(r'regenerate', views.regenerate_certificate_for_user, name="regenerate_certificate_for_user"), - url(r'generate', views.generate_certificate_for_user, name="generate_certificate_for_user"), + url(r'^search', views.search_certificates, name="search"), + url(r'^regenerate', views.regenerate_certificate_for_user, name="regenerate_certificate_for_user"), + url(r'^generate', views.generate_certificate_for_user, name="generate_certificate_for_user"), ] diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index 042288e8ab..413a0cb8e2 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -6,34 +6,34 @@ from django.conf.urls import url from django_comment_client.base import views urlpatterns = [ - url(r'upload$', views.upload, name='upload'), - url(r'threads/(?P[\w\-]+)/update$', views.update_thread, name='update_thread'), - url(r'threads/(?P[\w\-]+)/reply$', views.create_comment, name='create_comment'), - url(r'threads/(?P[\w\-]+)/delete', views.delete_thread, name='delete_thread'), - url(r'threads/(?P[\w\-]+)/upvote$', views.vote_for_thread, {'value': 'up'}, name='upvote_thread'), - url(r'threads/(?P[\w\-]+)/downvote$', views.vote_for_thread, {'value': 'down'}, name='downvote_thread'), - url(r'threads/(?P[\w\-]+)/flagAbuse$', views.flag_abuse_for_thread, name='flag_abuse_for_thread'), - url(r'threads/(?P[\w\-]+)/unFlagAbuse$', views.un_flag_abuse_for_thread, + url(r'^upload$', views.upload, name='upload'), + url(r'^threads/(?P[\w\-]+)/update$', views.update_thread, name='update_thread'), + url(r'^threads/(?P[\w\-]+)/reply$', views.create_comment, name='create_comment'), + url(r'^threads/(?P[\w\-]+)/delete', views.delete_thread, name='delete_thread'), + url(r'^threads/(?P[\w\-]+)/upvote$', views.vote_for_thread, {'value': 'up'}, name='upvote_thread'), + url(r'^threads/(?P[\w\-]+)/downvote$', views.vote_for_thread, {'value': 'down'}, name='downvote_thread'), + url(r'^threads/(?P[\w\-]+)/flagAbuse$', views.flag_abuse_for_thread, name='flag_abuse_for_thread'), + url(r'^threads/(?P[\w\-]+)/unFlagAbuse$', views.un_flag_abuse_for_thread, name='un_flag_abuse_for_thread'), - url(r'threads/(?P[\w\-]+)/unvote$', views.undo_vote_for_thread, name='undo_vote_for_thread'), - url(r'threads/(?P[\w\-]+)/pin$', views.pin_thread, name='pin_thread'), - url(r'threads/(?P[\w\-]+)/unpin$', views.un_pin_thread, name='un_pin_thread'), - url(r'threads/(?P[\w\-]+)/follow$', views.follow_thread, name='follow_thread'), - url(r'threads/(?P[\w\-]+)/unfollow$', views.unfollow_thread, name='unfollow_thread'), - url(r'threads/(?P[\w\-]+)/close$', views.openclose_thread, name='openclose_thread'), - url(r'comments/(?P[\w\-]+)/update$', views.update_comment, name='update_comment'), - url(r'comments/(?P[\w\-]+)/endorse$', views.endorse_comment, name='endorse_comment'), - url(r'comments/(?P[\w\-]+)/reply$', views.create_sub_comment, name='create_sub_comment'), - url(r'comments/(?P[\w\-]+)/delete$', views.delete_comment, name='delete_comment'), - url(r'comments/(?P[\w\-]+)/upvote$', views.vote_for_comment, {'value': 'up'}, name='upvote_comment'), - url(r'comments/(?P[\w\-]+)/downvote$', views.vote_for_comment, {'value': 'down'}, + url(r'^threads/(?P[\w\-]+)/unvote$', views.undo_vote_for_thread, name='undo_vote_for_thread'), + url(r'^threads/(?P[\w\-]+)/pin$', views.pin_thread, name='pin_thread'), + url(r'^threads/(?P[\w\-]+)/unpin$', views.un_pin_thread, name='un_pin_thread'), + url(r'^threads/(?P[\w\-]+)/follow$', views.follow_thread, name='follow_thread'), + url(r'^threads/(?P[\w\-]+)/unfollow$', views.unfollow_thread, name='unfollow_thread'), + url(r'^threads/(?P[\w\-]+)/close$', views.openclose_thread, name='openclose_thread'), + url(r'^comments/(?P[\w\-]+)/update$', views.update_comment, name='update_comment'), + url(r'^comments/(?P[\w\-]+)/endorse$', views.endorse_comment, name='endorse_comment'), + url(r'^comments/(?P[\w\-]+)/reply$', views.create_sub_comment, name='create_sub_comment'), + url(r'^comments/(?P[\w\-]+)/delete$', views.delete_comment, name='delete_comment'), + url(r'^comments/(?P[\w\-]+)/upvote$', views.vote_for_comment, {'value': 'up'}, name='upvote_comment'), + url(r'^comments/(?P[\w\-]+)/downvote$', views.vote_for_comment, {'value': 'down'}, name='downvote_comment'), - url(r'comments/(?P[\w\-]+)/unvote$', views.undo_vote_for_comment, name='undo_vote_for_comment'), - url(r'comments/(?P[\w\-]+)/flagAbuse$', views.flag_abuse_for_comment, name='flag_abuse_for_comment'), - url(r'comments/(?P[\w\-]+)/unFlagAbuse$', views.un_flag_abuse_for_comment, + url(r'^comments/(?P[\w\-]+)/unvote$', views.undo_vote_for_comment, name='undo_vote_for_comment'), + url(r'^comments/(?P[\w\-]+)/flagAbuse$', views.flag_abuse_for_comment, name='flag_abuse_for_comment'), + url(r'^comments/(?P[\w\-]+)/unFlagAbuse$', views.un_flag_abuse_for_comment, name='un_flag_abuse_for_comment'), url(r'^(?P[\w\-.]+)/threads/create$', views.create_thread, name='create_thread'), url(r'^(?P[\w\-.]+)/follow$', views.follow_commentable, name='follow_commentable'), url(r'^(?P[\w\-.]+)/unfollow$', views.unfollow_commentable, name='unfollow_commentable'), - url(r'users$', views.users, name='users'), + url(r'^users$', views.users, name='users'), ] diff --git a/lms/djangoapps/grades/apps.py b/lms/djangoapps/grades/apps.py index 9c992ca178..1afd6748cd 100644 --- a/lms/djangoapps/grades/apps.py +++ b/lms/djangoapps/grades/apps.py @@ -20,7 +20,7 @@ class GradesConfig(AppConfig): PluginURLs.CONFIG: { ProjectType.LMS: { PluginURLs.NAMESPACE: u'grades_api', - PluginURLs.REGEX: u'api/grades/', + PluginURLs.REGEX: u'^api/grades/', PluginURLs.RELATIVE_PATH: u'api.urls', } }, diff --git a/lms/djangoapps/instructor/apps.py b/lms/djangoapps/instructor/apps.py index 4915213128..1f9c20e2b1 100644 --- a/lms/djangoapps/instructor/apps.py +++ b/lms/djangoapps/instructor/apps.py @@ -19,7 +19,7 @@ class InstructorConfig(AppConfig): PluginURLs.CONFIG: { ProjectType.LMS: { PluginURLs.NAMESPACE: u'', - PluginURLs.REGEX: u'courses/{}/instructor/api/'.format(COURSE_ID_PATTERN), + PluginURLs.REGEX: u'^courses/{}/instructor/api/'.format(COURSE_ID_PATTERN), PluginURLs.RELATIVE_PATH: u'views.api_urls', } }, diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 674c9419eb..759c2c25b9 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -50,27 +50,27 @@ urlpatterns = [ # Grade downloads... url(r'^list_report_downloads$', api.list_report_downloads, name='list_report_downloads'), - url(r'calculate_grades_csv$', api.calculate_grades_csv, name='calculate_grades_csv'), - url(r'problem_grade_report$', api.problem_grade_report, name='problem_grade_report'), + url(r'^calculate_grades_csv$', api.calculate_grades_csv, name='calculate_grades_csv'), + url(r'^problem_grade_report$', api.problem_grade_report, name='problem_grade_report'), # Financial Report downloads.. url(r'^list_financial_report_downloads$', api.list_financial_report_downloads, name='list_financial_report_downloads'), # Registration Codes.. - url(r'get_registration_codes$', api.get_registration_codes, name='get_registration_codes'), - url(r'generate_registration_codes$', api.generate_registration_codes, name='generate_registration_codes'), - url(r'active_registration_codes$', api.active_registration_codes, name='active_registration_codes'), - url(r'spent_registration_codes$', api.spent_registration_codes, name='spent_registration_codes'), + url(r'^get_registration_codes$', api.get_registration_codes, name='get_registration_codes'), + url(r'^generate_registration_codes$', api.generate_registration_codes, name='generate_registration_codes'), + url(r'^active_registration_codes$', api.active_registration_codes, name='active_registration_codes'), + url(r'^spent_registration_codes$', api.spent_registration_codes, name='spent_registration_codes'), # Reports.. - url(r'get_enrollment_report$', api.get_enrollment_report, name='get_enrollment_report'), - url(r'get_exec_summary_report$', api.get_exec_summary_report, name='get_exec_summary_report'), - url(r'get_course_survey_results$', api.get_course_survey_results, name='get_course_survey_results'), - url(r'export_ora2_data', api.export_ora2_data, name='export_ora2_data'), + url(r'^get_enrollment_report$', api.get_enrollment_report, name='get_enrollment_report'), + url(r'^get_exec_summary_report$', api.get_exec_summary_report, name='get_exec_summary_report'), + url(r'^get_course_survey_results$', api.get_course_survey_results, name='get_course_survey_results'), + url(r'^export_ora2_data', api.export_ora2_data, name='export_ora2_data'), # Coupon Codes.. - url(r'get_coupon_codes', api.get_coupon_codes, name='get_coupon_codes'), + url(r'^get_coupon_codes', api.get_coupon_codes, name='get_coupon_codes'), # spoc gradebook url(r'^gradebook$', gradebook_api.spoc_gradebook, name='spoc_gradebook'), @@ -78,7 +78,7 @@ urlpatterns = [ url(r'^gradebook/(?P[0-9]+)$', gradebook_api.spoc_gradebook, name='spoc_gradebook'), # Cohort management - url(r'add_users_to_cohorts$', api.add_users_to_cohorts, name='add_users_to_cohorts'), + url(r'^add_users_to_cohorts$', api.add_users_to_cohorts, name='add_users_to_cohorts'), # Certificates url(r'^generate_example_certificates$', api.generate_example_certificates, name='generate_example_certificates'), diff --git a/lms/urls.py b/lms/urls.py index 9c62d0d12c..5f5febae7e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -103,9 +103,6 @@ urlpatterns = [ # Course API url(r'^api/courses/', include('course_api.urls')), - # Completion API - url(r'^api/completion/', include('completion.api.urls', namespace='completion_api')), - # User API endpoints url(r'^api/user/', include('openedx.core.djangoapps.user_api.urls')), @@ -875,7 +872,7 @@ urlpatterns += [ if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): urlpatterns += [ url(r'', include('third_party_auth.urls')), - url(r'api/third_party_auth/', include('third_party_auth.api.urls')), + url(r'^api/third_party_auth/', include('third_party_auth.api.urls')), ] # Enterprise @@ -930,10 +927,10 @@ if settings.FEATURES.get('ENABLE_LTI_PROVIDER'): ] urlpatterns += [ - url(r'config/self_paced', ConfigurationModelCurrentAPIView.as_view(model=SelfPacedConfiguration)), - url(r'config/programs', ConfigurationModelCurrentAPIView.as_view(model=ProgramsApiConfig)), - url(r'config/catalog', ConfigurationModelCurrentAPIView.as_view(model=CatalogIntegration)), - url(r'config/forums', ConfigurationModelCurrentAPIView.as_view(model=ForumsConfig)), + url(r'^config/self_paced', ConfigurationModelCurrentAPIView.as_view(model=SelfPacedConfiguration)), + url(r'^config/programs', ConfigurationModelCurrentAPIView.as_view(model=ProgramsApiConfig)), + url(r'^config/catalog', ConfigurationModelCurrentAPIView.as_view(model=CatalogIntegration)), + url(r'^config/forums', ConfigurationModelCurrentAPIView.as_view(model=ForumsConfig)), ] if settings.DEBUG: diff --git a/openedx/core/djangoapps/bookmarks/apps.py b/openedx/core/djangoapps/bookmarks/apps.py index 7c60e201b4..0c55983afe 100644 --- a/openedx/core/djangoapps/bookmarks/apps.py +++ b/openedx/core/djangoapps/bookmarks/apps.py @@ -17,7 +17,7 @@ class BookmarksConfig(AppConfig): PluginURLs.CONFIG: { ProjectType.LMS: { PluginURLs.NAMESPACE: u'', - PluginURLs.REGEX: u'api/bookmarks/', + PluginURLs.REGEX: u'^api/bookmarks/', PluginURLs.RELATIVE_PATH: u'urls', } }, diff --git a/openedx/core/djangoapps/theming/apps.py b/openedx/core/djangoapps/theming/apps.py index 14199272d6..e0da5456b8 100644 --- a/openedx/core/djangoapps/theming/apps.py +++ b/openedx/core/djangoapps/theming/apps.py @@ -3,7 +3,7 @@ from django.apps import AppConfig from openedx.core.djangoapps.plugins.constants import ProjectType, PluginURLs -plugin_urls_config = {PluginURLs.NAMESPACE: u'theming', PluginURLs.REGEX: u'theming/'} +plugin_urls_config = {PluginURLs.NAMESPACE: u'theming', PluginURLs.REGEX: r'^theming/'} class ThemingConfig(AppConfig): diff --git a/openedx/features/announcements/apps.py b/openedx/features/announcements/apps.py index fb8fd6f6c7..86759024b6 100644 --- a/openedx/features/announcements/apps.py +++ b/openedx/features/announcements/apps.py @@ -15,7 +15,7 @@ class AnnouncementsConfig(AppConfig): PluginURLs.CONFIG: { ProjectType.LMS: { PluginURLs.NAMESPACE: u'announcements', - PluginURLs.REGEX: u'announcements/', + PluginURLs.REGEX: u'^announcements/', PluginURLs.RELATIVE_PATH: u'urls', } }, diff --git a/openedx/tests/completion_integration/test_views.py b/openedx/tests/completion_integration/test_views.py index 2d9bf789e1..0ffa7fe156 100644 --- a/openedx/tests/completion_integration/test_views.py +++ b/openedx/tests/completion_integration/test_views.py @@ -31,7 +31,7 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase): Create the test data. """ super(CompletionBatchTestCase, self).setUp() - self.url = reverse('completion_api:v1:completion-batch') + self.url = reverse('completion:v1:completion-batch') # Enable the waffle flag for all tests self.override_waffle_switch(True) From a5e5591262b18e0c796ed2df888d974b48078e7b Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Tue, 23 Apr 2019 14:14:32 -0400 Subject: [PATCH 003/106] Automatically add master's to the group access of verified content --- common/djangoapps/course_modes/helpers.py | 39 +++++++++++++++++++ common/djangoapps/course_modes/signals.py | 18 +++++++++ .../course_modes/tests/test_signals.py | 29 +++++++++++++- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py index a7a04e85e9..1444d1a9ff 100644 --- a/common/djangoapps/course_modes/helpers.py +++ b/common/djangoapps/course_modes/helpers.py @@ -1,14 +1,25 @@ """ Helper methods for CourseModes. """ +from __future__ import absolute_import, unicode_literals +import logging + +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from course_modes.models import CourseMode from student.helpers import VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID + DISPLAY_VERIFIED = "verified" DISPLAY_HONOR = "honor" DISPLAY_AUDIT = "audit" DISPLAY_PROFESSIONAL = "professional" +MASTERS_ID = settings.COURSE_ENROLLMENT_MODES.get('masters', {}).get('id', None) +VERIFIED_ID = settings.COURSE_ENROLLMENT_MODES['verified']['id'] + +log = logging.getLogger(__name__) + def enrollment_mode_display(mode, verification_status, course_id): """ Select appropriate display strings and CSS classes. @@ -80,3 +91,31 @@ def _enrollment_mode_display(enrollment_mode, verification_status, course_id): display_mode = enrollment_mode return display_mode + + +def update_masters_access(item): + """ + Update the XBlock's group access to allow the master's group, + in addition to the verified content group. + """ + group_access = item.group_access + enrollment_groups = group_access.get(ENROLLMENT_TRACK_PARTITION_ID, None) + if enrollment_groups is not None: + if VERIFIED_ID in enrollment_groups and MASTERS_ID not in enrollment_groups: + enrollment_groups.append(MASTERS_ID) + item.group_access = group_access + return True + + +def update_masters_access_course(store, course_id, user_id): + """ + Update all blocks in the verified content group to include the master's content group + """ + + with store.bulk_operations(course_id): + items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False) + for item in items: + if update_masters_access(item): + log.info("Publishing %s with Master's group access", item.location) + store.update_item(item, user_id) + store.publish(item.location, user_id) diff --git a/common/djangoapps/course_modes/signals.py b/common/djangoapps/course_modes/signals.py index ed8c550211..06f959990a 100644 --- a/common/djangoapps/course_modes/signals.py +++ b/common/djangoapps/course_modes/signals.py @@ -1,11 +1,17 @@ """ Signal handler for setting default course mode expiration dates """ +from __future__ import absolute_import, unicode_literals + +from crum import get_current_user + from django.core.exceptions import ObjectDoesNotExist +from django.db.models.signals import post_save from django.dispatch.dispatcher import receiver from xmodule.modulestore.django import SignalHandler, modulestore +from .helpers import update_masters_access_course from .models import CourseMode, CourseModeExpirationConfig @@ -35,3 +41,15 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable def _should_update_date(verified_mode): """ Returns whether or not the verified mode should be updated. """ return not(verified_mode is None or verified_mode.expiration_datetime_is_explicit) + + +@receiver(post_save, sender=CourseMode) +def update_access_for_masters_mode(sender, instance=None, **kwargs): # pylint: disable=unused-argument + """ + Adds master's access to verified content when the master's mode is created + """ + if instance.mode_slug != CourseMode.MASTERS: + return + user = get_current_user() + user_id = user.id if user else None + update_masters_access_course(modulestore(), instance.course_id, user_id) diff --git a/common/djangoapps/course_modes/tests/test_signals.py b/common/djangoapps/course_modes/tests/test_signals.py index 853622808b..4c7dc66809 100644 --- a/common/djangoapps/course_modes/tests/test_signals.py +++ b/common/djangoapps/course_modes/tests/test_signals.py @@ -1,6 +1,7 @@ """ Unit tests for the course_mode signals """ +from __future__ import absolute_import, unicode_literals from datetime import datetime, timedelta @@ -10,8 +11,9 @@ from pytz import UTC from course_modes.models import CourseMode from course_modes.signals import _listen_for_course_publish +from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @ddt.ddt @@ -87,3 +89,28 @@ class CourseModeSignalTest(ModuleStoreTestCase): course_mode.refresh_from_db() self.assertEqual(course_mode.expiration_datetime, self.end - timedelta(days=verification_window)) + + def test_masters_mode(self): + # create an xblock with verified group access + verified_section = ItemFactory.create( + category="sequential", + metadata={'group_access': {50: [2]}} + ) + # and a section with no restriction + section2 = ItemFactory.create( + category="sequential", + ) + section3 = ItemFactory.create( + category='sequential', + metadata={'group_access': {50: [1]}} + ) + with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): + # create the master's mode. signal will add masters to the verified section + self.create_mode('masters', 'masters') + verified_section_ret = self.store.get_item(verified_section.location) + section2_ret = self.store.get_item(section2.location) + section3_ret = self.store.get_item(section3.location) + # group 2 is verified. 7 is masters + assert verified_section_ret.group_access[50] == [2, 7] + assert section2_ret.group_access == {} + assert section2_ret.group_access == {50: [1]} From 27437bc03f707f4923c9c6c3040129ba2c2add67 Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Tue, 23 Apr 2019 14:39:24 -0400 Subject: [PATCH 004/106] Ran python-modernize --- common/djangoapps/course_modes/admin.py | 6 ++- .../course_modes/api/serializers.py | 2 + common/djangoapps/course_modes/api/urls.py | 3 +- common/djangoapps/course_modes/api/v1/urls.py | 1 + .../djangoapps/course_modes/api/v1/views.py | 1 + common/djangoapps/course_modes/apps.py | 4 +- common/djangoapps/course_modes/helpers.py | 15 ++++-- .../course_modes/migrations/0001_initial.py | 3 +- ...rsemode_expiration_datetime_is_explicit.py | 2 +- .../migrations/0003_auto_20151113_1443.py | 2 +- .../migrations/0004_auto_20151113_1457.py | 2 +- .../migrations/0005_auto_20151217_0958.py | 2 +- .../migrations/0006_auto_20160208_1407.py | 2 +- .../migrations/0007_coursemode_bulk_sku.py | 2 +- .../0008_course_key_field_to_foreign_key.py | 2 +- .../0009_suggested_prices_to_charfield.py | 5 +- ..._archived_suggested_prices_to_charfield.py | 5 +- ...1_change_regex_for_comma_separated_ints.py | 5 +- common/djangoapps/course_modes/models.py | 11 ++-- common/djangoapps/course_modes/signals.py | 1 - .../course_modes/tests/factories.py | 2 + .../course_modes/tests/test_admin.py | 9 ++-- .../course_modes/tests/test_models.py | 19 +++---- .../course_modes/tests/test_signals.py | 2 +- .../course_modes/tests/test_views.py | 53 ++++++++++--------- common/djangoapps/course_modes/urls.py | 3 ++ common/djangoapps/course_modes/views.py | 19 ++++--- 27 files changed, 110 insertions(+), 73 deletions(-) diff --git a/common/djangoapps/course_modes/admin.py b/common/djangoapps/course_modes/admin.py index 38e6cda823..1b5cdd728d 100644 --- a/common/djangoapps/course_modes/admin.py +++ b/common/djangoapps/course_modes/admin.py @@ -1,3 +1,6 @@ +"""Django admin for course_modes""" +from __future__ import absolute_import, unicode_literals + import six from django import forms from django.conf import settings @@ -22,7 +25,8 @@ from lms.djangoapps.verify_student import models as verification_models from openedx.core.lib.courses import clean_course_id from util.date_utils import get_time_display -COURSE_MODE_SLUG_CHOICES = [(key, enrollment_mode['display_name']) for key, enrollment_mode in six.iteritems(settings.COURSE_ENROLLMENT_MODES)] +COURSE_MODE_SLUG_CHOICES = [(key, enrollment_mode['display_name']) + for key, enrollment_mode in six.iteritems(settings.COURSE_ENROLLMENT_MODES)] class CourseModeForm(forms.ModelForm): diff --git a/common/djangoapps/course_modes/api/serializers.py b/common/djangoapps/course_modes/api/serializers.py index ef3dfe5df7..2df0493b79 100644 --- a/common/djangoapps/course_modes/api/serializers.py +++ b/common/djangoapps/course_modes/api/serializers.py @@ -1,6 +1,8 @@ """ Course modes API serializers. """ +from __future__ import absolute_import + from rest_framework import serializers from course_modes.models import CourseMode diff --git a/common/djangoapps/course_modes/api/urls.py b/common/djangoapps/course_modes/api/urls.py index 3f1d1d4286..ce149243b2 100644 --- a/common/djangoapps/course_modes/api/urls.py +++ b/common/djangoapps/course_modes/api/urls.py @@ -1,8 +1,9 @@ """ URL definitions for the course_modes API. """ -from django.conf.urls import include, url +from __future__ import absolute_import +from django.conf.urls import include, url app_name = 'common.djangoapps.course_modes.api' diff --git a/common/djangoapps/course_modes/api/v1/urls.py b/common/djangoapps/course_modes/api/v1/urls.py index 8456b1f449..c660c788f2 100644 --- a/common/djangoapps/course_modes/api/v1/urls.py +++ b/common/djangoapps/course_modes/api/v1/urls.py @@ -1,6 +1,7 @@ """ URL definitions for the course_modes v1 API. """ +from __future__ import absolute_import from django.conf import settings from django.conf.urls import url diff --git a/common/djangoapps/course_modes/api/v1/views.py b/common/djangoapps/course_modes/api/v1/views.py index 5806b156db..5c61496f92 100644 --- a/common/djangoapps/course_modes/api/v1/views.py +++ b/common/djangoapps/course_modes/api/v1/views.py @@ -2,6 +2,7 @@ Defines the "ReSTful" API for course modes. """ +from __future__ import absolute_import import logging from django.shortcuts import get_object_or_404 diff --git a/common/djangoapps/course_modes/apps.py b/common/djangoapps/course_modes/apps.py index d0b1b2c32c..12f2869139 100644 --- a/common/djangoapps/course_modes/apps.py +++ b/common/djangoapps/course_modes/apps.py @@ -1,3 +1,5 @@ +"""Django App config for course_modes""" +from __future__ import absolute_import from django.apps import AppConfig @@ -7,4 +9,4 @@ class CourseModesConfig(AppConfig): verbose_name = "Course Modes" def ready(self): - import course_modes.signals # pylint: disable=unused-import + import course_modes.signals # pylint: disable=unused-variable diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py index 1444d1a9ff..c1a664fa89 100644 --- a/common/djangoapps/course_modes/helpers.py +++ b/common/djangoapps/course_modes/helpers.py @@ -1,15 +1,17 @@ """ Helper methods for CourseModes. """ from __future__ import absolute_import, unicode_literals + import logging +import six from django.conf import settings from django.utils.translation import ugettext_lazy as _ from course_modes.models import CourseMode from student.helpers import VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED +from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID - DISPLAY_VERIFIED = "verified" DISPLAY_HONOR = "honor" DISPLAY_AUDIT = "audit" @@ -59,10 +61,10 @@ def enrollment_mode_display(mode, verification_status, course_id): enrollment_value = _("Professional Ed") return { - 'enrollment_title': unicode(enrollment_title), - 'enrollment_value': unicode(enrollment_value), + 'enrollment_title': six.text_type(enrollment_title), + 'enrollment_value': six.text_type(enrollment_value), 'show_image': show_image, - 'image_alt': unicode(image_alt), + 'image_alt': six.text_type(image_alt), 'display_mode': _enrollment_mode_display(mode, verification_status, course_id) } @@ -113,7 +115,10 @@ def update_masters_access_course(store, course_id, user_id): """ with store.bulk_operations(course_id): - items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False) + try: + items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False) + except ItemNotFoundError: + return for item in items: if update_masters_access(item): log.info("Publishing %s with Master's group access", item.location) diff --git a/common/djangoapps/course_modes/migrations/0001_initial.py b/common/djangoapps/course_modes/migrations/0001_initial.py index a440411f6c..11692dcad0 100644 --- a/common/djangoapps/course_modes/migrations/0001_initial.py +++ b/common/djangoapps/course_modes/migrations/0001_initial.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models - from opaque_keys.edx.django.models import CourseKeyField diff --git a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py index ea6a601342..ad010d9a3b 100644 --- a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py +++ b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py index 04d20662aa..bca415f6d8 100644 --- a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py +++ b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py index ec11036d4d..de21813631 100644 --- a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py +++ b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from datetime import timedelta diff --git a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py index 1e6e3f7e82..574575749b 100644 --- a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py +++ b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py index 338a886745..7355c03817 100644 --- a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py +++ b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py index bdbbac49f2..8a81d980cd 100644 --- a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py +++ b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py index 75cb8ed0d7..2ed4bfaf74 100644 --- a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py +++ b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField diff --git a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py index 60ed720cfd..c6ba8b0b43 100644 --- a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py +++ b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from django.db import migrations, models import re + import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py index b307d06472..56254117ed 100644 --- a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py +++ b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from django.db import migrations, models import re + import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py index 73f1046eee..a69f54997d 100644 --- a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py +++ b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.8 on 2018-01-30 17:38 -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals + +import re import django.core.validators from django.db import migrations, models -import re class Migration(migrations.Migration): diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 11a97b24ea..143d67b5a1 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -1,9 +1,12 @@ """ Add and create new modes for running courses on this particular LMS """ +from __future__ import absolute_import + from collections import defaultdict, namedtuple from datetime import timedelta +import six from config_models.models import ConfigurationModel from django.conf import settings from django.core.exceptions import ValidationError @@ -14,8 +17,8 @@ from django.dispatch import receiver from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from edx_django_utils.cache import RequestCache -from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.django.models import CourseKeyField +from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.cache_utils import request_cached @@ -59,7 +62,7 @@ class CourseMode(models.Model): @course_id.setter def course_id(self, value): - if isinstance(value, basestring): + if isinstance(value, six.string_types): self._course_id = CourseKey.from_string(value) else: self._course_id = value @@ -296,7 +299,7 @@ class CourseMode(models.Model): mode for mode in modes if mode.expiration_datetime is None or mode.expiration_datetime >= now_dt ] - for course_id, modes in all_modes.iteritems() + for course_id, modes in six.iteritems(all_modes) } return (all_modes, unexpired_modes) @@ -910,4 +913,4 @@ class CourseModeExpirationConfig(ConfigurationModel): def __unicode__(self): """ Returns the unicode date of the verification window. """ - return unicode(self.verification_window) + return six.text_type(self.verification_window) diff --git a/common/djangoapps/course_modes/signals.py b/common/djangoapps/course_modes/signals.py index 06f959990a..23b217eb3d 100644 --- a/common/djangoapps/course_modes/signals.py +++ b/common/djangoapps/course_modes/signals.py @@ -4,7 +4,6 @@ Signal handler for setting default course mode expiration dates from __future__ import absolute_import, unicode_literals from crum import get_current_user - from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import post_save from django.dispatch.dispatcher import receiver diff --git a/common/djangoapps/course_modes/tests/factories.py b/common/djangoapps/course_modes/tests/factories.py index 7983440862..cbffbe05a1 100644 --- a/common/djangoapps/course_modes/tests/factories.py +++ b/common/djangoapps/course_modes/tests/factories.py @@ -1,6 +1,8 @@ """ Factories for course mode models. """ +from __future__ import absolute_import + import random from factory import lazy_attribute diff --git a/common/djangoapps/course_modes/tests/test_admin.py b/common/djangoapps/course_modes/tests/test_admin.py index 0827c5b258..18dbd08a3b 100644 --- a/common/djangoapps/course_modes/tests/test_admin.py +++ b/common/djangoapps/course_modes/tests/test_admin.py @@ -1,10 +1,13 @@ """ Tests for the course modes Django admin interface. """ +from __future__ import absolute_import, unicode_literals + import unittest from datetime import datetime, timedelta import ddt +import six from django.conf import settings from django.urls import reverse from pytz import UTC, timezone @@ -12,12 +15,12 @@ from pytz import UTC, timezone from course_modes.admin import CourseModeForm from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview # Technically, we shouldn't be importing verify_student, since it's # defined in the LMS and course_modes is in common. However, the benefits # of putting all this configuration in one place outweigh the downsides. # Once the course admin tool is deployed, we can remove this dependency. from lms.djangoapps.verify_student.models import VerificationDeadline +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from student.tests.factories import UserFactory from util.date_utils import get_time_display from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -44,7 +47,7 @@ class AdminCourseModePageTest(ModuleStoreTestCase): CourseOverview.load_from_module_store(course.id) data = { - 'course': unicode(course.id), + 'course': six.text_type(course.id), 'mode_slug': 'verified', 'mode_display_name': 'verified', 'min_price': 10, @@ -199,7 +202,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase): mode_slug=mode, ) return CourseModeForm({ - "course": unicode(self.course.id), + "course": six.text_type(self.course.id), "mode_slug": mode, "mode_display_name": mode, "_expiration_datetime": upgrade_deadline, diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 6055be65e8..de4b12ea6f 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -4,6 +4,7 @@ when you run "manage.py test". Replace this with more appropriate tests for your application. """ +from __future__ import absolute_import, unicode_literals import itertools from datetime import timedelta @@ -14,14 +15,13 @@ from django.test import TestCase, override_settings from django.utils.timezone import now from mock import patch from opaque_keys.edx.locator import CourseLocator +from six.moves import zip from course_modes.helpers import enrollment_mode_display -from course_modes.models import CourseMode, Mode, invalidate_course_mode_cache, get_cosmetic_display_price +from course_modes.models import CourseMode, Mode, get_cosmetic_display_price, invalidate_course_mode_cache from course_modes.tests.factories import CourseModeFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -from xmodule.modulestore.tests.django_utils import ( - ModuleStoreTestCase, -) @ddt.ddt @@ -41,6 +41,7 @@ class CourseModeModelTest(TestCase): CourseMode.objects.all().delete() def tearDown(self): + super(CourseModeModelTest, self).tearDown() invalidate_course_mode_cache(sender=None) def create_mode( @@ -398,11 +399,11 @@ class CourseModeModelTest(TestCase): # Check the selectable modes, which should exclude credit selectable_modes = CourseMode.modes_for_course_dict(self.course_key) - self.assertItemsEqual(selectable_modes.keys(), expected_selectable_modes) + self.assertItemsEqual(list(selectable_modes.keys()), expected_selectable_modes) # When we get all unexpired modes, we should see credit as well all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False) - self.assertItemsEqual(all_modes.keys(), available_modes) + self.assertItemsEqual(list(all_modes.keys()), available_modes) def _enrollment_display_modes_dicts(self, dict_type): """ @@ -421,11 +422,11 @@ class CourseModeModelTest(TestCase): 'professional'] } if dict_type in ['verify_need_to_verify', 'verify_submitted']: - return dict(zip(dict_keys, display_values.get('verify_need_to_verify'))) + return dict(list(zip(dict_keys, display_values.get('verify_need_to_verify')))) elif dict_type is None or dict_type == 'dummy': - return dict(zip(dict_keys, display_values.get('verify_none'))) + return dict(list(zip(dict_keys, display_values.get('verify_none')))) else: - return dict(zip(dict_keys, display_values.get(dict_type))) + return dict(list(zip(dict_keys, display_values.get(dict_type)))) def test_expiration_datetime_explicitly_set(self): """ Verify that setting the expiration_date property sets the explicit flag. """ diff --git a/common/djangoapps/course_modes/tests/test_signals.py b/common/djangoapps/course_modes/tests/test_signals.py index 4c7dc66809..951e99fb5f 100644 --- a/common/djangoapps/course_modes/tests/test_signals.py +++ b/common/djangoapps/course_modes/tests/test_signals.py @@ -113,4 +113,4 @@ class CourseModeSignalTest(ModuleStoreTestCase): # group 2 is verified. 7 is masters assert verified_section_ret.group_access[50] == [2, 7] assert section2_ret.group_access == {} - assert section2_ret.group_access == {50: [1]} + assert section3_ret.group_access == {50: [1]} diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index fed9623f8f..d836b4ea80 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -2,6 +2,8 @@ Tests for course_modes views. """ +from __future__ import absolute_import + import decimal import unittest from datetime import datetime, timedelta @@ -10,6 +12,7 @@ import ddt import freezegun import httpretty import pytz +import six from django.conf import settings from django.urls import reverse from mock import patch @@ -84,7 +87,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest ) # Configure whether we're upgrading or not - url = reverse('course_modes_choose', args=[unicode(course.id)]) + url = reverse('course_modes_choose', args=[six.text_type(course.id)]) response = self.client.get(url) # Check whether we were correctly redirected @@ -111,11 +114,11 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest ) # Configure whether we're upgrading or not - url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.get(url) # Check whether we were correctly redirected purchase_workflow = "?purchase_workflow=single" - start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow + start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow self.assertRedirects(response, start_flow_url) def test_no_id_redirect_otto(self): @@ -132,7 +135,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest user=self.user ) # Configure whether we're upgrading or not - url = reverse('course_modes_choose', args=[unicode(prof_course.id)]) + url = reverse('course_modes_choose', args=[six.text_type(prof_course.id)]) response = self.client.get(url) self.assertRedirects(response, 'http://testserver/test_basket/add/?sku=TEST', fetch_redirect_response=False) ecomm_test_utils.update_commerce_config(enabled=False) @@ -166,7 +169,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest # Verify that the prices render correctly response = self.client.get( - reverse('course_modes_choose', args=[unicode(self.course.id)]), + reverse('course_modes_choose', args=[six.text_type(self.course.id)]), follow=False, ) @@ -187,7 +190,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest # Check whether credit upsell is shown on the page # This should *only* be shown when a credit mode is available - url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.get(url) if show_upsell: @@ -201,13 +204,13 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=1) # Go to the "choose your track" page - choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.get(choose_track_url) # Since the only available track is professional ed, expect that # we're redirected immediately to the start of the payment flow. purchase_workflow = "?purchase_workflow=single" - start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow + start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow self.assertRedirects(response, start_flow_url) # Now enroll in the course @@ -215,7 +218,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest user=self.user, is_active=True, mode=mode, - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), ) # Expect that this time we're redirected to the dashboard (since we're already registered) @@ -244,7 +247,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=min_price) # Choose the mode (POST request) - choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[course_mode]) # Verify the redirect @@ -253,7 +256,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest elif expected_redirect == 'start-flow': redirect_url = reverse( 'verify_student_start_flow', - kwargs={'course_id': unicode(self.course.id)} + kwargs={'course_id': six.text_type(self.course.id)} ) else: self.fail("Must provide a valid redirect URL name") @@ -273,7 +276,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest self.assertIsNone(is_active) # Choose the audit mode (POST request) - choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[audit_mode]) # Assert learner is enrolled in Audit track post-POST @@ -301,14 +304,14 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest CourseModeFactory.create(mode_slug='verified', course_id=self.course.id, min_price=1) # Choose the mode (POST request) - choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['verified']) # Expect that the contribution amount is stored in the user's session self.assertIn('donation_for_course', self.client.session) - self.assertIn(unicode(self.course.id), self.client.session['donation_for_course']) + self.assertIn(six.text_type(self.course.id), self.client.session['donation_for_course']) - actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)] + actual_amount = self.client.session['donation_for_course'][six.text_type(self.course.id)] expected_amount = decimal.Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution']) self.assertEqual(actual_amount, expected_amount) @@ -321,12 +324,12 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest # automatic enrollment params = { 'enrollment_action': 'enroll', - 'course_id': unicode(self.course.id) + 'course_id': six.text_type(self.course.id) } self.client.post(reverse('change_enrollment'), params) # Explicitly select the honor mode (POST request) - choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[CourseMode.DEFAULT_MODE_SLUG]) # Verify that the user's enrollment remains unchanged @@ -340,7 +343,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) # Choose an unsupported mode (POST request) - choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['unsupported']) self.assertEqual(400, response.status_code) @@ -348,7 +351,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') def test_default_mode_creation(self): # Hit the mode creation endpoint with no querystring params, to create an honor mode - url = reverse('create_mode', args=[unicode(self.course.id)]) + url = reverse('create_mode', args=[six.text_type(self.course.id)]) response = self.client.get(url) self.assertEquals(response.status_code, 200) @@ -372,7 +375,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest parameters['suggested_prices'] = suggested_prices parameters['currency'] = currency - url = reverse('create_mode', args=[unicode(self.course.id)]) + url = reverse('create_mode', args=[six.text_type(self.course.id)]) response = self.client.get(url, parameters) self.assertEquals(response.status_code, 200) @@ -397,7 +400,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') def test_multiple_mode_creation(self): # Create an honor mode - base_url = reverse('create_mode', args=[unicode(self.course.id)]) + base_url = reverse('create_mode', args=[six.text_type(self.course.id)]) self.client.get(base_url) # Excluding the currency parameter implicitly tests the mode creation endpoint's ability to @@ -409,7 +412,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest parameters['suggested_prices'] = '10,20' # Create a verified mode - url = reverse('create_mode', args=[unicode(self.course.id)]) + url = reverse('create_mode', args=[six.text_type(self.course.id)]) self.client.get(url, parameters) honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None) @@ -428,7 +431,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) # Load the track selection page - url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.get(url) # Verify that the header navigation links are hidden for the edx.org version @@ -445,7 +448,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest self.course.enrollment_end = datetime(2015, 1, 1) modulestore().update_item(self.course, self.user.id) - url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) response = self.client.get(url) # URL-encoded version of 1/1/15, 12:00 AM redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM' @@ -472,7 +475,7 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): self.client.login(username=self.user.username, password="edx") # Construct the URL for the track selection page - self.url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + self.url = reverse('course_modes_choose', args=[six.text_type(self.course.id)]) @patch.dict(settings.FEATURES, {'EMBARGO': True}) def test_embargo_restrict(self): diff --git a/common/djangoapps/course_modes/urls.py b/common/djangoapps/course_modes/urls.py index 5253c164b2..96b8d1e4fe 100644 --- a/common/djangoapps/course_modes/urls.py +++ b/common/djangoapps/course_modes/urls.py @@ -1,3 +1,6 @@ +"""URLs for course_mode API""" +from __future__ import absolute_import, unicode_literals + from django.conf import settings from django.conf.urls import url diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 536c60bc0d..91ccacfbdf 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -1,10 +1,15 @@ """ Views for the course_mode module """ +from __future__ import absolute_import, unicode_literals import decimal import json -import urllib + +import six +import six.moves.urllib.error +import six.moves.urllib.parse +import six.moves.urllib.request import waffle from babel.dates import format_datetime @@ -96,7 +101,7 @@ class ChooseModeView(View): has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode(modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") - verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) + verify_url = reverse('verify_student_start_flow', kwargs={'course_id': six.text_type(course_key)}) redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL) @@ -121,12 +126,12 @@ class ChooseModeView(View): return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) - chosen_price = donation_for_course.get(unicode(course_key), None) + chosen_price = donation_for_course.get(six.text_type(course_key), None) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) - params = urllib.urlencode({'course_closed': enrollment_end_date}) + params = six.moves.urllib.parse.urlencode({'course_closed': enrollment_end_date}) return redirect('{0}?{1}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option @@ -278,13 +283,13 @@ class ChooseModeView(View): return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) - donation_for_course[unicode(course_key)] = amount_value + donation_for_course[six.text_type(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse( 'verify_student_start_flow', - kwargs={'course_id': unicode(course_key)} + kwargs={'course_id': six.text_type(course_key)} ) ) @@ -342,7 +347,7 @@ def create_mode(request, course_id): } # Try pulling querystring parameters out of the request - for parameter, default in PARAMETERS.iteritems(): + for parameter, default in six.iteritems(PARAMETERS): PARAMETERS[parameter] = request.GET.get(parameter, default) # Attempt to create the new mode for the given course From 14160b919f65c289419f7edf332b8c5e064f76db Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Fri, 12 Apr 2019 11:07:40 -0400 Subject: [PATCH 005/106] add LMS endpoint to redirect to identity provider --- .../third_party_auth/tests/test_saml.py | 30 +++++++++++++ .../third_party_auth/tests/test_views.py | 45 ++++++++++++++++++- .../third_party_auth/tests/testutil.py | 18 -------- common/djangoapps/third_party_auth/urls.py | 4 +- common/djangoapps/third_party_auth/views.py | 15 ++++++- 5 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 common/djangoapps/third_party_auth/tests/test_saml.py diff --git a/common/djangoapps/third_party_auth/tests/test_saml.py b/common/djangoapps/third_party_auth/tests/test_saml.py new file mode 100644 index 0000000000..fcf1d3d24c --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/test_saml.py @@ -0,0 +1,30 @@ +""" +Unit tests for third_party_auth SAML auth providers +""" + +import mock + +from third_party_auth.tests.testutil import SAMLTestCase +from third_party_auth.saml import EdXSAMLIdentityProvider, get_saml_idp_class +from third_party_auth.tests.data.saml_identity_provider_mock_data import mock_conf, mock_attributes,\ + expected_user_details + + +class TestEdXSAMLIdentityProvider(SAMLTestCase): + """ + Test EdXSAMLIdentityProvider. + """ + @mock.patch('third_party_auth.saml.log') + def test_get_saml_idp_class_with_fake_identifier(self, log_mock): + error_mock = log_mock.error + idp_class = get_saml_idp_class('fake_idp_class_option') + error_mock.assert_called_once_with( + u'%s is not a valid EdXSAMLIdentityProvider subclass; using EdXSAMLIdentityProvider base class.', + 'fake_idp_class_option' + ) + self.assertIs(idp_class, EdXSAMLIdentityProvider) + + def test_get_user_details(self): + """ test get_attr and get_user_details of EdXSAMLIdentityProvider""" + edx_saml_identity_provider = EdXSAMLIdentityProvider('demo', **mock_conf) + self.assertEqual(edx_saml_identity_provider.get_user_details(mock_attributes), expected_user_details) diff --git a/common/djangoapps/third_party_auth/tests/test_views.py b/common/djangoapps/third_party_auth/tests/test_views.py index eb9aee04d7..5bc78c4dfd 100644 --- a/common/djangoapps/third_party_auth/tests/test_views.py +++ b/common/djangoapps/third_party_auth/tests/test_views.py @@ -6,9 +6,11 @@ import unittest import ddt from django.conf import settings +from django.urls import reverse from lxml import etree from onelogin.saml2.errors import OneLogin_Saml2_Error +from third_party_auth import pipeline # Define some XML namespaces: from third_party_auth.tasks import SAML_XML_NS @@ -53,8 +55,8 @@ class SAMLMetadataTest(SAMLTestCase): self.enable_saml( other_config_str=( '{' - '"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},' # pylint: disable=unicode-format-string,line-too-long - '"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}' # pylint: disable=unicode-format-string,line-too-long + '"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},' # pylint: disable=unicode-format-string + '"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}' # pylint: disable=unicode-format-string '}' ) ) @@ -153,3 +155,42 @@ class SAMLAuthTest(SAMLTestCase): self.enable_saml(enabled=False) response = self.client.get(self.LOGIN_URL) self.assertEqual(response.status_code, 404) + + +@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled') +class IdPRedirectViewTest(SAMLTestCase): + """ + Test IdPRedirectView. + """ + def setUp(self): + super(IdPRedirectViewTest, self).setUp() + + self.enable_saml() + self.configure_saml_provider( + name="Test", + slug="test", + enabled=True, + ) + + def test_with_valid_provider_slug(self): + endpoint_url = self.get_idp_redirect_url('saml-test') + expected_url = pipeline.get_login_url('saml-test', pipeline.AUTH_ENTRY_LOGIN, reverse('dashboard')) + + response = self.client.get(endpoint_url) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, expected_url) + + def test_with_invalid_provider_slug(self): + endpoint_url = self.get_idp_redirect_url('saml-test-invalid') + + response = self.client.get(endpoint_url) + + self.assertEqual(response.status_code, 404) + + @staticmethod + def get_idp_redirect_url(provider_slug, next_destination=None): + return '{idp_redirect_url}?{next_destination}'.format( + idp_redirect_url=reverse('idp_redirect', kwargs={'provider_slug': provider_slug}), + next_destination=next_destination, + ) diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py index 8fdeba6cc2..b6dbabcc37 100644 --- a/common/djangoapps/third_party_auth/tests/testutil.py +++ b/common/djangoapps/third_party_auth/tests/testutil.py @@ -25,9 +25,6 @@ from third_party_auth.models import ( SAMLConfiguration, SAMLProviderConfig ) -from third_party_auth.saml import EdXSAMLIdentityProvider, get_saml_idp_class -from third_party_auth.tests.data.saml_identity_provider_mock_data import mock_conf, mock_attributes,\ - expected_user_details AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES @@ -217,21 +214,6 @@ class SAMLTestCase(TestCase): kwargs.setdefault('entity_id', "https://saml.example.none") super(SAMLTestCase, self).enable_saml(**kwargs) - @mock.patch('third_party_auth.saml.log') - def test_get_saml_idp_class_with_fake_identifier(self, log_mock): - error_mock = log_mock.error - idp_class = get_saml_idp_class('fake_idp_class_option') - error_mock.assert_called_once_with( - u'%s is not a valid EdXSAMLIdentityProvider subclass; using EdXSAMLIdentityProvider base class.', - 'fake_idp_class_option' - ) - self.assertIs(idp_class, EdXSAMLIdentityProvider) - - def test_get_user_details(self): - """ test get_attr and get_user_details of EdXSAMLIdentityProvider""" - edx_smal_identity_provider = EdXSAMLIdentityProvider('demo', **mock_conf) - self.assertEqual(edx_smal_identity_provider.get_user_details(mock_attributes), expected_user_details) - @contextmanager def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=None, username=None, **kwargs): diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py index ddea923dad..58be91558b 100644 --- a/common/djangoapps/third_party_auth/urls.py +++ b/common/djangoapps/third_party_auth/urls.py @@ -2,12 +2,14 @@ from django.conf.urls import include, url -from .views import inactive_user_view, lti_login_and_complete_view, post_to_custom_auth_form, saml_metadata_view +from .views import (inactive_user_view, lti_login_and_complete_view, + post_to_custom_auth_form, saml_metadata_view, IdPRedirectView) urlpatterns = [ url(r'^auth/inactive', inactive_user_view, name="third_party_inactive_redirect"), url(r'^auth/custom_auth_entry', post_to_custom_auth_form, name='tpa_post_to_custom_auth_form'), url(r'^auth/saml/metadata.xml', saml_metadata_view), url(r'^auth/login/(?Plti)/$', lti_login_and_complete_view), + url(r'^auth/idp_redirect/(?P.*)', IdPRedirectView.as_view(), name="idp_redirect"), url(r'^auth/', include('social_django.urls', namespace='social')), ] diff --git a/common/djangoapps/third_party_auth/views.py b/common/djangoapps/third_party_auth/views.py index deaabc593c..87ae9ffa2f 100644 --- a/common/djangoapps/third_party_auth/views.py +++ b/common/djangoapps/third_party_auth/views.py @@ -3,13 +3,15 @@ Extra views required for SSO """ from django.conf import settings from django.urls import reverse -from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError +from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError, HttpResponseNotFound from django.shortcuts import redirect, render +from django.views.generic.base import View from django.views.decorators.csrf import csrf_exempt from social_django.utils import load_strategy, load_backend, psa from social_django.views import complete from social_core.utils import setting_name +from student.helpers import get_next_url_for_login_page from student.models import UserProfile from student.views import compose_and_send_activation_email import third_party_auth @@ -110,3 +112,14 @@ def post_to_custom_auth_form(request): 'hmac': pipeline_data['hmac'], } return render(request, 'third_party_auth/post_custom_auth_entry.html', data) + + +class IdPRedirectView(View): + def get(self, request, *args, **kwargs): + next_destination_url = get_next_url_for_login_page(request) + + try: + url = pipeline.get_login_url(kwargs['provider_slug'], pipeline.AUTH_ENTRY_LOGIN, next_destination_url) + return redirect(url) + except ValueError: + return HttpResponseNotFound() From b4ccd03740e33e599a6f043bed2598fa33fd2a3e Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Mon, 18 Mar 2019 10:29:49 -0400 Subject: [PATCH 006/106] This adds a new django app -- edx-when -- that will copy start and due dates to mysql and allow per-learner overrides in the instructor dashboard, using the existing IDDE interface. It adds a data migration for existing IDDE data. --- .../lib/xmodule/xmodule/modulestore/django.py | 4 + .../tests/test_field_override_performance.py | 5 +- .../course_api/blocks/tests/test_api.py | 57 +----- lms/djangoapps/course_blocks/api.py | 10 +- .../transformers/access_denied_filter.py | 4 +- .../course_blocks/transformers/hide_empty.py | 4 +- .../transformers/library_content.py | 5 +- .../transformers/load_override_data.py | 3 +- .../migrations/0008_move_idde_to_edx_when.py | 37 ++++ lms/djangoapps/courseware/module_render.py | 7 +- lms/djangoapps/courseware/tests/test_views.py | 12 +- lms/djangoapps/grades/tests/test_tasks.py | 20 +-- lms/djangoapps/instructor/tests/test_api.py | 167 +++++++++--------- .../tests/test_api_email_localization.py | 2 +- .../instructor/tests/test_certificates.py | 10 +- .../instructor/tests/test_enrollment.py | 17 +- .../instructor/tests/test_proctoring.py | 10 +- .../tests/test_registration_codes.py | 2 +- lms/djangoapps/instructor/tests/test_tools.py | 61 +++---- lms/djangoapps/instructor/views/api.py | 154 ++++++++-------- .../instructor/views/gradebook_api.py | 2 +- .../instructor/views/instructor_dashboard.py | 22 +-- lms/djangoapps/instructor/views/tools.py | 65 ++----- .../tests/views/test_course_home.py | 2 +- .../tests/views/test_course_updates.py | 2 +- openedx/tests/settings.py | 1 + requirements/edx/base.in | 1 + requirements/edx/base.txt | 1 + requirements/edx/development.txt | 1 + requirements/edx/testing.txt | 1 + 30 files changed, 326 insertions(+), 363 deletions(-) create mode 100644 lms/djangoapps/courseware/migrations/0008_move_idde_to_edx_when.py diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index 0013cac544..75010a0f6a 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -198,6 +198,10 @@ class SignalHandler(object): log.info('Sent %s signal to %s with kwargs %s. Response was: %s', signal_name, receiver, kwargs, response) +# to allow easy imports +globals().update({sig.name.upper(): sig for sig in SignalHandler.all_signals()}) + + def load_function(path): """ Load a function by name. diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 36aeb39ee9..7b67ee3d46 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -239,7 +239,7 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): __test__ = True # TODO: decrease query count as part of REVO-28 - QUERY_COUNT = 30 + QUERY_COUNT = 32 TEST_DATA = { # (providers, course_width, enable_ccx, view_as_ccx): ( # # of sql queries to default, @@ -268,7 +268,8 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True # TODO: decrease query count as part of REVO-28 - QUERY_COUNT = 30 + QUERY_COUNT = 32 + TEST_DATA = { ('no_overrides', 1, True, False): (QUERY_COUNT, 3), ('no_overrides', 2, True, False): (QUERY_COUNT, 3), diff --git a/lms/djangoapps/course_api/blocks/tests/test_api.py b/lms/djangoapps/course_api/blocks/tests/test_api.py index a6e2d58436..5cc3084bb4 100644 --- a/lms/djangoapps/course_api/blocks/tests/test_api.py +++ b/lms/djangoapps/course_api/blocks/tests/test_api.py @@ -7,9 +7,6 @@ from mock import patch import ddt from django.test.client import RequestFactory -from django.test.utils import override_settings - -import course_blocks.api as course_blocks_api from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE, waffle @@ -209,7 +206,7 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase): self._get_blocks( course, expected_mongo_queries=0, - expected_sql_queries=10 if with_storage_backing else 9, + expected_sql_queries=12 if with_storage_backing else 11, ) @ddt.data( @@ -226,57 +223,9 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase): clear_course_from_cache(course.id) if with_storage_backing: - num_sql_queries = 20 + num_sql_queries = 22 else: - num_sql_queries = 10 - - self._get_blocks( - course, - expected_mongo_queries, - expected_sql_queries=num_sql_queries, - ) - - -@ddt.ddt -@override_settings(FIELD_OVERRIDE_PROVIDERS=(course_blocks_api.INDIVIDUAL_STUDENT_OVERRIDE_PROVIDER, )) -class TestQueryCountsWithIndividualOverrideProvider(TestGetBlocksQueryCountsBase): - """ - Tests query counts for the get_blocks function when IndividualStudentOverrideProvider is set. - """ - - @ddt.data( - *product( - (ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split), - (True, False), - ) - ) - @ddt.unpack - def test_query_counts_cached(self, store_type, with_storage_backing): - with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): - course = self._create_course(store_type) - self._get_blocks( - course, - expected_mongo_queries=0, - expected_sql_queries=11 if with_storage_backing else 10, - ) - - @ddt.data( - *product( - ((ModuleStoreEnum.Type.mongo, 5), (ModuleStoreEnum.Type.split, 3)), - (True, False), - ) - ) - @ddt.unpack - def test_query_counts_uncached(self, store_type_tuple, with_storage_backing): - store_type, expected_mongo_queries = store_type_tuple - with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): - course = self._create_course(store_type) - clear_course_from_cache(course.id) - - if with_storage_backing: - num_sql_queries = 21 - else: - num_sql_queries = 11 + num_sql_queries = 12 self._get_blocks( course, diff --git a/lms/djangoapps/course_blocks/api.py b/lms/djangoapps/course_blocks/api.py index e7139b64b1..9e8fc46941 100644 --- a/lms/djangoapps/course_blocks/api.py +++ b/lms/djangoapps/course_blocks/api.py @@ -4,17 +4,12 @@ get_course_blocks function. """ from django.conf import settings +from edx_when import field_data from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers from openedx.features.content_type_gating.block_transformers import ContentTypeGateTransformer -from .transformers import ( - library_content, - start_date, - user_partitions, - visibility, - load_override_data, -) +from .transformers import library_content, load_override_data, start_date, user_partitions, visibility from .usage_info import CourseUsageInfo INDIVIDUAL_STUDENT_OVERRIDE_PROVIDER = ( @@ -46,6 +41,7 @@ def get_course_block_access_transformers(user): ContentTypeGateTransformer(), user_partitions.UserPartitionTransformer(), visibility.VisibilityTransformer(), + field_data.DateOverrideTransformer(user), ] if has_individual_student_override_provider(): diff --git a/lms/djangoapps/course_blocks/transformers/access_denied_filter.py b/lms/djangoapps/course_blocks/transformers/access_denied_filter.py index adbcd5f740..be20d7ca6c 100644 --- a/lms/djangoapps/course_blocks/transformers/access_denied_filter.py +++ b/lms/djangoapps/course_blocks/transformers/access_denied_filter.py @@ -2,9 +2,7 @@ Access Denied Message Filter Transformer implementation. """ # TODO: Remove this file after REVE-52 lands and old-mobile-app traffic falls to < 5% of mobile traffic -from openedx.core.djangoapps.content.block_structure.transformer import ( - BlockStructureTransformer -) +from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer class AccessDeniedMessageFilterTransformer(BlockStructureTransformer): diff --git a/lms/djangoapps/course_blocks/transformers/hide_empty.py b/lms/djangoapps/course_blocks/transformers/hide_empty.py index cbae63cc04..a81a51624d 100644 --- a/lms/djangoapps/course_blocks/transformers/hide_empty.py +++ b/lms/djangoapps/course_blocks/transformers/hide_empty.py @@ -2,9 +2,7 @@ Hide Empty Transformer implementation. """ # TODO: Remove this file after REVE-52 lands and old-mobile-app traffic falls to < 5% of mobile traffic -from openedx.core.djangoapps.content.block_structure.transformer import ( - BlockStructureTransformer -) +from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer class HideEmptyTransformer(BlockStructureTransformer): diff --git a/lms/djangoapps/course_blocks/transformers/library_content.py b/lms/djangoapps/course_blocks/transformers/library_content.py index e566a1a0c7..de1a82580a 100644 --- a/lms/djangoapps/course_blocks/transformers/library_content.py +++ b/lms/djangoapps/course_blocks/transformers/library_content.py @@ -3,8 +3,9 @@ Content Library Transformer. """ import json -from courseware.models import StudentModule from eventtracking import tracker + +from courseware.models import StudentModule from openedx.core.djangoapps.content.block_structure.transformer import ( BlockStructureTransformer, FilteringTransformerMixin @@ -98,7 +99,7 @@ class ContentLibraryTransformer(FilteringTransformerMixin, BlockStructureTransfo # Save back any changes if any(block_keys[changed] for changed in ('invalid', 'overlimit', 'added')): state_dict['selected'] = list(selected) - StudentModule.save_state( # pylint: disable=no-value-for-parameter + StudentModule.save_state( student=usage_info.user, course_id=usage_info.course_key, module_state_key=block_key, diff --git a/lms/djangoapps/course_blocks/transformers/load_override_data.py b/lms/djangoapps/course_blocks/transformers/load_override_data.py index 7ab6eec772..6f59681aeb 100644 --- a/lms/djangoapps/course_blocks/transformers/load_override_data.py +++ b/lms/djangoapps/course_blocks/transformers/load_override_data.py @@ -3,9 +3,8 @@ Load Override Data Transformer """ import json -from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer - from courseware.models import StudentFieldOverride +from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer # The list of fields are in support of Individual due dates and could be expanded for other use cases. REQUESTED_FIELDS = [ diff --git a/lms/djangoapps/courseware/migrations/0008_move_idde_to_edx_when.py b/lms/djangoapps/courseware/migrations/0008_move_idde_to_edx_when.py new file mode 100644 index 0000000000..bc35678610 --- /dev/null +++ b/lms/djangoapps/courseware/migrations/0008_move_idde_to_edx_when.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-21 15:25 +from __future__ import unicode_literals + +import json +import logging +from django.db import migrations + + +def move_overrides_to_edx_when(apps, schema_editor): + from xmodule.fields import Date + from edx_when import api + date_field = Date() + StudentFieldOverride = apps.get_model('courseware', 'StudentFieldOverride') + log = logging.getLogger(__name__) + for override in StudentFieldOverride.objects.filter(field='due'): + try: + abs_date = date_field.from_json(json.loads(override.value)) + api.set_date_for_block( + override.course_id, + override.location, + 'due', + abs_date, + user=override.student) + except Exception: # pylint: disable=broad-except + log.exception("migrating %d %r: %r", override.id, override.location, override.value) + + +class Migration(migrations.Migration): + + dependencies = [ + ('courseware', '0007_remove_done_index'), + ] + + operations = [ + migrations.RunPython(move_overrides_to_edx_when) + ] diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 55f42f0a9d..a75814094e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -96,6 +96,8 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.x_module import XModuleDescriptor +from edx_when.field_data import DateLookupFieldData + log = logging.getLogger(__name__) @@ -164,7 +166,6 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_ field_data_cache must include data from the course module and 2 levels of its descendants ''' - with modulestore().bulk_operations(course.id): course_module = get_module_for_descriptor( user, request, course, field_data_cache, course.id, course=course @@ -660,6 +661,7 @@ def get_module_system_for_user( inner_system, real_user.id, [ + partial(DateLookupFieldData, course_id=course_id, user=user), partial(OverrideFieldData.wrap, real_user, course), partial(LmsFieldData, student_data=inner_student_data), ], @@ -755,7 +757,8 @@ def get_module_system_for_user( else: anonymous_student_id = anonymous_id_for_user(user, None) - field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access + field_data = DateLookupFieldData(descriptor._field_data, course_id, user) # pylint: disable=protected-access + field_data = LmsFieldData(field_data, student_data) user_is_staff = bool(has_access(user, u'staff', descriptor.location, course_id)) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index a8dac6a1b2..5135f7e8a4 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): NUM_PROBLEMS = 20 @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 176), - (ModuleStoreEnum.Type.split, 4, 170), + (ModuleStoreEnum.Type.mongo, 10, 178), + (ModuleStoreEnum.Type.split, 4, 172), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): @@ -1465,8 +1465,8 @@ class ProgressPageTests(ProgressPageBaseTests): self.assertContains(resp, u"Download Your Certificate") @ddt.data( - (True, 50), - (False, 49) + (True, 52), + (False, 51) ) @ddt.unpack def test_progress_queries_paced_courses(self, self_paced, query_count): @@ -1479,8 +1479,8 @@ class ProgressPageTests(ProgressPageBaseTests): @patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False}) @ddt.data( - (False, 58, 38), - (True, 49, 33) + (False, 60, 40), + (True, 51, 35) ) @ddt.unpack def test_progress_queries(self, enable_waffle, initial, subsequent): diff --git a/lms/djangoapps/grades/tests/test_tasks.py b/lms/djangoapps/grades/tests/test_tasks.py index 64a15aaa45..64b4c214b9 100644 --- a/lms/djangoapps/grades/tests/test_tasks.py +++ b/lms/djangoapps/grades/tests/test_tasks.py @@ -175,10 +175,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest self.assertEquals(mock_block_structure_create.call_count, 1) @ddt.data( - (ModuleStoreEnum.Type.mongo, 1, 36, True), - (ModuleStoreEnum.Type.mongo, 1, 36, False), - (ModuleStoreEnum.Type.split, 3, 36, True), - (ModuleStoreEnum.Type.split, 3, 36, False), + (ModuleStoreEnum.Type.mongo, 1, 38, True), + (ModuleStoreEnum.Type.mongo, 1, 38, False), + (ModuleStoreEnum.Type.split, 3, 38, True), + (ModuleStoreEnum.Type.split, 3, 38, False), ) @ddt.unpack def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections): @@ -190,8 +190,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest self._apply_recalculate_subsection_grade() @ddt.data( - (ModuleStoreEnum.Type.mongo, 1, 36), - (ModuleStoreEnum.Type.split, 3, 36), + (ModuleStoreEnum.Type.mongo, 1, 38), + (ModuleStoreEnum.Type.split, 3, 38), ) @ddt.unpack def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls): @@ -236,8 +236,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest ) @ddt.data( - (ModuleStoreEnum.Type.mongo, 1, 19), - (ModuleStoreEnum.Type.split, 3, 19), + (ModuleStoreEnum.Type.mongo, 1, 21), + (ModuleStoreEnum.Type.split, 3, 21), ) @ddt.unpack def test_persistent_grades_not_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries): @@ -251,8 +251,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest self.assertEqual(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0) @ddt.data( - (ModuleStoreEnum.Type.mongo, 1, 37), - (ModuleStoreEnum.Type.split, 3, 37), + (ModuleStoreEnum.Type.mongo, 1, 39), + (ModuleStoreEnum.Type.split, 3, 39), ) @ddt.unpack def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries): diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 0e377c185e..f7b045b6ca 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -3,6 +3,7 @@ Unit tests for instructor.api methods. """ from __future__ import print_function + import datetime import functools import io @@ -18,25 +19,21 @@ from django.conf import settings from django.contrib.auth.models import User from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse as django_reverse from django.http import HttpRequest, HttpResponse from django.test import RequestFactory, TestCase from django.test.utils import override_settings -from pytz import UTC +from django.urls import reverse as django_reverse from django.utils.translation import ugettext as _ from mock import Mock, NonCallableMock, patch from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import UsageKey +from pytz import UTC from six import text_type -import lms.djangoapps.instructor.views.api -import lms.djangoapps.instructor_task.api from bulk_email.models import BulkEmailFlag, CourseEmail, CourseEmailTemplate -from lms.djangoapps.certificates.models import CertificateStatuses -from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.models import StudentFieldOverride, StudentModule +from courseware.models import StudentModule from courseware.tests.factories import ( BetaTesterFactory, GlobalStaffFactory, @@ -47,6 +44,8 @@ from courseware.tests.factories import ( from courseware.tests.helpers import LoginEnrollmentTestCase from django_comment_common.models import FORUM_ROLE_COMMUNITY_TA from django_comment_common.utils import seed_permissions_roles +from lms.djangoapps.certificates.models import CertificateStatuses +from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from lms.djangoapps.instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo from lms.djangoapps.instructor.views.api import ( _split_input_list, @@ -63,6 +62,9 @@ from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin from openedx.core.lib.xblock_utils import grade_histogram +from edx_when.api import get_overrides_for_user +from edx_when.signals import extract_dates + from shoppingcart.models import ( Coupon, CouponRedemption, @@ -351,7 +353,7 @@ class TestEndpointHttpMethods(SharedModuleStoreTestCase, LoginEnrollmentTestCase """ Tests that POST endpoints are rejected with 405 when using GET. """ - url = reverse(data, kwargs={'course_id': unicode(self.course.id)}) + url = reverse(data, kwargs={'course_id': text_type(self.course.id)}) response = self.client.get(url) self.assertEqual( @@ -366,7 +368,7 @@ class TestEndpointHttpMethods(SharedModuleStoreTestCase, LoginEnrollmentTestCase """ Tests that GET endpoints are not rejected with 405 when using GET. """ - url = reverse(data, kwargs={'course_id': unicode(self.course.id)}) + url = reverse(data, kwargs={'course_id': text_type(self.course.id)}) response = self.client.get(url) self.assertNotEqual( @@ -630,10 +632,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas CourseModeFactory.create(course_id=cls.audit_course.id, mode_slug=CourseMode.AUDIT) cls.url = reverse( - 'register_and_enroll_students', kwargs={'course_id': unicode(cls.course.id)} + 'register_and_enroll_students', kwargs={'course_id': text_type(cls.course.id)} ) cls.audit_course_url = reverse( - 'register_and_enroll_students', kwargs={'course_id': unicode(cls.audit_course.id)} + 'register_and_enroll_students', kwargs={'course_id': text_type(cls.audit_course.id)} ) def setUp(self): @@ -649,7 +651,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas ) self.white_label_course_url = reverse( - 'register_and_enroll_students', kwargs={'course_id': unicode(self.white_label_course.id)} + 'register_and_enroll_students', kwargs={'course_id': text_type(self.white_label_course.id)} ) self.request = RequestFactory().request() @@ -969,8 +971,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas manual_enrollments = ManualEnrollmentAudit.objects.all() self.assertEqual(manual_enrollments.count(), 2) - @patch.object(lms.djangoapps.instructor.views.api, 'generate_random_string', - Mock(side_effect=['first', 'first', 'second'])) + @patch('lms.djangoapps.instructor.views.api', 'generate_random_string', + Mock(side_effect=['first', 'first', 'second'])) def test_generate_unique_password_no_reuse(self): """ generate_unique_password should generate a unique password string that hasn't been generated before. @@ -2899,7 +2901,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment """ url = reverse( 'get_problem_responses', - kwargs={'course_id': unicode(self.course.id)} + kwargs={'course_id': text_type(self.course.id)} ) problem_location = '' @@ -2934,7 +2936,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment """ url = reverse( 'get_problem_responses', - kwargs={'course_id': unicode(self.course.id)} + kwargs={'course_id': text_type(self.course.id)} ) problem_location = '' @@ -2954,7 +2956,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment """ url = reverse( 'get_problem_responses', - kwargs={'course_id': unicode(self.course.id)} + kwargs={'course_id': text_type(self.course.id)} ) task_type = 'problem_responses_csv' already_running_status = generate_already_running_error_message(task_type) @@ -3030,7 +3032,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment """ url = reverse( 'get_students_who_may_enroll', - kwargs={'course_id': unicode(self.course.id)} + kwargs={'course_id': text_type(self.course.id)} ) # Successful case: response = self.client.post(url, {}) @@ -3052,7 +3054,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment """ url = reverse( 'get_proctored_exam_results', - kwargs={'course_id': unicode(self.course.id)} + kwargs={'course_id': text_type(self.course.id)} ) # Successful case: @@ -3254,8 +3256,8 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url, {}) self.assertIn('The detailed enrollment report is being created.', response.content) - @patch.object(lms.djangoapps.instructor.views.api, 'anonymous_id_for_user', Mock(return_value='42')) - @patch.object(lms.djangoapps.instructor.views.api, 'unique_id_for_user', Mock(return_value='41')) + @patch('lms.djangoapps.instructor.views.api.anonymous_id_for_user', Mock(return_value='42')) + @patch('lms.djangoapps.instructor.views.api.unique_id_for_user', Mock(return_value='41')) def test_get_anon_ids(self): """ Test the CSV output for the anonymized user ids. @@ -3323,7 +3325,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment @ddt.unpack @valid_problem_location def test_calculate_report_csv_success(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs): - kwargs = {'course_id': unicode(self.course.id)} + kwargs = {'course_id': text_type(self.course.id)} kwargs.update(extra_instructor_api_kwargs) url = reverse(instructor_api_endpoint, kwargs=kwargs) success_status = u"The {report_type} report is being created.".format(report_type=report_type) @@ -3348,7 +3350,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment task_api_endpoint, extra_instructor_api_kwargs ): # pylint: disable=unused-argument - kwargs = {'course_id': unicode(self.course.id)} + kwargs = {'course_id': text_type(self.course.id)} kwargs.update(extra_instructor_api_kwargs) url = reverse(instructor_api_endpoint, kwargs=kwargs) @@ -3370,7 +3372,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment task_api_endpoint, extra_instructor_api_kwargs ): - kwargs = {'course_id': unicode(self.course.id)} + kwargs = {'course_id': text_type(self.course.id)} kwargs.update(extra_instructor_api_kwargs) url = reverse(instructor_api_endpoint, kwargs=kwargs) @@ -3494,7 +3496,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes ) # mock out the function which should be called to execute the action. - @patch.object(lms.djangoapps.instructor_task.api, 'submit_reset_problem_attempts_for_all_students') + @patch('lms.djangoapps.instructor_task.api.submit_reset_problem_attempts_for_all_students') def test_reset_student_attempts_all(self, act): """ Test reset all student attempts. """ url = reverse('reset_student_attempts', kwargs={'course_id': text_type(self.course.id)}) @@ -3544,7 +3546,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes }) self.assertEqual(response.status_code, 400) - @patch.object(lms.djangoapps.instructor_task.api, 'submit_rescore_problem_for_student') + @patch('lms.djangoapps.instructor_task.api.submit_rescore_problem_for_student') def test_rescore_problem_single(self, act): """ Test rescoring of a single student. """ url = reverse('rescore_problem', kwargs={'course_id': text_type(self.course.id)}) @@ -3555,7 +3557,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes self.assertEqual(response.status_code, 200) self.assertTrue(act.called) - @patch.object(lms.djangoapps.instructor_task.api, 'submit_rescore_problem_for_student') + @patch('lms.djangoapps.instructor_task.api.submit_rescore_problem_for_student') def test_rescore_problem_single_from_uname(self, act): """ Test rescoring of a single student. """ url = reverse('rescore_problem', kwargs={'course_id': text_type(self.course.id)}) @@ -3566,7 +3568,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes self.assertEqual(response.status_code, 200) self.assertTrue(act.called) - @patch.object(lms.djangoapps.instructor_task.api, 'submit_rescore_problem_for_all_students') + @patch('lms.djangoapps.instructor_task.api.submit_rescore_problem_for_all_students') def test_rescore_problem_all(self, act): """ Test rescoring for all students. """ url = reverse('rescore_problem', kwargs={'course_id': text_type(self.course.id)}) @@ -3694,7 +3696,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_reset_entrance_exam_student_attempts_delete_all(self): """ Make sure no one can delete all students state on entrance exam. """ url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course.id)}) + kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'all_students': True, 'delete_module': True, @@ -3704,7 +3706,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_reset_entrance_exam_student_attempts_single(self): """ Test reset single student attempts for entrance exam. """ url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course.id)}) + kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -3718,11 +3720,11 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE ) # mock out the function which should be called to execute the action. - @patch.object(lms.djangoapps.instructor_task.api, 'submit_reset_problem_attempts_in_entrance_exam') + @patch('lms.djangoapps.instructor_task.api.submit_reset_problem_attempts_in_entrance_exam') def test_reset_entrance_exam_all_student_attempts(self, act): """ Test reset all student attempts for entrance exam. """ url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course.id)}) + kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'all_students': True, }) @@ -3732,7 +3734,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_reset_student_attempts_invalid_entrance_exam(self): """ Test reset for invalid entrance exam. """ url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course_with_invalid_ee.id)}) + kwargs={'course_id': text_type(self.course_with_invalid_ee.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -3741,7 +3743,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_entrance_exam_student_delete_state(self): """ Test delete single student entrance exam state. """ url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course.id)}) + kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, 'delete_module': True, @@ -3757,7 +3759,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE staff_user = StaffFactory(course_key=self.course.id) self.client.login(username=staff_user.username, password='test') url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course.id)}) + kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, 'delete_module': True, @@ -3767,17 +3769,17 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_entrance_exam_reset_student_attempts_nonsense(self): """ Test failure with both unique_student_identifier and all_students. """ url = reverse('reset_student_attempts_for_entrance_exam', - kwargs={'course_id': unicode(self.course.id)}) + kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, 'all_students': True, }) self.assertEqual(response.status_code, 400) - @patch.object(lms.djangoapps.instructor_task.api, 'submit_rescore_entrance_exam_for_student') + @patch('lms.djangoapps.instructor_task.api.submit_rescore_entrance_exam_for_student') def test_rescore_entrance_exam_single_student(self, act): """ Test re-scoring of entrance exam for single student. """ - url = reverse('rescore_entrance_exam', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('rescore_entrance_exam', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -3786,7 +3788,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_rescore_entrance_exam_all_student(self): """ Test rescoring for all students. """ - url = reverse('rescore_entrance_exam', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('rescore_entrance_exam', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'all_students': True, }) @@ -3794,7 +3796,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_rescore_entrance_exam_if_higher_all_student(self): """ Test rescoring for all students only if higher. """ - url = reverse('rescore_entrance_exam', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('rescore_entrance_exam', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'all_students': True, 'only_if_higher': True, @@ -3803,7 +3805,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_rescore_entrance_exam_all_student_and_single(self): """ Test re-scoring with both all students and single student parameters. """ - url = reverse('rescore_entrance_exam', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('rescore_entrance_exam', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, 'all_students': True, @@ -3812,7 +3814,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_rescore_entrance_exam_with_invalid_exam(self): """ Test re-scoring of entrance exam with invalid exam. """ - url = reverse('rescore_entrance_exam', kwargs={'course_id': unicode(self.course_with_invalid_ee.id)}) + url = reverse('rescore_entrance_exam', kwargs={'course_id': text_type(self.course_with_invalid_ee.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -3821,13 +3823,13 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_list_entrance_exam_instructor_tasks_student(self): """ Test list task history for entrance exam AND student. """ # create a re-score entrance exam task - url = reverse('rescore_entrance_exam', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('rescore_entrance_exam', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) self.assertEqual(response.status_code, 200) - url = reverse('list_entrance_exam_instructor_tasks', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('list_entrance_exam_instructor_tasks', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -3840,7 +3842,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_list_entrance_exam_instructor_tasks_all_student(self): """ Test list task history for entrance exam AND all student. """ - url = reverse('list_entrance_exam_instructor_tasks', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('list_entrance_exam_instructor_tasks', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, {}) self.assertEqual(response.status_code, 200) @@ -3851,7 +3853,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_list_entrance_exam_instructor_with_invalid_exam_key(self): """ Test list task history for entrance exam failure if course has invalid exam. """ url = reverse('list_entrance_exam_instructor_tasks', - kwargs={'course_id': unicode(self.course_with_invalid_ee.id)}) + kwargs={'course_id': text_type(self.course_with_invalid_ee.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -3860,7 +3862,7 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE def test_skip_entrance_exam_student(self): """ Test skip entrance exam api for student. """ # create a re-score entrance exam task - url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { 'unique_student_identifier': self.student.email, }) @@ -4095,7 +4097,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC self.tasks = [self.FakeTask(mock_factory.mock_get_task_completion_info) for _ in xrange(7)] self.tasks[-1].make_invalid_output() - @patch.object(lms.djangoapps.instructor_task.api, 'get_running_instructor_tasks') + @patch('lms.djangoapps.instructor_task.api.get_running_instructor_tasks') def test_list_instructor_tasks_running(self, act): """ Test list of all running tasks. """ act.return_value = self.tasks @@ -4116,7 +4118,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC self.assertDictEqual(exp_task, act_task) self.assertEqual(actual_tasks, expected_tasks) - @patch.object(lms.djangoapps.instructor_task.api, 'get_instructor_task_history') + @patch('lms.djangoapps.instructor_task.api.get_instructor_task_history') def test_list_background_email_tasks(self, act): """Test list of background email tasks.""" act.return_value = self.tasks @@ -4137,7 +4139,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC self.assertDictEqual(exp_task, act_task) self.assertEqual(actual_tasks, expected_tasks) - @patch.object(lms.djangoapps.instructor_task.api, 'get_instructor_task_history') + @patch('lms.djangoapps.instructor_task.api.get_instructor_task_history') def test_list_instructor_tasks_problem(self, act): """ Test list task history for problem. """ act.return_value = self.tasks @@ -4160,7 +4162,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC self.assertDictEqual(exp_task, act_task) self.assertEqual(actual_tasks, expected_tasks) - @patch.object(lms.djangoapps.instructor_task.api, 'get_instructor_task_history') + @patch('lms.djangoapps.instructor_task.api.get_instructor_task_history') def test_list_instructor_tasks_problem_student(self, act): """ Test list task history for problem AND student. """ act.return_value = self.tasks @@ -4186,7 +4188,7 @@ class TestInstructorAPITaskLists(SharedModuleStoreTestCase, LoginEnrollmentTestC self.assertEqual(actual_tasks, expected_tasks) -@patch.object(lms.djangoapps.instructor_task.api, 'get_instructor_task_history', autospec=True) +@patch('lms.djangoapps.instructor_task.api.get_instructor_task_history', autospec=True) class TestInstructorEmailContentList(SharedModuleStoreTestCase, LoginEnrollmentTestCase): """ Test the instructor email content history endpoint. @@ -4347,7 +4349,7 @@ class TestInstructorAPIHelpers(TestCase): course_id = CourseKey.from_string('MITx/6.002x/2013_Spring') name = 'L2Node1' output = 'i4x://MITx/6.002x/problem/L2Node1' - self.assertEqual(unicode(msk_from_problem_urlname(course_id, name)), output) + self.assertEqual(text_type(msk_from_problem_urlname(course_id, name)), output) def test_msk_from_problem_urlname_error(self): args = ('notagoodcourse', 'L2Node1') @@ -4360,16 +4362,14 @@ def get_extended_due(course, unit, user): Gets the overridden due date for the given user on the given unit. Returns `None` if there is no override set. """ - try: - override = StudentFieldOverride.objects.get( - course_id=course.id, - student=user, - location=unit.location, - field='due' - ) - return DATE_FIELD.from_json(json.loads(override.value)) - except StudentFieldOverride.DoesNotExist: - return None + location = text_type(unit.location) + dates = get_overrides_for_user(course.id, user) + for override in dates: + if text_type(override['location']) == location: + return override['actual_date'] + print(unit.location) + print(dates) + return None class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): @@ -4453,6 +4453,7 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): self.user2 = user2 self.instructor = InstructorFactory(course_key=self.course.id) self.client.login(username=self.instructor.username, password='test') + extract_dates(None, self.course.id) def test_change_due_date(self): url = reverse('change_due_date', kwargs={'course_id': text_type(self.course.id)}) @@ -4500,18 +4501,10 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): }) self.assertEqual(response.status_code, 200, response.content) self.assertEqual( - None, + self.due, get_extended_due(self.course, self.week1, self.user1) ) - def test_reset_nonexistent_extension(self): - url = reverse('reset_due_date', kwargs={'course_id': text_type(self.course.id)}) - response = self.client.post(url, { - 'student': self.user1.username, - 'url': text_type(self.week1.location), - }) - self.assertEqual(response.status_code, 400, response.content) - def test_show_unit_extensions(self): self.test_change_due_date() url = reverse('show_unit_extensions', @@ -4619,6 +4612,7 @@ class TestDueDateExtensionsDeletedDate(ModuleStoreTestCase, LoginEnrollmentTestC self.user2 = user2 self.instructor = InstructorFactory(course_key=self.course.id) self.client.login(username=self.instructor.username, password='test') + extract_dates(None, self.course.id) def test_reset_extension_to_deleted_date(self): """ @@ -4638,6 +4632,7 @@ class TestDueDateExtensionsDeletedDate(ModuleStoreTestCase, LoginEnrollmentTestC self.week1.due = None self.week1 = self.store.update_item(self.week1, self.user1.id) + extract_dates(None, self.course.id) # Now, week1's normal due date is deleted but the extension still exists. url = reverse('reset_due_date', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, { @@ -4646,7 +4641,7 @@ class TestDueDateExtensionsDeletedDate(ModuleStoreTestCase, LoginEnrollmentTestC }) self.assertEqual(response.status_code, 200, response.content) self.assertEqual( - None, + self.due, get_extended_due(self.course, self.week1, self.user1) ) @@ -4681,7 +4676,7 @@ class TestCourseIssuedCertificatesData(SharedModuleStoreTestCase): """ Test certificates with status 'downloadable' should be in the response. """ - url = reverse('get_issued_certificates', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('get_issued_certificates', kwargs={'course_id': text_type(self.course.id)}) # firstly generating downloadable certificates with 'honor' mode certificate_count = 3 for __ in xrange(certificate_count): @@ -4703,7 +4698,7 @@ class TestCourseIssuedCertificatesData(SharedModuleStoreTestCase): """ Test for certificate csv features against mode. Certificates should be group by 'mode' in reponse. """ - url = reverse('get_issued_certificates', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('get_issued_certificates', kwargs={'course_id': text_type(self.course.id)}) # firstly generating downloadable certificates with 'honor' mode certificate_count = 3 for __ in xrange(certificate_count): @@ -4744,7 +4739,7 @@ class TestCourseIssuedCertificatesData(SharedModuleStoreTestCase): """ Test for certificate csv features. """ - url = reverse('get_issued_certificates', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('get_issued_certificates', kwargs={'course_id': text_type(self.course.id)}) # firstly generating downloadable certificates with 'honor' mode certificate_count = 3 for __ in xrange(certificate_count): @@ -4929,8 +4924,8 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): and row_data[1].endswith('/shoppingcart/register/redeem/{0}/"'.format(code))) index += 1 - @patch.object(lms.djangoapps.instructor.views.api, 'random_code_generator', - Mock(side_effect=['first', 'second', 'third', 'fourth'])) + @patch('lms.djangoapps.instructor.views.api.random_code_generator', + Mock(side_effect=['first', 'second', 'third', 'fourth'])) def test_generate_course_registration_codes_matching_existing_coupon_code(self): """ Test the generated course registration code is already in the Coupon Table @@ -4955,8 +4950,8 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 5) # 1 for headers, 1 for new line at the end and 3 for the actual data - @patch.object(lms.djangoapps.instructor.views.api, 'random_code_generator', - Mock(side_effect=['first', 'first', 'second', 'third'])) + @patch('lms.djangoapps.instructor.views.api.random_code_generator', + Mock(side_effect=['first', 'first', 'second', 'third'])) def test_generate_course_registration_codes_integrity_error(self): """ Test for the Integrity error against the generated code @@ -5248,7 +5243,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): with open(file_name, 'w') as file_pointer: file_pointer.write(csv_data.encode('utf-8')) with open(file_name, 'r') as file_pointer: - url = reverse('add_users_to_cohorts', kwargs={'course_id': unicode(self.course.id)}) + url = reverse('add_users_to_cohorts', kwargs={'course_id': text_type(self.course.id)}) return self.client.post(url, {'uploaded-file': file_pointer}) def expect_error_on_file_content(self, file_content, error, file_suffix='.csv'): @@ -5318,7 +5313,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): response = self.call_add_users_to_cohorts('') self.assertEqual(response.status_code, 403) - @patch('lms.djangoapps.instructor.views.api.lms.djangoapps.instructor_task.api.submit_cohort_students') + @patch('lms.djangoapps.instructor_task.api.submit_cohort_students') @patch('lms.djangoapps.instructor.views.api.store_uploaded_file') def test_success_username(self, mock_store_upload, mock_cohort_task): """ @@ -5329,7 +5324,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): 'username,cohort\nfoo_username,bar_cohort', mock_store_upload, mock_cohort_task ) - @patch('lms.djangoapps.instructor.views.api.lms.djangoapps.instructor_task.api.submit_cohort_students') + @patch('lms.djangoapps.instructor_task.api.submit_cohort_students') @patch('lms.djangoapps.instructor.views.api.store_uploaded_file') def test_success_email(self, mock_store_upload, mock_cohort_task): """ @@ -5340,7 +5335,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): 'email,cohort\nfoo_email,bar_cohort', mock_store_upload, mock_cohort_task ) - @patch('lms.djangoapps.instructor.views.api.lms.djangoapps.instructor_task.api.submit_cohort_students') + @patch('lms.djangoapps.instructor_task.api.submit_cohort_students') @patch('lms.djangoapps.instructor.views.api.store_uploaded_file') def test_success_username_and_email(self, mock_store_upload, mock_cohort_task): """ @@ -5351,7 +5346,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): 'username,email,cohort\nfoo_username,bar_email,baz_cohort', mock_store_upload, mock_cohort_task ) - @patch('lms.djangoapps.instructor.views.api.lms.djangoapps.instructor_task.api.submit_cohort_students') + @patch('lms.djangoapps.instructor_task.api.submit_cohort_students') @patch('lms.djangoapps.instructor.views.api.store_uploaded_file') def test_success_carriage_return(self, mock_store_upload, mock_cohort_task): """ @@ -5362,7 +5357,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): 'username,email,cohort\rfoo_username,bar_email,baz_cohort', mock_store_upload, mock_cohort_task ) - @patch('lms.djangoapps.instructor.views.api.lms.djangoapps.instructor_task.api.submit_cohort_students') + @patch('lms.djangoapps.instructor_task.api.submit_cohort_students') @patch('lms.djangoapps.instructor.views.api.store_uploaded_file') def test_success_carriage_return_line_feed(self, mock_store_upload, mock_cohort_task): """ diff --git a/lms/djangoapps/instructor/tests/test_api_email_localization.py b/lms/djangoapps/instructor/tests/test_api_email_localization.py index fc578f5942..d7f38b21b8 100644 --- a/lms/djangoapps/instructor/tests/test_api_email_localization.py +++ b/lms/djangoapps/instructor/tests/test_api_email_localization.py @@ -4,8 +4,8 @@ Unit tests for the localization of emails sent by instructor.api methods. """ from django.core import mail -from django.urls import reverse from django.test.utils import override_settings +from django.urls import reverse from six import text_type from courseware.tests.factories import InstructorFactory diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index 50bc3282bb..e285dbb704 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -4,17 +4,19 @@ import io import json from datetime import datetime, timedelta -import ddt import mock import pytz -from config_models.models import cache from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse from django.test.utils import override_settings +from django.urls import reverse +import ddt from capa.xqueue_interface import XQueueInterface +from config_models.models import cache +from course_modes.models import CourseMode +from courseware.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, @@ -28,8 +30,6 @@ from lms.djangoapps.certificates.tests.factories import ( CertificateWhitelistFactory, GeneratedCertificateFactory ) -from course_modes.models import CourseMode -from courseware.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory from lms.djangoapps.grades.tests.utils import mock_passing_grade from lms.djangoapps.verify_student.services import IDVerificationService from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index e844bcd3b9..af2f809741 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -8,15 +8,15 @@ import json from abc import ABCMeta import ddt -import mock from ccx_keys.locator import CCXLocator +from crum import set_current_request from django.conf import settings -from django.utils.translation import override as override_language from django.utils.translation import get_language +from django.utils.translation import override as override_language from mock import patch from opaque_keys.edx.locator import CourseLocator from six import text_type -from crum import set_current_request +from submissions import api as sub_api from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from courseware.models import StudentModule @@ -38,7 +38,6 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, get_moc from student.models import CourseEnrollment, CourseEnrollmentAllowed, anonymous_id_for_user from student.roles import CourseCcxCoachRole from student.tests.factories import AdminFactory, UserFactory -from submissions import api as sub_api from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -372,7 +371,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user) self.assertEqual(json.loads(module().state)['attempts'], 0) - @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') + @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') def test_delete_student_attempts(self, _mock_signal): msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) @@ -398,9 +397,9 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): # Disable the score change signal to prevent other components from being # pulled into tests. - @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') - @mock.patch('lms.djangoapps.grades.signals.handlers.submissions_score_set_handler') - @mock.patch('lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler') + @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') + @patch('lms.djangoapps.grades.signals.handlers.submissions_score_set_handler') + @patch('lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler') def test_delete_submission_scores(self, _mock_send_signal, mock_set_receiver, mock_reset_receiver): user = UserFactory() problem_location = self.course_key.make_usage_key('dummy', 'module') @@ -746,7 +745,7 @@ class TestGetEmailParams(SharedModuleStoreTestCase): def test_marketing_params(self): # For a site with a marketing front end, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. - with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): + with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): result = get_email_params(self.course, True) self.assertEqual(result['auto_enroll'], True) diff --git a/lms/djangoapps/instructor/tests/test_proctoring.py b/lms/djangoapps/instructor/tests/test_proctoring.py index 221f9cf84d..b82d803b4f 100644 --- a/lms/djangoapps/instructor/tests/test_proctoring.py +++ b/lms/djangoapps/instructor/tests/test_proctoring.py @@ -2,18 +2,16 @@ Unit tests for Edx Proctoring feature flag in new instructor dashboard. """ -import ddt from django.apps import apps from django.conf import settings from django.urls import reverse - -from edx_proctoring.api import create_exam -from edx_proctoring.backends.tests.test_backend import TestBackendProvider - from mock import patch from six import text_type -from student.roles import CourseStaffRole, CourseInstructorRole +import ddt +from edx_proctoring.api import create_exam +from edx_proctoring.backends.tests.test_backend import TestBackendProvider +from student.roles import CourseInstructorRole, CourseStaffRole from student.tests.factories import AdminFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory diff --git a/lms/djangoapps/instructor/tests/test_registration_codes.py b/lms/djangoapps/instructor/tests/test_registration_codes.py index b7cb56b0ef..601dcedbfb 100644 --- a/lms/djangoapps/instructor/tests/test_registration_codes.py +++ b/lms/djangoapps/instructor/tests/test_registration_codes.py @@ -3,8 +3,8 @@ Test for the registration code status information. """ import json -from django.urls import reverse from django.test.utils import override_settings +from django.urls import reverse from django.utils.translation import ugettext as _ from six import text_type diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index 6a035c7ee0..f241dd4c3a 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -1,6 +1,7 @@ """ Tests for views/tools.py. """ +from __future__ import absolute_import, unicode_literals import datetime import json @@ -11,13 +12,11 @@ import six from django.contrib.auth.models import User from django.core.exceptions import MultipleObjectsReturned from django.test import TestCase -from django.test.utils import override_settings -from pytz import UTC from opaque_keys.edx.keys import CourseKey -from six import text_type +from pytz import UTC -from lms.djangoapps.courseware.field_overrides import OverrideFieldData -from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides +from edx_when import signals +from edx_when.field_data import DateLookupFieldData from student.tests.factories import UserFactory from xmodule.fields import Date from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase @@ -118,7 +117,7 @@ class TestFindUnit(SharedModuleStoreTestCase): """ Test finding a nested unit. """ - url = text_type(self.homework.location) + url = six.text_type(self.homework.location) found_unit = tools.find_unit(self.course, url) self.assertEqual(found_unit.location, self.homework.location) @@ -161,7 +160,7 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase): """ URLs for sequence of nodes. """ - return sorted(text_type(i.location) for i in seq) + return sorted(six.text_type(i.location) for i in seq) self.assertEquals( urls(tools.get_units_with_due_date(self.course)), @@ -177,6 +176,7 @@ class TestTitleOrUrl(unittest.TestCase): self.assertEquals(tools.title_or_url(unit), 'hello') def test_url(self): + # pylint: disable=unused-argument def mock_location_text(self): """ Mock implementation of __unicode__ or __str__ for the unit's location. @@ -191,10 +191,13 @@ class TestTitleOrUrl(unittest.TestCase): self.assertEquals(tools.title_or_url(unit), u'test:hello') -@override_settings( - FIELD_OVERRIDE_PROVIDERS=( - 'lms.djangoapps.courseware.student_field_overrides.IndividualStudentOverrideProvider',), -) +def inject_field_data(blocks, course, user): + use_cached = False + for block in blocks: + block._field_data = DateLookupFieldData(block._field_data, course.id, user, use_cached=use_cached) # pylint: disable=protected-access + use_cached = True + + class TestSetDueDateExtension(ModuleStoreTestCase): """ Test the set_due_date_extensions function. @@ -212,6 +215,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase): week3 = ItemFactory.create(parent=course) homework = ItemFactory.create(parent=week1) assignment = ItemFactory.create(parent=homework, due=due) + signals.extract_dates(None, course.id) user = UserFactory.create() @@ -223,11 +227,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase): self.week3 = week3 self.user = user - inject_field_overrides((course, week1, week2, week3, homework, assignment), course, user) - - def tearDown(self): - super(TestSetDueDateExtension, self).tearDown() - OverrideFieldData.provider_classes = None + inject_field_data((course, week1, week2, week3, homework, assignment), course, user) def _clear_field_data_cache(self): """ @@ -237,22 +237,18 @@ class TestSetDueDateExtension(ModuleStoreTestCase): """ for block in (self.week1, self.week2, self.week3, self.homework, self.assignment): + block._field_data._load_dates(self.course.id, self.user, use_cached=False) # pylint: disable=protected-access block.fields['due']._del_cached_value(block) # pylint: disable=protected-access def test_set_due_date_extension(self): extended = datetime.datetime(2013, 12, 25, 0, 0, tzinfo=UTC) tools.set_due_date_extension(self.course, self.week1, self.user, extended) + tools.set_due_date_extension(self.course, self.assignment, self.user, extended) self._clear_field_data_cache() self.assertEqual(self.week1.due, extended) self.assertEqual(self.homework.due, extended) self.assertEqual(self.assignment.due, extended) - def test_set_due_date_extension_num_queries(self): - extended = datetime.datetime(2013, 12, 25, 0, 0, tzinfo=UTC) - with self.assertNumQueries(5): - tools.set_due_date_extension(self.course, self.week1, self.user, extended) - self._clear_field_data_cache() - def test_set_due_date_extension_invalid_date(self): extended = datetime.datetime(2009, 1, 1, 0, 0, tzinfo=UTC) with self.assertRaises(tools.DashboardError): @@ -299,6 +295,7 @@ class TestDataDumps(ModuleStoreTestCase): self.week2 = week2 self.user1 = user1 self.user2 = user2 + signals.extract_dates(None, course.id) def test_dump_module_extensions(self): extended = datetime.datetime(2013, 12, 25, 0, 0, tzinfo=UTC) @@ -307,12 +304,12 @@ class TestDataDumps(ModuleStoreTestCase): tools.set_due_date_extension(self.course, self.week1, self.user2, extended) report = tools.dump_module_extensions(self.course, self.week1) - self.assertEqual( - report['title'], u'Users with due date extensions for ' + + assert ( + report['title'] == 'Users with due date extensions for ' + self.week1.display_name) - self.assertEqual( - report['header'], ["Username", "Full Name", "Extended Due Date"]) - self.assertEqual(report['data'], [ + assert ( + report['header'] == ["Username", "Full Name", "Extended Due Date"]) + assert (report['data'] == [ {"Username": self.user1.username, "Full Name": self.user1.profile.name, "Extended Due Date": "2013-12-25 00:00"}, @@ -327,12 +324,12 @@ class TestDataDumps(ModuleStoreTestCase): tools.set_due_date_extension(self.course, self.week2, self.user1, extended) report = tools.dump_student_extensions(self.course, self.user1) - self.assertEqual( - report['title'], u'Due date extensions for %s (%s)' % + assert ( + report['title'] == 'Due date extensions for %s (%s)' % (self.user1.profile.name, self.user1.username)) - self.assertEqual( - report['header'], ["Unit", "Extended Due Date"]) - self.assertEqual(report['data'], [ + assert ( + report['header'] == ["Unit", "Extended Due Date"]) + assert (report['data'] == [ {"Unit": self.week1.display_name, "Extended Due Date": "2013-12-25 00:00"}, {"Unit": self.week2.display_name, diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 6786b53380..d4617fae8c 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -18,18 +18,13 @@ import time import unicodecsv from django.conf import settings from django.contrib.auth.models import User -from django.core.exceptions import ( - MultipleObjectsReturned, - ObjectDoesNotExist, - PermissionDenied, - ValidationError -) +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, PermissionDenied, ValidationError from django.core.mail.message import EmailMessage -from django.urls import reverse from django.core.validators import validate_email from django.db import IntegrityError, transaction from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound from django.shortcuts import redirect +from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.html import strip_tags from django.utils.translation import ugettext as _ @@ -44,34 +39,37 @@ from rest_framework import permissions, status from rest_framework.response import Response from rest_framework.views import APIView from six import text_type +from submissions import api as sub_api # installed from the edx-submissions repository import instructor_analytics.basic import instructor_analytics.csvs import instructor_analytics.distributions -import lms.djangoapps.instructor.enrollment as enrollment -import lms.djangoapps.instructor_task.api from bulk_email.models import BulkEmailFlag, CourseEmail -from lms.djangoapps.certificates import api as certs_api -from lms.djangoapps.certificates.models import ( - CertificateInvalidation, CertificateStatuses, CertificateWhitelist, GeneratedCertificate, -) from courseware.access import has_access from courseware.courses import get_course_by_id, get_course_with_access from courseware.models import StudentModule from django_comment_client.utils import ( - has_forum_access, get_course_discussion_settings, + get_group_id_for_user, get_group_name, - get_group_id_for_user + has_forum_access ) from django_comment_common.models import ( - Role, FORUM_ROLE_ADMINISTRATOR, - FORUM_ROLE_MODERATOR, - FORUM_ROLE_GROUP_MODERATOR, FORUM_ROLE_COMMUNITY_TA, + FORUM_ROLE_GROUP_MODERATOR, + FORUM_ROLE_MODERATOR, + Role ) from edxmako.shortcuts import render_to_string +from lms.djangoapps.certificates import api as certs_api +from lms.djangoapps.certificates.models import ( + CertificateInvalidation, + CertificateStatuses, + CertificateWhitelist, + GeneratedCertificate +) +from lms.djangoapps.instructor import enrollment from lms.djangoapps.instructor.access import ROLES, allow_access, list_with_level, revoke_access, update_forum_role from lms.djangoapps.instructor.enrollment import ( enroll_email, @@ -83,7 +81,7 @@ from lms.djangoapps.instructor.enrollment import ( ) from lms.djangoapps.instructor.views import INVOICE_KEY from lms.djangoapps.instructor.views.instructor_task_helpers import extract_email_features, extract_task_features -from lms.djangoapps.instructor_task.api import submit_override_score +from lms.djangoapps.instructor_task import api as task_api from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError, QueueConnectionError from lms.djangoapps.instructor_task.models import ReportStore from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -119,11 +117,10 @@ from student.models import ( UserProfile, anonymous_id_for_user, get_user_by_username_or_email, - unique_id_for_user, - is_email_retired + is_email_retired, + unique_id_for_user ) from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole -from submissions import api as sub_api # installed from the edx-submissions repository from util.file import ( FileValidationException, UniversalNewlineIterator, @@ -210,7 +207,7 @@ def require_post_params(*args, **kwargs): error_response_data['parameters'].append(param) error_response_data['info'][param] = extra - if len(error_response_data['parameters']) > 0: + if error_response_data['parameters']: return JsonResponse(error_response_data, status=400) else: return func(*args, **kwargs) @@ -319,11 +316,12 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man -If the email address already exists, but the username is different, match on the email address only and continue to enroll the user in the course using the email address - as the matching criteria. Note the change of username as a warning message (but not a failure). Send a standard enrollment email - which is the same as the existing manual enrollment + as the matching criteria. Note the change of username as a warning message (but not a failure). + Send a standard enrollment email which is the same as the existing manual enrollment - -If the username already exists (but not the email), assume it is a different user and fail to create the new account. - The failure will be messaged in a response in the browser. + -If the username already exists (but not the email), assume it is a different user and fail + to create the new account. + The failure will be messaged in a response in the browser. """ if not configuration_helpers.get_value( @@ -373,11 +371,13 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man # verify that we have exactly four columns in every row but allow for blank lines if len(student) != 4: - if len(student) > 0: + if student: + error = _(u'Data in row #{row_num} must have exactly four columns: ' + 'email, username, full name, and country').format(row_num=row_num) general_errors.append({ 'username': '', 'email': '', - 'response': _(u'Data in row #{row_num} must have exactly four columns: email, username, full name, and country').format(row_num=row_num) + 'response': error }) continue @@ -392,7 +392,10 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man validate_email(email) # Raises ValidationError if invalid except ValidationError: row_errors.append({ - 'username': username, 'email': email, 'response': _('Invalid email {email_address}.').format(email_address=email)}) + 'username': username, + 'email': email, + 'response': _(u'Invalid email {email_address}.').format(email_address=email) + }) else: if User.objects.filter(email=email).exists(): # Email address already exists. assume it is the correct user @@ -429,7 +432,11 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man reason='Enrolling via csv upload', state_transition=UNENROLLED_TO_ENROLLED, ) - enroll_email(course_id=course_id, student_email=email, auto_enroll=True, email_students=True, email_params=email_params) + enroll_email(course_id=course_id, + student_email=email, + auto_enroll=True, + email_students=True, + email_params=email_params) elif is_email_retired(email): # We are either attempting to enroll a retired user or create a new user with an email which is # already associated with a retired account. Simply block these attempts. @@ -573,7 +580,9 @@ def create_and_enroll_user(email, username, name, country, password, course_id, ) except IntegrityError: errors.append({ - 'username': username, 'email': email, 'response': _(u'Username {user} already exists.').format(user=username) + 'username': username, + 'email': email, + 'response': _(u'Username {user} already exists.').format(user=username) }) except Exception as ex: # pylint: disable=broad-except log.exception(type(ex).__name__) @@ -1027,7 +1036,7 @@ def get_problem_responses(request, course_id): except InvalidKeyError: return JsonResponseBadRequest(_("Could not find problem with this location.")) - task = lms.djangoapps.instructor_task.api.submit_calculate_problem_responses_csv( + task = task_api.submit_calculate_problem_responses_csv( request, course_key, problem_location ) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) @@ -1317,7 +1326,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red return JsonResponse(response_payload) else: - lms.djangoapps.instructor_task.api.submit_calculate_students_features_csv( + task_api.submit_calculate_students_features_csv( request, course_key, query_features @@ -1345,7 +1354,7 @@ def get_students_who_may_enroll(request, course_id): course_key = CourseKey.from_string(course_id) query_features = ['email'] report_type = _('enrollment') - lms.djangoapps.instructor_task.api.submit_calculate_may_enroll_csv(request, course_key, query_features) + task_api.submit_calculate_may_enroll_csv(request, course_key, query_features) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -1392,7 +1401,7 @@ def add_users_to_cohorts(request, course_id): validator=_cohorts_csv_validator ) # The task will assume the default file storage. - lms.djangoapps.instructor_task.api.submit_cohort_students(request, course_key, filename) + task_api.submit_cohort_students(request, course_key, filename) except (FileValidationException, PermissionDenied) as err: return JsonResponse({"error": unicode(err)}, status=400) @@ -1436,7 +1445,7 @@ class CohortCSV(DeveloperErrorViewMixin, APIView): max_file_size=2000000, # limit to 2 MB validator=_cohorts_csv_validator ) - lms.djangoapps.instructor_task.api.submit_cohort_students(request, course_key, file_name) + task_api.submit_cohort_students(request, course_key, file_name) except (FileValidationException, ValueError) as e: raise self.api_error(status.HTTP_400_BAD_REQUEST, str(e), 'failed-validation') return Response(status=status.HTTP_204_NO_CONTENT) @@ -1484,7 +1493,7 @@ def get_enrollment_report(request, course_id): """ course_key = CourseKey.from_string(course_id) report_type = _('detailed enrollment') - lms.djangoapps.instructor_task.api.submit_detailed_enrollment_features_csv(request, course_key) + task_api.submit_detailed_enrollment_features_csv(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -1503,7 +1512,7 @@ def get_exec_summary_report(request, course_id): """ course_key = CourseKey.from_string(course_id) report_type = _('executive summary') - lms.djangoapps.instructor_task.api.submit_executive_summary_report(request, course_key) + task_api.submit_executive_summary_report(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -1521,7 +1530,7 @@ def get_course_survey_results(request, course_id): """ course_key = CourseKey.from_string(course_id) report_type = _('survey') - lms.djangoapps.instructor_task.api.submit_course_survey_report(request, course_key) + task_api.submit_course_survey_report(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -1539,7 +1548,7 @@ def get_proctored_exam_results(request, course_id): """ course_key = CourseKey.from_string(course_id) report_type = _('proctored exam results') - lms.djangoapps.instructor_task.api.submit_proctored_exam_results_report(request, course_key) + task_api.submit_proctored_exam_results_report(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -1916,7 +1925,8 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument courseenrollment__course_id=course_id, ).order_by('id') header = ['User ID', 'Anonymized User ID', 'Course Specific Anonymized User ID'] - rows = [[s.id, unique_id_for_user(s, save=False), anonymous_id_for_user(s, course_id, save=False)] for s in students] + rows = [[s.id, unique_id_for_user(s, save=False), anonymous_id_for_user(s, course_id, save=False)] + for s in students] return csv_response(text_type(course_id).replace('/', '-') + '-anon-ids.csv', header, rows) @@ -2085,7 +2095,7 @@ def reset_student_attempts(request, course_id): return HttpResponse(error_msg, status=500) response_payload['student'] = student_identifier elif all_students: - lms.djangoapps.instructor_task.api.submit_reset_problem_attempts_for_all_students(request, module_state_key) + task_api.submit_reset_problem_attempts_for_all_students(request, module_state_key) response_payload['task'] = TASK_SUBMISSION_OK response_payload['student'] = 'All Students' else: @@ -2151,13 +2161,13 @@ def reset_student_attempts_for_entrance_exam(request, course_id): try: entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id) if delete_module: - lms.djangoapps.instructor_task.api.submit_delete_entrance_exam_state_for_student( + task_api.submit_delete_entrance_exam_state_for_student( request, entrance_exam_key, student ) else: - lms.djangoapps.instructor_task.api.submit_reset_problem_attempts_in_entrance_exam( + task_api.submit_reset_problem_attempts_in_entrance_exam( request, entrance_exam_key, student @@ -2220,7 +2230,7 @@ def rescore_problem(request, course_id): if student: response_payload['student'] = student_identifier try: - lms.djangoapps.instructor_task.api.submit_rescore_problem_for_student( + task_api.submit_rescore_problem_for_student( request, module_state_key, student, @@ -2231,7 +2241,7 @@ def rescore_problem(request, course_id): elif all_students: try: - lms.djangoapps.instructor_task.api.submit_rescore_problem_for_all_students( + task_api.submit_rescore_problem_for_all_students( request, module_state_key, only_if_higher, @@ -2286,7 +2296,7 @@ def override_problem_score(request, course_id): 'student': student_identifier } try: - submit_override_score( + task_api.submit_override_score( request, usage_key, student, @@ -2353,7 +2363,7 @@ def rescore_entrance_exam(request, course_id): else: response_payload['student'] = _("All Students") - lms.djangoapps.instructor_task.api.submit_rescore_entrance_exam_for_student( + task_api.submit_rescore_entrance_exam_for_student( request, entrance_exam_key, student, only_if_higher, ) response_payload['task'] = TASK_SUBMISSION_OK @@ -2371,7 +2381,7 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a course_id = CourseKey.from_string(course_id) task_type = 'bulk_course_email' # Specifying for the history of a single task type - tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history( + tasks = task_api.get_instructor_task_history( course_id, task_type=task_type ) @@ -2393,7 +2403,7 @@ def list_email_content(request, course_id): # pylint: disable=unused-argument course_id = CourseKey.from_string(course_id) task_type = 'bulk_course_email' # First get tasks list of bulk emails sent - emails = lms.djangoapps.instructor_task.api.get_instructor_task_history(course_id, task_type=task_type) + emails = task_api.get_instructor_task_history(course_id, task_type=task_type) response_payload = { 'emails': map(extract_email_features, emails), @@ -2433,13 +2443,13 @@ def list_instructor_tasks(request, course_id): return HttpResponseBadRequest() if student: # Specifying for a single student's history on this problem - tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history(course_id, module_state_key, student) + tasks = task_api.get_instructor_task_history(course_id, module_state_key, student) else: # Specifying for single problem's history - tasks = lms.djangoapps.instructor_task.api.get_instructor_task_history(course_id, module_state_key) + tasks = task_api.get_instructor_task_history(course_id, module_state_key) else: # If no problem or student, just get currently running tasks - tasks = lms.djangoapps.instructor_task.api.get_running_instructor_tasks(course_id) + tasks = task_api.get_running_instructor_tasks(course_id) response_payload = { 'tasks': map(extract_task_features, tasks), @@ -2471,14 +2481,14 @@ def list_entrance_exam_instructor_tasks(request, course_id): return HttpResponseBadRequest(_("Course has no valid entrance exam section.")) if student: # Specifying for a single student's entrance exam history - tasks = lms.djangoapps.instructor_task.api.get_entrance_exam_instructor_task_history( + tasks = task_api.get_entrance_exam_instructor_task_history( course_id, entrance_exam_key, student ) else: # Specifying for all student's entrance exam history - tasks = lms.djangoapps.instructor_task.api.get_entrance_exam_instructor_task_history( + tasks = task_api.get_entrance_exam_instructor_task_history( course_id, entrance_exam_key ) @@ -2546,7 +2556,7 @@ def export_ora2_data(request, course_id): """ course_key = CourseKey.from_string(course_id) report_type = _('ORA data') - lms.djangoapps.instructor_task.api.submit_export_ora2_data(request, course_key) + task_api.submit_export_ora2_data(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -2564,7 +2574,7 @@ def calculate_grades_csv(request, course_id): """ report_type = _('grade') course_key = CourseKey.from_string(course_id) - lms.djangoapps.instructor_task.api.submit_calculate_grades_csv(request, course_key) + task_api.submit_calculate_grades_csv(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -2586,7 +2596,7 @@ def problem_grade_report(request, course_id): """ course_key = CourseKey.from_string(course_id) report_type = _('problem grade') - lms.djangoapps.instructor_task.api.submit_problem_grade_report(request, course_key) + task_api.submit_problem_grade_report(request, course_key) success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) return JsonResponse({"status": success_status}) @@ -2730,7 +2740,7 @@ def send_email(request, course_id): return HttpResponseBadRequest(repr(err)) # Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes) - lms.djangoapps.instructor_task.api.submit_bulk_course_email(request, course_id, email.id) + task_api.submit_bulk_course_email(request, course_id, email.id) response_payload = { 'course_id': text_type(course_id), @@ -2846,7 +2856,7 @@ def change_due_date(request, course_id): student = require_student_from_identifier(request.POST.get('student')) unit = find_unit(course, request.POST.get('url')) due_date = parse_datetime(request.POST.get('due_datetime')) - set_due_date_extension(course, unit, student, due_date) + set_due_date_extension(course, unit, student, due_date, request.user) return JsonResponse(_( u'Successfully changed due date for student {0} for {1} ' @@ -2867,7 +2877,7 @@ def reset_due_date(request, course_id): course = get_course_by_id(CourseKey.from_string(course_id)) student = require_student_from_identifier(request.POST.get('student')) unit = find_unit(course, request.POST.get('url')) - set_due_date_extension(course, unit, student, None) + set_due_date_extension(course, unit, student, None, request.user) if not getattr(unit, "due", None): # It's possible the normal due date was deleted after an extension was granted: return JsonResponse( @@ -3020,7 +3030,7 @@ def start_certificate_generation(request, course_id): Start generating certificates for all students enrolled in given course. """ course_key = CourseKey.from_string(course_id) - task = lms.djangoapps.instructor_task.api.generate_certificates_for_students(request, course_key) + task = task_api.generate_certificates_for_students(request, course_key) message = _('Certificate generation task for all students of this course has been started. ' 'You can view the status of the generation task in the "Pending Tasks" section.') response_payload = { @@ -3064,7 +3074,7 @@ def start_certificate_regeneration(request, course_id): status=400 ) - lms.djangoapps.instructor_task.api.regenerate_certificates(request, course_key, certificates_statuses) + task_api.regenerate_certificates(request, course_key, certificates_statuses) response_payload = { 'message': _('Certificate regeneration task has been started. ' 'You can view the status of the generation task in the "Pending Tasks" section.'), @@ -3121,7 +3131,7 @@ def add_certificate_exception(course_key, student, certificate_exception): :param certificate_exception: A dict object containing certificate exception info. :return: CertificateWhitelist item in dict format containing certificate exception info. """ - if len(CertificateWhitelist.get_certificate_white_list(course_key, student)) > 0: + if CertificateWhitelist.get_certificate_white_list(course_key, student): raise ValueError( _(u"Student (username/email={user}) already in certificate exception list.").format(user=student.username) ) @@ -3283,7 +3293,7 @@ def generate_certificate_exceptions(request, course_id, generate_for=None): status=400 ) - lms.djangoapps.instructor_task.api.generate_certificates_for_students(request, course_key, student_set=students) + task_api.generate_certificates_for_students(request, course_key, student_set=students) response_payload = { 'success': True, 'message': _('Certificate generation started for white listed students.'), @@ -3343,7 +3353,7 @@ def generate_bulk_certificate_exceptions(request, course_id): # verify that we have exactly two column in every row either email or username and notes but allow for # blank lines if len(student) != 2: - if len(student) > 0: + if student: build_row_errors('data_format_error', student[user_index], row_num) log.info(u'invalid data/format in csv row# %s', row_num) continue @@ -3355,7 +3365,7 @@ def generate_bulk_certificate_exceptions(request, course_id): build_row_errors('user_not_exist', user, row_num) log.info(u'student %s does not exist', user) else: - if len(CertificateWhitelist.get_certificate_white_list(course_key, user)) > 0: + if CertificateWhitelist.get_certificate_white_list(course_key, user): build_row_errors('user_already_white_listed', user, row_num) log.warning(u'student %s already exist.', user.username) @@ -3433,10 +3443,10 @@ def invalidate_certificate(request, generated_certificate, certificate_invalidat :param certificate_invalidation_data: dict object containing data for CertificateInvalidation. :return: dict object containing updated certificate invalidation data. """ - if len(CertificateInvalidation.get_certificate_invalidations( + if CertificateInvalidation.get_certificate_invalidations( generated_certificate.course_id, generated_certificate.user, - )) > 0: + ): raise ValueError( _(u"Certificate of {user} has already been invalidated. Please check your spelling and retry.").format( user=generated_certificate.user.username, @@ -3493,7 +3503,7 @@ def re_validate_certificate(request, course_key, generated_certificate): # We need to generate certificate only for a single student here student = certificate_invalidation.generated_certificate.user - lms.djangoapps.instructor_task.api.generate_certificates_for_students( + task_api.generate_certificates_for_students( request, course_key, student_set="specific_student", specific_student_id=student.id ) @@ -3542,4 +3552,4 @@ def _create_error_response(request, msg): Creates the appropriate error response for the current request, in JSON form. """ - return JsonResponse({"error": _(msg)}, 400) + return JsonResponse({"error": msg}, 400) diff --git a/lms/djangoapps/instructor/views/gradebook_api.py b/lms/djangoapps/instructor/views/gradebook_api.py index 922d351d08..f4603e59d4 100644 --- a/lms/djangoapps/instructor/views/gradebook_api.py +++ b/lms/djangoapps/instructor/views/gradebook_api.py @@ -5,8 +5,8 @@ which is currently use by ccx and instructor apps. import math from django.contrib.auth.models import User -from django.urls import reverse from django.db import transaction +from django.urls import reverse from django.views.decorators.cache import cache_control from opaque_keys.edx.keys import CourseKey diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index c60fc0167f..8e9de402c2 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -10,8 +10,8 @@ from urlparse import urljoin import pytz from django.conf import settings from django.contrib.auth.decorators import login_required -from django.urls import reverse from django.http import Http404, HttpResponseServerError +from django.urls import reverse from django.utils.html import escape from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop @@ -26,6 +26,13 @@ from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from bulk_email.models import BulkEmailFlag +from class_dashboard.dashboard_data import get_array_section_has_problem, get_section_display_name +from course_modes.models import CourseMode, CourseModesArchive +from courseware.access import has_access +from courseware.courses import get_course_by_id, get_studio_url +from django_comment_client.utils import available_division_schemes, has_forum_access +from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, CourseDiscussionSettings +from edxmako.shortcuts import render_to_response from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, @@ -35,15 +42,8 @@ from lms.djangoapps.certificates.models import ( CertificateWhitelist, GeneratedCertificate ) -from class_dashboard.dashboard_data import get_array_section_has_problem, get_section_display_name -from course_modes.models import CourseMode, CourseModesArchive -from courseware.access import has_access -from courseware.courses import get_course_by_id, get_studio_url -from django_comment_client.utils import available_division_schemes, has_forum_access -from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, CourseDiscussionSettings -from edxmako.shortcuts import render_to_response from lms.djangoapps.courseware.module_render import get_module_by_usage_id -from lms.djangoapps.grades.config.waffle import waffle_flags, WRITABLE_GRADEBOOK +from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags from openedx.core.djangoapps.course_groups.cohorts import DEFAULT_COHORT_NAME, get_course_cohorts, is_course_cohorted from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse @@ -52,7 +52,7 @@ from openedx.core.lib.url_utils import quote_slashes from openedx.core.lib.xblock_utils import wrap_xblock from shoppingcart.models import Coupon, CourseRegCodeItem, PaidCourseRegistration from student.models import CourseEnrollment -from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole, CourseStaffRole, CourseInstructorRole +from student.roles import CourseFinanceAdminRole, CourseInstructorRole, CourseSalesAdminRole, CourseStaffRole from util.json_request import JsonResponse from xmodule.html_module import HtmlDescriptor from xmodule.modulestore.django import modulestore @@ -159,7 +159,7 @@ def instructor_dashboard_2(request, course_id): unicode(course_key), len(paid_modes) ) - if settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']: + if access['instructor']: sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization diff --git a/lms/djangoapps/instructor/views/tools.py b/lms/djangoapps/instructor/views/tools.py index e9109ff4a6..80e52e9b1a 100644 --- a/lms/djangoapps/instructor/views/tools.py +++ b/lms/djangoapps/instructor/views/tools.py @@ -2,26 +2,18 @@ Tools for the instructor dashboard """ import json +import operator import dateutil from django.contrib.auth.models import User from django.http import HttpResponseBadRequest -from pytz import UTC from django.utils.translation import ugettext as _ from opaque_keys.edx.keys import UsageKey -from six import text_type, string_types +from pytz import UTC +from six import string_types, text_type -from courseware.models import StudentFieldOverride -from lms.djangoapps.courseware.field_overrides import disable_overrides -from lms.djangoapps.courseware.student_field_overrides import ( - clear_override_for_user, - get_override_for_user, - override_field_for_user, -) +from edx_when import api from student.models import get_user_by_username_or_email -from xmodule.fields import Date - -DATE_FIELD = Date() class DashboardError(Exception): @@ -158,29 +150,20 @@ def title_or_url(node): return title -def set_due_date_extension(course, unit, student, due_date): +def set_due_date_extension(course, unit, student, due_date, actor=None): """ Sets a due date extension. Raises DashboardError if the unit or extended due date is invalid. """ if due_date: - # Check that the new due date is valid: - with disable_overrides(): - original_due_date = getattr(unit, 'due', None) - - if not original_due_date: + try: + api.set_date_for_block(course.id, unit.location, 'due', due_date, user=student, reason=None, actor=actor) + except api.MissingDateError: raise DashboardError(_(u"Unit {0} has no due date to extend.").format(unit.location)) - if due_date < original_due_date: + except api.InvalidDateError: raise DashboardError(_("An extended due date must be later than the original due date.")) - - override_field_for_user(student, unit, 'due', due_date) - else: - # We are deleting a due date extension. Check that it exists: - if not get_override_for_user(student, unit, 'due'): - raise DashboardError(_("No due date extension is set for that student and unit.")) - - clear_override_for_user(student, unit, 'due') + api.set_date_for_block(course.id, unit.location, 'due', None, user=student, reason=None, actor=actor) def dump_module_extensions(course, unit): @@ -188,20 +171,12 @@ def dump_module_extensions(course, unit): Dumps data about students with due date extensions for a particular module, specified by 'url', in a particular course. """ - data = [] header = [_("Username"), _("Full Name"), _("Extended Due Date")] - query = StudentFieldOverride.objects.filter( - course_id=course.id, - location=unit.location, - field='due') - for override in query: - due = DATE_FIELD.from_json(json.loads(override.value)) - due = due.strftime(u"%Y-%m-%d %H:%M") - fullname = override.student.profile.name - data.append(dict(zip( - header, - (override.student.username, fullname, due)))) - data.sort(key=lambda x: x[header[0]]) + data = [] + for username, fullname, due_date in api.get_overrides_for_block(course.id, unit.location): + due_date = due_date.strftime(u'%Y-%m-%d %H:%M') + data.append(dict(zip(header, (username, fullname, due_date)))) + data.sort(key=operator.itemgetter(_("Username"))) return { "header": header, "title": _(u"Users with due date extensions for {0}").format( @@ -219,18 +194,16 @@ def dump_student_extensions(course, student): header = [_("Unit"), _("Extended Due Date")] units = get_units_with_due_date(course) units = {u.location: u for u in units} - query = StudentFieldOverride.objects.filter( - course_id=course.id, - student=student, - field='due') + query = api.get_overrides_for_user(course.id, student) for override in query: - location = override.location.replace(course_key=course.id) + location = override['location'].replace(course_key=course.id) if location not in units: continue - due = DATE_FIELD.from_json(json.loads(override.value)) + due = override['actual_date'] due = due.strftime(u"%Y-%m-%d %H:%M") title = title_or_url(units[location]) data.append(dict(zip(header, (title, due)))) + data.sort(key=operator.itemgetter(_("Unit"))) return { "header": header, "title": _(u"Due date extensions for {0} {1} ({2})").format( diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index ba9730a022..e30b359900 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -213,7 +213,7 @@ class TestCourseHomePage(CourseHomePageTestCase): # Fetch the view and verify the query counts # TODO: decrease query count as part of REVO-28 - with self.assertNumQueries(89, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(91, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_home_url(self.course) self.client.get(url) diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index 41f5f8932f..809640d55e 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -130,7 +130,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase): # Fetch the view and verify that the query counts haven't changed # TODO: decrease query count as part of REVO-28 - with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(52, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_updates_url(self.course) self.client.get(url) diff --git a/openedx/tests/settings.py b/openedx/tests/settings.py index 7403435b4f..1fe06b2d7d 100644 --- a/openedx/tests/settings.py +++ b/openedx/tests/settings.py @@ -88,6 +88,7 @@ INSTALLED_APPS = ( 'milestones', 'celery_utils', 'waffle', + 'edx_when', # Django 1.11 demands to have imported models supported by installed apps. 'completion', diff --git a/requirements/edx/base.in b/requirements/edx/base.in index 139674ea5f..aa994d0ac9 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -85,6 +85,7 @@ edx-rest-api-client edx-search edx-submissions edx-user-state-client +edx-when edxval enum34==1.1.6 # Backport of Enum from Python 3.4+ event-tracking diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 020b4af75b..25c478c1fa 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -125,6 +125,7 @@ edx-rest-api-client==1.9.2 edx-search==1.2.2 edx-submissions==2.1.1 edx-user-state-client==1.0.4 +edx-when==0.1.1 edxval==1.1.25 elasticsearch==1.9.0 # via edx-search enum34==1.1.6 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 83b3cafe6d..1155d0594a 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -148,6 +148,7 @@ edx-search==1.2.2 edx-sphinx-theme==1.4.0 edx-submissions==2.1.1 edx-user-state-client==1.0.4 +edx-when==0.1.1 edxval==1.1.25 elasticsearch==1.9.0 entrypoints==0.3 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 1de98e45e4..d48d833889 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -143,6 +143,7 @@ edx-rest-api-client==1.9.2 edx-search==1.2.2 edx-submissions==2.1.1 edx-user-state-client==1.0.4 +edx-when==0.1.1 edxval==1.1.25 elasticsearch==1.9.0 entrypoints==0.3 # via flake8 From 0abba0e6f86abce63382871a810b7246c136a869 Mon Sep 17 00:00:00 2001 From: Constanza Abarca Date: Wed, 24 Apr 2019 13:44:27 -0400 Subject: [PATCH 007/106] INCR-162 --- common/djangoapps/course_modes/migrations/0001_initial.py | 1 + .../0002_coursemode_expiration_datetime_is_explicit.py | 1 + .../course_modes/migrations/0003_auto_20151113_1443.py | 1 + .../course_modes/migrations/0004_auto_20151113_1457.py | 1 + .../course_modes/migrations/0005_auto_20151217_0958.py | 1 + .../course_modes/migrations/0006_auto_20160208_1407.py | 1 + .../course_modes/migrations/0007_coursemode_bulk_sku.py | 1 + .../migrations/0008_course_key_field_to_foreign_key.py | 1 + .../migrations/0009_suggested_prices_to_charfield.py | 1 + .../migrations/0010_archived_suggested_prices_to_charfield.py | 1 + .../migrations/0011_change_regex_for_comma_separated_ints.py | 1 + 11 files changed, 11 insertions(+) diff --git a/common/djangoapps/course_modes/migrations/0001_initial.py b/common/djangoapps/course_modes/migrations/0001_initial.py index a440411f6c..ef476b5d50 100644 --- a/common/djangoapps/course_modes/migrations/0001_initial.py +++ b/common/djangoapps/course_modes/migrations/0001_initial.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField diff --git a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py index ea6a601342..84402e3d07 100644 --- a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py +++ b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py index 04d20662aa..16f918459c 100644 --- a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py +++ b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py index ec11036d4d..27973eedf9 100644 --- a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py +++ b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from datetime import timedelta import django.db.models.deletion diff --git a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py index 1e6e3f7e82..ef715a21ba 100644 --- a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py +++ b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py index 338a886745..0277c63494 100644 --- a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py +++ b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py index bdbbac49f2..0262ad5756 100644 --- a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py +++ b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py index 75cb8ed0d7..4cfe33fe14 100644 --- a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py +++ b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField diff --git a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py index 60ed720cfd..bca1792f18 100644 --- a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py +++ b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models import re import django.core.validators diff --git a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py index b307d06472..6659a21c9b 100644 --- a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py +++ b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from __future__ import absolute_import from django.db import migrations, models import re import django.core.validators diff --git a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py index 73f1046eee..6384887859 100644 --- a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py +++ b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py @@ -2,6 +2,7 @@ # Generated by Django 1.11.8 on 2018-01-30 17:38 from __future__ import unicode_literals +from __future__ import absolute_import import django.core.validators from django.db import migrations, models import re From 926cb5bb89493329ba783cecdd22b53c35642966 Mon Sep 17 00:00:00 2001 From: Ana Maria Rodriguez Date: Wed, 24 Apr 2019 13:38:12 -0500 Subject: [PATCH 008/106] INCR-165 Run python-modernize -w openedx/core/djangoapps/plugins --- openedx/core/djangoapps/plugins/apps.py | 1 + openedx/core/djangoapps/plugins/plugin_apps.py | 1 + openedx/core/djangoapps/plugins/plugin_settings.py | 1 + openedx/core/djangoapps/plugins/plugin_signals.py | 1 + openedx/core/djangoapps/plugins/plugin_urls.py | 1 + openedx/core/djangoapps/plugins/registry.py | 4 +++- openedx/core/djangoapps/plugins/utils.py | 1 + 7 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/plugins/apps.py b/openedx/core/djangoapps/plugins/apps.py index 7f67af0deb..41d42b5154 100644 --- a/openedx/core/djangoapps/plugins/apps.py +++ b/openedx/core/djangoapps/plugins/apps.py @@ -4,6 +4,7 @@ Plugins Application Configuration Signal handlers are connected here. """ +from __future__ import absolute_import from django.apps import AppConfig from django.conf import settings from . import constants, plugin_signals diff --git a/openedx/core/djangoapps/plugins/plugin_apps.py b/openedx/core/djangoapps/plugins/plugin_apps.py index 8ded7ebee1..1b64ffb632 100644 --- a/openedx/core/djangoapps/plugins/plugin_apps.py +++ b/openedx/core/djangoapps/plugins/plugin_apps.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from logging import getLogger from . import constants, registry diff --git a/openedx/core/djangoapps/plugins/plugin_settings.py b/openedx/core/djangoapps/plugins/plugin_settings.py index e33ca97e02..5cea7ccc90 100644 --- a/openedx/core/djangoapps/plugins/plugin_settings.py +++ b/openedx/core/djangoapps/plugins/plugin_settings.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from logging import getLogger from . import constants, registry, utils diff --git a/openedx/core/djangoapps/plugins/plugin_signals.py b/openedx/core/djangoapps/plugins/plugin_signals.py index 088ca077ff..1540f71756 100644 --- a/openedx/core/djangoapps/plugins/plugin_signals.py +++ b/openedx/core/djangoapps/plugins/plugin_signals.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from logging import getLogger from . import constants, registry, utils diff --git a/openedx/core/djangoapps/plugins/plugin_urls.py b/openedx/core/djangoapps/plugins/plugin_urls.py index f15a47d0cb..da31a91df7 100644 --- a/openedx/core/djangoapps/plugins/plugin_urls.py +++ b/openedx/core/djangoapps/plugins/plugin_urls.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from logging import getLogger from django.conf.urls import include, url from . import constants, registry, utils diff --git a/openedx/core/djangoapps/plugins/registry.py b/openedx/core/djangoapps/plugins/registry.py index 5b21d5fcae..105b535053 100644 --- a/openedx/core/djangoapps/plugins/registry.py +++ b/openedx/core/djangoapps/plugins/registry.py @@ -1,4 +1,6 @@ +from __future__ import absolute_import from openedx.core.lib.plugins import PluginManager +import six class DjangoAppRegistry(PluginManager): @@ -9,4 +11,4 @@ class DjangoAppRegistry(PluginManager): def get_app_configs(project_type): - return DjangoAppRegistry.get_available_plugins(project_type).itervalues() + return six.itervalues(DjangoAppRegistry.get_available_plugins(project_type)) diff --git a/openedx/core/djangoapps/plugins/utils.py b/openedx/core/djangoapps/plugins/utils.py index 131b24d376..19fcc6f5f1 100644 --- a/openedx/core/djangoapps/plugins/utils.py +++ b/openedx/core/djangoapps/plugins/utils.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from importlib import import_module as system_import_module from django.utils.module_loading import import_string From d2f7acf29c7669c676699bf02da458eb5043a9c0 Mon Sep 17 00:00:00 2001 From: Ana Maria Rodriguez Date: Wed, 24 Apr 2019 13:52:53 -0500 Subject: [PATCH 009/106] INCR-193 Run python-modernize on openedx/features/course_experience/views and openedx/features/course_experience/tests --- .../course_experience/tests/views/helpers.py | 2 ++ .../tests/views/test_course_dates.py | 9 +++-- .../tests/views/test_course_home.py | 35 ++++++++++--------- .../tests/views/test_course_outline.py | 20 ++++++----- .../tests/views/test_course_sock.py | 2 ++ .../tests/views/test_course_updates.py | 8 +++-- .../tests/views/test_welcome_message.py | 9 +++-- .../course_experience/views/course_dates.py | 2 ++ .../course_experience/views/course_home.py | 19 ++++++---- .../views/course_home_messages.py | 13 ++++--- .../course_experience/views/course_outline.py | 7 ++-- .../course_experience/views/course_reviews.py | 11 +++--- .../course_experience/views/course_sock.py | 2 ++ .../course_experience/views/course_updates.py | 9 +++-- .../course_experience/views/latest_update.py | 2 ++ .../views/welcome_message.py | 12 ++++--- 16 files changed, 104 insertions(+), 58 deletions(-) diff --git a/openedx/features/course_experience/tests/views/helpers.py b/openedx/features/course_experience/tests/views/helpers.py index 435d562862..7faa9a9a70 100644 --- a/openedx/features/course_experience/tests/views/helpers.py +++ b/openedx/features/course_experience/tests/views/helpers.py @@ -1,6 +1,8 @@ """ Test helpers for the course experience. """ +from __future__ import absolute_import + from datetime import timedelta from django.core.exceptions import ObjectDoesNotExist diff --git a/openedx/features/course_experience/tests/views/test_course_dates.py b/openedx/features/course_experience/tests/views/test_course_dates.py index 1df810ec55..36f0f9e577 100644 --- a/openedx/features/course_experience/tests/views/test_course_dates.py +++ b/openedx/features/course_experience/tests/views/test_course_dates.py @@ -1,7 +1,11 @@ """ Tests for course dates fragment. """ -from datetime import timedelta, datetime +from __future__ import absolute_import + +from datetime import datetime, timedelta + +import six from django.urls import reverse from student.tests.factories import UserFactory @@ -9,7 +13,6 @@ from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory - TEST_PASSWORD = 'test' @@ -32,7 +35,7 @@ class TestCourseDatesFragmentView(ModuleStoreTestCase): self.dates_fragment_url = reverse( 'openedx.course_experience.mobile_dates_fragment_view', kwargs={ - 'course_id': unicode(self.course.id) + 'course_id': six.text_type(self.course.id) } ) diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index ba9730a022..1afebcc2c2 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -2,40 +2,43 @@ """ Tests for the course home page. """ +from __future__ import absolute_import + from datetime import datetime, timedelta import ddt import mock +import six from django.conf import settings -from django.urls import reverse from django.http import QueryDict +from django.urls import reverse from django.utils.http import urlquote_plus from django.utils.timezone import now from pytz import UTC from waffle.models import Flag from waffle.testutils import override_flag -from django_comment_common.models import ( - FORUM_ROLE_ADMINISTRATOR, - FORUM_ROLE_MODERATOR, - FORUM_ROLE_GROUP_MODERATOR, - FORUM_ROLE_COMMUNITY_TA -) -from django_comment_client.tests.factories import RoleFactory from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory from courseware.tests.helpers import get_expiration_banner_text +from django_comment_client.tests.factories import RoleFactory +from django_comment_common.models import ( + FORUM_ROLE_ADMINISTRATOR, + FORUM_ROLE_COMMUNITY_TA, + FORUM_ROLE_GROUP_MODERATOR, + FORUM_ROLE_MODERATOR +) from experiments.models import ExperimentData from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal from lms.djangoapps.courseware.tests.factories import ( - InstructorFactory, - StaffFactory, BetaTesterFactory, - OrgStaffFactory, - OrgInstructorFactory, GlobalStaffFactory, + InstructorFactory, + OrgInstructorFactory, + OrgStaffFactory, + StaffFactory ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.dark_lang.models import DarkLangConfig @@ -44,15 +47,15 @@ from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, overri from openedx.features.course_duration_limits.config import EXPERIMENT_DATA_HOLDBACK_KEY, EXPERIMENT_ID from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience import ( + COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, SHOW_REVIEWS_TOOL_FLAG, SHOW_UPGRADE_MSG_ON_COURSE_HOME, - UNIFIED_COURSE_TAB_FLAG, - COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, + UNIFIED_COURSE_TAB_FLAG ) from student.models import CourseEnrollment from student.tests.factories import UserFactory from util.date_utils import strftime_localized -from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC_OUTLINE, COURSE_VISIBILITY_PUBLIC +from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls @@ -89,7 +92,7 @@ def course_home_url(course): Arguments: course (CourseDescriptor): The course being tested. """ - return course_home_url_from_string(unicode(course.id)) + return course_home_url_from_string(six.text_type(course.id)) def course_home_url_from_string(course_key_string): diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 9f89403a30..6b7266c5c3 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -1,17 +1,23 @@ """ Tests for the Course Outline view and supporting views. """ +from __future__ import absolute_import + import datetime import json import re +import six from completion import waffle from completion.models import BlockCompletion from completion.test_utils import CompletionWaffleTestMixin from django.contrib.sites.models import Site -from django.urls import reverse from django.test import override_settings +from django.urls import reverse +from milestones.tests.utils import MilestonesTestCaseMixin from mock import Mock, patch +from opaque_keys.edx.keys import CourseKey, UsageKey +from pyquery import PyQuery as pq from six import text_type from waffle.models import Switch from waffle.testutils import override_switch @@ -19,13 +25,11 @@ from waffle.testutils import override_switch from courseware.tests.factories import StaffFactory from gating import api as lms_gating_api from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer -from milestones.tests.utils import MilestonesTestCaseMixin -from opaque_keys.edx.keys import CourseKey, UsageKey from openedx.core.lib.gating import api as gating_api from openedx.features.course_experience.views.course_outline import ( - CourseOutlineFragmentView, DEFAULT_COMPLETION_TRACKING_START + DEFAULT_COMPLETION_TRACKING_START, + CourseOutlineFragmentView ) -from pyquery import PyQuery as pq from student.models import CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore import ModuleStoreEnum @@ -212,7 +216,7 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston gating_block: (The prerequisite) The block that must be completed to get access to the gated block """ - gating_api.add_prerequisite(self.course.id, unicode(gating_block.location)) + gating_api.add_prerequisite(self.course.id, six.text_type(gating_block.location)) gating_api.set_required_content(self.course.id, gated_block.location, gating_block.location, 100) def test_content_locked(self): @@ -362,7 +366,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT """ course_key = CourseKey.from_string(str(course.id)) # Fake a visit to sequence2/vertical2 - block_key = UsageKey.from_string(unicode(sequential.location)) + block_key = UsageKey.from_string(six.text_type(sequential.location)) completion = 1.0 BlockCompletion.objects.submit_completion( user=self.user, @@ -595,7 +599,7 @@ class TestCourseOutlinePreview(SharedModuleStoreTestCase): masquerade_url = reverse( 'masquerade_update', kwargs={ - 'course_key_string': unicode(course.id), + 'course_key_string': six.text_type(course.id), } ) response = self.client.post( diff --git a/openedx/features/course_experience/tests/views/test_course_sock.py b/openedx/features/course_experience/tests/views/test_course_sock.py index a2992cf638..78d2ca3f10 100644 --- a/openedx/features/course_experience/tests/views/test_course_sock.py +++ b/openedx/features/course_experience/tests/views/test_course_sock.py @@ -2,6 +2,8 @@ Tests for course verification sock """ +from __future__ import absolute_import + import ddt from course_modes.models import CourseMode diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index 41f5f8932f..19cbee92f2 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -1,10 +1,14 @@ """ Tests for the course updates page. """ +from __future__ import absolute_import + from datetime import datetime -from courseware.courses import get_course_info_usage_key +import six from django.urls import reverse + +from courseware.courses import get_course_info_usage_key from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_experience.views.course_updates import STATUS_VISIBLE @@ -28,7 +32,7 @@ def course_updates_url(course): return reverse( 'openedx.course_experience.course_updates', kwargs={ - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), } ) diff --git a/openedx/features/course_experience/tests/views/test_welcome_message.py b/openedx/features/course_experience/tests/views/test_welcome_message.py index 4d908e7cca..5253444bf6 100644 --- a/openedx/features/course_experience/tests/views/test_welcome_message.py +++ b/openedx/features/course_experience/tests/views/test_welcome_message.py @@ -1,7 +1,10 @@ """ Tests for course welcome messages. """ +from __future__ import absolute_import + import ddt +import six from django.urls import reverse from student.models import CourseEnrollment @@ -23,7 +26,7 @@ def welcome_message_url(course): return reverse( 'openedx.course_experience.welcome_message_fragment_view', kwargs={ - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), } ) @@ -35,7 +38,7 @@ def latest_update_url(course): return reverse( 'openedx.course_experience.latest_update_fragment_view', kwargs={ - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), } ) @@ -47,7 +50,7 @@ def dismiss_message_url(course): return reverse( 'openedx.course_experience.dismiss_welcome_message', kwargs={ - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), } ) diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py index 5c634144fe..d272c037da 100644 --- a/openedx/features/course_experience/views/course_dates.py +++ b/openedx/features/course_experience/views/course_dates.py @@ -1,6 +1,8 @@ """ Fragment for rendering the course dates sidebar. """ +from __future__ import absolute_import + from django.http import Http404 from django.template.loader import render_to_string from django.utils.translation import get_language_bidi diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index a8ac5536ad..e740fd8cc5 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -2,9 +2,12 @@ Views for the course home page. """ -from django.urls import reverse +from __future__ import absolute_import + +import six from django.template.context_processors import csrf from django.template.loader import render_to_string +from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie @@ -23,17 +26,20 @@ from lms.djangoapps.course_goals.api import ( ) from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.courseware.views.views import CourseTabView +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_banner -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.features.course_experience.course_tools import CourseToolsPluginManager from openedx.features.course_duration_limits.access import generate_course_expired_fragment +from openedx.features.course_experience.course_tools import CourseToolsPluginManager from student.models import CourseEnrollment from util.views import ensure_valid_course_key -from xmodule.course_module import COURSE_VISIBILITY_PUBLIC_OUTLINE, COURSE_VISIBILITY_PUBLIC +from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE from .. import ( - LATEST_UPDATE_FLAG, SHOW_UPGRADE_MSG_ON_COURSE_HOME, USE_BOOTSTRAP_FLAG, COURSE_ENABLE_UNENROLLED_ACCESS_FLAG + COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, + LATEST_UPDATE_FLAG, + SHOW_UPGRADE_MSG_ON_COURSE_HOME, + USE_BOOTSTRAP_FLAG ) from ..utils import get_course_outline_block_tree, get_resume_block from .course_dates import CourseDatesFragmentView @@ -43,7 +49,6 @@ from .course_sock import CourseSockFragmentView from .latest_update import LatestUpdateFragmentView from .welcome_message import WelcomeMessageFragmentView - EMPTY_HANDOUTS_HTML = u'
    ' @@ -68,7 +73,7 @@ class CourseHomeView(CourseTabView): return USE_BOOTSTRAP_FLAG.is_enabled(course.id) def render_to_fragment(self, request, course=None, tab=None, **kwargs): - course_id = unicode(course.id) + course_id = six.text_type(course.id) home_fragment_view = CourseHomeFragmentView() return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs) diff --git a/openedx/features/course_experience/views/course_home_messages.py b/openedx/features/course_experience/views/course_home_messages.py index 3edce019e9..655fb14092 100644 --- a/openedx/features/course_experience/views/course_home_messages.py +++ b/openedx/features/course_experience/views/course_home_messages.py @@ -2,16 +2,22 @@ View logic for handling course messages. """ +from __future__ import absolute_import + from datetime import datetime from babel.dates import format_date, format_timedelta -from course_modes.models import CourseMode -from courseware.courses import get_course_date_blocks, get_course_with_access from django.contrib import auth from django.template.loader import render_to_string from django.utils.http import urlquote_plus from django.utils.translation import get_language, to_locale from django.utils.translation import ugettext as _ +from opaque_keys.edx.keys import CourseKey +from pytz import UTC +from web_fragments.fragment import Fragment + +from course_modes.models import CourseMode +from courseware.courses import get_course_date_blocks, get_course_with_access from lms.djangoapps.course_goals.api import ( get_course_goal, get_course_goal_options, @@ -21,13 +27,10 @@ from lms.djangoapps.course_goals.api import ( ) from lms.djangoapps.course_goals.models import GOAL_KEY_CHOICES from lms.djangoapps.courseware.courses import allow_public_access -from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangolib.markup import HTML, Text from openedx.features.course_experience import CourseHomeMessages -from pytz import UTC from student.models import CourseEnrollment -from web_fragments.fragment import Fragment from xmodule.course_module import COURSE_VISIBILITY_PUBLIC diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py index d63ce19411..f97b01972e 100644 --- a/openedx/features/course_experience/views/course_outline.py +++ b/openedx/features/course_experience/views/course_outline.py @@ -1,8 +1,10 @@ """ Views to show a course outline. """ -import re +from __future__ import absolute_import + import datetime +import re from completion import waffle as completion_waffle from django.contrib.auth.models import User @@ -16,12 +18,11 @@ from web_fragments.fragment import Fragment from courseware.courses import get_course_overview_with_access from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from student.models import CourseEnrollment - from util.milestones_helpers import get_course_content_milestones from xmodule.course_module import COURSE_VISIBILITY_PUBLIC from xmodule.modulestore.django import modulestore -from ..utils import get_course_outline_block_tree, get_resume_block +from ..utils import get_course_outline_block_tree, get_resume_block DEFAULT_COMPLETION_TRACKING_START = datetime.datetime(2018, 1, 24, tzinfo=UTC) diff --git a/openedx/features/course_experience/views/course_reviews.py b/openedx/features/course_experience/views/course_reviews.py index 11ae627d10..5fac94e7c1 100644 --- a/openedx/features/course_experience/views/course_reviews.py +++ b/openedx/features/course_experience/views/course_reviews.py @@ -1,20 +1,23 @@ """ Fragment for rendering the course reviews panel """ +from __future__ import absolute_import + +import six from django.conf import settings from django.contrib.auth.decorators import login_required -from django.urls import reverse from django.template.loader import render_to_string +from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment from courseware.courses import get_course_with_access -from student.models import CourseEnrollment from lms.djangoapps.courseware.views.views import CourseTabView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.features.course_experience import default_course_url_name +from student.models import CourseEnrollment from .. import USE_BOOTSTRAP_FLAG @@ -38,7 +41,7 @@ class CourseReviewsView(CourseTabView): return USE_BOOTSTRAP_FLAG.is_enabled(course.id) def render_to_fragment(self, request, course=None, tab=None, **kwargs): - course_id = unicode(course.id) + course_id = six.text_type(course.id) reviews_fragment_view = CourseReviewsFragmentView() return reviews_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs) @@ -55,7 +58,7 @@ class CourseReviewsFragmentView(EdxFragmentView): course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) course_url_name = default_course_url_name(course.id) - course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)}) + course_url = reverse(course_url_name, kwargs={'course_id': six.text_type(course.id)}) is_enrolled = CourseEnrollment.is_enrolled(request.user, course.id) diff --git a/openedx/features/course_experience/views/course_sock.py b/openedx/features/course_experience/views/course_sock.py index 461a0616c1..7d73db331b 100644 --- a/openedx/features/course_experience/views/course_sock.py +++ b/openedx/features/course_experience/views/course_sock.py @@ -1,6 +1,8 @@ """ Fragment for rendering the course's sock and associated toggle button. """ +from __future__ import absolute_import + from django.template.loader import render_to_string from web_fragments.fragment import Fragment diff --git a/openedx/features/course_experience/views/course_updates.py b/openedx/features/course_experience/views/course_updates.py index b385ba2ad7..e6a417fbc5 100644 --- a/openedx/features/course_experience/views/course_updates.py +++ b/openedx/features/course_experience/views/course_updates.py @@ -1,12 +1,15 @@ """ Views that handle course updates. """ +from __future__ import absolute_import + from datetime import datetime +import six from django.contrib.auth.decorators import login_required from django.template.context_processors import csrf -from django.urls import reverse from django.template.loader import render_to_string +from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from opaque_keys.edx.keys import CourseKey @@ -70,7 +73,7 @@ class CourseUpdatesView(CourseTabView): return USE_BOOTSTRAP_FLAG.is_enabled(course.id) def render_to_fragment(self, request, course=None, tab=None, **kwargs): - course_id = unicode(course.id) + course_id = six.text_type(course.id) updates_fragment_view = CourseUpdatesFragmentView() return updates_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs) @@ -86,7 +89,7 @@ class CourseUpdatesFragmentView(EdxFragmentView): course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_url_name = default_course_url_name(course.id) - course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)}) + course_url = reverse(course_url_name, kwargs={'course_id': six.text_type(course.id)}) ordered_updates = get_ordered_updates(request, course) plain_html_updates = '' diff --git a/openedx/features/course_experience/views/latest_update.py b/openedx/features/course_experience/views/latest_update.py index 765760761d..c70b154c15 100644 --- a/openedx/features/course_experience/views/latest_update.py +++ b/openedx/features/course_experience/views/latest_update.py @@ -6,6 +6,8 @@ this fragment dismisses the message for a limited time so new updates will continue to appear, where the welcome message gets permanently dismissed. """ +from __future__ import absolute_import + from django.template.loader import render_to_string from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment diff --git a/openedx/features/course_experience/views/welcome_message.py b/openedx/features/course_experience/views/welcome_message.py index f9be8070e2..5edcf5d7e2 100644 --- a/openedx/features/course_experience/views/welcome_message.py +++ b/openedx/features/course_experience/views/welcome_message.py @@ -2,17 +2,21 @@ View logic for handling course welcome messages. """ -from django.urls import reverse +from __future__ import absolute_import + +import six from django.http import HttpResponse from django.template.loader import render_to_string +from django.urls import reverse from django.views.decorators.csrf import ensure_csrf_cookie from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment -from course_updates import get_ordered_updates from courseware.courses import get_course_with_access from openedx.core.djangoapps.plugin_api.views import EdxFragmentView -from openedx.core.djangoapps.user_api.course_tag.api import set_course_tag, get_course_tag +from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag + +from .course_updates import get_ordered_updates PREFERENCE_KEY = 'view-welcome-message' @@ -34,7 +38,7 @@ class WelcomeMessageFragmentView(EdxFragmentView): return None dismiss_url = reverse( - 'openedx.course_experience.dismiss_welcome_message', kwargs={'course_id': unicode(course_key)} + 'openedx.course_experience.dismiss_welcome_message', kwargs={'course_id': six.text_type(course_key)} ) context = { From 75cae7ebd231b83d521941790a21db1ccb024875 Mon Sep 17 00:00:00 2001 From: Ana Maria Rodriguez Date: Wed, 24 Apr 2019 14:07:32 -0500 Subject: [PATCH 010/106] INCR-174 Run python-modernize on openedx/core/djangoapps/heartbeat and openedx/core/djangoapps/signals --- openedx/core/djangoapps/password_policy/apps.py | 1 + openedx/core/djangoapps/password_policy/compliance.py | 3 ++- openedx/core/djangoapps/password_policy/forms.py | 1 + openedx/core/djangoapps/password_policy/tests/test_apps.py | 1 + .../core/djangoapps/password_policy/tests/test_compliance.py | 1 + openedx/core/djangoapps/password_policy/tests/test_forms.py | 1 + openedx/core/djangoapps/signals/signals.py | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/password_policy/apps.py b/openedx/core/djangoapps/password_policy/apps.py index 8bf29da123..2ef04c6136 100644 --- a/openedx/core/djangoapps/password_policy/apps.py +++ b/openedx/core/djangoapps/password_policy/apps.py @@ -1,6 +1,7 @@ """ Configuration for password_policy Django app """ +from __future__ import absolute_import import logging import six from dateutil.parser import parse as parse_date diff --git a/openedx/core/djangoapps/password_policy/compliance.py b/openedx/core/djangoapps/password_policy/compliance.py index 742ba79ae8..c8888f6feb 100644 --- a/openedx/core/djangoapps/password_policy/compliance.py +++ b/openedx/core/djangoapps/password_policy/compliance.py @@ -1,6 +1,7 @@ """ Utilities for enforcing and tracking compliance with password policy rules. """ +from __future__ import absolute_import from datetime import datetime import pytz @@ -142,7 +143,7 @@ def _get_compliance_deadline_for_user(user): staff_deadline = staff_deadline if staff_deadline and user.is_staff else None # Take minimum remaining deadline - filtered_deadlines = filter(None, (staff_deadline, privilege_deadline, general_deadline,)) + filtered_deadlines = [_f for _f in (staff_deadline, privilege_deadline, general_deadline,) if _f] return min(filtered_deadlines) if filtered_deadlines else None diff --git a/openedx/core/djangoapps/password_policy/forms.py b/openedx/core/djangoapps/password_policy/forms.py index 379479dc05..2d3afc5d71 100644 --- a/openedx/core/djangoapps/password_policy/forms.py +++ b/openedx/core/djangoapps/password_policy/forms.py @@ -1,6 +1,7 @@ """ Forms for the password policy app. """ +from __future__ import absolute_import from django.contrib import messages from django.contrib.admin.forms import AdminAuthenticationForm from django.forms import ValidationError diff --git a/openedx/core/djangoapps/password_policy/tests/test_apps.py b/openedx/core/djangoapps/password_policy/tests/test_apps.py index 4fdf8252b0..5d2de31f6c 100644 --- a/openedx/core/djangoapps/password_policy/tests/test_apps.py +++ b/openedx/core/djangoapps/password_policy/tests/test_apps.py @@ -2,6 +2,7 @@ Test password policy settings """ +from __future__ import absolute_import import datetime from dateutil.parser import parse as parse_date from django.conf import settings diff --git a/openedx/core/djangoapps/password_policy/tests/test_compliance.py b/openedx/core/djangoapps/password_policy/tests/test_compliance.py index e05b371290..9a41aca4d7 100644 --- a/openedx/core/djangoapps/password_policy/tests/test_compliance.py +++ b/openedx/core/djangoapps/password_policy/tests/test_compliance.py @@ -1,6 +1,7 @@ """ Test password policy utilities """ +from __future__ import absolute_import from datetime import datetime, timedelta import pytz diff --git a/openedx/core/djangoapps/password_policy/tests/test_forms.py b/openedx/core/djangoapps/password_policy/tests/test_forms.py index be9c3fd98e..9a5cb7e2fc 100644 --- a/openedx/core/djangoapps/password_policy/tests/test_forms.py +++ b/openedx/core/djangoapps/password_policy/tests/test_forms.py @@ -1,6 +1,7 @@ """ Test password policy forms """ +from __future__ import absolute_import import mock from django.forms import ValidationError diff --git a/openedx/core/djangoapps/signals/signals.py b/openedx/core/djangoapps/signals/signals.py index d76aa1ea3a..796635c369 100644 --- a/openedx/core/djangoapps/signals/signals.py +++ b/openedx/core/djangoapps/signals/signals.py @@ -2,6 +2,7 @@ This module contains all general use signals. """ +from __future__ import absolute_import from django.dispatch import Signal # Signal that fires when a user is graded From f88061a3b3626b8e07c6f531764b5165dd624974 Mon Sep 17 00:00:00 2001 From: Constanza Abarca Date: Wed, 24 Apr 2019 15:14:34 -0400 Subject: [PATCH 011/106] ran isort --- common/djangoapps/course_modes/migrations/0001_initial.py | 4 +--- .../0002_coursemode_expiration_datetime_is_explicit.py | 3 +-- .../course_modes/migrations/0003_auto_20151113_1443.py | 3 +-- .../course_modes/migrations/0004_auto_20151113_1457.py | 3 +-- .../course_modes/migrations/0005_auto_20151217_0958.py | 3 +-- .../course_modes/migrations/0006_auto_20160208_1407.py | 3 +-- .../course_modes/migrations/0007_coursemode_bulk_sku.py | 3 +-- .../migrations/0008_course_key_field_to_foreign_key.py | 3 +-- .../migrations/0009_suggested_prices_to_charfield.py | 6 +++--- .../0010_archived_suggested_prices_to_charfield.py | 6 +++--- .../0011_change_regex_for_comma_separated_ints.py | 6 +++--- 11 files changed, 17 insertions(+), 26 deletions(-) diff --git a/common/djangoapps/course_modes/migrations/0001_initial.py b/common/djangoapps/course_modes/migrations/0001_initial.py index ef476b5d50..11692dcad0 100644 --- a/common/djangoapps/course_modes/migrations/0001_initial.py +++ b/common/djangoapps/course_modes/migrations/0001_initial.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models - from opaque_keys.edx.django.models import CourseKeyField diff --git a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py index 84402e3d07..ad010d9a3b 100644 --- a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py +++ b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py index 16f918459c..bca415f6d8 100644 --- a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py +++ b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py index 27973eedf9..de21813631 100644 --- a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py +++ b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from datetime import timedelta import django.db.models.deletion diff --git a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py index ef715a21ba..574575749b 100644 --- a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py +++ b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py index 0277c63494..7355c03817 100644 --- a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py +++ b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py index 0262ad5756..8a81d980cd 100644 --- a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py +++ b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models diff --git a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py index 4cfe33fe14..2ed4bfaf74 100644 --- a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py +++ b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField diff --git a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py index bca1792f18..c6ba8b0b43 100644 --- a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py +++ b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import -from django.db import migrations, models import re + import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py index 6659a21c9b..56254117ed 100644 --- a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py +++ b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from __future__ import absolute_import -from django.db import migrations, models import re + import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py index 6384887859..a69f54997d 100644 --- a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py +++ b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.8 on 2018-01-30 17:38 -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals + +import re -from __future__ import absolute_import import django.core.validators from django.db import migrations, models -import re class Migration(migrations.Migration): From 0e590c0f7e6475317c56c47bddfea5f6c4907eb4 Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Wed, 24 Apr 2019 09:36:13 -0400 Subject: [PATCH 012/106] Moved these functions out of helpers because they're not needed elsewhere --- common/djangoapps/course_modes/helpers.py | 41 ------------------- common/djangoapps/course_modes/signals.py | 37 +++++++++++++++-- .../course_modes/tests/test_signals.py | 15 ++++--- common/djangoapps/course_modes/views.py | 1 - 4 files changed, 43 insertions(+), 51 deletions(-) diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py index c1a664fa89..9d90cefcc1 100644 --- a/common/djangoapps/course_modes/helpers.py +++ b/common/djangoapps/course_modes/helpers.py @@ -1,27 +1,17 @@ """ Helper methods for CourseModes. """ from __future__ import absolute_import, unicode_literals -import logging - import six -from django.conf import settings from django.utils.translation import ugettext_lazy as _ from course_modes.models import CourseMode from student.helpers import VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED -from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID DISPLAY_VERIFIED = "verified" DISPLAY_HONOR = "honor" DISPLAY_AUDIT = "audit" DISPLAY_PROFESSIONAL = "professional" -MASTERS_ID = settings.COURSE_ENROLLMENT_MODES.get('masters', {}).get('id', None) -VERIFIED_ID = settings.COURSE_ENROLLMENT_MODES['verified']['id'] - -log = logging.getLogger(__name__) - def enrollment_mode_display(mode, verification_status, course_id): """ Select appropriate display strings and CSS classes. @@ -93,34 +83,3 @@ def _enrollment_mode_display(enrollment_mode, verification_status, course_id): display_mode = enrollment_mode return display_mode - - -def update_masters_access(item): - """ - Update the XBlock's group access to allow the master's group, - in addition to the verified content group. - """ - group_access = item.group_access - enrollment_groups = group_access.get(ENROLLMENT_TRACK_PARTITION_ID, None) - if enrollment_groups is not None: - if VERIFIED_ID in enrollment_groups and MASTERS_ID not in enrollment_groups: - enrollment_groups.append(MASTERS_ID) - item.group_access = group_access - return True - - -def update_masters_access_course(store, course_id, user_id): - """ - Update all blocks in the verified content group to include the master's content group - """ - - with store.bulk_operations(course_id): - try: - items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False) - except ItemNotFoundError: - return - for item in items: - if update_masters_access(item): - log.info("Publishing %s with Master's group access", item.location) - store.update_item(item, user_id) - store.publish(item.location, user_id) diff --git a/common/djangoapps/course_modes/signals.py b/common/djangoapps/course_modes/signals.py index 23b217eb3d..f225dc021f 100644 --- a/common/djangoapps/course_modes/signals.py +++ b/common/djangoapps/course_modes/signals.py @@ -3,16 +3,22 @@ Signal handler for setting default course mode expiration dates """ from __future__ import absolute_import, unicode_literals +import logging + from crum import get_current_user +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import post_save from django.dispatch.dispatcher import receiver from xmodule.modulestore.django import SignalHandler, modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID -from .helpers import update_masters_access_course from .models import CourseMode, CourseModeExpirationConfig +log = logging.getLogger(__name__) + @receiver(SignalHandler.course_published) def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument @@ -43,12 +49,35 @@ def _should_update_date(verified_mode): @receiver(post_save, sender=CourseMode) -def update_access_for_masters_mode(sender, instance=None, **kwargs): # pylint: disable=unused-argument +def update_masters_access_course(sender, instance, **kwargs): # pylint: disable=unused-argument """ - Adds master's access to verified content when the master's mode is created + Update all blocks in the verified content group to include the master's content group """ if instance.mode_slug != CourseMode.MASTERS: return + masters_id = getattr(settings, 'COURSE_ENROLLMENT_MODES', {}).get('masters', {}).get('id', None) + verified_id = getattr(settings, 'COURSE_ENROLLMENT_MODES', {}).get('verified', {}).get('id', None) + if not (masters_id and verified_id): + log.error("Missing settings.COURSE_ENROLLMENT_MODES -> verified:%s masters:%s", verified, masters) + return + + course_id = instance.course_id user = get_current_user() user_id = user.id if user else None - update_masters_access_course(modulestore(), instance.course_id, user_id) + store = modulestore() + + with store.bulk_operations(course_id): + try: + items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False) + except ItemNotFoundError: + return + for item in items: + group_access = item.group_access + enrollment_groups = group_access.get(ENROLLMENT_TRACK_PARTITION_ID, None) + if enrollment_groups is not None: + if verified_id in enrollment_groups and masters_id not in enrollment_groups: + enrollment_groups.append(masters_id) + item.group_access = group_access + log.info("Publishing %s with Master's group access", item.location) + store.update_item(item, user_id) + store.publish(item.location, user_id) diff --git a/common/djangoapps/course_modes/tests/test_signals.py b/common/djangoapps/course_modes/tests/test_signals.py index 951e99fb5f..b3675e4f3c 100644 --- a/common/djangoapps/course_modes/tests/test_signals.py +++ b/common/djangoapps/course_modes/tests/test_signals.py @@ -11,9 +11,11 @@ from pytz import UTC from course_modes.models import CourseMode from course_modes.signals import _listen_for_course_publish +from django.conf import settings from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID @ddt.ddt @@ -92,9 +94,12 @@ class CourseModeSignalTest(ModuleStoreTestCase): def test_masters_mode(self): # create an xblock with verified group access + AUDIT_ID = settings.COURSE_ENROLLMENT_MODES['audit']['id'] + VERIFIED_ID = settings.COURSE_ENROLLMENT_MODES['verified']['id'] + MASTERS_ID = settings.COURSE_ENROLLMENT_MODES['masters']['id'] verified_section = ItemFactory.create( category="sequential", - metadata={'group_access': {50: [2]}} + metadata={'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [VERIFIED_ID]}} ) # and a section with no restriction section2 = ItemFactory.create( @@ -102,7 +107,7 @@ class CourseModeSignalTest(ModuleStoreTestCase): ) section3 = ItemFactory.create( category='sequential', - metadata={'group_access': {50: [1]}} + metadata={'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [AUDIT_ID]}} ) with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): # create the master's mode. signal will add masters to the verified section @@ -110,7 +115,7 @@ class CourseModeSignalTest(ModuleStoreTestCase): verified_section_ret = self.store.get_item(verified_section.location) section2_ret = self.store.get_item(section2.location) section3_ret = self.store.get_item(section3.location) - # group 2 is verified. 7 is masters - assert verified_section_ret.group_access[50] == [2, 7] + # the verified section will now also be visible to master's + assert verified_section_ret.group_access[ENROLLMENT_TRACK_PARTITION_ID] == [VERIFIED_ID, MASTERS_ID] assert section2_ret.group_access == {} - assert section3_ret.group_access == {50: [1]} + assert section3_ret.group_access == {ENROLLMENT_TRACK_PARTITION_ID: [AUDIT_ID]} diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 91ccacfbdf..9eb116bf27 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -10,7 +10,6 @@ import six import six.moves.urllib.error import six.moves.urllib.parse import six.moves.urllib.request - import waffle from babel.dates import format_datetime from django.contrib.auth.decorators import login_required From 42ede5738308c58d089b56f694ae5417fc90ae93 Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Wed, 24 Apr 2019 15:38:22 -0400 Subject: [PATCH 013/106] Hide edx-when feature behind waffle flag --- lms/djangoapps/courseware/tests/test_views.py | 4 ++-- lms/djangoapps/instructor/tests/test_tools.py | 3 ++- lms/djangoapps/instructor/views/instructor_dashboard.py | 3 ++- .../course_experience/tests/views/test_course_updates.py | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/testing.txt | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 5135f7e8a4..bb59250149 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): NUM_PROBLEMS = 20 @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 178), - (ModuleStoreEnum.Type.split, 4, 172), + (ModuleStoreEnum.Type.mongo, 10, 176), + (ModuleStoreEnum.Type.split, 4, 170), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index f241dd4c3a..5073a539e2 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -15,7 +15,7 @@ from django.test import TestCase from opaque_keys.edx.keys import CourseKey from pytz import UTC -from edx_when import signals +from edx_when import signals, api from edx_when.field_data import DateLookupFieldData from student.tests.factories import UserFactory from xmodule.fields import Date @@ -240,6 +240,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase): block._field_data._load_dates(self.course.id, self.user, use_cached=False) # pylint: disable=protected-access block.fields['due']._del_cached_value(block) # pylint: disable=protected-access + @api.override_enabled() def test_set_due_date_extension(self): extended = datetime.datetime(2013, 12, 25, 0, 0, tzinfo=UTC) tools.set_due_date_extension(self.course, self.week1, self.user, extended) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 8e9de402c2..fd28558de1 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -33,6 +33,7 @@ from courseware.courses import get_course_by_id, get_studio_url from django_comment_client.utils import available_division_schemes, has_forum_access from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, CourseDiscussionSettings from edxmako.shortcuts import render_to_response +from edx_when.api import is_enabled_for_course from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, @@ -159,7 +160,7 @@ def instructor_dashboard_2(request, course_id): unicode(course_key), len(paid_modes) ) - if access['instructor']: + if access['instructor'] and is_enabled_for_course(course_key, request=request): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index 809640d55e..41f5f8932f 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -130,7 +130,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase): # Fetch the view and verify that the query counts haven't changed # TODO: decrease query count as part of REVO-28 - with self.assertNumQueries(52, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_updates_url(self.course) self.client.get(url) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 25c478c1fa..3630759fe7 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -125,7 +125,7 @@ edx-rest-api-client==1.9.2 edx-search==1.2.2 edx-submissions==2.1.1 edx-user-state-client==1.0.4 -edx-when==0.1.1 +edx-when==0.1.2 edxval==1.1.25 elasticsearch==1.9.0 # via edx-search enum34==1.1.6 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 1155d0594a..12c530d054 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -148,7 +148,7 @@ edx-search==1.2.2 edx-sphinx-theme==1.4.0 edx-submissions==2.1.1 edx-user-state-client==1.0.4 -edx-when==0.1.1 +edx-when==0.1.2 edxval==1.1.25 elasticsearch==1.9.0 entrypoints==0.3 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index d48d833889..4131625cf6 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -143,7 +143,7 @@ edx-rest-api-client==1.9.2 edx-search==1.2.2 edx-submissions==2.1.1 edx-user-state-client==1.0.4 -edx-when==0.1.1 +edx-when==0.1.2 edxval==1.1.25 elasticsearch==1.9.0 entrypoints==0.3 # via flake8 From 6d2e2738198e3a703c7d790c8d5c6b77a15ac76f Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Wed, 24 Apr 2019 21:02:35 -0400 Subject: [PATCH 014/106] address review --- common/djangoapps/third_party_auth/urls.py | 2 +- common/djangoapps/third_party_auth/views.py | 27 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py index 58be91558b..76ea17d10a 100644 --- a/common/djangoapps/third_party_auth/urls.py +++ b/common/djangoapps/third_party_auth/urls.py @@ -10,6 +10,6 @@ urlpatterns = [ url(r'^auth/custom_auth_entry', post_to_custom_auth_form, name='tpa_post_to_custom_auth_form'), url(r'^auth/saml/metadata.xml', saml_metadata_view), url(r'^auth/login/(?Plti)/$', lti_login_and_complete_view), - url(r'^auth/idp_redirect/(?P.*)', IdPRedirectView.as_view(), name="idp_redirect"), + url(r'^auth/idp_redirect/(?P[\w-]+)', IdPRedirectView.as_view(), name="idp_redirect"), url(r'^auth/', include('social_django.urls', namespace='social')), ] diff --git a/common/djangoapps/third_party_auth/views.py b/common/djangoapps/third_party_auth/views.py index 87ae9ffa2f..b13ff7bda0 100644 --- a/common/djangoapps/third_party_auth/views.py +++ b/common/djangoapps/third_party_auth/views.py @@ -115,7 +115,34 @@ def post_to_custom_auth_form(request): class IdPRedirectView(View): + """ + Redirect to an IdP's login page if the IdP exists; otherwise, return a 404. + + Example usage: + + GET auth/idp_redirect/saml-default + + """ def get(self, request, *args, **kwargs): + """ + Return either a redirect to the login page of an identity provider that + corresponds to the provider_slug keyword argument or a 404 if the + provider_slug does not correspond to an identity provider. + + Args: + request (HttpRequest) + + Keyword Args: + provider_slug (str): a slug corresponding to a configured identity provider + + Returns: + HttpResponse: 302 to a provider's login url if the provider_slug kwarg matches an identity provider + HttpResponse: 404 if the provider_slug kwarg does not match an identity provider + """ + # this gets the url to redirect to after login/registration/third_party_auth + # it also handles checking the safety of the redirect url (next query parameter) + # it checks against settings.LOGIN_REDIRECT_WHITELIST, so be sure to add the url + # to this setting next_destination_url = get_next_url_for_login_page(request) try: From 5acbb10e7c23dc8f1b77becef9a62b52c3d1b80a Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Wed, 24 Apr 2019 22:37:26 -0400 Subject: [PATCH 015/106] Account API: enforce Enterprise policy on backend --- .../core/djangoapps/user_api/accounts/api.py | 36 ++++++++++--------- .../user_api/accounts/settings_views.py | 2 +- .../user_api/accounts/tests/test_api.py | 27 ++++++++++++++ .../djangoapps/user_api/accounts/views.py | 3 ++ openedx/features/enterprise_support/api.py | 2 +- openedx/features/enterprise_support/utils.py | 7 ++++ 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index d49cfe8367..b925347b82 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -41,6 +41,7 @@ from openedx.core.djangoapps.user_api.errors import ( ) from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences from openedx.core.lib.api.view_utils import add_serializer_errors +from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields from .serializers import ( AccountLegacyProfileSerializer, AccountUserSerializer, @@ -143,6 +144,25 @@ def update_account_settings(requesting_user, update, username=None): if requesting_user.username != username: raise errors.UserNotAuthorized() + # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400. + read_only_fields = set(update.keys()).intersection( + # Remove email since it is handled separately below when checking for changing_email. + (set(AccountUserSerializer.get_read_only_fields()) - set(["email"])) | + set(AccountLegacyProfileSerializer.get_read_only_fields() or set()) | + get_enterprise_readonly_account_fields(existing_user) + ) + + # Build up all field errors, whether read-only, validation, or email errors. + field_errors = {} + + if read_only_fields: + for read_only_field in read_only_fields: + field_errors[read_only_field] = { + "developer_message": u"This field is not editable via this API", + "user_message": _(u"The '{field_name}' field cannot be edited.").format(field_name=read_only_field) + } + del update[read_only_field] + # If user has requested to change email, we must call the multi-step process to handle this. # It is not handled by the serializer (which considers email to be read-only). changing_email = False @@ -163,22 +183,6 @@ def update_account_settings(requesting_user, update, username=None): if "secondary_email" in update: changing_secondary_email = True - # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400. - read_only_fields = set(update.keys()).intersection( - AccountUserSerializer.get_read_only_fields() + AccountLegacyProfileSerializer.get_read_only_fields() - ) - - # Build up all field errors, whether read-only, validation, or email errors. - field_errors = {} - - if read_only_fields: - for read_only_field in read_only_fields: - field_errors[read_only_field] = { - "developer_message": u"This field is not editable via this API", - "user_message": _(u"The '{field_name}' field cannot be edited.").format(field_name=read_only_field) - } - del update[read_only_field] - user_serializer = AccountUserSerializer(existing_user, data=update) legacy_profile_serializer = AccountLegacyProfileSerializer(existing_user_profile, data=update) diff --git a/openedx/core/djangoapps/user_api/accounts/settings_views.py b/openedx/core/djangoapps/user_api/accounts/settings_views.py index ea64db1f3c..ff1f3b11bd 100644 --- a/openedx/core/djangoapps/user_api/accounts/settings_views.py +++ b/openedx/core/djangoapps/user_api/accounts/settings_views.py @@ -125,7 +125,7 @@ def account_settings_context(request): 'beta_language': beta_language } - enterprise_customer = get_enterprise_customer_for_learner(site=request.site, user=request.user) + enterprise_customer = get_enterprise_customer_for_learner(user=request.user) update_account_settings_context_for_enterprise(context, enterprise_customer) if third_party_auth.is_enabled(): diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py index 7be20ff0ef..a3d6f6aeea 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -58,6 +58,7 @@ from openedx.core.djangoapps.user_api.errors import ( UserNotFound ) from openedx.core.djangolib.testing.utils import skip_unless_lms +from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerUserFactory from student.models import PendingEmailChange from student.tests.factories import UserFactory from student.tests.tests import UserSettingsEventTestMixin @@ -69,6 +70,7 @@ def mock_render_to_string(template_name, context): @skip_unless_lms +@ddt.ddt class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, RetirementTestCase): """ These tests specifically cover the parts of the API methods that are not covered by test_views.py. @@ -220,6 +222,31 @@ class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, Retireme with self.assertRaises(AccountUpdateError): update_account_settings(self.user, {"social_links": social_links}) + def test_update_success_for_enterprise(self): + EnterpriseCustomerUserFactory(user_id=self.user.id) + level_of_education = "m" + successful_update = { + "level_of_education": level_of_education, + } + update_account_settings(self.user, successful_update) + account_settings = get_account_settings(self.default_request)[0] + self.assertEqual(level_of_education, account_settings["level_of_education"]) + + @ddt.data( + ("email", "new_email@example.com"), + ("name", "New Name"), + ("country", "New Country"), + ) + @ddt.unpack + def test_update_validation_error_for_enterprise(self, field_name, field_value): + EnterpriseCustomerUserFactory(user_id=self.user.id) + update_data = {field_name: field_value} + + with self.assertRaises(AccountValidationError) as validation_error: + update_account_settings(self.user, update_data) + field_errors = validation_error.exception.field_errors + self.assertEqual("This field is not editable via this API", field_errors[field_name]["developer_message"]) + def test_update_error_validating(self): """Test that AccountValidationError is thrown if incorrect values are supplied.""" with self.assertRaises(AccountValidationError): diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 55ea0692ec..8f9bac8e4a 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -165,6 +165,9 @@ class AccountViewSet(ViewSet): * email: Email address for the user. New email addresses must be confirmed via a confirmation email, so GET does not reflect the change until the address has been confirmed. + * secondary_email: A secondary email address for the user. Unlike + the email field, GET will reflect the latest update to this field + even if changes have yet to be confirmed. * gender: One of the following values: * null diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py index ff835be5e2..8fa48190e2 100644 --- a/openedx/features/enterprise_support/api.py +++ b/openedx/features/enterprise_support/api.py @@ -554,7 +554,7 @@ def get_enterprise_learner_data(user): @enterprise_is_enabled(otherwise={}) -def get_enterprise_customer_for_learner(site, user): +def get_enterprise_customer_for_learner(user): """ Return enterprise customer to whom given learner belongs. """ diff --git a/openedx/features/enterprise_support/utils.py b/openedx/features/enterprise_support/utils.py index 817997cb84..feaa789680 100644 --- a/openedx/features/enterprise_support/utils.py +++ b/openedx/features/enterprise_support/utils.py @@ -260,6 +260,13 @@ def update_account_settings_context_for_enterprise(context, enterprise_customer) context.update(enterprise_context) +def get_enterprise_readonly_account_fields(user): + """ + Returns a set of account fields that are read-only for enterprise users. + """ + return set(settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS) if is_enterprise_learner(user) else set() + + def get_enterprise_learner_generic_name(request): """ Get a generic name concatenating the Enterprise Customer name and 'Learner'. From 34c08a4ac2bbb78ab1f876aa7df09cd1b53aaa51 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Wed, 24 Apr 2019 19:30:14 +0500 Subject: [PATCH 016/106] update class to fix footer lang button for firefox --- lms/static/sass/shared/_footer-edx.scss | 22 ++++++++++++++++--- .../sass/partials/lms/theme/_extras.scss | 12 ---------- themes/edx.org/lms/templates/footer.html | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lms/static/sass/shared/_footer-edx.scss b/lms/static/sass/shared/_footer-edx.scss index 8bb2426553..0f33bb6126 100644 --- a/lms/static/sass/shared/_footer-edx.scss +++ b/lms/static/sass/shared/_footer-edx.scss @@ -516,7 +516,7 @@ footer#footer-edx-v3 { .edx-footer-logo { display: inline-flex; - @include margin-left(20px); + @include margin-left(0); margin-top: 5px; } @@ -602,7 +602,7 @@ footer#footer-edx-v3 { float: none; display: block; - @include margin-left($baseline); + @include margin-left(0); } ul.social li { @@ -659,7 +659,7 @@ footer#footer-edx-v3 { .col-xl-2 .edx-footer-logo { - @include margin-left($baseline); + @include margin-left(0); @include padding(0, 0, $baseline, $baseline); } @@ -726,6 +726,7 @@ footer#footer-edx-v3 { .col-xl-2 .footer-language-selector { display: inline-block; width: max-content; + margin-left: 0; } ul.social li { @@ -842,5 +843,20 @@ footer#footer-edx-v3 { } } + .select-lang-button { + padding: 3px; + color: $m-blue-d3 !important; + height: 30px; + margin-left: 0; + + &:hover, + &:active, + &:focus { + background: $m-blue-d3 !important; + color: white !important; + } +} + + @extend %ui-print-excluded; } diff --git a/themes/edx.org/lms/static/sass/partials/lms/theme/_extras.scss b/themes/edx.org/lms/static/sass/partials/lms/theme/_extras.scss index b538c03da5..449252f3ff 100644 --- a/themes/edx.org/lms/static/sass/partials/lms/theme/_extras.scss +++ b/themes/edx.org/lms/static/sass/partials/lms/theme/_extras.scss @@ -21,15 +21,3 @@ } } } -.select-lang-button { - padding: 3px; - color: $m-blue-d3 !important; - height: 30px; - - &:hover, - &:active, - &:focus { - background: $m-blue-d3 !important; - color: white !important; - } -} diff --git a/themes/edx.org/lms/templates/footer.html b/themes/edx.org/lms/templates/footer.html index 1fa1c0ad3d..c9f7540ab6 100755 --- a/themes/edx.org/lms/templates/footer.html +++ b/themes/edx.org/lms/templates/footer.html @@ -24,7 +24,7 @@
    -
    +