From 29de9b2dc404e7ffd5e170f35c41be74c8a15f74 Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Mon, 10 Feb 2025 14:39:13 -0500 Subject: [PATCH] feat!: Legacy account, profile, order history removal (#36219) * feat!: Legacy account, profile, order history removal This removes the legacy account and profile applications, and the order history page. This is primarily a reapplication of #31893, which was rolled back due to prior blockers. FIXES: APER-3884 FIXES: openedx/public-engineering#71 Co-authored-by: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com> Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com> --- cms/envs/test.py | 190 ++-- .../djangoapps/student/tests/test_filters.py | 3 +- common/djangoapps/student/tests/test_views.py | 2 +- common/djangoapps/student/views/dashboard.py | 6 +- .../third_party_auth/api/tests/test_views.py | 8 +- .../djangoapps/third_party_auth/api/views.py | 3 +- .../third_party_auth/tests/specs/base.py | 583 +++++++----- .../tests/specs/test_testshib.py | 517 ++++++----- conf/locale/config.yaml | 39 + lms/djangoapps/bulk_email/tasks.py | 2 +- lms/djangoapps/bulk_email/tests/test_email.py | 2 +- lms/djangoapps/courseware/views/views.py | 2 +- lms/djangoapps/discussion/views.py | 3 +- lms/envs/common.py | 1 - lms/envs/devstack.py | 1 + lms/envs/test.py | 2 +- .../account_settings_factory_spec.js | 334 ------- .../account_settings_fields_helpers.js | 34 - .../account_settings_fields_spec.js | 216 ----- .../account_settings_view_spec.js | 91 -- .../views/account_section_view.js | 48 - .../views/account_settings_factory.js | 495 ---------- .../views/account_settings_fields.js | 466 ---------- .../views/account_settings_view.js | 157 ---- lms/static/learner_profile | 1 - lms/static/lms/js/build.js | 2 - lms/static/lms/js/spec/main.js | 7 - lms/static/sass/_build-lms-v1.scss | 2 - .../sass/features/_learner-profile.scss | 875 ------------------ .../partials/lms/theme/_variables-v1.scss | 3 - lms/static/sass/views/_account-settings.scss | 683 -------------- .../_dashboard_third_party_error.html | 14 - lms/templates/header/user_dropdown.html | 8 +- .../student_account/account_settings.html | 91 -- .../account_settings.underscore | 30 - .../account_settings_section.underscore | 31 - lms/templates/user_dropdown.html | 4 +- lms/urls.py | 6 - .../tests/test_theme_style_overrides.py | 15 - .../user_api/accounts/settings_views.py | 300 ------ .../user_api/accounts/tests/test_filters.py | 241 ----- .../accounts/tests/test_settings_views.py | 287 ------ .../djangoapps/user_api/accounts/toggles.py | 44 - .../core/djangoapps/user_api/legacy_urls.py | 2 - openedx/core/djangoapps/user_authn/cookies.py | 5 +- .../user_authn/tests/test_cookies.py | 5 +- .../user_authn/views/tests/test_login.py | 2 +- openedx/features/learner_profile/README.rst | 8 - openedx/features/learner_profile/__init__.py | 0 .../fixtures/learner_profile.html | 40 - .../js/learner_profile_factory.js | 219 ----- .../js/spec/learner_profile_factory_spec.js | 79 -- .../spec/views/learner_profile_fields_spec.js | 381 -------- .../spec/views/learner_profile_view_spec.js | 218 ----- .../js/spec/views/section_two_tab_spec.js | 113 --- .../js/spec_helpers/helpers.js | 133 --- .../js/views/learner_profile_fields.js | 169 ---- .../js/views/learner_profile_view.js | 150 --- .../js/views/section_two_tab.js | 33 - .../templates/section_two.underscore | 10 - .../templates/social_icons.underscore | 9 - .../templates/third_party_auth.html | 47 - .../learner-achievements-fragment.html | 69 -- .../learner_profile/learner_profile.html | 79 -- .../learner_profile/tests/__init__.py | 0 .../learner_profile/tests/views/__init__.py | 0 .../tests/views/test_learner_profile.py | 281 ------ openedx/features/learner_profile/toggles.py | 29 - openedx/features/learner_profile/urls.py | 24 - .../learner_profile/views/__init__.py | 0 .../views/learner_achievements.py | 58 -- .../learner_profile/views/learner_profile.py | 128 --- webpack-config/file-lists.js | 3 - 73 files changed, 752 insertions(+), 7391 deletions(-) delete mode 100644 lms/static/js/spec/student_account/account_settings_factory_spec.js delete mode 100644 lms/static/js/spec/student_account/account_settings_fields_helpers.js delete mode 100644 lms/static/js/spec/student_account/account_settings_fields_spec.js delete mode 100644 lms/static/js/spec/student_account/account_settings_view_spec.js delete mode 100644 lms/static/js/student_account/views/account_section_view.js delete mode 100644 lms/static/js/student_account/views/account_settings_factory.js delete mode 100644 lms/static/js/student_account/views/account_settings_fields.js delete mode 100644 lms/static/js/student_account/views/account_settings_view.js delete mode 120000 lms/static/learner_profile delete mode 100644 lms/static/sass/features/_learner-profile.scss delete mode 100644 lms/static/sass/views/_account-settings.scss delete mode 100644 lms/templates/dashboard/_dashboard_third_party_error.html delete mode 100644 lms/templates/student_account/account_settings.html delete mode 100644 lms/templates/student_account/account_settings.underscore delete mode 100644 lms/templates/student_account/account_settings_section.underscore delete mode 100644 openedx/core/djangoapps/user_api/accounts/settings_views.py delete mode 100644 openedx/core/djangoapps/user_api/accounts/tests/test_filters.py delete mode 100644 openedx/core/djangoapps/user_api/accounts/tests/test_settings_views.py delete mode 100644 openedx/core/djangoapps/user_api/accounts/toggles.py delete mode 100644 openedx/features/learner_profile/README.rst delete mode 100644 openedx/features/learner_profile/__init__.py delete mode 100644 openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/section_two_tab_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/section_two_tab.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/section_two.underscore delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/third_party_auth.html delete mode 100644 openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html delete mode 100644 openedx/features/learner_profile/templates/learner_profile/learner_profile.html delete mode 100644 openedx/features/learner_profile/tests/__init__.py delete mode 100644 openedx/features/learner_profile/tests/views/__init__.py delete mode 100644 openedx/features/learner_profile/tests/views/test_learner_profile.py delete mode 100644 openedx/features/learner_profile/toggles.py delete mode 100644 openedx/features/learner_profile/urls.py delete mode 100644 openedx/features/learner_profile/views/__init__.py delete mode 100644 openedx/features/learner_profile/views/learner_achievements.py delete mode 100644 openedx/features/learner_profile/views/learner_profile.py diff --git a/cms/envs/test.py b/cms/envs/test.py index 6a7c17b001..500c8d538d 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -26,7 +26,8 @@ from xmodule.modulestore.modulestore_settings import update_module_store_setting from .common import * # import settings from LMS for consistent behavior with CMS -from lms.envs.test import ( # pylint: disable=wrong-import-order +from lms.envs.test import ( # pylint: disable=wrong-import-order, disable=unused-import + ACCOUNT_MICROFRONTEND_URL, COMPREHENSIVE_THEME_DIRS, # unimport:skip DEFAULT_FILE_STORAGE, ECOMMERCE_API_URL, @@ -35,8 +36,10 @@ from lms.envs.test import ( # pylint: disable=wrong-import-order LOGIN_ISSUE_SUPPORT_LINK, MEDIA_ROOT, MEDIA_URL, + ORDER_HISTORY_MICROFRONTEND_URL, PLATFORM_DESCRIPTION, PLATFORM_NAME, + PROFILE_MICROFRONTEND_URL, REGISTRATION_EXTRA_FIELDS, GRADES_DOWNLOAD, SITE_NAME, @@ -51,28 +54,26 @@ STUDIO_NAME = gettext_lazy("Your Platform 𝓢𝓽𝓾𝓭𝓲𝓸") STUDIO_SHORT_NAME = gettext_lazy("𝓢𝓽𝓾𝓭𝓲𝓸") # Allow all hosts during tests, we use a lot of different ones all over the codebase. -ALLOWED_HOSTS = [ - '*' -] +ALLOWED_HOSTS = ["*"] # mongo connection settings -MONGO_PORT_NUM = int(os.environ.get('EDXAPP_TEST_MONGO_PORT', '27017')) -MONGO_HOST = os.environ.get('EDXAPP_TEST_MONGO_HOST', 'localhost') +MONGO_PORT_NUM = int(os.environ.get("EDXAPP_TEST_MONGO_PORT", "27017")) +MONGO_HOST = os.environ.get("EDXAPP_TEST_MONGO_HOST", "localhost") THIS_UUID = uuid4().hex[:5] -TEST_ROOT = path('test_root') +TEST_ROOT = path("test_root") # Want static files in the same dir for running on jenkins. STATIC_ROOT = TEST_ROOT / "staticfiles" -WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json" +WEBPACK_LOADER["DEFAULT"]["STATS_FILE"] = STATIC_ROOT / "webpack-stats.json" GITHUB_REPO_ROOT = TEST_ROOT / "data" DATA_DIR = TEST_ROOT / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" # For testing "push to lms" -FEATURES['ENABLE_EXPORT_GIT'] = True +FEATURES["ENABLE_EXPORT_GIT"] = True GIT_REPO_EXPORT_DIR = TEST_ROOT / "export_course_repos" # TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing # lint-amnesty, pylint: disable=line-too-long @@ -90,51 +91,47 @@ STATICFILES_DIRS += [ # If we don't add these settings, then Django templates that can't # find pipelined assets will raise a ValueError. # http://stackoverflow.com/questions/12816941/unit-testing-with-django-pipeline -STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage' +STATICFILES_STORAGE = "pipeline.storage.NonPackagingPipelineStorage" STATIC_URL = "/static/" # Update module store settings per defaults for tests update_module_store_settings( MODULESTORE, module_store_options={ - 'default_class': 'xmodule.hidden_block.HiddenBlock', - 'fs_root': TEST_ROOT / "data", + "default_class": "xmodule.hidden_block.HiddenBlock", + "fs_root": TEST_ROOT / "data", }, doc_store_settings={ - 'db': f'test_xmodule_{THIS_UUID}', - 'host': MONGO_HOST, - 'port': MONGO_PORT_NUM, - 'collection': 'test_modulestore', + "db": f"test_xmodule_{THIS_UUID}", + "host": MONGO_HOST, + "port": MONGO_PORT_NUM, + "collection": "test_modulestore", }, ) CONTENTSTORE = { - 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', - 'DOC_STORE_CONFIG': { - 'host': MONGO_HOST, - 'db': f'test_xcontent_{THIS_UUID}', - 'port': MONGO_PORT_NUM, - 'collection': 'dont_trip', + "ENGINE": "xmodule.contentstore.mongo.MongoContentStore", + "DOC_STORE_CONFIG": { + "host": MONGO_HOST, + "db": f"test_xcontent_{THIS_UUID}", + "port": MONGO_PORT_NUM, + "collection": "dont_trip", }, # allow for additional options that can be keyed on a name, e.g. 'trashcan' - 'ADDITIONAL_OPTIONS': { - 'trashcan': { - 'bucket': 'trash_fs' - } - } + "ADDITIONAL_OPTIONS": {"trashcan": {"bucket": "trash_fs"}}, } DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': TEST_ROOT / "db" / "cms.db", - 'ATOMIC_REQUESTS': True, + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": TEST_ROOT / "db" / "cms.db", + "ATOMIC_REQUESTS": True, }, } LMS_BASE = "localhost:8000" LMS_ROOT_URL = f"http://{LMS_BASE}" -FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost" +FEATURES["PREVIEW_LMS_BASE"] = "preview.localhost" CMS_BASE = "localhost:8001" CMS_ROOT_URL = f"http://{CMS_BASE}" @@ -145,49 +142,47 @@ DISCUSSIONS_MICROFRONTEND_URL = "http://discussions-mfe" CACHES = { # This is the cache used for most things. # In staging/prod envs, the sessions also live here. - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'edx_loc_mem_cache', - 'KEY_FUNCTION': 'common.djangoapps.util.memcache.safe_key', + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "edx_loc_mem_cache", + "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", }, - # The general cache is what you get if you use our util.cache. It's used for # things like caching the course.xml file for different A/B test groups. # We set it to be a DummyCache to force reloading of course.xml in dev. # In staging environments, we would grab VERSION from data uploaded by the # push process. - 'general': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', - 'KEY_PREFIX': 'general', - 'VERSION': 4, - 'KEY_FUNCTION': 'common.djangoapps.util.memcache.safe_key', + "general": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + "KEY_PREFIX": "general", + "VERSION": 4, + "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", }, - - 'mongo_metadata_inheritance': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': os.path.join(tempfile.gettempdir(), 'mongo_metadata_inheritance'), - 'TIMEOUT': 300, - 'KEY_FUNCTION': 'common.djangoapps.util.memcache.safe_key', + "mongo_metadata_inheritance": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": os.path.join(tempfile.gettempdir(), "mongo_metadata_inheritance"), + "TIMEOUT": 300, + "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", }, - 'loc_cache': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'edx_location_mem_cache', + "loc_cache": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "edx_location_mem_cache", }, - 'course_structure_cache': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + "course_structure_cache": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", }, } ################################# CELERY ###################################### CELERY_ALWAYS_EAGER = True -CELERY_RESULT_BACKEND = 'django-cache' +CELERY_RESULT_BACKEND = "django-cache" CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION = False # test_status_cancel in cms/cms_user_tasks/test.py is failing without this # @override_setting for BROKER_URL is not working in testcase, so updating here -BROKER_URL = 'memory://localhost/' +BROKER_URL = "memory://localhost/" ########################### Server Ports ################################### @@ -202,99 +197,99 @@ VIDEO_SOURCE_PORT = 8777 ################### Make tests faster # http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/ PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', + "django.contrib.auth.hashers.SHA1PasswordHasher", + "django.contrib.auth.hashers.MD5PasswordHasher", ] # No segment key CMS_SEGMENT_KEY = None -FEATURES['DISABLE_SET_JWT_COOKIES_FOR_TESTS'] = True +FEATURES["DISABLE_SET_JWT_COOKIES_FOR_TESTS"] = True -FEATURES['ENABLE_SERVICE_STATUS'] = True +FEATURES["ENABLE_SERVICE_STATUS"] = True # Toggles embargo on for testing -FEATURES['EMBARGO'] = True +FEATURES["EMBARGO"] = True TEST_THEME = COMMON_ROOT / "test" / "test-theme" # For consistency in user-experience, keep the value of this setting in sync with # the one in lms/envs/test.py -FEATURES['ENABLE_DISCUSSION_SERVICE'] = False +FEATURES["ENABLE_DISCUSSION_SERVICE"] = False # Enable a parental consent age limit for testing PARENTAL_CONSENT_AGE_LIMIT = 13 # Enable certificates for the tests -FEATURES['CERTIFICATES_HTML_VIEW'] = True +FEATURES["CERTIFICATES_HTML_VIEW"] = True # Enable content libraries code for the tests -FEATURES['ENABLE_CONTENT_LIBRARIES'] = True +FEATURES["ENABLE_CONTENT_LIBRARIES"] = True -FEATURES['ENABLE_EDXNOTES'] = True +FEATURES["ENABLE_EDXNOTES"] = True # MILESTONES -FEATURES['MILESTONES_APP'] = True +FEATURES["MILESTONES_APP"] = True # ENTRANCE EXAMS -FEATURES['ENTRANCE_EXAMS'] = True +FEATURES["ENTRANCE_EXAMS"] = True ENTRANCE_EXAM_MIN_SCORE_PCT = 50 -VIDEO_CDN_URL = { - 'CN': 'http://api.xuetangx.com/edx/video?s3_url=' -} +VIDEO_CDN_URL = {"CN": "http://api.xuetangx.com/edx/video?s3_url="} # Courseware Search Index -FEATURES['ENABLE_COURSEWARE_INDEX'] = True -FEATURES['ENABLE_LIBRARY_INDEX'] = True +FEATURES["ENABLE_COURSEWARE_INDEX"] = True +FEATURES["ENABLE_LIBRARY_INDEX"] = True SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" -FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = True +FEATURES["ENABLE_ENROLLMENT_TRACK_USER_PARTITION"] = True ########################## AUTHOR PERMISSION ####################### -FEATURES['ENABLE_CREATOR_GROUP'] = False +FEATURES["ENABLE_CREATOR_GROUP"] = False # teams feature -FEATURES['ENABLE_TEAMS'] = True +FEATURES["ENABLE_TEAMS"] = True # Dummy secret key for dev/test -SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' +SECRET_KEY = "85920908f28904ed733fe576320db18cabd7b6cd" ######### custom courses ######### INSTALLED_APPS += [ - 'openedx.core.djangoapps.ccxcon.apps.CCXConnectorConfig', - 'common.djangoapps.third_party_auth.apps.ThirdPartyAuthConfig', + "openedx.core.djangoapps.ccxcon.apps.CCXConnectorConfig", + "common.djangoapps.third_party_auth.apps.ThirdPartyAuthConfig", ] -FEATURES['CUSTOM_COURSES_EDX'] = True +FEATURES["CUSTOM_COURSES_EDX"] = True ########################## VIDEO IMAGE STORAGE ############################ VIDEO_IMAGE_SETTINGS = dict( - VIDEO_IMAGE_MAX_BYTES=2 * 1024 * 1024, # 2 MB - VIDEO_IMAGE_MIN_BYTES=2 * 1024, # 2 KB + VIDEO_IMAGE_MAX_BYTES=2 * 1024 * 1024, # 2 MB + VIDEO_IMAGE_MIN_BYTES=2 * 1024, # 2 KB STORAGE_KWARGS=dict( location=MEDIA_ROOT, ), - DIRECTORY_PREFIX='video-images/', + DIRECTORY_PREFIX="video-images/", BASE_URL=MEDIA_URL, ) -VIDEO_IMAGE_DEFAULT_FILENAME = 'default_video_image.png' +VIDEO_IMAGE_DEFAULT_FILENAME = "default_video_image.png" ########################## VIDEO TRANSCRIPTS STORAGE ############################ VIDEO_TRANSCRIPTS_SETTINGS = dict( - VIDEO_TRANSCRIPTS_MAX_BYTES=3 * 1024 * 1024, # 3 MB + VIDEO_TRANSCRIPTS_MAX_BYTES=3 * 1024 * 1024, # 3 MB STORAGE_KWARGS=dict( location=MEDIA_ROOT, base_url=MEDIA_URL, ), - DIRECTORY_PREFIX='video-transcripts/', + DIRECTORY_PREFIX="video-transcripts/", ) ####################### Plugin Settings ########################## # pylint: disable=wrong-import-position, wrong-import-order from edx_django_utils.plugins import add_plugins + # pylint: disable=wrong-import-position, wrong-import-order from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType + add_plugins(__name__, ProjectType.CMS, SettingsType.TEST) ########################## Derive Any Derived Settings ####################### @@ -310,22 +305,22 @@ PROCTORING_SETTINGS = {} # Used in edx-proctoring for ID generation in lieu of SECRET_KEY - dummy value # (ref MST-637) -PROCTORING_USER_OBFUSCATION_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' +PROCTORING_USER_OBFUSCATION_KEY = "85920908f28904ed733fe576320db18cabd7b6cd" ##### LOGISTRATION RATE LIMIT SETTINGS ##### -LOGISTRATION_RATELIMIT_RATE = '5/5m' -LOGISTRATION_PER_EMAIL_RATELIMIT_RATE = '6/5m' -LOGISTRATION_API_RATELIMIT = '5/m' +LOGISTRATION_RATELIMIT_RATE = "5/5m" +LOGISTRATION_PER_EMAIL_RATELIMIT_RATE = "6/5m" +LOGISTRATION_API_RATELIMIT = "5/m" -REGISTRATION_VALIDATION_RATELIMIT = '5/minute' -REGISTRATION_RATELIMIT = '5/minute' -OPTIONAL_FIELD_API_RATELIMIT = '5/m' +REGISTRATION_VALIDATION_RATELIMIT = "5/minute" +REGISTRATION_RATELIMIT = "5/minute" +OPTIONAL_FIELD_API_RATELIMIT = "5/m" -RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = '2/m' -RESET_PASSWORD_API_RATELIMIT = '2/m' +RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = "2/m" +RESET_PASSWORD_API_RATELIMIT = "2/m" ############### Settings for proctoring ############### -PROCTORING_USER_OBFUSCATION_KEY = 'test_key' +PROCTORING_USER_OBFUSCATION_KEY = "test_key" #################### Network configuration #################### # Tests are not behind any proxies @@ -339,10 +334,5 @@ COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = { ############## openedx-learning (Learning Core) config ############## OPENEDX_LEARNING = { - 'MEDIA': { - 'BACKEND': 'django.core.files.storage.InMemoryStorage', - 'OPTIONS': { - 'location': MEDIA_ROOT + "_private" - } - } + "MEDIA": {"BACKEND": "django.core.files.storage.InMemoryStorage", "OPTIONS": {"location": MEDIA_ROOT + "_private"}} } diff --git a/common/djangoapps/student/tests/test_filters.py b/common/djangoapps/student/tests/test_filters.py index 376595a850..bf79ed7ae4 100644 --- a/common/djangoapps/student/tests/test_filters.py +++ b/common/djangoapps/student/tests/test_filters.py @@ -1,6 +1,7 @@ """ Test that various filters are fired for models/views in the student app. """ +from django.conf import settings from django.http import HttpResponse from django.test import override_settings from django.urls import reverse @@ -421,7 +422,7 @@ class StudentDashboardFiltersTest(ModuleStoreTestCase): response = self.client.get(self.dashboard_url) self.assertEqual(status.HTTP_302_FOUND, response.status_code) - self.assertEqual(reverse("account_settings"), response.url) + self.assertEqual(settings.ACCOUNT_MICROFRONTEND_URL, response.url) @override_settings( OPEN_EDX_FILTERS_CONFIG={ diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index 15ecdab236..b63c522bbd 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -233,7 +233,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, """ UserProfile.objects.get(user=self.user).delete() response = self.client.get(self.path) - self.assertRedirects(response, reverse('account_settings')) + self.assertRedirects(response, settings.ACCOUNT_MICROFRONTEND_URL, target_status_code=302) @patch('common.djangoapps.student.views.dashboard.learner_home_mfe_enabled') def test_redirect_to_learner_home(self, mock_learner_home_mfe_enabled): diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index f729a2aee1..05279fe8cd 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -518,7 +518,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem """ user = request.user if not UserProfile.objects.filter(user=user).exists(): - return redirect(reverse('account_settings')) + return redirect(settings.ACCOUNT_MICROFRONTEND_URL) if learner_home_mfe_enabled(): return redirect(settings.LEARNER_HOME_MICROFRONTEND_URL) @@ -623,7 +623,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem "Go to {link_start}your Account Settings{link_end}.") ).format( link_start=HTML("").format( - account_setting_page=reverse('account_settings'), + account_setting_page=settings.ACCOUNT_MICROFRONTEND_URL, ), link_end=HTML("") ) @@ -892,7 +892,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem except DashboardRenderStarted.RenderInvalidDashboard as exc: response = render_to_response(exc.dashboard_template, exc.template_context) except DashboardRenderStarted.RedirectToPage as exc: - response = HttpResponseRedirect(exc.redirect_to or reverse('account_settings')) + response = HttpResponseRedirect(exc.redirect_to or settings.ACCOUNT_MICROFRONTEND_URL) except DashboardRenderStarted.RenderCustomResponse as exc: response = exc.response else: diff --git a/common/djangoapps/third_party_auth/api/tests/test_views.py b/common/djangoapps/third_party_auth/api/tests/test_views.py index 948314a067..670caf04c7 100644 --- a/common/djangoapps/third_party_auth/api/tests/test_views.py +++ b/common/djangoapps/third_party_auth/api/tests/test_views.py @@ -2,10 +2,11 @@ Tests for the Third Party Auth REST API """ +import urllib from unittest.mock import patch import ddt -import six +from django.conf import settings from django.http import QueryDict from django.test.utils import override_settings from django.urls import reverse @@ -219,7 +220,7 @@ class UserViewV2APITests(UserViewsMixin, TpaAPITestCase): """ return '?'.join([ reverse('third_party_auth_users_api_v2'), - six.moves.urllib.parse.urlencode(identifier) + urllib.parse.urlencode(identifier) ]) @@ -377,11 +378,12 @@ class TestThirdPartyAuthUserStatusView(ThirdPartyAuthTestMixin, APITestCase): """ self.client.login(username=self.user.username, password=PASSWORD) response = self.client.get(self.url, content_type="application/json") + next_url = urllib.parse.quote(settings.ACCOUNT_MICROFRONTEND_URL, safe="") assert response.status_code == 200 assert (response.data == [{ 'accepts_logins': True, 'name': 'Google', 'disconnect_url': '/auth/disconnect/google-oauth2/?', - 'connect_url': '/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings', + 'connect_url': f'/auth/login/google-oauth2/?auth_entry=account_settings&next={next_url}', 'connected': False, 'id': 'oa2-google-oauth2' }]) diff --git a/common/djangoapps/third_party_auth/api/views.py b/common/djangoapps/third_party_auth/api/views.py index 97d1a7d6db..c1127f8e33 100644 --- a/common/djangoapps/third_party_auth/api/views.py +++ b/common/djangoapps/third_party_auth/api/views.py @@ -9,7 +9,6 @@ from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.db.models import Q from django.http import Http404 -from django.urls import reverse from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from rest_framework import exceptions, permissions, status, throttling @@ -425,7 +424,7 @@ class ThirdPartyAuthUserStatusView(APIView): state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. - redirect_url=reverse('account_settings'), + redirect_url=settings.ACCOUNT_MICROFRONTEND_URL, ), 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index 8f96235017..524cd64ff3 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -2,7 +2,6 @@ Base integration test for provider implementations. """ - import json import unittest from contextlib import contextmanager @@ -11,7 +10,7 @@ from unittest import mock import pytest from django import test from django.conf import settings -from django.contrib import auth +from django.contrib import auth, messages from django.contrib.auth import models as auth_models from django.contrib.messages.storage import fallback from django.contrib.sessions.backends import cache @@ -28,7 +27,6 @@ from openedx.core.djangoapps.user_authn.views.login_form import login_and_regist from openedx.core.djangoapps.user_authn.views.register import RegistrationView from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory -from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context from common.djangoapps.student import models as student_models from common.djangoapps.student.tests.factories import UserFactory @@ -56,9 +54,9 @@ class HelperMixin: test_username (str): username to check the form initialization with. expected (str): expected cleaned username after the form initialization. """ - form_data['username'] = test_username + form_data["username"] = test_username form_field_data = self.provider.get_register_form_data(form_data) - assert form_field_data['username'] == expected + assert form_field_data["username"] == expected def assert_redirect_to_provider_looks_correct(self, response): """Asserts the redirect to the provider's site looks correct. @@ -70,9 +68,11 @@ class HelperMixin: example, more details about the format of the Location header. """ assert 302 == response.status_code - assert response.has_header('Location') + assert response.has_header("Location") - def assert_register_response_in_pipeline_looks_correct(self, response, pipeline_kwargs, required_fields): # lint-amnesty, pylint: disable=invalid-name + def assert_register_response_in_pipeline_looks_correct( + self, response, pipeline_kwargs, required_fields + ): # lint-amnesty, pylint: disable=invalid-name """Performs spot checks of the rendered register.html page. When we display the new account registration form after the user signs @@ -84,10 +84,7 @@ class HelperMixin: assertions in your test, override this method. """ # Check that the correct provider was selected. - self.assertContains( - response, - '"errorMessage": null' - ) + self.assertContains(response, '"errorMessage": null') self.assertContains( response, f'"currentProvider": "{self.provider.name}"', @@ -99,46 +96,68 @@ class HelperMixin: if prepopulated_form_data in required_fields: self.assertContains(response, form_field_data[prepopulated_form_data]) - def assert_register_form_populates_unicode_username_correctly(self, request): # lint-amnesty, pylint: disable=invalid-name + def _get_user_providers_state(self, request): + """ + Return provider user states and duplicated providers. + """ + data = { + "auth": {}, + } + data["duplicate_provider"] = pipeline.get_duplicate_provider(messages.get_messages(request)) + auth_states = pipeline.get_provider_user_states(request.user) + data["auth"]["providers"] = [ + { + "name": state.provider.name, + "connected": state.has_account, + } + for state in auth_states + if state.provider.display_for_login or state.has_account + ] + return data + + def assert_third_party_accounts_state(self, request, duplicate=False, linked=None): + """ + Asserts the user's third party account in the expected state. + + If duplicate is True, we expect data['duplicate_provider'] to contain + the duplicate provider backend name. If linked is passed, we conditionally + check that the provider is included in data['auth']['providers'] and + its connected state is correct. + """ + data = self._get_user_providers_state(request) + if duplicate: + assert data["duplicate_provider"] == self.provider.backend_name + else: + assert data["duplicate_provider"] is None + + if linked is not None: + expected_provider = [ + provider for provider in data["auth"]["providers"] if provider["name"] == self.provider.name + ][0] + assert expected_provider is not None + assert expected_provider["connected"] == linked + + def assert_register_form_populates_unicode_username_correctly( + self, request + ): # lint-amnesty, pylint: disable=invalid-name """ Check the registration form username field behaviour with unicode values. The field could be empty or prefilled depending on whether ENABLE_UNICODE_USERNAME feature is disabled/enabled. """ - unicode_username = 'Червона_Калина' - ascii_substring = 'untouchable' + unicode_username = "Червона_Калина" + ascii_substring = "untouchable" partial_unicode_username = unicode_username + ascii_substring - pipeline_kwargs = pipeline.get(request)['kwargs'] + pipeline_kwargs = pipeline.get(request)["kwargs"] - assert settings.FEATURES['ENABLE_UNICODE_USERNAME'] is False + assert settings.FEATURES["ENABLE_UNICODE_USERNAME"] is False - self._check_registration_form_username(pipeline_kwargs, unicode_username, '') + self._check_registration_form_username(pipeline_kwargs, unicode_username, "") self._check_registration_form_username(pipeline_kwargs, partial_unicode_username, ascii_substring) - with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_UNICODE_USERNAME': True}): + with mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_UNICODE_USERNAME": True}): self._check_registration_form_username(pipeline_kwargs, unicode_username, unicode_username) - # pylint: disable=invalid-name - def assert_account_settings_context_looks_correct(self, context, duplicate=False, linked=None): - """Asserts the user's account settings page context is in the expected state. - - If duplicate is True, we expect context['duplicate_provider'] to contain - the duplicate provider backend name. If linked is passed, we conditionally - check that the provider is included in context['auth']['providers'] and - its connected state is correct. - """ - if duplicate: - assert context['duplicate_provider'] == self.provider.backend_name - else: - assert context['duplicate_provider'] is None - - if linked is not None: - expected_provider = [ - provider for provider in context['auth']['providers'] if provider['name'] == self.provider.name - ][0] - assert expected_provider is not None - assert expected_provider['connected'] == linked - def assert_exception_redirect_looks_correct(self, expected_uri, auth_entry=None): """Tests middleware conditional redirection. @@ -147,49 +166,48 @@ class HelperMixin: """ exception_middleware = middleware.ExceptionMiddleware(get_response=lambda request: None) request, _ = self.get_request_and_strategy(auth_entry=auth_entry) - response = exception_middleware.process_exception( - request, exceptions.AuthCanceled(request.backend)) - location = response.get('Location') + response = exception_middleware.process_exception(request, exceptions.AuthCanceled(request.backend)) + location = response.get("Location") assert 302 == response.status_code - assert 'canceled' in location + assert "canceled" in location assert self.backend_name in location - assert location.startswith(expected_uri + '?') + assert location.startswith(expected_uri + "?") def assert_json_failure_response_is_inactive_account(self, response): """Asserts failure on /login for inactive account looks right.""" assert 400 == response.status_code - payload = json.loads(response.content.decode('utf-8')) + payload = json.loads(response.content.decode("utf-8")) context = { - 'platformName': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), - 'supportLink': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK) + "platformName": configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), + "supportLink": configuration_helpers.get_value("SUPPORT_SITE_LINK", settings.SUPPORT_SITE_LINK), } - assert not payload.get('success') - assert 'inactive-user' in payload.get('error_code') - assert context == payload.get('context') + assert not payload.get("success") + assert "inactive-user" in payload.get("error_code") + assert context == payload.get("context") def assert_json_failure_response_is_missing_social_auth(self, response): """Asserts failure on /login for missing social auth looks right.""" assert 403 == response.status_code - payload = json.loads(response.content.decode('utf-8')) - assert not payload.get('success') - assert payload.get('error_code') == 'third-party-auth-with-no-linked-account' + payload = json.loads(response.content.decode("utf-8")) + assert not payload.get("success") + assert payload.get("error_code") == "third-party-auth-with-no-linked-account" def assert_json_failure_response_is_username_collision(self, response): """Asserts the json response indicates a username collision.""" assert 409 == response.status_code - payload = json.loads(response.content.decode('utf-8')) - assert not payload.get('success') - assert 'It looks like this username is already taken' == payload['username'][0]['user_message'] + payload = json.loads(response.content.decode("utf-8")) + assert not payload.get("success") + assert "It looks like this username is already taken" == payload["username"][0]["user_message"] def assert_json_success_response_looks_correct(self, response, verify_redirect_url): """Asserts the json response indicates success and redirection.""" assert 200 == response.status_code - payload = json.loads(response.content.decode('utf-8')) - assert payload.get('success') + payload = json.loads(response.content.decode("utf-8")) + assert payload.get("success") if verify_redirect_url: - assert pipeline.get_complete_url(self.provider.backend_name) == payload.get('redirect_url') + assert pipeline.get_complete_url(self.provider.backend_name) == payload.get("redirect_url") def assert_login_response_before_pipeline_looks_correct(self, response): """Asserts a GET of /login not in the pipeline looks correct.""" @@ -218,19 +236,19 @@ class HelperMixin: assert 302 == response.status_code # NOTE: Ideally we should use assertRedirects(), however it errors out due to the hostname, testserver, # not being properly set. This may be an issue with the call made by PSA, but we are not certain. - assert response.get('Location').endswith( + assert response.get("Location").endswith( expected_redirect_url or django_settings.SOCIAL_AUTH_LOGIN_REDIRECT_URL ) def assert_redirect_to_login_looks_correct(self, response): """Asserts a response would redirect to /login.""" assert 302 == response.status_code - assert '/login' == response.get('Location') + assert "/login" == response.get("Location") def assert_redirect_to_register_looks_correct(self, response): """Asserts a response would redirect to /register.""" assert 302 == response.status_code - assert '/register' == response.get('Location') + assert "/register" == response.get("Location") def assert_register_response_before_pipeline_looks_correct(self, response): """Asserts a GET of /register not in the pipeline looks correct.""" @@ -241,43 +259,41 @@ class HelperMixin: def assert_social_auth_does_not_exist_for_user(self, user, strategy): """Asserts a user does not have an auth with the expected provider.""" - social_auths = strategy.storage.user.get_social_auth_for_user( - user, provider=self.provider.backend_name) + social_auths = strategy.storage.user.get_social_auth_for_user(user, provider=self.provider.backend_name) assert 0 == len(social_auths) def assert_social_auth_exists_for_user(self, user, strategy): """Asserts a user has a social auth with the expected provider.""" - social_auths = strategy.storage.user.get_social_auth_for_user( - user, provider=self.provider.backend_name) + social_auths = strategy.storage.user.get_social_auth_for_user(user, provider=self.provider.backend_name) assert 1 == len(social_auths) assert self.backend_name == social_auths[0].provider def assert_logged_in_cookie_redirect(self, response): - """Verify that the user was redirected in order to set the logged in cookie. """ + """Verify that the user was redirected in order to set the logged in cookie.""" assert response.status_code == 302 - assert response['Location'] == pipeline.get_complete_url(self.provider.backend_name) - assert response.cookies[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME].value == 'true' + assert response["Location"] == pipeline.get_complete_url(self.provider.backend_name) + assert response.cookies[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME].value == "true" assert django_settings.EDXMKTG_USER_INFO_COOKIE_NAME in response.cookies @property def backend_name(self): - """ Shortcut for the backend name """ + """Shortcut for the backend name""" return self.provider.backend_name def get_registration_post_vars(self, overrides=None): """POST vars generated by the registration form.""" defaults = { - 'username': 'username', - 'name': 'First Last', - 'gender': '', - 'year_of_birth': '', - 'level_of_education': '', - 'goals': '', - 'honor_code': 'true', - 'terms_of_service': 'true', - 'password': 'password', - 'mailing_address': '', - 'email': 'user@email.com', + "username": "username", + "name": "First Last", + "gender": "", + "year_of_birth": "", + "level_of_education": "", + "goals": "", + "honor_code": "true", + "terms_of_service": "true", + "password": "password", + "mailing_address": "", + "email": "user@email.com", } if overrides: @@ -294,12 +310,13 @@ class HelperMixin: social_django.utils.strategy(). """ request = self.request_factory.get( - pipeline.get_complete_url(self.backend_name) + - '?redirect_state=redirect_state_value&code=code_value&state=state_value') + pipeline.get_complete_url(self.backend_name) + + "?redirect_state=redirect_state_value&code=code_value&state=state_value" + ) request.site = SiteFactory.create() request.user = auth_models.AnonymousUser() request.session = cache.SessionStore() - request.session[self.backend_name + '_state'] = 'state_value' + request.session[self.backend_name + "_state"] = "state_value" if auth_entry: request.session[pipeline.AUTH_ENTRY_KEY] = auth_entry @@ -312,7 +329,7 @@ class HelperMixin: def _get_login_post_request(self, strategy): """Gets a fully-configured login POST request given a strategy and pipeline.""" - request = self.request_factory.post(reverse('login_api')) + request = self.request_factory.post(reverse("login_api")) # Note: The shared GET request can't be used for login, which is now POST-only, # so this POST request is given a copy of all configuration from the GET request @@ -329,7 +346,7 @@ class HelperMixin: def _patch_edxmako_current_request(self, request): """Make ``request`` be the current request for edxmako template rendering.""" - with mock.patch('common.djangoapps.edxmako.request_context.get_current_request', return_value=request): + with mock.patch("common.djangoapps.edxmako.request_context.get_current_request", return_value=request): yield def get_user_by_email(self, strategy, email): @@ -337,11 +354,13 @@ class HelperMixin: return strategy.storage.user.user_model().objects.get(email=email) def set_logged_in_cookies(self, request): - """Simulate setting the marketing site cookie on the request. """ - request.COOKIES[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME] = 'true' - request.COOKIES[django_settings.EDXMKTG_USER_INFO_COOKIE_NAME] = json.dumps({ - 'version': django_settings.EDXMKTG_USER_INFO_COOKIE_VERSION, - }) + """Simulate setting the marketing site cookie on the request.""" + request.COOKIES[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME] = "true" + request.COOKIES[django_settings.EDXMKTG_USER_INFO_COOKIE_NAME] = json.dumps( + { + "version": django_settings.EDXMKTG_USER_INFO_COOKIE_VERSION, + } + ) def create_user_models_for_existing_account(self, strategy, email, password, username, skip_social_auth=False): """Creates user, profile, registration, and (usually) social auth. @@ -371,10 +390,10 @@ class HelperMixin: """ args = () kwargs = { - 'request': strategy.request, - 'backend': strategy.request.backend, - 'user': None, - 'response': self.get_response_data(), + "request": strategy.request, + "backend": strategy.request.backend, + "user": None, + "response": self.get_response_data(), } return strategy.authenticate(*args, **kwargs) @@ -386,6 +405,7 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): currently less comprehensive. Some providers are tested with this, others with IntegrationTest. """ + # Provider information: PROVIDER_NAME = "override" PROVIDER_BACKEND = "override" @@ -399,8 +419,8 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): super().setUp() self.request_factory = test.RequestFactory() - self.login_page_url = reverse('signin_user') - self.register_page_url = reverse('register_user') + self.login_page_url = reverse("signin_user") + self.register_page_url = reverse("register_user") patcher = testutil.patch_mako_templates() patcher.start() self.addCleanup(patcher.stop) @@ -415,47 +435,44 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): try_login_response = self.client.get(provider_register_url) # The user should be redirected to the provider's login page: assert try_login_response.status_code == 302 - provider_response = self.do_provider_login(try_login_response['Location']) + provider_response = self.do_provider_login(try_login_response["Location"]) # We should be redirected to the register screen since this account is not linked to an edX account: assert provider_response.status_code == 302 - assert provider_response['Location'] == self.register_page_url + assert provider_response["Location"] == self.register_page_url register_response = self.client.get(self.register_page_url) tpa_context = register_response.context["data"]["third_party_auth"] - assert tpa_context['errorMessage'] is None + assert tpa_context["errorMessage"] is None # Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown. - assert tpa_context['currentProvider'] == self.PROVIDER_NAME + assert tpa_context["currentProvider"] == self.PROVIDER_NAME # Check that the data (e.g. email) from the provider is displayed in the form: - form_data = register_response.context['data']['registration_form_desc'] - form_fields = {field['name']: field for field in form_data['fields']} - assert form_fields['email']['defaultValue'] == self.USER_EMAIL - assert form_fields['name']['defaultValue'] == self.USER_NAME - assert form_fields['username']['defaultValue'] == self.USER_USERNAME + form_data = register_response.context["data"]["registration_form_desc"] + form_fields = {field["name"]: field for field in form_data["fields"]} + assert form_fields["email"]["defaultValue"] == self.USER_EMAIL + assert form_fields["name"]["defaultValue"] == self.USER_NAME + assert form_fields["username"]["defaultValue"] == self.USER_USERNAME for field_name, value in extra_defaults.items(): - assert form_fields[field_name]['defaultValue'] == value + assert form_fields[field_name]["defaultValue"] == value registration_values = { - 'email': 'email-edited@tpa-test.none', - 'name': 'My Customized Name', - 'username': 'new_username', - 'honor_code': True, + "email": "email-edited@tpa-test.none", + "name": "My Customized Name", + "username": "new_username", + "honor_code": True, } # Now complete the form: - ajax_register_response = self.client.post( - reverse('user_api_registration'), - registration_values - ) + ajax_register_response = self.client.post(reverse("user_api_registration"), registration_values) assert ajax_register_response.status_code == 200 # Then the AJAX will finish the third party auth: continue_response = self.client.get(tpa_context["finishAuthUrl"]) # And we should be redirected to the dashboard: assert continue_response.status_code == 302 - assert continue_response['Location'] == reverse('dashboard') + assert continue_response["Location"] == reverse("dashboard") # Now check that we can login again, whether or not we have yet verified the account: self.client.logout() self._test_return_login(user_is_activated=False) self.client.logout() - self.verify_user_email('email-edited@tpa-test.none') + self.verify_user_email("email-edited@tpa-test.none") self._test_return_login(user_is_activated=True) def _test_login(self): @@ -468,27 +485,27 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): try_login_response = self.client.get(provider_login_url) # The user should be redirected to the provider's login page: assert try_login_response.status_code == 302 - complete_response = self.do_provider_login(try_login_response['Location']) + complete_response = self.do_provider_login(try_login_response["Location"]) # We should be redirected to the login screen since this account is not linked to an edX account: assert complete_response.status_code == 302 - assert complete_response['Location'] == self.login_page_url + assert complete_response["Location"] == self.login_page_url login_response = self.client.get(self.login_page_url) tpa_context = login_response.context["data"]["third_party_auth"] - assert tpa_context['errorMessage'] is None + assert tpa_context["errorMessage"] is None # Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown. - assert tpa_context['currentProvider'] == self.PROVIDER_NAME + assert tpa_context["currentProvider"] == self.PROVIDER_NAME # Now the user enters their username and password. # The AJAX on the page will log them in: ajax_login_response = self.client.post( - reverse('user_api_login_session', kwargs={'api_version': 'v1'}), - {'email': self.user.email, 'password': 'Password1234'} + reverse("user_api_login_session", kwargs={"api_version": "v1"}), + {"email": self.user.email, "password": "Password1234"}, ) assert ajax_login_response.status_code == 200 # Then the AJAX will finish the third party auth: continue_response = self.client.get(tpa_context["finishAuthUrl"]) # And we should be redirected to the dashboard: assert continue_response.status_code == 302 - assert continue_response['Location'] == reverse('dashboard') + assert continue_response["Location"] == reverse("dashboard") # Now check that we can login again: self.client.logout() @@ -502,9 +519,9 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): raise NotImplementedError def _test_return_login(self, user_is_activated=True, previous_session_timed_out=False): - """ Test logging in to an account that is already linked. """ + """Test logging in to an account that is already linked.""" # Make sure we're not logged in: - dashboard_response = self.client.get(reverse('dashboard')) + dashboard_response = self.client.get(reverse("dashboard")) assert dashboard_response.status_code == 302 # The user goes to the login page, and sees a button to login with this provider: provider_login_url = self._check_login_page() @@ -512,22 +529,22 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): try_login_response = self.client.get(provider_login_url) # The user should be redirected to the provider: assert try_login_response.status_code == 302 - login_response = self.do_provider_login(try_login_response['Location']) + login_response = self.do_provider_login(try_login_response["Location"]) # If the previous session was manually logged out, there will be one weird redirect # required to set the login cookie (it sticks around if the main session times out): if not previous_session_timed_out: assert login_response.status_code == 302 - assert login_response['Location'] == (self.complete_url + '?') + assert login_response["Location"] == (self.complete_url + "?") # And then we should be redirected to the dashboard: - login_response = self.client.get(login_response['Location']) + login_response = self.client.get(login_response["Location"]) assert login_response.status_code == 302 if user_is_activated: - url_expected = reverse('dashboard') + url_expected = reverse("dashboard") else: - url_expected = reverse('third_party_inactive_redirect') + '?next=' + reverse('dashboard') - assert login_response['Location'] == url_expected + url_expected = reverse("third_party_inactive_redirect") + "?next=" + reverse("dashboard") + assert login_response["Location"] == url_expected # Now we are logged in: - dashboard_response = self.client.get(reverse('dashboard')) + dashboard_response = self.client.get(reverse("dashboard")) assert dashboard_response.status_code == 200 def _check_login_page(self): @@ -545,22 +562,23 @@ class IntegrationTestMixin(testutil.TestCase, test.TestCase, HelperMixin): return self._check_login_or_register_page(self.register_page_url, "registerUrl") def _check_login_or_register_page(self, url, url_to_return): - """ Shared logic for _check_login_page() and _check_register_page() """ + """Shared logic for _check_login_page() and _check_register_page()""" response = self.client.get(url) self.assertContains(response, self.PROVIDER_NAME) - context_data = response.context['data']['third_party_auth'] - provider_urls = {provider['id']: provider[url_to_return] for provider in context_data['providers']} + context_data = response.context["data"]["third_party_auth"] + provider_urls = {provider["id"]: provider[url_to_return] for provider in context_data["providers"]} assert self.PROVIDER_ID in provider_urls return provider_urls[self.PROVIDER_ID] @property def complete_url(self): - """ Get the auth completion URL for this provider """ - return reverse('social:complete', kwargs={'backend': self.PROVIDER_BACKEND}) + """Get the auth completion URL for this provider""" + return reverse("social:complete", kwargs={"backend": self.PROVIDER_BACKEND}) @unittest.skipUnless( - testutil.AUTH_FEATURES_KEY in django_settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES') + testutil.AUTH_FEATURES_KEY in django_settings.FEATURES, testutil.AUTH_FEATURES_KEY + " not in settings.FEATURES" +) @django_utils.override_settings() # For settings reversion on a method-by-method basis. class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): """Abstract base class for provider integration tests.""" @@ -572,46 +590,51 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # Actual tests, executed once per child. def test_canceling_authentication_redirects_to_login_when_auth_entry_login(self): - self.assert_exception_redirect_looks_correct('/login', auth_entry=pipeline.AUTH_ENTRY_LOGIN) + self.assert_exception_redirect_looks_correct("/login", auth_entry=pipeline.AUTH_ENTRY_LOGIN) def test_canceling_authentication_redirects_to_register_when_auth_entry_register(self): - self.assert_exception_redirect_looks_correct('/register', auth_entry=pipeline.AUTH_ENTRY_REGISTER) + self.assert_exception_redirect_looks_correct("/register", auth_entry=pipeline.AUTH_ENTRY_REGISTER) def test_canceling_authentication_redirects_to_account_settings_when_auth_entry_account_settings(self): self.assert_exception_redirect_looks_correct( - '/account/settings', auth_entry=pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS + "/account/settings", auth_entry=pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS ) def test_canceling_authentication_redirects_to_root_when_auth_entry_not_set(self): - self.assert_exception_redirect_looks_correct('/') + self.assert_exception_redirect_looks_correct("/") - @mock.patch('common.djangoapps.third_party_auth.pipeline.segment.track') + @mock.patch("common.djangoapps.third_party_auth.pipeline.segment.track") def test_full_pipeline_succeeds_for_linking_account(self, _mock_segment_track): # First, create, the GET request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. get_request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) get_request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) get_request.user = self.create_user_models_for_existing_account( - strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True) - partial_pipeline_token = strategy.session_get('partial_pipeline_token') + strategy, "user@example.com", "password", self.get_username(), skip_social_auth=True + ) + partial_pipeline_token = strategy.session_get("partial_pipeline_token") partial_data = strategy.storage.partial.load(partial_pipeline_token) # Instrument the pipeline to get to the dashboard with the full # expected state. - self.client.get( - pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(get_request.backend, social_views._do_login, # pylint: disable=protected-access - request=get_request) + self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) + actions.do_complete( + get_request.backend, social_views._do_login, request=get_request # pylint: disable=protected-access + ) post_request = self._get_login_post_request(strategy) login_user(post_request) - actions.do_complete(post_request.backend, social_views._do_login, # pylint: disable=protected-access, no-member - request=post_request) + actions.do_complete( + post_request.backend, + social_views._do_login, # pylint: disable=protected-access, no-member + request=post_request, + ) # First we expect that we're in the unlinked state, and that there # really is no association in the backend. - self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=False) + self.assert_third_party_accounts_state(get_request, linked=False) self.assert_social_auth_does_not_exist_for_user(get_request.user, strategy) # We should be redirected back to the complete page, setting @@ -630,16 +653,18 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # Now we expect to be in the linked state, with a backend entry. self.assert_social_auth_exists_for_user(get_request.user, strategy) - self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=True) + self.assert_third_party_accounts_state(get_request, linked=True) def test_full_pipeline_succeeds_for_unlinking_account(self): # First, create, the GET request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. get_request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) get_request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( - strategy, 'user@example.com', 'password', self.get_username()) + strategy, "user@example.com", "password", self.get_username() + ) self.assert_social_auth_exists_for_user(user, strategy) # We're already logged in, so simulate that the cookie is set correctly @@ -647,36 +672,37 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # Instrument the pipeline to get to the dashboard with the full # expected state. - self.client.get( - pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(get_request.backend, social_views._do_login, # pylint: disable=protected-access - request=get_request) + self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) + actions.do_complete( + get_request.backend, social_views._do_login, request=get_request # pylint: disable=protected-access + ) post_request = self._get_login_post_request(strategy) with self._patch_edxmako_current_request(post_request): login_user(post_request) - actions.do_complete(post_request.backend, social_views._do_login, user=user, # pylint: disable=protected-access, no-member - request=post_request) + actions.do_complete( + post_request.backend, + social_views._do_login, # pylint: disable=protected-access + user=user, # pylint: disable=no-member + request=post_request, + ) # Copy the user that was set on the post_request object back to the original get_request object. get_request.user = post_request.user # First we expect that we're in the linked state, with a backend entry. - self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=True) + self.assert_third_party_accounts_state(get_request, linked=True) self.assert_social_auth_exists_for_user(get_request.user, strategy) # Fire off the disconnect pipeline to unlink. self.assert_redirect_after_pipeline_completes( actions.do_disconnect( - get_request.backend, - get_request.user, - None, - redirect_field_name=auth.REDIRECT_FIELD_NAME + get_request.backend, get_request.user, None, redirect_field_name=auth.REDIRECT_FIELD_NAME ) ) # Now we expect to be in the unlinked state, with no backend entry. - self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=False) + self.assert_third_party_accounts_state(get_request, linked=False) self.assert_social_auth_does_not_exist_for_user(user, strategy) def test_linking_already_associated_account_raises_auth_already_associated(self): @@ -684,16 +710,18 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # test_already_associated_exception_populates_dashboard_with_error. It # verifies the exception gets raised when we expect; the latter test # covers exception handling. - email = 'user@example.com' - password = 'password' + email = "user@example.com" + password = "password" username = self.get_username() _, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) backend = strategy.request.backend backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) linked_user = self.create_user_models_for_existing_account(strategy, email, password, username) unlinked_user = social_utils.Storage.user.create_user( - email='other_' + email, password=password, username='other_' + username) + email="other_" + email, password=password, username="other_" + username + ) self.assert_social_auth_exists_for_user(linked_user, strategy) self.assert_social_auth_does_not_exist_for_user(unlinked_user, strategy) @@ -711,42 +739,50 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # covered in other tests. Using linked=True does, however, let us test # that the duplicate error has no effect on the state of the controls. get_request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( - strategy, 'user@example.com', 'password', self.get_username()) + strategy, "user@example.com", "password", self.get_username() + ) self.assert_social_auth_exists_for_user(user, strategy) - self.client.get('/login') + self.client.get("/login") self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(get_request.backend, social_views._do_login, # pylint: disable=protected-access - request=get_request) + actions.do_complete( + get_request.backend, social_views._do_login, request=get_request # pylint: disable=protected-access + ) post_request = self._get_login_post_request(strategy) with self._patch_edxmako_current_request(post_request): login_user(post_request) - actions.do_complete(post_request.backend, social_views._do_login, # pylint: disable=protected-access, no-member - user=user, request=post_request) + actions.do_complete( + post_request.backend, + social_views._do_login, # pylint: disable=protected-access, no-member + user=user, + request=post_request, + ) # Monkey-patch storage for messaging; pylint: disable=protected-access post_request._messages = fallback.FallbackStorage(post_request) middleware.ExceptionMiddleware(get_response=lambda request: None).process_exception( - post_request, - exceptions.AuthAlreadyAssociated(self.provider.backend_name, 'account is already in use.')) + post_request, exceptions.AuthAlreadyAssociated(self.provider.backend_name, "account is already in use.") + ) - self.assert_account_settings_context_looks_correct( - account_settings_context(post_request), duplicate=True, linked=True) + self.assert_third_party_accounts_state(post_request, duplicate=True, linked=True) - @mock.patch('common.djangoapps.third_party_auth.pipeline.segment.track') + @mock.patch("common.djangoapps.third_party_auth.pipeline.segment.track") def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self, _mock_segment_track): # First, create, the GET request and strategy that store pipeline state, # configure the backend, and mock out wire traffic. get_request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( - strategy, 'user@example.com', 'password', self.get_username()) - partial_pipeline_token = strategy.session_get('partial_pipeline_token') + strategy, "user@example.com", "password", self.get_username() + ) + partial_pipeline_token = strategy.session_get("partial_pipeline_token") partial_data = strategy.storage.partial.load(partial_pipeline_token) self.assert_social_auth_exists_for_user(user, strategy) @@ -754,19 +790,21 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # Begin! Ensure that the login form contains expected controls before # the user starts the pipeline. - self.assert_login_response_before_pipeline_looks_correct(self.client.get('/login')) + self.assert_login_response_before_pipeline_looks_correct(self.client.get("/login")) # The pipeline starts by a user GETting /auth/login/. # Synthesize that request and check that it redirects to the correct # provider page. - self.assert_redirect_to_provider_looks_correct(self.client.get( - pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) + self.assert_redirect_to_provider_looks_correct( + self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) + ) # Next, the provider makes a request against /auth/complete/ # to resume the pipeline. # pylint: disable=protected-access - self.assert_redirect_to_login_looks_correct(actions.do_complete(get_request.backend, social_views._do_login, - request=get_request)) + self.assert_redirect_to_login_looks_correct( + actions.do_complete(get_request.backend, social_views._do_login, request=get_request) + ) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the login form and posts it via JS. @@ -781,10 +819,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # We should be redirected back to the complete page, setting # the "logged in" cookie for the marketing site. - self.assert_logged_in_cookie_redirect(actions.do_complete( - post_request.backend, social_views._do_login, post_request.user, None, # pylint: disable=protected-access, no-member - redirect_field_name=auth.REDIRECT_FIELD_NAME, request=post_request - )) + self.assert_logged_in_cookie_redirect( + actions.do_complete( + post_request.backend, + social_views._do_login, + post_request.user, + None, # pylint: disable=protected-access, no-member + redirect_field_name=auth.REDIRECT_FIELD_NAME, + request=post_request, + ) + ) # Set the cookie and try again self.set_logged_in_cookies(get_request) @@ -795,14 +839,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.assert_redirect_after_pipeline_completes( self.do_complete(strategy, get_request, partial_pipeline_token, partial_data, user) ) - self.assert_account_settings_context_looks_correct(account_settings_context(get_request)) + self.assert_third_party_accounts_state(get_request) def test_signin_fails_if_account_not_active(self): _, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) - user = self.create_user_models_for_existing_account(strategy, 'user@example.com', 'password', - self.get_username()) + user = self.create_user_models_for_existing_account( + strategy, "user@example.com", "password", self.get_username() + ) user.is_active = False user.save() @@ -813,25 +859,28 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): def test_signin_fails_if_no_account_associated(self): _, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) self.create_user_models_for_existing_account( - strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True) + strategy, "user@example.com", "password", self.get_username(), skip_social_auth=True + ) post_request = self._get_login_post_request(strategy) self.assert_json_failure_response_is_missing_social_auth(login_user(post_request)) def test_signin_associates_user_if_oauth_provider_and_tpa_is_required(self): - username, email, password = self.get_username(), 'user@example.com', 'password' + username, email, password = self.get_username(), "user@example.com", "password" _, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) user = self.create_user_models_for_existing_account(strategy, email, password, username, skip_social_auth=True) with mock.patch( - 'common.djangoapps.third_party_auth.pipeline.get_associated_user_by_email_response', - return_value=[{'user': user}, True], + "common.djangoapps.third_party_auth.pipeline.get_associated_user_by_email_response", + return_value=[{"user": user}, True], ): strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) @@ -839,30 +888,37 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.assert_json_success_response_looks_correct(login_user(post_request), verify_redirect_url=True) def test_first_party_auth_trumps_third_party_auth_but_is_invalid_when_only_email_in_request(self): - self.assert_first_party_auth_trumps_third_party_auth(email='user@example.com') + self.assert_first_party_auth_trumps_third_party_auth(email="user@example.com") def test_first_party_auth_trumps_third_party_auth_but_is_invalid_when_only_password_in_request(self): - self.assert_first_party_auth_trumps_third_party_auth(password='password') + self.assert_first_party_auth_trumps_third_party_auth(password="password") def test_first_party_auth_trumps_third_party_auth_and_fails_when_credentials_bad(self): self.assert_first_party_auth_trumps_third_party_auth( - email='user@example.com', password='password', success=False) + email="user@example.com", password="password", success=False + ) def test_first_party_auth_trumps_third_party_auth_and_succeeds_when_credentials_good(self): self.assert_first_party_auth_trumps_third_party_auth( - email='user@example.com', password='password', success=True) + email="user@example.com", password="password", success=True + ) def test_pipeline_redirects_to_requested_url(self): - requested_redirect_url = 'foo' # something different from '/dashboard' - request, strategy = self.get_request_and_strategy(redirect_uri='social:complete') + requested_redirect_url = "foo" # something different from '/dashboard' + request, strategy = self.get_request_and_strategy(redirect_uri="social:complete") strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) request.session[pipeline.AUTH_REDIRECT_KEY] = requested_redirect_url - user = self.create_user_models_for_existing_account(strategy, 'user@foo.com', 'password', self.get_username()) + user = self.create_user_models_for_existing_account(strategy, "user@foo.com", "password", self.get_username()) self.set_logged_in_cookies(request) self.assert_redirect_after_pipeline_completes( - actions.do_complete(request.backend, social_views._do_login, user=user, request=request), # pylint: disable=protected-access + actions.do_complete( + request.backend, + social_views._do_login, # pylint: disable=protected-access + user=user, + request=request, + ), requested_redirect_url, ) @@ -870,44 +926,47 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): # First, create, the request and strategy that store pipeline state. # Mock out wire traffic. request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri="social:complete" + ) strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) - partial_pipeline_token = strategy.session_get('partial_pipeline_token') + partial_pipeline_token = strategy.session_get("partial_pipeline_token") partial_data = strategy.storage.partial.load(partial_pipeline_token) # Begin! Grab the registration page and check the login control on it. - self.assert_register_response_before_pipeline_looks_correct(self.client.get('/register')) + self.assert_register_response_before_pipeline_looks_correct(self.client.get("/register")) # The pipeline starts by a user GETting /auth/login/. # Synthesize that request and check that it redirects to the correct # provider page. - self.assert_redirect_to_provider_looks_correct(self.client.get( - pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) + self.assert_redirect_to_provider_looks_correct( + self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) + ) # Next, the provider makes a request against /auth/complete/. # pylint: disable=protected-access - self.assert_redirect_to_register_looks_correct(actions.do_complete(request.backend, social_views._do_login, - request=request)) + self.assert_redirect_to_register_looks_correct( + actions.do_complete(request.backend, social_views._do_login, request=request) + ) # At this point we know the pipeline has resumed correctly. Next we # fire off the view that displays the registration form. with self._patch_edxmako_current_request(request): self.assert_register_form_populates_unicode_username_correctly(request) self.assert_register_response_in_pipeline_looks_correct( - login_and_registration_form(strategy.request, initial_mode='register'), - pipeline.get(request)['kwargs'], - ['name', 'username', 'email'] + login_and_registration_form(strategy.request, initial_mode="register"), + pipeline.get(request)["kwargs"], + ["name", "username", "email"], ) # Next, we invoke the view that handles the POST. Not all providers # supply email. Manually add it as the user would have to; this # also serves as a test of overriding provider values. Always provide a # password for us to check that we override it properly. - overridden_password = strategy.request.POST.get('password') - email = 'new@example.com' + overridden_password = strategy.request.POST.get("password") + email = "new@example.com" - if not strategy.request.POST.get('email'): - strategy.request.POST = self.get_registration_post_vars({'email': email}) + if not strategy.request.POST.get("email"): + strategy.request.POST = self.get_registration_post_vars({"email": email}) # The user must not exist yet... with pytest.raises(auth_models.User.DoesNotExist): @@ -935,41 +994,44 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.assert_redirect_after_pipeline_completes( self.do_complete(strategy, request, partial_pipeline_token, partial_data, created_user) ) - # Now the user has been redirected to the dashboard. Their third party account should now be linked. + # Their third party account should now be linked. self.assert_social_auth_exists_for_user(created_user, strategy) - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True) + self.assert_third_party_accounts_state(request, linked=True) def test_new_account_registration_assigns_distinct_username_on_collision(self): original_username = self.get_username() request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri="social:complete" + ) # Create a colliding username in the backend, then proceed with # assignment via pipeline to make sure a distinct username is created. - strategy.storage.user.create_user(username=self.get_username(), email='user@email.com', password='password') + strategy.storage.user.create_user(username=self.get_username(), email="user@email.com", password="password") backend = strategy.request.backend backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) # pylint: disable=protected-access response = actions.do_complete(backend, social_views._do_login, request=request) assert response.status_code == 302 - response = json.loads(create_account(strategy.request).content.decode('utf-8')) - assert response['username'] != original_username + response = json.loads(create_account(strategy.request).content.decode("utf-8")) + assert response["username"] != original_username def test_new_account_registration_fails_if_email_exists(self): request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_REGISTER, redirect_uri="social:complete" + ) backend = strategy.request.backend backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) # pylint: disable=protected-access - self.assert_redirect_to_register_looks_correct(actions.do_complete(backend, social_views._do_login, - request=request)) + self.assert_redirect_to_register_looks_correct( + actions.do_complete(backend, social_views._do_login, request=request) + ) with self._patch_edxmako_current_request(request): self.assert_register_response_in_pipeline_looks_correct( - login_and_registration_form(strategy.request, initial_mode='register'), - pipeline.get(request)['kwargs'], - ['name', 'username', 'email'] + login_and_registration_form(strategy.request, initial_mode="register"), + pipeline.get(request)["kwargs"], + ["name", "username", "email"], ) with self._patch_edxmako_current_request(strategy.request): @@ -979,18 +1041,18 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): self.assert_json_failure_response_is_username_collision(create_account(strategy.request)) def test_pipeline_raises_auth_entry_error_if_auth_entry_invalid(self): - auth_entry = 'invalid' + auth_entry = "invalid" assert auth_entry not in pipeline._AUTH_ENTRY_CHOICES # pylint: disable=protected-access - _, strategy = self.get_request_and_strategy(auth_entry=auth_entry, redirect_uri='social:complete') + _, strategy = self.get_request_and_strategy(auth_entry=auth_entry, redirect_uri="social:complete") with pytest.raises(pipeline.AuthEntryError): strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) def test_pipeline_assumes_login_if_auth_entry_missing(self): - _, strategy = self.get_request_and_strategy(auth_entry=None, redirect_uri='social:complete') + _, strategy = self.get_request_and_strategy(auth_entry=None, redirect_uri="social:complete") response = self.fake_auth_complete(strategy) - assert response.url == reverse('signin_user') + assert response.url == reverse("signin_user") def assert_first_party_auth_trumps_third_party_auth(self, email=None, password=None, success=None): """Asserts first party auth was used in place of third party auth. @@ -1004,33 +1066,35 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): one of username or password will be missing). """ _, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) self.create_user_models_for_existing_account( - strategy, email, password, self.get_username(), skip_social_auth=True) + strategy, email, password, self.get_username(), skip_social_auth=True + ) post_request = self._get_login_post_request(strategy) post_request.POST = dict(post_request.POST) if email: - post_request.POST['email'] = email + post_request.POST["email"] = email if password: - post_request.POST['password'] = 'bad_' + password if success is False else password + post_request.POST["password"] = "bad_" + password if success is False else password self.assert_pipeline_running(post_request) - payload = json.loads(login_user(post_request).content.decode('utf-8')) + payload = json.loads(login_user(post_request).content.decode("utf-8")) if success is None: # Request malformed -- just one of email/password given. - assert not payload.get('success') - assert 'There was an error receiving your login information' in payload.get('value') + assert not payload.get("success") + assert "There was an error receiving your login information" in payload.get("value") elif success: # Request well-formed and credentials good. - assert payload.get('success') + assert payload.get("success") else: # Request well-formed but credentials bad. - assert not payload.get('success') - assert 'incorrect' in payload.get('value') + assert not payload.get("success") + assert "incorrect" in payload.get("value") def get_response_data(self): """Gets a dict of response data of the form given by the provider. @@ -1064,8 +1128,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): if not user: user = request.user return actions.do_complete( - request.backend, social_views._do_login, user, None, # pylint: disable=protected-access - redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request, partial_token=partial_pipeline_token + request.backend, + social_views._do_login, # pylint: disable=protected-access + user, + None, + redirect_field_name=auth.REDIRECT_FIELD_NAME, + request=request, + partial_token=partial_pipeline_token, ) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index ec3efd8286..caddd325ba 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -2,7 +2,6 @@ Third_party_auth integration tests using a mock version of the TestShib provider """ - import datetime import json import logging @@ -27,16 +26,15 @@ from common.djangoapps.third_party_auth.saml import SapSuccessFactorsIdentityPro from common.djangoapps.third_party_auth.saml import log as saml_log from common.djangoapps.third_party_auth.tasks import fetch_saml_metadata from common.djangoapps.third_party_auth.tests import testutil, utils -from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context from openedx.core.djangoapps.user_authn.views.login import login_user from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerFactory from .base import IntegrationTestMixin -TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth' -TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml' -TESTSHIB_METADATA_URL_WITH_CACHE_DURATION = 'https://mock.testshib.org/metadata/testshib-providers-cache.xml' -TESTSHIB_SSO_URL = 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO' +TESTSHIB_ENTITY_ID = "https://idp.testshib.org/idp/shibboleth" +TESTSHIB_METADATA_URL = "https://mock.testshib.org/metadata/testshib-providers.xml" +TESTSHIB_METADATA_URL_WITH_CACHE_DURATION = "https://mock.testshib.org/metadata/testshib-providers-cache.xml" +TESTSHIB_SSO_URL = "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" class SamlIntegrationTestUtilities: @@ -44,6 +42,7 @@ class SamlIntegrationTestUtilities: Class contains methods particular to SAML integration testing so that they can be separated out from the actual test methods. """ + PROVIDER_ID = "saml-testshib" PROVIDER_NAME = "TestShib" PROVIDER_BACKEND = "tpa-saml" @@ -67,51 +66,59 @@ class SamlIntegrationTestUtilities: self.addCleanup(httpretty.disable) # lint-amnesty, pylint: disable=no-member def metadata_callback(_request, _uri, headers): - """ Return a cached copy of TestShib's metadata by reading it from disk """ - return (200, headers, self.read_data_file('testshib_metadata.xml')) # lint-amnesty, pylint: disable=no-member + """Return a cached copy of TestShib's metadata by reading it from disk""" + return ( + 200, + headers, + self.read_data_file("testshib_metadata.xml"), + ) # lint-amnesty, pylint: disable=no-member - httpretty.register_uri(httpretty.GET, TESTSHIB_METADATA_URL, content_type='text/xml', body=metadata_callback) + httpretty.register_uri(httpretty.GET, TESTSHIB_METADATA_URL, content_type="text/xml", body=metadata_callback) def cache_duration_metadata_callback(_request, _uri, headers): """Return a cached copy of TestShib's metadata with a cacheDuration attribute""" - return (200, headers, self.read_data_file('testshib_metadata_with_cache_duration.xml')) # lint-amnesty, pylint: disable=no-member + return ( + 200, + headers, + self.read_data_file("testshib_metadata_with_cache_duration.xml"), + ) # lint-amnesty, pylint: disable=no-member httpretty.register_uri( httpretty.GET, TESTSHIB_METADATA_URL_WITH_CACHE_DURATION, - content_type='text/xml', - body=cache_duration_metadata_callback + content_type="text/xml", + body=cache_duration_metadata_callback, ) # Configure the SAML library to use the same request ID for every request. # Doing this and freezing the time allows us to play back recorded request/response pairs - uid_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id', return_value='TESTID') + uid_patch = patch("onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id", return_value="TESTID") uid_patch.start() self.addCleanup(uid_patch.stop) # lint-amnesty, pylint: disable=no-member self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. def _freeze_time(self, timestamp): - """ Mock the current time for SAML, so we can replay canned requests/responses """ - now_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.now', return_value=timestamp) + """Mock the current time for SAML, so we can replay canned requests/responses""" + now_patch = patch("onelogin.saml2.utils.OneLogin_Saml2_Utils.now", return_value=timestamp) now_patch.start() self.addCleanup(now_patch.stop) # lint-amnesty, pylint: disable=no-member def _configure_testshib_provider(self, **kwargs): - """ Enable and configure the TestShib SAML IdP as a third_party_auth provider """ - fetch_metadata = kwargs.pop('fetch_metadata', True) - assert_metadata_updates = kwargs.pop('assert_metadata_updates', True) - kwargs.setdefault('name', self.PROVIDER_NAME) - kwargs.setdefault('enabled', True) - kwargs.setdefault('visible', True) + """Enable and configure the TestShib SAML IdP as a third_party_auth provider""" + fetch_metadata = kwargs.pop("fetch_metadata", True) + assert_metadata_updates = kwargs.pop("assert_metadata_updates", True) + kwargs.setdefault("name", self.PROVIDER_NAME) + kwargs.setdefault("enabled", True) + kwargs.setdefault("visible", True) kwargs.setdefault("backend_name", "tpa-saml") - kwargs.setdefault('slug', self.PROVIDER_IDP_SLUG) - kwargs.setdefault('entity_id', TESTSHIB_ENTITY_ID) - kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL) - kwargs.setdefault('icon_class', 'fa-university') - kwargs.setdefault('attr_email', 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6') # eduPersonPrincipalName - kwargs.setdefault('max_session_length', None) - kwargs.setdefault('send_to_registration_first', False) - kwargs.setdefault('skip_email_verification', False) + kwargs.setdefault("slug", self.PROVIDER_IDP_SLUG) + kwargs.setdefault("entity_id", TESTSHIB_ENTITY_ID) + kwargs.setdefault("metadata_source", TESTSHIB_METADATA_URL) + kwargs.setdefault("icon_class", "fa-university") + kwargs.setdefault("attr_email", "urn:oid:1.3.6.1.4.1.5923.1.1.1.6") # eduPersonPrincipalName + kwargs.setdefault("max_session_length", None) + kwargs.setdefault("send_to_registration_first", False) + kwargs.setdefault("skip_email_verification", False) saml_provider = self.configure_saml_provider(**kwargs) # pylint: disable=no-member if fetch_metadata: @@ -127,17 +134,17 @@ class SamlIntegrationTestUtilities: return saml_provider def do_provider_login(self, provider_redirect_url): - """ Mocked: the user logs in to TestShib and then gets redirected back """ + """Mocked: the user logs in to TestShib and then gets redirected back""" # The SAML provider (TestShib) will authenticate the user, then get the browser to POST a response: assert provider_redirect_url.startswith(TESTSHIB_SSO_URL) # lint-amnesty, pylint: disable=no-member saml_response_xml = utils.read_and_pre_process_xml( - os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'testshib_saml_response.xml') + os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "testshib_saml_response.xml") ) return self.client.post( # lint-amnesty, pylint: disable=no-member self.complete_url, # lint-amnesty, pylint: disable=no-member - content_type='application/x-www-form-urlencoded', + content_type="application/x-www-form-urlencoded", data=utils.prepare_saml_response_from_xml(saml_response_xml), ) @@ -150,16 +157,16 @@ class TestIndexExceptionTest(SamlIntegrationTestUtilities, IntegrationTestMixin, """ TOKEN_RESPONSE_DATA = { - 'access_token': 'access_token_value', - 'expires_in': 'expires_in_value', + "access_token": "access_token_value", + "expires_in": "expires_in_value", } USER_RESPONSE_DATA = { - 'lastName': 'lastName_value', - 'id': 'id_value', - 'firstName': 'firstName_value', - 'idp_name': 'testshib', - 'attributes': {'urn:oid:0.9.2342.19200300.100.1.1': [], 'name_id': '1'}, - 'session_index': '1', + "lastName": "lastName_value", + "id": "id_value", + "firstName": "firstName_value", + "idp_name": "testshib", + "attributes": {"urn:oid:0.9.2342.19200300.100.1.1": [], "name_id": "1"}, + "session_index": "1", } def test_index_error_from_empty_list_saml_attribute(self): @@ -169,7 +176,8 @@ class TestIndexExceptionTest(SamlIntegrationTestUtilities, IntegrationTestMixin, """ self.provider = self._configure_testshib_provider() request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) with self.assertRaises(IncorrectConfigurationException): request.backend.auth_complete = MagicMock(return_value=self.fake_auth_complete(strategy)) @@ -188,16 +196,16 @@ class TestKeyExceptionTest(SamlIntegrationTestUtilities, IntegrationTestMixin, t """ TOKEN_RESPONSE_DATA = { - 'access_token': 'access_token_value', - 'expires_in': 'expires_in_value', + "access_token": "access_token_value", + "expires_in": "expires_in_value", } USER_RESPONSE_DATA = { - 'lastName': 'lastName_value', - 'id': 'id_value', - 'firstName': 'firstName_value', - 'idp_name': 'testshib', - 'attributes': {'name_id': '1'}, - 'session_index': '1', + "lastName": "lastName_value", + "id": "id_value", + "firstName": "firstName_value", + "idp_name": "testshib", + "attributes": {"name_id": "1"}, + "session_index": "1", } def test_key_error_from_missing_saml_attributes(self): @@ -207,7 +215,8 @@ class TestKeyExceptionTest(SamlIntegrationTestUtilities, IntegrationTestMixin, t """ self.provider = self._configure_testshib_provider() request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) with self.assertRaises(IncorrectConfigurationException): request.backend.auth_complete = MagicMock(return_value=self.fake_auth_complete(strategy)) @@ -226,25 +235,23 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin """ TOKEN_RESPONSE_DATA = { - 'access_token': 'access_token_value', - 'expires_in': 'expires_in_value', + "access_token": "access_token_value", + "expires_in": "expires_in_value", } USER_RESPONSE_DATA = { - 'lastName': 'lastName_value', - 'id': 'id_value', - 'firstName': 'firstName_value', - 'idp_name': 'testshib', - 'attributes': {'urn:oid:0.9.2342.19200300.100.1.1': ['myself'], 'name_id': '1'}, - 'session_index': '1', + "lastName": "lastName_value", + "id": "id_value", + "firstName": "firstName_value", + "idp_name": "testshib", + "attributes": {"urn:oid:0.9.2342.19200300.100.1.1": ["myself"], "name_id": "1"}, + "session_index": "1", } - @patch('openedx.features.enterprise_support.api.enterprise_customer_for_request') - @patch('openedx.core.djangoapps.user_api.accounts.settings_views.enterprise_customer_for_request') - @patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get') + @patch("openedx.features.enterprise_support.api.enterprise_customer_for_request") + @patch("openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get") def test_full_pipeline_succeeds_for_unlinking_testshib_account( self, mock_auth_provider, - mock_enterprise_customer_for_request_settings_view, mock_enterprise_customer_for_request, ): @@ -252,10 +259,12 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin # configure the backend, and mock out wire traffic. self.provider = self._configure_testshib_provider() request, strategy = self.get_request_and_strategy( - auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete') + auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri="social:complete" + ) request.backend.auth_complete = MagicMock(return_value=self.fake_auth_complete(strategy)) user = self.create_user_models_for_existing_account( - strategy, 'user@example.com', 'password', self.get_username()) + strategy, "user@example.com", "password", self.get_username() + ) self.assert_social_auth_exists_for_user(user, strategy) request.user = user @@ -267,70 +276,67 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin enterprise_customer = EnterpriseCustomerFactory() assert EnterpriseCustomerUser.objects.count() == 0, "Precondition check: no link records should exist" EnterpriseCustomerUser.objects.link_user(enterprise_customer, user.email) - assert (EnterpriseCustomerUser.objects - .filter(enterprise_customer=enterprise_customer, user_id=user.id).count() == 1) - EnterpriseCustomerIdentityProvider.objects.get_or_create(enterprise_customer=enterprise_customer, - provider_id=self.provider.provider_id) + assert ( + EnterpriseCustomerUser.objects.filter(enterprise_customer=enterprise_customer, user_id=user.id).count() == 1 + ) + EnterpriseCustomerIdentityProvider.objects.get_or_create( + enterprise_customer=enterprise_customer, provider_id=self.provider.provider_id + ) enterprise_customer_data = { - 'uuid': enterprise_customer.uuid, - 'name': enterprise_customer.name, - 'identity_provider': 'saml-default', - 'identity_providers': [ + "uuid": enterprise_customer.uuid, + "name": enterprise_customer.name, + "identity_provider": "saml-default", + "identity_providers": [ { "provider_id": "saml-default", } ], } - mock_auth_provider.return_value.backend_name = 'tpa-saml' + mock_auth_provider.return_value.backend_name = "tpa-saml" mock_enterprise_customer_for_request.return_value = enterprise_customer_data - mock_enterprise_customer_for_request_settings_view.return_value = enterprise_customer_data # Instrument the pipeline to get to the dashboard with the full expected state. - self.client.get( - pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) + self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) - actions.do_complete(request.backend, social_views._do_login, # pylint: disable=protected-access - request=request) + actions.do_complete( + request.backend, social_views._do_login, request=request # pylint: disable=protected-access + ) with self._patch_edxmako_current_request(strategy.request): login_user(strategy.request) - actions.do_complete(request.backend, social_views._do_login, user=user, # pylint: disable=protected-access - request=request) + actions.do_complete( + request.backend, social_views._do_login, user=user, request=request # pylint: disable=protected-access + ) # First we expect that we're in the linked state, with a backend entry. - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True) self.assert_social_auth_exists_for_user(request.user, strategy) FEATURES_WITH_ENTERPRISE_ENABLED = settings.FEATURES.copy() - FEATURES_WITH_ENTERPRISE_ENABLED['ENABLE_ENTERPRISE_INTEGRATION'] = True + FEATURES_WITH_ENTERPRISE_ENABLED["ENABLE_ENTERPRISE_INTEGRATION"] = True with patch.dict("django.conf.settings.FEATURES", FEATURES_WITH_ENTERPRISE_ENABLED): # Fire off the disconnect pipeline without the user information. actions.do_disconnect( - request.backend, - None, - None, - redirect_field_name=auth.REDIRECT_FIELD_NAME, - request=request + request.backend, None, None, redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request + ) + assert ( + EnterpriseCustomerUser.objects.filter(enterprise_customer=enterprise_customer, user_id=user.id).count() + != 0 ) - assert EnterpriseCustomerUser.objects\ - .filter(enterprise_customer=enterprise_customer, user_id=user.id).count() != 0 # Fire off the disconnect pipeline to unlink. self.assert_redirect_after_pipeline_completes( actions.do_disconnect( - request.backend, - user, - None, - redirect_field_name=auth.REDIRECT_FIELD_NAME, - request=request + request.backend, user, None, redirect_field_name=auth.REDIRECT_FIELD_NAME, request=request ) ) # Now we expect to be in the unlinked state, with no backend entry. - self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=False) + self.assert_third_party_accounts_state(request, linked=False) self.assert_social_auth_does_not_exist_for_user(user, strategy) - assert EnterpriseCustomerUser.objects\ - .filter(enterprise_customer=enterprise_customer, user_id=user.id).count() == 0 + assert ( + EnterpriseCustomerUser.objects.filter(enterprise_customer=enterprise_customer, user_id=user.id).count() + == 0 + ) def get_response_data(self): """Gets dict (string -> object) of merged data about the user.""" @@ -340,7 +346,7 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin def get_username(self): response_data = self.get_response_data() - return response_data.get('idp_name') + return response_data.get("idp_name") def test_login_before_metadata_fetched(self): self._configure_testshib_provider(fetch_metadata=False) @@ -350,18 +356,18 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin try_login_response = self.client.get(testshib_login_url) # The user should be redirected to back to the login page: assert try_login_response.status_code == 302 - assert try_login_response['Location'] == self.login_page_url + assert try_login_response["Location"] == self.login_page_url # When loading the login page, the user will see an error message: response = self.client.get(self.login_page_url) - self.assertContains(response, 'Authentication with TestShib is currently unavailable.') + self.assertContains(response, "Authentication with TestShib is currently unavailable.") def test_login(self): - """ Configure TestShib before running the login test """ + """Configure TestShib before running the login test""" self._configure_testshib_provider() self._test_login() def test_register(self): - """ Configure TestShib before running the register test """ + """Configure TestShib before running the register test""" self._configure_testshib_provider() self._test_register() @@ -374,17 +380,17 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin user=self.user, provider=self.PROVIDER_BACKEND, uid__startswith=self.PROVIDER_IDP_SLUG ) attributes = record.extra_data - assert attributes.get('urn:oid:1.3.6.1.4.1.5923.1.1.1.9') == ['Member@testshib.org', 'Staff@testshib.org'] - assert attributes.get('urn:oid:2.5.4.3') == ['Me Myself And I'] - assert attributes.get('urn:oid:0.9.2342.19200300.100.1.1') == ['myself'] - assert attributes.get('urn:oid:2.5.4.20') == ['555-5555'] + assert attributes.get("urn:oid:1.3.6.1.4.1.5923.1.1.1.9") == ["Member@testshib.org", "Staff@testshib.org"] + assert attributes.get("urn:oid:2.5.4.3") == ["Me Myself And I"] + assert attributes.get("urn:oid:0.9.2342.19200300.100.1.1") == ["myself"] + assert attributes.get("urn:oid:2.5.4.20") == ["555-5555"] # Phone number @ddt.data(True, False) def test_debug_mode_login(self, debug_mode_enabled): - """ Test SAML login logs with debug mode enabled or not """ + """Test SAML login logs with debug mode enabled or not""" self._configure_testshib_provider(debug_mode=debug_mode_enabled) - with patch.object(saml_log, 'info') as mock_log: + with patch.object(saml_log, "info") as mock_log: self._test_login() if debug_mode_enabled: # We expect that test_login() does two full logins, and each attempt generates two @@ -393,38 +399,37 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin expected_next_url = "/dashboard" (msg, action_type, idp_name, request_data, next_url, xml), _kwargs = mock_log.call_args_list[0] - assert msg.startswith('SAML login %s') - assert action_type == 'request' + assert msg.startswith("SAML login %s") + assert action_type == "request" assert idp_name == self.PROVIDER_IDP_SLUG self.assertDictContainsSubset( - {"idp": idp_name, "auth_entry": "login", "next": expected_next_url}, - request_data + {"idp": idp_name, "auth_entry": "login", "next": expected_next_url}, request_data ) assert next_url == expected_next_url - assert ''); - }); - - it('shows loading error when UserAccountModel fails to load', function() { - requests = AjaxHelpers.requests(this); - - var accountSettingsView = createAccountSettingsPage(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - var request = requests[0]; - expect(request.method).toBe('GET'); - expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL); - - AjaxHelpers.respondWithError(requests, 500); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); - }); - - it('shows loading error when UserPreferencesModel fails to load', function() { - requests = AjaxHelpers.requests(this); - - var accountSettingsView = createAccountSettingsPage(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - var request = requests[0]; - expect(request.method).toBe('GET'); - expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL); - - AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - request = requests[1]; - expect(request.method).toBe('GET'); - expect(request.url).toBe('/api/user/v1/preferences/time_zones/?country_code=1'); - AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); - - request = requests[2]; - expect(request.method).toBe('GET'); - expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL); - - AjaxHelpers.respondWithError(requests, 500); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); - }); - - it('renders fields after the models are successfully fetched', function() { - requests = AjaxHelpers.requests(this); - - var accountSettingsView = createAccountSettingsPage(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); - AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); - AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); - - accountSettingsView.render(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView); - }); - - it('expects all fields to behave correctly', function() { - var i, view; - - requests = AjaxHelpers.requests(this); - - var accountSettingsView = createAccountSettingsPage(); - - AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); - AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); - AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); - AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event - - var sectionsData = accountSettingsView.options.tabSections.aboutTabSections; - - expect(sectionsData[0].fields.length).toBe(7); - - var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]]; - for (i = 0; i < textFields.length; i++) { - view = textFields[i].view; - FieldViewsSpecHelpers.verifyTextField(view, { - title: view.options.title, - valueAttribute: view.options.valueAttribute, - helpMessage: view.options.helpMessage, - validValue: 'My Name', - invalidValue1: '', - invalidValue2: '@', - validationError: 'Think again!', - defaultValue: '' - }, requests); - } - - expect(sectionsData[1].fields.length).toBe(4); - var dropdownFields = [ - sectionsData[1].fields[0], - sectionsData[1].fields[1], - sectionsData[1].fields[2] - ]; - _.each(dropdownFields, function(field) { - // eslint-disable-next-line no-shadow - var view = field.view; - FieldViewsSpecHelpers.verifyDropDownField(view, { - title: view.options.title, - valueAttribute: view.options.valueAttribute, - helpMessage: '', - validValue: Helpers.FIELD_OPTIONS[1][0], - invalidValue1: Helpers.FIELD_OPTIONS[2][0], - invalidValue2: Helpers.FIELD_OPTIONS[3][0], - validationError: 'Nope, this will not do!', - defaultValue: null - }, requests); - }); - }); - }); - - describe('edx.user.AccountSettingsFactory', function() { - var createEnterpriseLearnerAccountSettingsPage = function() { - var context = AccountSettingsPage( - Helpers.FIELDS_DATA, - false, - [], - Helpers.AUTH_DATA, - Helpers.PASSWORD_RESET_SUPPORT_LINK, - Helpers.USER_ACCOUNTS_API_URL, - Helpers.USER_PREFERENCES_API_URL, - 1, - Helpers.PLATFORM_NAME, - Helpers.CONTACT_EMAIL, - true, - Helpers.ENABLE_COPPA_COMPLIANCE, - '', - - Helpers.SYNC_LEARNER_PROFILE_DATA, - Helpers.ENTERPRISE_NAME, - Helpers.ENTERPRISE_READ_ONLY_ACCOUNT_FIELDS, - Helpers.EDX_SUPPORT_URL - ); - return context.accountSettingsView; - }; - - var requests; - var accountInfoTab = { - BASIC_ACCOUNT_INFORMATION: 0, - ADDITIONAL_INFORMATION: 1 - }; - var basicAccountInfoFields = { - USERNAME: 0, - FULL_NAME: 1, - EMAIL_ADDRESS: 2, - PASSWORD: 3, - LANGUAGE: 4, - COUNTRY: 5, - TIMEZONE: 6 - }; - var additionalInfoFields = { - EDUCATION: 0, - GENDER: 1, - YEAR_OF_BIRTH: 2, - PREFERRED_LANGUAGE: 3 - }; - - beforeEach(function() { - setFixtures(''); - }); - - it('shows loading error when UserAccountModel fails to load for enterprise learners', function() { - var accountSettingsView, request; - requests = AjaxHelpers.requests(this); - - accountSettingsView = createEnterpriseLearnerAccountSettingsPage(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - request = requests[0]; - expect(request.method).toBe('GET'); - expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL); - - AjaxHelpers.respondWithError(requests, 500); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); - }); - - it('shows loading error when UserPreferencesModel fails to load for enterprise learners', function() { - var accountSettingsView, request; - requests = AjaxHelpers.requests(this); - - accountSettingsView = createEnterpriseLearnerAccountSettingsPage(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - request = requests[0]; - expect(request.method).toBe('GET'); - expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL); - - AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - request = requests[1]; - expect(request.method).toBe('GET'); - expect(request.url).toBe('/api/user/v1/preferences/time_zones/?country_code=1'); - AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); - - request = requests[2]; - expect(request.method).toBe('GET'); - expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL); - - AjaxHelpers.respondWithError(requests, 500); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); - }); - - it('renders fields after the models are successfully fetched for enterprise learners', function() { - var accountSettingsView; - requests = AjaxHelpers.requests(this); - - accountSettingsView = createEnterpriseLearnerAccountSettingsPage(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); - AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); - AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); - - accountSettingsView.render(); - - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - Helpers.expectSettingsSectionsAndFieldsToBeRenderedWithMessage(accountSettingsView); - }); - - it('expects all fields to behave correctly for enterprise learners', function() { - var accountSettingsView, i, view, sectionsData, textFields, dropdownFields; - requests = AjaxHelpers.requests(this); - - accountSettingsView = createEnterpriseLearnerAccountSettingsPage(); - - AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); - AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); - AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); - AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event - - sectionsData = accountSettingsView.options.tabSections.aboutTabSections; - - expect(sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields.length).toBe(7); - - // Verify that username, name and email fields are readonly - textFields = [ - sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields[basicAccountInfoFields.USERNAME], - sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields[basicAccountInfoFields.FULL_NAME], - sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields[basicAccountInfoFields.EMAIL_ADDRESS] - ]; - for (i = 0; i < textFields.length; i++) { - view = textFields[i].view; - - FieldViewsSpecHelpers.verifyReadonlyTextField(view, { - title: view.options.title, - valueAttribute: view.options.valueAttribute, - helpMessage: view.options.helpMessage, - validValue: 'My Name', - defaultValue: '' - }, requests); - } - - // Verify un-editable country dropdown field - view = sectionsData[ - accountInfoTab.BASIC_ACCOUNT_INFORMATION - ].fields[basicAccountInfoFields.COUNTRY].view; - - FieldViewsSpecHelpers.verifyReadonlyDropDownField(view, { - title: view.options.title, - valueAttribute: view.options.valueAttribute, - helpMessage: '', - validValue: Helpers.FIELD_OPTIONS[1][0], - editable: 'never', - defaultValue: null - }); - - expect(sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields.length).toBe(4); - dropdownFields = [ - sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields[additionalInfoFields.EDUCATION], - sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields[additionalInfoFields.GENDER], - sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields[additionalInfoFields.YEAR_OF_BIRTH] - ]; - _.each(dropdownFields, function(field) { - view = field.view; - FieldViewsSpecHelpers.verifyDropDownField(view, { - title: view.options.title, - valueAttribute: view.options.valueAttribute, - helpMessage: '', - validValue: Helpers.FIELD_OPTIONS[1][0], // dummy option for dropdown field - invalidValue1: Helpers.FIELD_OPTIONS[2][0], // dummy option for dropdown field - invalidValue2: Helpers.FIELD_OPTIONS[3][0], // dummy option for dropdown field - validationError: 'Nope, this will not do!', - defaultValue: null - }, requests); - }); - }); - }); -}); diff --git a/lms/static/js/spec/student_account/account_settings_fields_helpers.js b/lms/static/js/spec/student_account/account_settings_fields_helpers.js deleted file mode 100644 index 4aea86b235..0000000000 --- a/lms/static/js/spec/student_account/account_settings_fields_helpers.js +++ /dev/null @@ -1,34 +0,0 @@ -define(['backbone', - 'jquery', - 'underscore', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/spec/views/fields_helpers', - 'string_utils'], -function(Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers) { - 'use strict'; - - var verifyAuthField = function(view, data, requests) { - var selector = '.u-field-value .u-field-link-title-' + view.options.valueAttribute; - - spyOn(view, 'redirect_to'); - - FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, data.title, data.helpMessage); - expect(view.$(selector).text().trim()).toBe('Unlink This Account'); - view.$(selector).click(); - FieldViewsSpecHelpers.expectMessageContains(view, 'Unlinking'); - AjaxHelpers.expectRequest(requests, 'POST', data.disconnectUrl); - AjaxHelpers.respondWithNoContent(requests); - - expect(view.$(selector).text().trim()).toBe('Link Your Account'); - FieldViewsSpecHelpers.expectMessageContains(view, 'Successfully unlinked.'); - - view.$(selector).click(); - FieldViewsSpecHelpers.expectMessageContains(view, 'Linking'); - expect(view.redirect_to).toHaveBeenCalledWith(data.connectUrl); - }; - - return { - verifyAuthField: verifyAuthField - }; -}); diff --git a/lms/static/js/spec/student_account/account_settings_fields_spec.js b/lms/static/js/spec/student_account/account_settings_fields_spec.js deleted file mode 100644 index 76ea7c512b..0000000000 --- a/lms/static/js/spec/student_account/account_settings_fields_spec.js +++ /dev/null @@ -1,216 +0,0 @@ -define(['backbone', - 'jquery', - 'underscore', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/student_account/models/user_account_model', - 'js/views/fields', - 'js/spec/views/fields_helpers', - 'js/spec/student_account/account_settings_fields_helpers', - 'js/student_account/views/account_settings_fields', - 'js/student_account/models/user_account_model', - 'string_utils'], -function(Backbone, $, _, AjaxHelpers, TemplateHelpers, UserAccountModel, FieldViews, FieldViewsSpecHelpers, - AccountSettingsFieldViewSpecHelpers, AccountSettingsFieldViews) { - 'use strict'; - - describe('edx.AccountSettingsFieldViews', function() { - var requests, - timerCallback, // eslint-disable-line no-unused-vars - data; - - beforeEach(function() { - timerCallback = jasmine.createSpy('timerCallback'); - jasmine.clock().install(); - }); - - afterEach(function() { - jasmine.clock().uninstall(); - }); - - it('sends request to reset password on clicking link in PasswordFieldView', function() { - requests = AjaxHelpers.requests(this); - - var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.PasswordFieldView, { - linkHref: '/password_reset', - emailAttribute: 'email', - valueAttribute: 'password' - }); - - var view = new AccountSettingsFieldViews.PasswordFieldView(fieldData).render(); - expect(view.$('.u-field-value > button').is(':disabled')).toBe(false); - view.$('.u-field-value > button').click(); - expect(view.$('.u-field-value > button').is(':disabled')).toBe(true); - AjaxHelpers.expectRequest(requests, 'POST', '/password_reset', 'email=legolas%40woodland.middlearth'); - AjaxHelpers.respondWithJson(requests, {success: 'true'}); - FieldViewsSpecHelpers.expectMessageContains( - view, - "We've sent a message to legolas@woodland.middlearth. " - + 'Click the link in the message to reset your password.' - ); - }); - - it('update time zone dropdown after country dropdown changes', function() { - var baseSelector = '.u-field-value > select'; - var groupsSelector = baseSelector + '> optgroup'; - var groupOptionsSelector = groupsSelector + '> option'; - - var timeZoneData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.TimeZoneFieldView, { - valueAttribute: 'time_zone', - groupOptions: [{ - groupTitle: gettext('All Time Zones'), - selectOptions: FieldViewsSpecHelpers.SELECT_OPTIONS, - nullValueOptionLabel: 'Default (Local Time Zone)' - }], - persistChanges: true, - required: true - }); - var countryData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { - valueAttribute: 'country', - options: [['KY', 'Cayman Islands'], ['CA', 'Canada'], ['GY', 'Guyana']], - persistChanges: true - }); - - var countryChange = {country: 'GY'}; - var timeZoneChange = {time_zone: 'Pacific/Kosrae'}; - - var timeZoneView = new AccountSettingsFieldViews.TimeZoneFieldView(timeZoneData).render(); - var countryView = new AccountSettingsFieldViews.DropdownFieldView(countryData).render(); - - requests = AjaxHelpers.requests(this); - - timeZoneView.listenToCountryView(countryView); - - // expect time zone dropdown to have single subheader ('All Time Zones') - expect(timeZoneView.$(groupsSelector).length).toBe(1); - expect(timeZoneView.$(groupOptionsSelector).length).toBe(3); - expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]); - - // change country - countryView.$(baseSelector).val(countryChange[countryData.valueAttribute]).change(); - countryView.$(baseSelector).focusout(); - FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, countryChange); - AjaxHelpers.respondWithJson(requests, {success: 'true'}); - - AjaxHelpers.expectRequest( - requests, - 'GET', - '/api/user/v1/preferences/time_zones/?country_code=GY' - ); - AjaxHelpers.respondWithJson(requests, [ - {time_zone: 'America/Guyana', description: 'America/Guyana (ECT, UTC-0500)'}, - {time_zone: 'Pacific/Kosrae', description: 'Pacific/Kosrae (KOST, UTC+1100)'} - ]); - - // expect time zone dropdown to have two subheaders (country/all time zone sub-headers) with new values - expect(timeZoneView.$(groupsSelector).length).toBe(2); - expect(timeZoneView.$(groupOptionsSelector).length).toBe(6); - expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('America/Guyana'); - - // select time zone option from option - timeZoneView.$(baseSelector).val(timeZoneChange[timeZoneData.valueAttribute]).change(); - timeZoneView.$(baseSelector).focusout(); - FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, timeZoneChange); - AjaxHelpers.respondWithJson(requests, {success: 'true'}); - timeZoneView.render(); - - // expect time zone dropdown to have three subheaders (currently selected/country/all time zones) - expect(timeZoneView.$(groupsSelector).length).toBe(3); - expect(timeZoneView.$(groupOptionsSelector).length).toBe(6); - expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('Pacific/Kosrae'); - }); - - it('sends request to /i18n/setlang/ after changing language in LanguagePreferenceFieldView', function() { - requests = AjaxHelpers.requests(this); - - var selector = '.u-field-value > select'; - var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { - valueAttribute: 'language', - options: FieldViewsSpecHelpers.SELECT_OPTIONS, - persistChanges: true - }); - - var view = new AccountSettingsFieldViews.LanguagePreferenceFieldView(fieldData).render(); - - data = {language: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0]}; - view.$(selector).val(data[fieldData.valueAttribute]).change(); - view.$(selector).focusout(); - FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data); - AjaxHelpers.respondWithNoContent(requests); - - AjaxHelpers.expectRequest( - requests, - 'POST', - '/i18n/setlang/', - $.param({ - language: data[fieldData.valueAttribute], - next: window.location.href - }) - ); - // Django will actually respond with a 302 redirect, but that would cause a page load during these - // unittests. 204 should work fine for testing. - AjaxHelpers.respondWithNoContent(requests); - FieldViewsSpecHelpers.expectMessageContains(view, 'Your changes have been saved.'); - - data = {language: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}; - view.$(selector).val(data[fieldData.valueAttribute]).change(); - view.$(selector).focusout(); - FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data); - AjaxHelpers.respondWithNoContent(requests); - - AjaxHelpers.expectRequest( - requests, - 'POST', - '/i18n/setlang/', - $.param({ - language: data[fieldData.valueAttribute], - next: window.location.href - }) - ); - AjaxHelpers.respondWithError(requests, 500); - FieldViewsSpecHelpers.expectMessageContains( - view, - 'You must sign out and sign back in before your language changes take effect.' - ); - }); - - it('reads and saves the value correctly for LanguageProficienciesFieldView', function() { - requests = AjaxHelpers.requests(this); - - var selector = '.u-field-value > select'; - var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { - valueAttribute: 'language_proficiencies', - options: FieldViewsSpecHelpers.SELECT_OPTIONS, - persistChanges: true - }); - fieldData.model.set({language_proficiencies: [{code: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]}); - - var view = new AccountSettingsFieldViews.LanguageProficienciesFieldView(fieldData).render(); - - expect(view.modelValue()).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]); - - data = {language_proficiencies: [{code: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}]}; - view.$(selector).val(FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]).change(); - view.$(selector).focusout(); - FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data); - AjaxHelpers.respondWithNoContent(requests); - }); - - it('correctly links and unlinks from AuthFieldView', function() { - requests = AjaxHelpers.requests(this); - - var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, { - title: 'Yet another social network', - helpMessage: '', - valueAttribute: 'auth-yet-another', - connected: true, - acceptsLogins: 'true', - connectUrl: 'yetanother.com/auth/connect', - disconnectUrl: 'yetanother.com/auth/disconnect' - }); - var view = new AccountSettingsFieldViews.AuthFieldView(fieldData).render(); - - AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, fieldData, requests); - }); - }); -}); diff --git a/lms/static/js/spec/student_account/account_settings_view_spec.js b/lms/static/js/spec/student_account/account_settings_view_spec.js deleted file mode 100644 index c0c213cf3c..0000000000 --- a/lms/static/js/spec/student_account/account_settings_view_spec.js +++ /dev/null @@ -1,91 +0,0 @@ -define(['backbone', - 'jquery', - 'underscore', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/spec/student_account/helpers', - 'js/views/fields', - 'js/student_account/models/user_account_model', - 'js/student_account/views/account_settings_view' -], -function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, FieldViews, UserAccountModel, - AccountSettingsView) { - 'use strict'; - - describe('edx.user.AccountSettingsView', function() { - var createAccountSettingsView = function() { - var model = new UserAccountModel(); - model.set(Helpers.createAccountSettingsData()); - - var aboutSectionsData = [ - { - title: 'Basic Account Information', - messageType: 'info', - message: 'Your profile settings are managed by Test Enterprise. ' - + 'Contact your administrator or edX Support for help.', - fields: [ - { - view: new FieldViews.ReadonlyFieldView({ - model: model, - title: 'Username', - valueAttribute: 'username' - }) - }, - { - view: new FieldViews.TextFieldView({ - model: model, - title: 'Full Name', - valueAttribute: 'name' - }) - } - ] - }, - { - title: 'Additional Information', - fields: [ - { - view: new FieldViews.DropdownFieldView({ - model: model, - title: 'Education Completed', - valueAttribute: 'level_of_education', - options: Helpers.FIELD_OPTIONS - }) - } - ] - } - ]; - - var accountSettingsView = new AccountSettingsView({ - el: $('.wrapper-account-settings'), - model: model, - tabSections: { - aboutTabSections: aboutSectionsData - } - }); - - return accountSettingsView; - }; - - beforeEach(function() { - setFixtures(''); - }); - - it('shows loading error correctly', function() { - var accountSettingsView = createAccountSettingsView(); - - accountSettingsView.render(); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - - accountSettingsView.showLoadingError(); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); - }); - - it('renders all fields as expected', function() { - var accountSettingsView = createAccountSettingsView(); - - accountSettingsView.render(); - Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); - Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView); - }); - }); -}); diff --git a/lms/static/js/student_account/views/account_section_view.js b/lms/static/js/student_account/views/account_section_view.js deleted file mode 100644 index 70cd217477..0000000000 --- a/lms/static/js/student_account/views/account_section_view.js +++ /dev/null @@ -1,48 +0,0 @@ -// eslint-disable-next-line no-shadow-restricted-names -(function(define, undefined) { - 'use strict'; - - define([ - 'gettext', - 'jquery', - 'underscore', - 'backbone', - 'edx-ui-toolkit/js/utils/html-utils', - 'text!templates/student_account/account_settings_section.underscore' - ], function(gettext, $, _, Backbone, HtmlUtils, sectionTemplate) { - var AccountSectionView = Backbone.View.extend({ - - initialize: function(options) { - this.options = options; - _.bindAll(this, 'render', 'renderFields'); - }, - - render: function() { - HtmlUtils.setHtml( - this.$el, - HtmlUtils.template(sectionTemplate)({ - HtmlUtils: HtmlUtils, - sections: this.options.sections, - tabName: this.options.tabName, - tabLabel: this.options.tabLabel - }) - ); - - this.renderFields(); - }, - - renderFields: function() { - var view = this; - - _.each(view.$('.' + view.options.tabName + '-section-body'), function(sectionEl, index) { - _.each(view.options.sections[index].fields, function(field) { - $(sectionEl).append(field.view.render().el); - }); - }); - return this; - } - }); - - return AccountSectionView; - }); -}).call(this, define || RequireJS.define); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js deleted file mode 100644 index 70d3ad205c..0000000000 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ /dev/null @@ -1,495 +0,0 @@ -// eslint-disable-next-line no-shadow-restricted-names -(function(define, undefined) { - 'use strict'; - - define([ - 'gettext', 'jquery', 'underscore', 'backbone', 'logger', - 'js/student_account/models/user_account_model', - 'js/student_account/models/user_preferences_model', - 'js/student_account/views/account_settings_fields', - 'js/student_account/views/account_settings_view', - 'edx-ui-toolkit/js/utils/string-utils', - 'edx-ui-toolkit/js/utils/html-utils' - ], function(gettext, $, _, Backbone, Logger, UserAccountModel, UserPreferencesModel, - AccountSettingsFieldViews, AccountSettingsView, StringUtils, HtmlUtils) { - return function( - fieldsData, - disableOrderHistoryTab, - ordersHistoryData, - authData, - passwordResetSupportUrl, - userAccountsApiUrl, - userPreferencesApiUrl, - accountUserId, - platformName, - contactEmail, - allowEmailChange, - enableCoppaCompliance, - socialPlatforms, - syncLearnerProfileData, - enterpriseName, - enterpriseReadonlyAccountFields, - edxSupportUrl, - extendedProfileFields, - displayAccountDeletion, - isSecondaryEmailFeatureEnabled, - betaLanguage - ) { - var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, - accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, - showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField, - emailFieldView, secondaryEmailFieldView, socialFields, accountDeletionFields, platformData, - aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView, - fullNameFieldData, emailFieldData, secondaryEmailFieldData, countryFieldData, additionalFields, - fieldItem, emailFieldViewIndex, focusId, yearOfBirthViewIndex, levelOfEducationFieldData, - tabIndex = 0; - - $accountSettingsElement = $('.wrapper-account-settings'); - - userAccountModel = new UserAccountModel(); - userAccountModel.url = userAccountsApiUrl; - - userPreferencesModel = new UserPreferencesModel(); - userPreferencesModel.url = userPreferencesApiUrl; - - if (syncLearnerProfileData && enterpriseName) { - aboutSectionMessageType = 'info'; - aboutSectionMessage = HtmlUtils.interpolateHtml( - gettext('Your profile settings are managed by {enterprise_name}. Contact your administrator or {link_start}edX Support{link_end} for help.'), // eslint-disable-line max-len - { - enterprise_name: enterpriseName, - link_start: HtmlUtils.HTML( - StringUtils.interpolate( - '', { - edx_support_url: edxSupportUrl - } - ) - ), - link_end: HtmlUtils.HTML('') - } - ); - } - - emailFieldData = { - model: userAccountModel, - title: gettext('Email Address (Sign In)'), - valueAttribute: 'email', - helpMessage: StringUtils.interpolate( - gettext('You receive messages from {platform_name} and course teams at this address.'), // eslint-disable-line max-len - {platform_name: platformName} - ), - persistChanges: true - }; - if (!allowEmailChange || (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('email') !== -1)) { // eslint-disable-line max-len - emailFieldView = { - view: new AccountSettingsFieldViews.ReadonlyFieldView(emailFieldData) - }; - } else { - emailFieldView = { - view: new AccountSettingsFieldViews.EmailFieldView(emailFieldData) - }; - } - - secondaryEmailFieldData = { - model: userAccountModel, - title: gettext('Recovery Email Address'), - valueAttribute: 'secondary_email', - helpMessage: gettext('You may access your account with this address if single-sign on or access to your primary email is not available.'), // eslint-disable-line max-len - persistChanges: true - }; - - fullNameFieldData = { - model: userAccountModel, - title: gettext('Full Name'), - valueAttribute: 'name', - helpMessage: gettext('The name that is used for ID verification and that appears on your certificates.'), // eslint-disable-line max-len, - persistChanges: true - }; - if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('name') !== -1) { - fullnameFieldView = { - view: new AccountSettingsFieldViews.ReadonlyFieldView(fullNameFieldData) - }; - } else { - fullnameFieldView = { - view: new AccountSettingsFieldViews.TextFieldView(fullNameFieldData) - }; - } - - countryFieldData = { - model: userAccountModel, - required: true, - title: gettext('Country or Region of Residence'), - valueAttribute: 'country', - options: fieldsData.country.options, - persistChanges: true, - helpMessage: gettext('The country or region where you live.') - }; - if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('country') !== -1) { - countryFieldData.editable = 'never'; - countryFieldView = { - view: new AccountSettingsFieldViews.DropdownFieldView( - countryFieldData - ) - }; - } else { - countryFieldView = { - view: new AccountSettingsFieldViews.DropdownFieldView(countryFieldData) - }; - } - - levelOfEducationFieldData = fieldsData.level_of_education.options; - if (enableCoppaCompliance) { - levelOfEducationFieldData = levelOfEducationFieldData.filter(option => option[0] !== 'el'); - } - - aboutSectionsData = [ - { - title: gettext('Basic Account Information'), - subtitle: gettext('These settings include basic information about your account.'), - - messageType: aboutSectionMessageType, - message: aboutSectionMessage, - - fields: [ - { - view: new AccountSettingsFieldViews.ReadonlyFieldView({ - model: userAccountModel, - title: gettext('Username'), - valueAttribute: 'username', - helpMessage: StringUtils.interpolate( - gettext('The name that identifies you on {platform_name}. You cannot change your username.'), // eslint-disable-line max-len - {platform_name: platformName} - ) - }) - }, - fullnameFieldView, - emailFieldView, - { - view: new AccountSettingsFieldViews.PasswordFieldView({ - model: userAccountModel, - title: gettext('Password'), - screenReaderTitle: gettext('Reset Your Password'), - valueAttribute: 'password', - emailAttribute: 'email', - passwordResetSupportUrl: passwordResetSupportUrl, - linkTitle: gettext('Reset Your Password'), - linkHref: fieldsData.password.url, - helpMessage: gettext('Check your email account for instructions to reset your password.') // eslint-disable-line max-len - }) - }, - { - view: new AccountSettingsFieldViews.LanguagePreferenceFieldView({ - model: userPreferencesModel, - title: gettext('Language'), - valueAttribute: 'pref-lang', - required: true, - refreshPageOnSave: true, - helpMessage: StringUtils.interpolate( - gettext('The language used throughout this site. This site is currently available in a limited number of languages. Changing the value of this field will cause the page to refresh.'), // eslint-disable-line max-len - {platform_name: platformName} - ), - options: fieldsData.language.options, - persistChanges: true, - focusNextID: '#u-field-select-country' - }) - }, - countryFieldView, - { - view: new AccountSettingsFieldViews.TimeZoneFieldView({ - model: userPreferencesModel, - required: true, - title: gettext('Time Zone'), - valueAttribute: 'time_zone', - helpMessage: gettext('Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browser\'s local time zone.'), // eslint-disable-line max-len - groupOptions: [{ - groupTitle: gettext('All Time Zones'), - selectOptions: fieldsData.time_zone.options, - nullValueOptionLabel: gettext('Default (Local Time Zone)') - }], - persistChanges: true - }) - } - ] - }, - { - title: gettext('Additional Information'), - fields: [ - { - view: new AccountSettingsFieldViews.DropdownFieldView({ - model: userAccountModel, - title: gettext('Education Completed'), - valueAttribute: 'level_of_education', - options: levelOfEducationFieldData, - persistChanges: true - }) - }, - { - view: new AccountSettingsFieldViews.DropdownFieldView({ - model: userAccountModel, - title: gettext('Gender'), - valueAttribute: 'gender', - options: fieldsData.gender.options, - persistChanges: true - }) - }, - { - view: new AccountSettingsFieldViews.DropdownFieldView({ - model: userAccountModel, - title: gettext('Year of Birth'), - valueAttribute: 'year_of_birth', - options: fieldsData.year_of_birth.options, - persistChanges: true - }) - }, - { - view: new AccountSettingsFieldViews.LanguageProficienciesFieldView({ - model: userAccountModel, - title: gettext('Preferred Language'), - valueAttribute: 'language_proficiencies', - options: fieldsData.preferred_language.options, - persistChanges: true - }) - } - ] - } - ]; - - if (enableCoppaCompliance) { - yearOfBirthViewIndex = aboutSectionsData[1].fields.findIndex(function(field) { - return field.view.options.valueAttribute === 'year_of_birth'; - }); - aboutSectionsData[1].fields.splice(yearOfBirthViewIndex, 1); - } - - // Secondary email address - if (isSecondaryEmailFeatureEnabled) { - secondaryEmailFieldView = { - view: new AccountSettingsFieldViews.EmailFieldView(secondaryEmailFieldData), - successMessage: function() { - return HtmlUtils.joinHtml( - this.indicators.success, - StringUtils.interpolate( - gettext('We\'ve sent a confirmation message to {new_secondary_email_address}. Click the link in the message to update your secondary email address.'), // eslint-disable-line max-len - { - new_secondary_email_address: this.fieldValue() - } - ) - ); - } - }; - emailFieldViewIndex = aboutSectionsData[0].fields.indexOf(emailFieldView); - - // Insert secondary email address after email address field. - aboutSectionsData[0].fields.splice( - emailFieldViewIndex + 1, 0, secondaryEmailFieldView - ); - } - - // Add the extended profile fields - additionalFields = aboutSectionsData[1]; - for (var field in extendedProfileFields) { // eslint-disable-line guard-for-in, no-restricted-syntax, vars-on-top, max-len - fieldItem = extendedProfileFields[field]; - if (fieldItem.field_type === 'TextField') { - additionalFields.fields.push({ - view: new AccountSettingsFieldViews.ExtendedFieldTextFieldView({ - model: userAccountModel, - title: fieldItem.field_label, - fieldName: fieldItem.field_name, - valueAttribute: 'extended_profile', - persistChanges: true - }) - }); - } else { - if (fieldItem.field_type === 'ListField') { - additionalFields.fields.push({ - view: new AccountSettingsFieldViews.ExtendedFieldListFieldView({ - model: userAccountModel, - title: fieldItem.field_label, - fieldName: fieldItem.field_name, - options: fieldItem.field_options, - valueAttribute: 'extended_profile', - persistChanges: true - }) - }); - } - } - } - - // Add the social link fields - socialFields = { - title: gettext('Social Media Links'), - subtitle: gettext('Optionally, link your personal accounts to the social media icons on your edX profile.'), // eslint-disable-line max-len - fields: [] - }; - - for (var socialPlatform in socialPlatforms) { // eslint-disable-line guard-for-in, no-restricted-syntax, vars-on-top, max-len - platformData = socialPlatforms[socialPlatform]; - socialFields.fields.push( - { - view: new AccountSettingsFieldViews.SocialLinkTextFieldView({ - model: userAccountModel, - title: StringUtils.interpolate( - gettext('{platform_display_name} Link'), - {platform_display_name: platformData.display_name} - ), - valueAttribute: 'social_links', - helpMessage: StringUtils.interpolate( - gettext('Enter your {platform_display_name} username or the URL to your {platform_display_name} page. Delete the URL to remove the link.'), // eslint-disable-line max-len - {platform_display_name: platformData.display_name} - ), - platform: socialPlatform, - persistChanges: true, - placeholder: platformData.example - }) - } - ); - } - aboutSectionsData.push(socialFields); - - // Add account deletion fields - if (displayAccountDeletion) { - accountDeletionFields = { - title: gettext('Delete My Account'), - fields: [], - // Used so content can be rendered external to Backbone - domHookId: 'account-deletion-container' - }; - aboutSectionsData.push(accountDeletionFields); - } - - // set TimeZoneField to listen to CountryField - - getUserField = function(list, search) { - // eslint-disable-next-line no-shadow - return _.find(list, function(field) { - return field.view.options.valueAttribute === search; - }).view; - }; - userFields = _.find(aboutSectionsData, function(section) { - return section.title === gettext('Basic Account Information'); - }).fields; - timeZoneDropdownField = getUserField(userFields, 'time_zone'); - countryDropdownField = getUserField(userFields, 'country'); - timeZoneDropdownField.listenToCountryView(countryDropdownField); - - accountsSectionData = [ - { - title: gettext('Linked Accounts'), - subtitle: StringUtils.interpolate( - gettext('You can link your social media accounts to simplify signing in to {platform_name}.'), - {platform_name: platformName} - ), - fields: _.map(authData.providers, function(provider) { - return { - view: new AccountSettingsFieldViews.AuthFieldView({ - title: provider.name, - valueAttribute: 'auth-' + provider.id, - helpMessage: '', - connected: provider.connected, - connectUrl: provider.connect_url, - acceptsLogins: provider.accepts_logins, - disconnectUrl: provider.disconnect_url, - platformName: platformName - }) - }; - }) - } - ]; - - ordersHistoryData.unshift( - { - title: gettext('ORDER NAME'), - order_date: gettext('ORDER PLACED'), - price: gettext('TOTAL'), - number: gettext('ORDER NUMBER') - } - ); - - ordersSectionData = [ - { - title: gettext('My Orders'), - subtitle: StringUtils.interpolate( - gettext('This page contains information about orders that you have placed with {platform_name}.'), // eslint-disable-line max-len - {platform_name: platformName} - ), - fields: _.map(ordersHistoryData, function(order) { - orderNumber = order.number; - if (orderNumber === 'ORDER NUMBER') { - orderNumber = 'orderId'; - } - return { - view: new AccountSettingsFieldViews.OrderHistoryFieldView({ - totalPrice: order.price, - orderId: order.number, - orderDate: order.order_date, - receiptUrl: order.receipt_url, - valueAttribute: 'order-' + orderNumber, - lines: order.lines - }) - }; - }) - } - ]; - - accountSettingsView = new AccountSettingsView({ - model: userAccountModel, - accountUserId: accountUserId, - el: $accountSettingsElement, - tabSections: { - aboutTabSections: aboutSectionsData, - accountsTabSections: accountsSectionData, - ordersTabSections: ordersSectionData - }, - userPreferencesModel: userPreferencesModel, - disableOrderHistoryTab: disableOrderHistoryTab, - betaLanguage: betaLanguage - }); - - accountSettingsView.render(); - focusId = $.cookie('focus_id'); - if (focusId) { - // eslint-disable-next-line no-bitwise - if (~focusId.indexOf('beta-language')) { - tabIndex = -1; - - // Scroll to top of selected element - $('html, body').animate({ - scrollTop: $(focusId).offset().top - }, 'slow'); - } - $(focusId).attr({tabindex: tabIndex}).focus(); - // Deleting the cookie - document.cookie = 'focus_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/account;'; - } - showAccountSettingsPage = function() { - // Record that the account settings page was viewed. - Logger.log('edx.user.settings.viewed', { - page: 'account', - visibility: null, - user_id: accountUserId - }); - }; - - showLoadingError = function() { - accountSettingsView.showLoadingError(); - }; - - userAccountModel.fetch({ - success: function() { - // Fetch the user preferences model - userPreferencesModel.fetch({ - success: showAccountSettingsPage, - error: showLoadingError - }); - }, - error: showLoadingError - }); - - return { - userAccountModel: userAccountModel, - userPreferencesModel: userPreferencesModel, - accountSettingsView: accountSettingsView - }; - }; - }); -}).call(this, define || RequireJS.define); diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js deleted file mode 100644 index 1fc174f935..0000000000 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ /dev/null @@ -1,466 +0,0 @@ -// eslint-disable-next-line no-shadow-restricted-names -(function(define, undefined) { - 'use strict'; - - define([ - 'gettext', - 'jquery', - 'underscore', - 'backbone', - 'js/views/fields', - 'text!templates/fields/field_text_account.underscore', - 'text!templates/fields/field_readonly_account.underscore', - 'text!templates/fields/field_link_account.underscore', - 'text!templates/fields/field_dropdown_account.underscore', - 'text!templates/fields/field_social_link_account.underscore', - 'text!templates/fields/field_order_history.underscore', - 'edx-ui-toolkit/js/utils/string-utils', - 'edx-ui-toolkit/js/utils/html-utils' - ], function( - gettext, $, _, Backbone, - FieldViews, - field_text_account_template, - field_readonly_account_template, - field_link_account_template, - field_dropdown_account_template, - field_social_link_template, - field_order_history_template, - StringUtils, - HtmlUtils - ) { - var AccountSettingsFieldViews = { - ReadonlyFieldView: FieldViews.ReadonlyFieldView.extend({ - fieldTemplate: field_readonly_account_template - }), - TextFieldView: FieldViews.TextFieldView.extend({ - fieldTemplate: field_text_account_template - }), - DropdownFieldView: FieldViews.DropdownFieldView.extend({ - fieldTemplate: field_dropdown_account_template - }), - EmailFieldView: FieldViews.TextFieldView.extend({ - fieldTemplate: field_text_account_template, - successMessage: function() { - return HtmlUtils.joinHtml( - this.indicators.success, - StringUtils.interpolate( - gettext('We\'ve sent a confirmation message to {new_email_address}. Click the link in the message to update your email address.'), // eslint-disable-line max-len - {new_email_address: this.fieldValue()} - ) - ); - } - }), - LanguagePreferenceFieldView: FieldViews.DropdownFieldView.extend({ - fieldTemplate: field_dropdown_account_template, - - initialize: function(options) { - this._super(options); // eslint-disable-line no-underscore-dangle - this.listenTo(this.model, 'revertValue', this.revertValue); - }, - - revertValue: function(event) { - var attributes = {}, - oldPrefLang = $(event.target).data('old-lang-code'); - - if (oldPrefLang) { - attributes['pref-lang'] = oldPrefLang; - this.saveAttributes(attributes); - } - }, - - saveSucceeded: function() { - var data = { - language: this.modelValue(), - next: window.location.href - }; - - var view = this; - $.ajax({ - type: 'POST', - url: '/i18n/setlang/', - data: data, - dataType: 'html', - success: function() { - view.showSuccessMessage(); - }, - error: function() { - view.showNotificationMessage( - HtmlUtils.joinHtml( - view.indicators.error, - gettext('You must sign out and sign back in before your language changes take effect.') // eslint-disable-line max-len - ) - ); - } - }); - } - - }), - TimeZoneFieldView: FieldViews.DropdownFieldView.extend({ - fieldTemplate: field_dropdown_account_template, - - initialize: function(options) { - this.options = _.extend({}, options); - _.bindAll(this, 'listenToCountryView', 'updateCountrySubheader', 'replaceOrAddGroupOption'); - this._super(options); // eslint-disable-line no-underscore-dangle - }, - - listenToCountryView: function(view) { - this.listenTo(view.model, 'change:country', this.updateCountrySubheader); - }, - - updateCountrySubheader: function(user) { - var view = this; - $.ajax({ - type: 'GET', - url: '/api/user/v1/preferences/time_zones/', - data: {country_code: user.attributes.country}, - success: function(data) { - var countryTimeZones = $.map(data, function(timeZoneInfo) { - return [[timeZoneInfo.time_zone, timeZoneInfo.description]]; - }); - view.replaceOrAddGroupOption( - 'Country Time Zones', - countryTimeZones - ); - view.render(); - } - }); - }, - - updateValueInField: function() { - var options; - if (this.modelValue()) { - options = [[this.modelValue(), this.displayValue(this.modelValue())]]; - this.replaceOrAddGroupOption( - 'Currently Selected Time Zone', - options - ); - } - this._super(); // eslint-disable-line no-underscore-dangle - }, - - replaceOrAddGroupOption: function(title, options) { - var groupOption = { - groupTitle: gettext(title), - selectOptions: options - }; - - var index = _.findIndex(this.options.groupOptions, function(group) { - return group.groupTitle === gettext(title); - }); - if (index >= 0) { - this.options.groupOptions[index] = groupOption; - } else { - this.options.groupOptions.unshift(groupOption); - } - } - - }), - PasswordFieldView: FieldViews.LinkFieldView.extend({ - fieldType: 'button', - fieldTemplate: field_link_account_template, - events: { - 'click button': 'linkClicked' - }, - initialize: function(options) { - this.options = _.extend({}, options); - this._super(options); - _.bindAll(this, 'resetPassword'); - }, - linkClicked: function(event) { - event.preventDefault(); - this.toggleDisableButton(true); - this.resetPassword(event); - }, - resetPassword: function() { - var data = {}; - data[this.options.emailAttribute] = this.model.get(this.options.emailAttribute); - - var view = this; - $.ajax({ - type: 'POST', - url: view.options.linkHref, - data: data, - success: function() { - view.showSuccessMessage(); - view.setMessageTimeout(); - }, - error: function(xhr) { - view.showErrorMessage(xhr); - view.setMessageTimeout(); - view.toggleDisableButton(false); - } - }); - }, - toggleDisableButton: function(disabled) { - var button = this.$('#u-field-link-' + this.options.valueAttribute); - if (button) { - button.prop('disabled', disabled); - } - }, - setMessageTimeout: function() { - var view = this; - setTimeout(function() { - view.showHelpMessage(); - }, 6000); - }, - successMessage: function() { - return HtmlUtils.joinHtml( - this.indicators.success, - HtmlUtils.interpolateHtml( - gettext('We\'ve sent a message to {email}. Click the link in the message to reset your password. Didn\'t receive the message? Contact {anchorStart}technical support{anchorEnd}.'), // eslint-disable-line max-len - { - email: this.model.get(this.options.emailAttribute), - anchorStart: HtmlUtils.HTML( - StringUtils.interpolate( - '', { - passwordResetSupportUrl: this.options.passwordResetSupportUrl - } - ) - ), - anchorEnd: HtmlUtils.HTML('') - } - ) - ); - } - }), - LanguageProficienciesFieldView: FieldViews.DropdownFieldView.extend({ - fieldTemplate: field_dropdown_account_template, - modelValue: function() { - var modelValue = this.model.get(this.options.valueAttribute); - if (_.isArray(modelValue) && modelValue.length > 0) { - return modelValue[0].code; - } else { - return null; - } - }, - saveValue: function() { - var attributes = {}, - value = ''; - if (this.persistChanges === true) { - value = this.fieldValue() ? [{code: this.fieldValue()}] : []; - attributes[this.options.valueAttribute] = value; - this.saveAttributes(attributes); - } - } - }), - SocialLinkTextFieldView: FieldViews.TextFieldView.extend({ - render: function() { - HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_text_account_template)({ - id: this.options.valueAttribute + '_' + this.options.platform, - title: this.options.title, - value: this.modelValue(), - message: this.options.helpMessage, - placeholder: this.options.placeholder || '' - })); - this.delegateEvents(); - return this; - }, - - modelValue: function() { - var socialLinks = this.model.get(this.options.valueAttribute); - for (var i = 0; i < socialLinks.length; i++) { // eslint-disable-line vars-on-top - if (socialLinks[i].platform === this.options.platform) { - return socialLinks[i].social_link; - } - } - return null; - }, - saveValue: function() { - var attributes, value; - if (this.persistChanges === true) { - attributes = {}; - value = this.fieldValue() != null ? [{ - platform: this.options.platform, - social_link: this.fieldValue() - }] : []; - attributes[this.options.valueAttribute] = value; - this.saveAttributes(attributes); - } - } - }), - ExtendedFieldTextFieldView: FieldViews.TextFieldView.extend({ - render: function() { - HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_text_account_template)({ - id: this.options.valueAttribute + '_' + this.options.field_name, - title: this.options.title, - value: this.modelValue(), - message: this.options.helpMessage, - placeholder: this.options.placeholder || '' - })); - this.delegateEvents(); - return this; - }, - - modelValue: function() { - var extendedProfileFields = this.model.get(this.options.valueAttribute); - for (var i = 0; i < extendedProfileFields.length; i++) { // eslint-disable-line vars-on-top - if (extendedProfileFields[i].field_name === this.options.fieldName) { - return extendedProfileFields[i].field_value; - } - } - return null; - }, - saveValue: function() { - var attributes, value; - if (this.persistChanges === true) { - attributes = {}; - value = this.fieldValue() != null ? [{ - field_name: this.options.fieldName, - field_value: this.fieldValue() - }] : []; - attributes[this.options.valueAttribute] = value; - this.saveAttributes(attributes); - } - } - }), - ExtendedFieldListFieldView: FieldViews.DropdownFieldView.extend({ - fieldTemplate: field_dropdown_account_template, - modelValue: function() { - var extendedProfileFields = this.model.get(this.options.valueAttribute); - for (var i = 0; i < extendedProfileFields.length; i++) { // eslint-disable-line vars-on-top - if (extendedProfileFields[i].field_name === this.options.fieldName) { - return extendedProfileFields[i].field_value; - } - } - return null; - }, - saveValue: function() { - var attributes = {}, - value; - if (this.persistChanges === true) { - value = this.fieldValue() ? [{ - field_name: this.options.fieldName, - field_value: this.fieldValue() - }] : []; - attributes[this.options.valueAttribute] = value; - this.saveAttributes(attributes); - } - } - }), - AuthFieldView: FieldViews.LinkFieldView.extend({ - fieldTemplate: field_social_link_template, - className: function() { - return 'u-field u-field-social u-field-' + this.options.valueAttribute; - }, - initialize: function(options) { - this.options = _.extend({}, options); - this._super(options); - _.bindAll(this, 'redirect_to', 'disconnect', 'successMessage', 'inProgressMessage'); - }, - render: function() { - var linkTitle = '', - linkClass = '', - subTitle = '', - screenReaderTitle = StringUtils.interpolate( - gettext('Link your {accountName} account'), - {accountName: this.options.title} - ); - if (this.options.connected) { - linkTitle = gettext('Unlink This Account'); - linkClass = 'social-field-linked'; - subTitle = StringUtils.interpolate( - gettext('You can use your {accountName} account to sign in to your {platformName} account.'), // eslint-disable-line max-len - {accountName: this.options.title, platformName: this.options.platformName} - ); - screenReaderTitle = StringUtils.interpolate( - gettext('Unlink your {accountName} account'), - {accountName: this.options.title} - ); - } else if (this.options.acceptsLogins) { - linkTitle = gettext('Link Your Account'); - linkClass = 'social-field-unlinked'; - subTitle = StringUtils.interpolate( - gettext('Link your {accountName} account to your {platformName} account and use {accountName} to sign in to {platformName}.'), // eslint-disable-line max-len - {accountName: this.options.title, platformName: this.options.platformName} - ); - } - - HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({ - id: this.options.valueAttribute, - title: this.options.title, - screenReaderTitle: screenReaderTitle, - linkTitle: linkTitle, - subTitle: subTitle, - linkClass: linkClass, - linkHref: '#', - message: this.helpMessage - })); - this.delegateEvents(); - return this; - }, - linkClicked: function(event) { - event.preventDefault(); - - this.showInProgressMessage(); - - if (this.options.connected) { - this.disconnect(); - } else { - // Direct the user to the providers site to start the authentication process. - // See python-social-auth docs for more information. - this.redirect_to(this.options.connectUrl); - } - }, - redirect_to: function(url) { - window.location.href = url; - }, - disconnect: function() { - var data = {}; - - // Disconnects the provider from the user's edX account. - // See python-social-auth docs for more information. - var view = this; - $.ajax({ - type: 'POST', - url: this.options.disconnectUrl, - data: data, - dataType: 'html', - success: function() { - view.options.connected = false; - view.render(); - view.showSuccessMessage(); - }, - error: function(xhr) { - view.showErrorMessage(xhr); - } - }); - }, - inProgressMessage: function() { - return HtmlUtils.joinHtml(this.indicators.inProgress, ( - this.options.connected ? gettext('Unlinking') : gettext('Linking') - )); - }, - successMessage: function() { - return HtmlUtils.joinHtml(this.indicators.success, gettext('Successfully unlinked.')); - } - }), - - OrderHistoryFieldView: FieldViews.ReadonlyFieldView.extend({ - fieldType: 'orderHistory', - fieldTemplate: field_order_history_template, - - initialize: function(options) { - this.options = options; - this._super(options); - this.template = HtmlUtils.template(this.fieldTemplate); - }, - - render: function() { - HtmlUtils.setHtml(this.$el, this.template({ - totalPrice: this.options.totalPrice, - orderId: this.options.orderId, - orderDate: this.options.orderDate, - receiptUrl: this.options.receiptUrl, - valueAttribute: this.options.valueAttribute, - lines: this.options.lines - })); - this.delegateEvents(); - return this; - } - }) - }; - - return AccountSettingsFieldViews; - }); -}).call(this, define || RequireJS.define); diff --git a/lms/static/js/student_account/views/account_settings_view.js b/lms/static/js/student_account/views/account_settings_view.js deleted file mode 100644 index 6ee9c9101d..0000000000 --- a/lms/static/js/student_account/views/account_settings_view.js +++ /dev/null @@ -1,157 +0,0 @@ -// eslint-disable-next-line no-shadow-restricted-names -(function(define, undefined) { - 'use strict'; - - define([ - 'gettext', - 'jquery', - 'underscore', - 'common/js/components/views/tabbed_view', - 'edx-ui-toolkit/js/utils/html-utils', - 'js/student_account/views/account_section_view', - 'text!templates/student_account/account_settings.underscore' - ], function(gettext, $, _, TabbedView, HtmlUtils, AccountSectionView, accountSettingsTemplate) { - var AccountSettingsView = TabbedView.extend({ - - navLink: '.account-nav-link', - activeTab: 'aboutTabSections', - events: { - 'click .account-nav-link': 'switchTab', - 'keydown .account-nav-link': 'keydownHandler', - 'click .btn-alert-primary': 'revertValue' - }, - - initialize: function(options) { - this.options = options; - _.bindAll(this, 'render', 'switchTab', 'setActiveTab', 'showLoadingError'); - }, - - render: function() { - var tabName, betaLangMessage, helpTranslateText, helpTranslateLink, betaLangCode, oldLangCode, - view = this; - var accountSettingsTabs = [ - { - name: 'aboutTabSections', - id: 'about-tab', - label: gettext('Account Information'), - class: 'active', - tabindex: 0, - selected: true, - expanded: true - }, - { - name: 'accountsTabSections', - id: 'accounts-tab', - label: gettext('Linked Accounts'), - tabindex: -1, - selected: false, - expanded: false - } - ]; - if (!view.options.disableOrderHistoryTab) { - accountSettingsTabs.push({ - name: 'ordersTabSections', - id: 'orders-tab', - label: gettext('Order History'), - tabindex: -1, - selected: false, - expanded: false - }); - } - - if (!_.isEmpty(view.options.betaLanguage) && $.cookie('old-pref-lang')) { - betaLangMessage = HtmlUtils.interpolateHtml( - gettext('You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.'), // eslint-disable-line max-len - { - beta_language: view.options.betaLanguage.name - } - ); - helpTranslateText = HtmlUtils.interpolateHtml( - gettext('Help Translate into {beta_language}'), - { - beta_language: view.options.betaLanguage.name - } - ); - betaLangCode = this.options.betaLanguage.code.split('-'); - if (betaLangCode.length > 1) { - betaLangCode = betaLangCode[0] + '_' + betaLangCode[1].toUpperCase(); - } else { - betaLangCode = betaLangCode[0]; - } - helpTranslateLink = 'https://www.transifex.com/open-edx/edx-platform/translate/#' + betaLangCode; - oldLangCode = $.cookie('old-pref-lang'); - // Deleting the cookie - document.cookie = 'old-pref-lang=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/account;'; - - $.cookie('focus_id', '#beta-language-message'); - } - HtmlUtils.setHtml(this.$el, HtmlUtils.template(accountSettingsTemplate)({ - accountSettingsTabs: accountSettingsTabs, - HtmlUtils: HtmlUtils, - message: betaLangMessage, - helpTranslateText: helpTranslateText, - helpTranslateLink: helpTranslateLink, - oldLangCode: oldLangCode - })); - _.each(accountSettingsTabs, function(tab) { - tabName = tab.name; - view.renderSection(view.options.tabSections[tabName], tabName, tab.label); - }); - return this; - }, - - switchTab: function(e) { - var $currentTab, - $accountNavLink = $('.account-nav-link'); - - if (e) { - e.preventDefault(); - $currentTab = $(e.target); - this.activeTab = $currentTab.data('name'); - - _.each(this.$('.account-settings-tabpanels'), function(tabPanel) { - $(tabPanel).addClass('hidden'); - }); - - $('#' + this.activeTab + '-tabpanel').removeClass('hidden'); - - $accountNavLink.attr('tabindex', -1); - $accountNavLink.attr('aria-selected', false); - $accountNavLink.attr('aria-expanded', false); - - $currentTab.attr('tabindex', 0); - $currentTab.attr('aria-selected', true); - $currentTab.attr('aria-expanded', true); - - $(this.navLink).removeClass('active'); - $currentTab.addClass('active'); - } - }, - - setActiveTab: function() { - this.switchTab(); - }, - - renderSection: function(tabSections, tabName, tabLabel) { - var accountSectionView = new AccountSectionView({ - tabName: tabName, - tabLabel: tabLabel, - sections: tabSections, - el: '#' + tabName + '-tabpanel' - }); - - accountSectionView.render(); - }, - - showLoadingError: function() { - this.$('.ui-loading-error').removeClass('is-hidden'); - }, - - revertValue: function(event) { - this.options.userPreferencesModel.trigger('revertValue', event); - } - }); - - return AccountSettingsView; - }); -}).call(this, define || RequireJS.define); diff --git a/lms/static/learner_profile b/lms/static/learner_profile deleted file mode 120000 index ca7ce1f797..0000000000 --- a/lms/static/learner_profile +++ /dev/null @@ -1 +0,0 @@ -../../openedx/features/learner_profile/static/learner_profile \ No newline at end of file diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index c22f366c5d..1d5e1a983b 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -33,10 +33,8 @@ 'js/discussions_management/views/discussions_dashboard_factory', 'js/header_factory', 'js/student_account/logistration_factory', - 'js/student_account/views/account_settings_factory', 'js/student_account/views/finish_auth_factory', 'js/views/message_banner', - 'learner_profile/js/learner_profile_factory', 'lms/js/preview/preview_factory', 'support/js/certificates_factory', 'support/js/enrollment_factory', diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index cd8668ddc4..4b6b0d9ec6 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -761,9 +761,6 @@ 'js/spec/shoppingcart/shoppingcart_spec.js', 'js/spec/staff_debug_actions_spec.js', 'js/spec/student_account/access_spec.js', - 'js/spec/student_account/account_settings_factory_spec.js', - 'js/spec/student_account/account_settings_fields_spec.js', - 'js/spec/student_account/account_settings_view_spec.js', 'js/spec/student_account/emailoptin_spec.js', 'js/spec/student_account/enrollment_spec.js', 'js/spec/student_account/finish_auth_spec.js', @@ -787,10 +784,6 @@ 'js/spec/views/file_uploader_spec.js', 'js/spec/views/message_banner_spec.js', 'js/spec/views/notification_spec.js', - 'learner_profile/js/spec/learner_profile_factory_spec.js', - 'learner_profile/js/spec/views/learner_profile_fields_spec.js', - 'learner_profile/js/spec/views/learner_profile_view_spec.js', - 'learner_profile/js/spec/views/section_two_tab_spec.js', 'support/js/spec/collections/enrollment_spec.js', 'support/js/spec/models/enrollment_spec.js', 'support/js/spec/views/certificates_spec.js', diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index 7a77eb34ca..90c5077c1f 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -52,7 +52,6 @@ @import 'multicourse/survey-page'; // base - specific views -@import 'views/account-settings'; @import 'views/course-entitlements'; @import 'views/login-register'; @import 'views/verification'; @@ -68,7 +67,6 @@ // features @import 'features/bookmarks-v1'; -@import 'features/learner-profile'; @import 'features/_unsupported-browser-alert'; @import 'features/content-type-gating'; @import 'features/course-duration-limits'; diff --git a/lms/static/sass/features/_learner-profile.scss b/lms/static/sass/features/_learner-profile.scss deleted file mode 100644 index 8d35a7eccc..0000000000 --- a/lms/static/sass/features/_learner-profile.scss +++ /dev/null @@ -1,875 +0,0 @@ -// lms - application - learner profile -// ==================== - -.learner-achievements { - .learner-message { - @extend %no-content; - - margin: $baseline*0.75 0; - - .message-header, - .message-actions { - text-align: center; - } - - .message-actions { - margin-top: $baseline/2; - - .btn-brand { - color: $white; - } - } - } -} - -.certificate-card { - display: flex; - flex-direction: row; - margin-bottom: $baseline; - padding: $baseline/2; - border: 1px; - border-style: solid; - background-color: $white; - cursor: pointer; - - &:hover { - box-shadow: 0 0 1px 1px $gray-l2; - } - - .card-logo { - @include margin-right($baseline); - - width: 100px; - height: 100px; - - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - display: none; - } - } - - .card-content { - color: $body-color; - margin-top: $baseline/2; - } - - .card-supertitle { - @extend %t-title6; - - color: $lightest-base-font-color; - } - - .card-title { - @extend %t-title5; - @extend %t-strong; - - margin-bottom: $baseline/2; - } - - .card-text { - @extend %t-title8; - - color: $lightest-base-font-color; - } - - &.mode-audit { - border-color: $audit-mode-color; - - .card-logo { - background-image: url('#{$static-path}/images/certificates/audit.png'); - } - } - - &.mode-honor { - border-color: $honor-mode-color; - - .card-logo { - background-image: url('#{$static-path}/images/certificates/honor.png'); - } - } - - &.mode-verified { - border-color: $verified-mode-color; - - .card-logo { - background-image: url('#{$static-path}/images/certificates/verified.png'); - } - } - - &.mode-professional { - border-color: $professional-certificate-color; - - .card-logo { - background-image: url('#{$static-path}/images/certificates/professional.png'); - } - } -} - -.view-profile { - $profile-image-dimension: 120px; - - .window-wrap, - .content-wrapper { - background-color: $body-bg; - padding: 0; - margin-top: 0; - } - - .page-banner { - background-color: $gray-l4; - max-width: none; - - .user-messages { - max-width: map-get($container-max-widths, xl); - margin: auto; - padding: $baseline/2; - } - } - - .ui-loading-indicator { - @extend .ui-loading-base; - - padding-bottom: $baseline; - - // center horizontally - @include margin-left(auto); - @include margin-right(auto); - - width: ($baseline*5); - } - - .profile-image-field { - button { - background: transparent !important; - border: none !important; - padding: 0; - } - - .u-field-image { - padding-top: 0; - padding-bottom: ($baseline/4); - } - - .image-wrapper { - width: $profile-image-dimension; - position: relative; - margin: auto; - - .image-frame { - display: block; - position: relative; - width: $profile-image-dimension; - height: $profile-image-dimension; - border-radius: ($profile-image-dimension/2); - overflow: hidden; - border: 3px solid $gray-l6; - margin-top: $baseline*-0.75; - background: $white; - } - - .u-field-upload-button { - position: absolute; - top: 0; - opacity: 0; - width: $profile-image-dimension; - height: $profile-image-dimension; - border-radius: ($profile-image-dimension/2); - border: 2px dashed transparent; - background: rgba(229, 241, 247, 0.8); - color: $link-color; - text-shadow: none; - - @include transition(all $tmg-f1 ease-in-out 0s); - - z-index: 6; - - i { - color: $link-color; - } - - &:focus, - &:hover { - @include show-hover-state(); - - border-color: $link-color; - } - - &.in-progress { - opacity: 1; - } - } - - .button-visible { - @include show-hover-state(); - } - - .upload-button-icon, - .upload-button-title { - display: block; - margin-bottom: ($baseline/4); - - @include transform(translateY(35px)); - - line-height: 1.3em; - text-align: center; - z-index: 7; - color: $body-color; - } - - .upload-button-input { - position: absolute; - top: 0; - - @include left(0); - - width: $profile-image-dimension; - border-radius: ($profile-image-dimension/2); - height: 100%; - cursor: pointer; - z-index: 5; - outline: 0; - opacity: 0; - } - - .u-field-remove-button { - position: relative; - display: block; - width: $profile-image-dimension; - margin-top: ($baseline / 4); - padding: ($baseline / 5) 0 0; - text-align: center; - opacity: 0; - transition: opacity 0.5s; - } - - &:hover, - &:active { - .u-field-remove-button { - opacity: 1; - } - } - } - } - - .wrapper-profile { - min-height: 200px; - background-color: $gray-l6; - - .ui-loading-indicator { - margin-top: 100px; - } - } - - .profile-self { - .wrapper-profile-field-account-privacy { - @include clearfix(); - - box-sizing: border-box; - width: 100%; - margin: 0 auto; - border-bottom: 1px solid $gray-l3; - background-color: $gray-l4; - padding: ($baseline*0.75) 5%; - display: table; - - .wrapper-profile-records { - display: table-row; - - button { - @extend %btn-secondary-blue-outline; - - margin-top: 1em; - background: $blue; - color: #fff; - } - } - - @include media-breakpoint-up(sm) { - .wrapper-profile-records { - display: table-cell; - vertical-align: middle; - white-space: nowrap; - - button { - margin-top: 0; - } - } - } - - .u-field-account_privacy { - @extend .container; - - display: table-cell; - border: none; - box-shadow: none; - padding: 0; - margin: 0; - vertical-align: middle; - - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - max-width: calc(100% - 40px); - min-width: auto; - } - - .btn-change-privacy { - @extend %btn-primary-blue; - - padding-top: 4px; - padding-bottom: 5px; - background-image: none; - box-shadow: none; - } - } - - .u-field-title { - @extend %t-strong; - - width: auto; - color: $body-color; - cursor: text; - text-shadow: none; // override bad lms styles on labels - } - - .u-field-value { - width: auto; - - @include margin-left($baseline/2); - } - - .u-field-message { - @include float(left); - - width: 100%; - padding: 0; - color: $body-color; - - .u-field-message-notification { - color: $gray-d2; - } - } - } - } - - .wrapper-profile-sections { - @extend .container; - - @include padding($baseline*1.5, 5%, $baseline*1.5, 5%); - - display: flex; - min-width: 0; - max-width: 100%; - - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - @include margin-left(0); - - flex-wrap: wrap; - } - } - - .profile-header { - max-width: map-get($container-max-widths, xl); - margin: auto; - padding: $baseline 5% 0; - - .header { - @extend %t-title4; - @extend %t-ultrastrong; - - display: inline-block; - color: #222; - } - - .subheader { - @extend %t-title6; - } - } - - .wrapper-profile-section-container-one { - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - width: 100%; - } - - .wrapper-profile-section-one { - width: 300px; - background-color: $white; - border-top: 5px solid $blue; - padding-bottom: $baseline; - - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - @include margin-left(0); - - width: 100%; - } - - .profile-section-one-fields { - margin: 0 $baseline/2; - - .social-links { - @include padding($baseline/4, 0, 0, $baseline/4); - - font-size: 2rem; - - & > span { - color: $gray-l4; - } - - a { - .fa-facebook-square { - color: $facebook-blue; - } - - .fa-twitter-square { - color: $twitter-blue; - } - - .fa-linkedin-square { - color: $linkedin-blue; - } - } - } - - .u-field { - font-weight: $font-semibold; - - @include padding(0, 0, 0, 3px); - - color: $body-color; - margin-top: $baseline/5; - - .u-field-value, - .u-field-title { - font-weight: 500; - width: calc(100% - 40px); - color: $lightest-base-font-color; - } - - .u-field-value-readonly { - font-family: $font-family-sans-serif; - color: $darkest-base-font-color; - } - - &.u-field-dropdown { - position: relative; - - &:not(.editable-never) { - cursor: pointer; - } - } - - &:not(.u-field-readonly) { - &.u-field-value { - @extend %t-weight3; - } - - &:not(:last-child) { - padding-bottom: $baseline/4; - border-bottom: 1px solid $border-color; - - &:hover.mode-placeholder { - padding-bottom: $baseline/5; - border-bottom: 2px dashed $link-color; - } - } - } - } - - & > .u-field { - &:not(:first-child) { - font-size: $body-font-size; - color: $body-color; - font-weight: $font-light; - margin-bottom: 0; - } - - &:first-child { - @extend %t-title4; - @extend %t-weight4; - - font-size: em(24); - } - } - - select { - width: 85%; - } - - .u-field-message { - @include right(0); - - position: absolute; - top: 0; - width: 20px; - - .icon { - vertical-align: baseline; - } - } - } - } - } - - - .wrapper-profile-section-container-two { - @include float(left); - @include padding-left($baseline); - - font-family: $font-family-sans-serif; - flex-grow: 1; - - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - width: 90%; - margin-top: $baseline; - padding: 0; - } - - .u-field-textarea { - @include padding(0, ($baseline*0.75), ($baseline*0.75), 0); - - margin-bottom: ($baseline/2); - - @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - @include padding-left($baseline/4); - } - - .u-field-header { - position: relative; - - .u-field-message { - @include right(0); - - top: $baseline/4; - position: absolute; - } - } - - &.editable-toggle { - cursor: pointer; - } - } - - .u-field-title { - @extend %t-title6; - - display: inline-block; - margin-top: 0; - margin-bottom: ($baseline/4); - color: $gray-d3; - width: 100%; - font: $font-semibold 1.4em/1.4em $font-family-sans-serif; - } - - .u-field-value { - @extend %t-copy-base; - - width: 100%; - overflow: auto; - - textarea { - width: 100%; - background-color: transparent; - border-radius: 5px; - border-color: $gray-d1; - resize: none; - white-space: pre-line; - outline: 0; - box-shadow: none; - -webkit-appearance: none; - } - - a { - color: inherit; - } - } - - .u-field-message { - @include float(right); - - width: auto; - - .message-can-edit { - position: absolute; - } - } - - .u-field.mode-placeholder { - padding: $baseline; - margin: $baseline*0.75 0; - border: 2px dashed $gray-l3; - - i { - font-size: 12px; - - @include padding-right(5px); - - vertical-align: middle; - color: $body-color; - } - - .u-field-title { - width: 100%; - text-align: center; - } - - .u-field-value { - text-align: center; - line-height: 1.5em; - - @extend %t-copy-sub1; - - color: $body-color; - } - - &:hover { - border: 2px dashed $link-color; - - .u-field-title, - i { - color: $link-color; - } - } - } - - .wrapper-u-field { - font-size: $body-font-size; - color: $body-color; - - .u-field-header .u-field-title { - color: $body-color; - } - - .u-field-footer { - .field-textarea-character-count { - @extend %t-weight1; - - @include float(right); - - margin-top: $baseline/4; - } - } - } - - .profile-private-message { - @include padding-left($baseline*0.75); - - line-height: 3em; - } - } - - .badge-paging-header { - padding-top: $baseline; - } - - .page-content-nav { - @extend %page-content-nav; - } - - .badge-set-display { - @extend .container; - - padding: 0; - - .badge-list { - // We're using a div instead of ul for accessibility, so we have to match the style - // used by ul. - margin: 1em 0; - padding: 0 0 0 40px; - } - - .badge-display { - width: 50%; - display: inline-block; - vertical-align: top; - padding: 2em 0; - - .badge-image-container { - padding-right: $baseline; - margin-left: 1em; - width: 20%; - vertical-align: top; - display: inline-block; - - img.badge { - width: 100%; - } - - .accomplishment-placeholder { - border: 4px dotted $gray-l4; - border-radius: 50%; - display: block; - width: 100%; - padding-bottom: 100%; - } - } - - .badge-details { - @extend %t-copy-sub1; - @extend %t-regular; - - max-width: 70%; - display: inline-block; - color: $gray-d1; - - .badge-name { - @extend %t-strong; - @extend %t-copy-base; - - color: $gray-d3; - } - - .badge-description { - padding-bottom: $baseline; - line-height: 1.5em; - } - - .badge-date-stamp { - @extend %t-copy-sub1; - } - - .find-button-container { - border: 1px solid $blue-l1; - padding: ($baseline / 2) $baseline ($baseline / 2) $baseline; - display: inline-block; - border-radius: 5px; - font-weight: bold; - color: $blue-s3; - } - - .share-button { - @extend %t-action3; - @extend %button-reset; - - background: $gray-l6; - color: $gray-d1; - padding: ($baseline / 4) ($baseline / 2); - margin-bottom: ($baseline / 2); - display: inline-block; - border-radius: 5px; - border: 2px solid $gray-d1; - cursor: pointer; - transition: background 0.5s; - - .share-prefix { - display: inline-block; - vertical-align: middle; - } - - .share-icon-container { - display: inline-block; - - img.icon-mozillaopenbadges { - max-width: 1.5em; - margin-right: 0.25em; - } - } - - &:hover { - background: $gray-l4; - } - - &:active { - box-shadow: inset 0 4px 15px 0 $black-t2; - transition: none; - } - } - } - } - - .badge-placeholder { - background-color: $gray-l7; - box-shadow: inset 0 0 4px 0 $gray-l4; - } - } - - // ------------------------------ - // #BADGES MODAL - // ------------------------------ - .badges-overlay { - @extend %ui-depth1; - - position: fixed; - top: 0; - left: 0; - background-color: $dark-trans-bg; /* dim the background */ - width: 100%; - height: 100%; - vertical-align: middle; - - .badges-modal { - @extend %t-copy-lead1; - @extend %ui-depth2; - - color: $lighter-base-font-color; - box-sizing: content-box; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 80%; - max-width: 700px; - max-height: calc(100% - 100px); - margin-right: auto; - margin-left: auto; - border-top: rem(10) solid $blue-l2; - background: $light-gray3; - padding-right: ($baseline * 2); - padding-left: ($baseline * 2); - padding-bottom: ($baseline); - overflow-x: hidden; - - .modal-header { - margin-top: ($baseline / 2); - margin-bottom: ($baseline / 2); - } - - .close { - @extend %button-reset; - @extend %t-strong; - - color: $lighter-base-font-color; - position: absolute; - right: ($baseline); - top: $baseline; - cursor: pointer; - padding: ($baseline / 4) ($baseline / 2); - - @include transition(all $tmg-f2 ease-in-out 0s); - - &:focus, - &:hover { - background-color: $blue-d2; - border-radius: 3px; - color: $white; - } - } - - .badges-steps { - display: table; - } - - .image-container { - // Lines the image up with the content of the above list. - @include ltr { - @include padding-left(2em); - } - - @include rtl { - @include padding-right(1em); - - float: right; - } - } - - .backpack-logo { - @include float(right); - @include margin-left($baseline); - } - } - } - - .modal-hr { - display: block; - border: none; - background-color: $light-gray; - height: rem(2); - width: 100%; - } -} diff --git a/lms/static/sass/partials/lms/theme/_variables-v1.scss b/lms/static/sass/partials/lms/theme/_variables-v1.scss index 1cff0168ac..5dca9b8495 100644 --- a/lms/static/sass/partials/lms/theme/_variables-v1.scss +++ b/lms/static/sass/partials/lms/theme/_variables-v1.scss @@ -527,9 +527,6 @@ $palette-success-border: #b9edb9; $palette-success-back: #ecfaec; $palette-success-text: #008100; -// learner profile elements -$learner-profile-container-flex: 768px; - // course elements $course-bg-color: $uxpl-grayscale-x-back !default; $account-content-wrapper-bg: shade($body-bg, 2%) !default; diff --git a/lms/static/sass/views/_account-settings.scss b/lms/static/sass/views/_account-settings.scss deleted file mode 100644 index a4e5ff76ea..0000000000 --- a/lms/static/sass/views/_account-settings.scss +++ /dev/null @@ -1,683 +0,0 @@ -// lms - application - account settings -// ==================== - -// Table of Contents -// * +Container - Account Settings -// * +Main - Header -// * +Settings Section -// * +Alert Messages - - -// +Container - Account Settings -.wrapper-account-settings { - background: $white; - width: 100%; - - .account-settings-container { - max-width: grid-width(12); - padding: 10px; - margin: 0 auto; - } - - .ui-loading-indicator, - .ui-loading-error { - @extend .ui-loading-base; - // center horizontally - @include margin-left(auto); - @include margin-right(auto); - - padding: ($baseline*3); - text-align: center; - - .message-error { - color: $alert-color; - } - } -} - -// +Main - Header -.wrapper-account-settings { - .wrapper-header { - max-width: grid-width(12); - height: 139px; - border-bottom: 4px solid $m-gray-l4; - - .header-title { - @extend %t-title4; - - margin-bottom: ($baseline/2); - padding-top: ($baseline*2); - } - - .header-subtitle { - color: $gray-l2; - } - - .account-nav { - @include float(left); - - margin: ($baseline/2) 0; - padding: 0; - list-style: none; - - .account-nav-link { - @include float(left); - - font-size: em(14); - color: $gray; - padding: $baseline/4 $baseline*1.25 $baseline; - display: inline-block; - box-shadow: none; - border-bottom: 4px solid transparent; - border-radius: 0; - background: transparent none; - } - - button { - @extend %ui-clear-button; - @extend %btn-no-style; - - @include appearance(none); - - display: block; - padding: ($baseline/4); - - &:hover, - &:focus { - text-decoration: none; - border-bottom-color: $courseware-border-bottom-color; - } - - &.active { - border-bottom-color: theme-color("dark"); - } - } - } - - @include media-breakpoint-down(md) { - border-bottom-color: transparent; - - .account-nav { - display: flex; - border-bottom: none; - - .account-nav-link { - border-bottom: 4px solid theme-color("light"); - } - } - } - } -} - -// +Settings Section -.account-settings-sections { - .section-header { - @extend %t-title5; - @extend %t-strong; - - padding-top: ($baseline/2)*3; - color: $dark-gray1; - } - - .section { - background-color: $white; - margin: $baseline 5% 0; - border-bottom: 4px solid $m-gray-l4; - - .account-settings-header-subtitle { - font-size: em(14); - line-height: normal; - color: $dark-gray; - padding-bottom: 10px; - } - - .account-settings-header-subtitle-warning { - @extend .account-settings-header-subtitle; - - color: $alert-color; - } - - .account-settings-section-body { - .u-field { - border-bottom: 2px solid $m-gray-l4; - padding: $baseline*0.75 0; - - .field { - width: 30%; - vertical-align: top; - display: inline-block; - position: relative; - - select { - @include appearance(none); - - padding: 14px 30px 14px 15px; - border: 1px solid $gray58-border; - background-color: transparent; - border-radius: 2px; - position: relative; - z-index: 10; - - &::-ms-expand { - display: none; - } - - ~ .icon-caret-down { - &::after { - content: ""; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 7px solid $blue; - position: absolute; - right: 10px; - bottom: 20px; - z-index: 0; - } - } - } - - .field-label { - display: block; - width: auto; - margin-bottom: 0.625rem; - font-size: 1rem; - line-height: 1; - color: $dark-gray; - white-space: nowrap; - } - - .field-input { - @include transition(all 0.125s ease-in-out 0s); - - display: inline-block; - padding: 0.625rem; - border: 1px solid $gray58-border; - border-radius: 2px; - background: $white; - font-size: $body-font-size; - color: $dark-gray; - width: 100%; - height: 48px; - box-shadow: none; - } - - .u-field-link { - @extend %ui-clear-button; - - // set styles - @extend %btn-pl-default-base; - - @include font-size(18); - - width: 100%; - border: 1px solid $blue; - color: $blue; - padding: 11px 14px; - line-height: normal; - } - } - - .u-field-order { - display: flex; - align-items: center; - font-size: em(16); - font-weight: 600; - color: $dark-gray; - width: 100%; - padding-top: $baseline; - padding-bottom: $baseline; - line-height: normal; - flex-flow: row wrap; - - span { - padding: $baseline; - } - - .u-field-order-number { - @include float(left); - - width: 30%; - } - - .u-field-order-date { - @include float(left); - - padding-left: 30px; - width: 20%; - } - - .u-field-order-price { - @include float(left); - - width: 15%; - } - - .u-field-order-link { - width: 10%; - padding: 0; - - .u-field-link { - @extend %ui-clear-button; - @extend %btn-pl-default-base; - - @include font-size(14); - - border: 1px solid $blue; - color: $blue; - line-height: normal; - padding: 10px; - width: 110px; - } - } - } - - .u-field-order-lines { - @extend .u-field-order; - - padding: 5px 0 0; - font-weight: 100; - - .u-field-order-number { - padding: 20px 10px 20px 30px; - } - } - - .social-field-linked { - background: $m-gray-l4; - box-shadow: 0 1px 2px 1px $shadow-l2; - padding: 1.25rem; - box-sizing: border-box; - margin: 10px; - width: 100%; - - .field-label { - @include font-size(24); - } - - .u-field-social-help { - display: inline-block; - padding: 20px 0 6px; - } - - .u-field-link { - @include font-size(14); - @include text-align(left); - - border: none; - margin-top: $baseline; - font-weight: $font-semibold; - padding: 0; - - &:focus, - &:hover, - &:active { - background-color: transparent; - color: $m-blue-d3; - border: none; - } - } - } - - .social-field-unlinked { - background: $m-gray-l4; - box-shadow: 0 1px 2px 1px $shadow-l2; - padding: 1.25rem; - box-sizing: border-box; - text-align: center; - margin: 10px; - width: 100%; - - .field-label { - @include font-size(24); - - text-align: center; - } - - .u-field-link { - @include font-size(14); - - margin-top: $baseline; - font-weight: $font-semibold; - } - } - - .u-field-message { - position: relative; - padding: $baseline*0.75 0 0 ($baseline*4); - width: 60%; - - .u-field-message-notification { - position: absolute; - left: 0; - top: 0; - bottom: 0; - margin: auto; - padding: 38px 0 0 ($baseline*5); - } - } - - &:last-child { - border-bottom: none; - margin-bottom: ($baseline*2); - } - - // Responsive behavior - @include media-breakpoint-down(md) { - .u-field-value { - width: 100%; - } - - .u-field-message { - width: 100%; - padding: $baseline/2 0; - - .u-field-message-notification { - position: relative; - padding: 0; - } - } - - .u-field-order { - display: flex; - flex-wrap: nowrap; - - .u-field-order-number, - .u-field-order-date, - .u-field-order-price, - .u-field-order-link { - width: auto; - float: none; - flex-grow: 1; - - &:first-of-type { - flex-grow: 2; - } - } - } - } - } - - .u-field { - &.u-field-dropdown, - &.editable-never &.mode-display { - .u-field-value { - margin-bottom: ($baseline); - - .u-field-title { - font-size: 16px; - line-height: 22px; - margin-bottom: 18px; - } - - .u-field-value-readonly { - font-size: 22px; - color: #636c72; - line-height: 30px; - white-space: nowrap; - } - } - } - } - - .u-field-readonly .u-field-title { - font-size: 16px; - color: #636c72; - line-height: 22px; - padding-top: ($baseline/2); - padding-bottom: 0; - margin-bottom: 8px !important; - } - - .u-field-readonly .u-field-value { - font-size: 22px; - color: #636c72; - line-height: 30px; - padding-top: 8px; - padding-bottom: ($baseline); - white-space: nowrap; - } - - .u-field-orderHistory { - border-bottom: none; - border: 1px solid $m-gray-l4; - margin-bottom: $baseline; - padding: 0; - - &:last-child { - border-bottom: 1px solid $m-gray-l4; - } - - &:hover, - &:focus { - background-color: $light-gray4; - } - } - - .u-field-order-orderId { - border: none; - margin-top: $baseline; - margin-bottom: 0; - padding-bottom: 0; - - &:hover, - &:focus { - background-color: transparent; - } - - .u-field-order { - font-weight: $font-semibold; - padding-top: 0; - padding-bottom: 0; - - .u-field-order-title { - font-size: em(16); - } - } - } - - .u-field-social { - border-bottom: none; - margin-right: 20px; - width: 30%; - display: inline-block; - vertical-align: top; - - .u-field-social-help { - @include font-size(12); - - color: $m-gray-d1; - } - } - } - - .account-deletion-details { - .btn-outline-primary { - @extend %ui-clear-button; - - // set styles - @extend %btn-pl-default-base; - - @include font-size(18); - - border: 1px solid $blue; - color: $blue; - padding: 11px 14px; - line-height: normal; - margin: 20px 0; - } - - .paragon__modal-open { - overflow-y: scroll; - color: $dark-gray; - - .paragon__modal-title { - font-weight: $font-semibold; - } - - .paragon__modal-body { - line-height: 1.5; - - .alert-title { - line-height: 1.5; - } - } - - .paragon__alert-warning { - color: $dark-gray; - } - - .next-steps { - margin-bottom: 10px; - font-weight: $font-semibold; - } - - .confirm-password-input { - width: 50%; - } - - .paragon__btn:not(.cancel-btn) { - @extend %btn-primary-blue; - } - } - - .modal-alert { - display: flex; - - .icon-wrapper { - padding-right: 15px; - } - - .alert-content { - .alert-title { - color: $dark-gray; - margin-bottom: 10px; - font: { - size: 1rem; - weight: $font-semibold; - } - } - - a { - color: $blue-u1; - } - } - } - - .delete-confirmation-wrapper { - .paragon__modal-footer { - .paragon__btn-outline-primary { - @extend %ui-clear-button; - - // set styles - @extend %btn-pl-default-base; - - @include margin-left(25px); - - border-color: $blue; - color: $blue; - padding: 11px 14px; - line-height: normal; - } - } - } - } - - &:last-child { - border-bottom: none; - } - } -} - -// * +Alert Messages -.account-settings-message, -.account-settings-section-message { - font-size: 16px; - line-height: 22px; - margin-top: 15px; - margin-bottom: 30px; - - .alert-message { - color: #292b2c; - font-family: $font-family-sans-serif; - position: relative; - padding: 10px 10px 10px 35px; - border: 1px solid transparent; - border-radius: 0; - box-shadow: none; - margin-bottom: 8px; - - & > .fa { - position: absolute; - left: 11px; - top: 13px; - font-size: 16px; - } - - span { - display: block; - - a { - text-decoration: underline; - } - } - } - - .success { - background-color: #ecfaec; - border-color: #b9edb9; - } - - .info { - background-color: #d8edf8; - border-color: #bbdff2; - } - - .warning { - background-color: #fcf8e3; - border-color: #faebcc; - } - - .error { - background-color: #f2dede; - border-color: #ebccd1; - } -} - -.account-settings-message { - margin-bottom: 0; - - .alert-message { - padding: 10px; - - .alert-actions { - margin-top: 10px; - - .btn-alert-primary { - @extend %btn-primary-blue; - - @include font-size(18); - - border: 1px solid $m-blue-d3; - border-radius: 3px; - box-shadow: none; - padding: 11px 14px; - line-height: normal; - } - - .btn-alert-secondary { - @extend %ui-clear-button; - - // set styles - @extend %btn-pl-default-base; - - @include font-size(18); - - background-color: white; - border: 1px solid $blue; - color: $blue; - padding: 11px 14px; - line-height: normal; - } - } - } -} diff --git a/lms/templates/dashboard/_dashboard_third_party_error.html b/lms/templates/dashboard/_dashboard_third_party_error.html deleted file mode 100644 index 5b9efe0bbd..0000000000 --- a/lms/templates/dashboard/_dashboard_third_party_error.html +++ /dev/null @@ -1,14 +0,0 @@ -<%page expression_filter="h"/> - -<%! from django.utils.translation import gettext as _ %> -
-
-
-

${_("Could Not Link Accounts")}

-
- ## Translators: this message is displayed when a user tries to link their account with a third-party authentication provider (for example, Google or LinkedIn) with a given edX account, but their third-party account is already associated with another edX account. provider_name is the name of the third-party authentication provider, and platform_name is the name of the edX deployment. -

${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name=duplicate_provider, platform_name=platform_name)}

-
-
-
-
diff --git a/lms/templates/header/user_dropdown.html b/lms/templates/header/user_dropdown.html index 2e7e168a69..b4b22e0e32 100644 --- a/lms/templates/header/user_dropdown.html +++ b/lms/templates/header/user_dropdown.html @@ -4,13 +4,13 @@ <%! import json +from urllib.parse import urljoin from django.conf import settings from django.urls import reverse from django.utils.translation import gettext as _ from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user -from openedx.core.djangoapps.user_api.accounts.toggles import should_redirect_to_order_history_microfrontend from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name, get_enterprise_learner_portal %> @@ -23,7 +23,7 @@ displayname = get_enterprise_learner_generic_name(request) or username enterprise_customer_portal = get_enterprise_learner_portal(request) ## Enterprises with the learner portal enabled should not show order history, as it does ## not apply to the learner's method of purchasing content. -should_show_order_history = should_redirect_to_order_history_microfrontend() and not enterprise_customer_portal +should_show_order_history = not enterprise_customer_portal %> @@ -36,7 +36,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium'] % else: diff --git a/lms/urls.py b/lms/urls.py index f97bc7f0d0..9c7dd433a6 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -658,12 +658,6 @@ urlpatterns += [ include('openedx.features.calendar_sync.urls'), ), - # Learner profile - path( - 'u/', - include('openedx.features.learner_profile.urls'), - ), - # Survey Report re_path( fr'^survey_report/', diff --git a/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py b/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py index d7578f25eb..41f91c7d1e 100644 --- a/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py +++ b/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py @@ -44,21 +44,6 @@ class TestComprehensiveThemeLMS(TestCase): # This string comes from header.html of test-theme self.assertContains(resp, "This is a footer for test-theme.") - @with_comprehensive_theme("edx.org") - def test_account_settings_hide_nav(self): - """ - Test that theme header doesn't show marketing site links for Account Settings page. - """ - self._login() - - account_settings_url = reverse('account_settings') - response = self.client.get(account_settings_url) - - # Verify that the header navigation links are hidden for the edx.org version - self.assertNotContains(response, "How it Works") - self.assertNotContains(response, "Find courses") - self.assertNotContains(response, "Schools & Partners") - @with_comprehensive_theme("test-theme") def test_logo_image(self): """ diff --git a/openedx/core/djangoapps/user_api/accounts/settings_views.py b/openedx/core/djangoapps/user_api/accounts/settings_views.py deleted file mode 100644 index 79e01e0bf0..0000000000 --- a/openedx/core/djangoapps/user_api/accounts/settings_views.py +++ /dev/null @@ -1,300 +0,0 @@ -""" Views related to Account Settings. """ - - -import logging -import urllib -from datetime import datetime - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.http import HttpResponseRedirect -from django.shortcuts import redirect -from django.urls import reverse -from django.utils.translation import gettext as _ -from django.views.decorators.http import require_http_methods -from django_countries import countries - -from openedx_filters.learning.filters import AccountSettingsRenderStarted -from common.djangoapps import third_party_auth -from common.djangoapps.edxmako.shortcuts import render_to_response -from common.djangoapps.student.models import UserProfile -from common.djangoapps.third_party_auth import pipeline -from common.djangoapps.util.date_utils import strftime_localized -from lms.djangoapps.commerce.models import CommerceConfiguration -from lms.djangoapps.commerce.utils import EcommerceService -from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client -from openedx.core.djangoapps.dark_lang.models import DarkLangConfig -from openedx.core.djangoapps.lang_pref.api import all_languages, released_languages -from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.core.djangoapps.user_api.accounts.toggles import ( - should_redirect_to_account_microfrontend, - should_redirect_to_order_history_microfrontend -) -from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences -from openedx.core.lib.edx_api_utils import get_api_data -from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES -from openedx.features.enterprise_support.api import enterprise_customer_for_request -from openedx.features.enterprise_support.utils import update_account_settings_context_for_enterprise - -log = logging.getLogger(__name__) - - -@login_required -@require_http_methods(['GET']) -def account_settings(request): - """Render the current user's account settings page. - - Args: - request (HttpRequest) - - Returns: - HttpResponse: 200 if the page was sent successfully - HttpResponse: 302 if not logged in (redirect to login page) - HttpResponse: 405 if using an unsupported HTTP method - - Example usage: - - GET /account/settings - - """ - if should_redirect_to_account_microfrontend(): - url = settings.ACCOUNT_MICROFRONTEND_URL - - duplicate_provider = pipeline.get_duplicate_provider(messages.get_messages(request)) - if duplicate_provider: - url = '{url}?{params}'.format( - url=url, - params=urllib.parse.urlencode({ - 'duplicate_provider': duplicate_provider, - }), - ) - - return redirect(url) - - context = account_settings_context(request) - - account_settings_template = 'student_account/account_settings.html' - - try: - # .. filter_implemented_name: AccountSettingsRenderStarted - # .. filter_type: org.openedx.learning.student.settings.render.started.v1 - context, account_settings_template = AccountSettingsRenderStarted.run_filter( - context=context, template_name=account_settings_template, - ) - except AccountSettingsRenderStarted.RenderInvalidAccountSettings as exc: - response = render_to_response(exc.account_settings_template, exc.template_context) - except AccountSettingsRenderStarted.RedirectToPage as exc: - response = HttpResponseRedirect(exc.redirect_to or reverse('dashboard')) - except AccountSettingsRenderStarted.RenderCustomResponse as exc: - response = exc.response - else: - response = render_to_response(account_settings_template, context) - - return response - - -def account_settings_context(request): - """ Context for the account settings page. - - Args: - request: The request object. - - Returns: - dict - - """ - user = request.user - - year_of_birth_options = [(str(year), str(year)) for year in UserProfile.VALID_YEARS] - try: - user_orders = get_user_orders(user) - except: # pylint: disable=bare-except - log.exception('Error fetching order history from Otto.') - # Return empty order list as account settings page expect a list and - # it will be broken if exception raised - user_orders = [] - - beta_language = {} - dark_lang_config = DarkLangConfig.current() - if dark_lang_config.enable_beta_languages: - user_preferences = get_user_preferences(user) - pref_language = user_preferences.get('pref-lang') - if pref_language in dark_lang_config.beta_languages_list: - beta_language['code'] = pref_language - beta_language['name'] = settings.LANGUAGE_DICT.get(pref_language) - - context = { - 'auth': {}, - 'duplicate_provider': None, - 'nav_hidden': True, - 'fields': { - 'country': { - 'options': list(countries), - }, 'gender': { - 'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # lint-amnesty, pylint: disable=translation-of-non-string - }, 'language': { - 'options': released_languages(), - }, 'level_of_education': { - 'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # lint-amnesty, pylint: disable=translation-of-non-string - }, 'password': { - 'url': reverse('password_reset'), - }, 'year_of_birth': { - 'options': year_of_birth_options, - }, 'preferred_language': { - 'options': all_languages(), - }, 'time_zone': { - 'options': TIME_ZONE_CHOICES, - } - }, - 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), - 'password_reset_support_link': configuration_helpers.get_value( - 'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK - ) or settings.SUPPORT_SITE_LINK, - 'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}), - 'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}), - 'disable_courseware_js': True, - 'show_program_listing': ProgramsApiConfig.is_enabled(), - 'show_dashboard_tabs': True, - 'order_history': user_orders, - 'disable_order_history_tab': should_redirect_to_order_history_microfrontend(), - 'enable_account_deletion': configuration_helpers.get_value( - 'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False) - ), - 'extended_profile_fields': _get_extended_profile_fields(), - 'beta_language': beta_language, - 'enable_coppa_compliance': settings.ENABLE_COPPA_COMPLIANCE, - } - - enterprise_customer = enterprise_customer_for_request(request) - update_account_settings_context_for_enterprise(context, enterprise_customer, user) - - if third_party_auth.is_enabled(): - # If the account on the third party provider is already connected with another edX account, - # we display a message to the user. - context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request)) - - auth_states = pipeline.get_provider_user_states(user) - - context['auth']['providers'] = [{ - 'id': state.provider.provider_id, - 'name': state.provider.name, # The name of the provider e.g. Facebook - 'connected': state.has_account, # Whether the user's edX account is connected with the provider. - # If the user is not connected, they should be directed to this page to authenticate - # with the particular provider, as long as the provider supports initiating a login. - 'connect_url': pipeline.get_login_url( - state.provider.provider_id, - pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, - # The url the user should be directed to after the auth process has completed. - redirect_url=reverse('account_settings'), - ), - 'accepts_logins': state.provider.accepts_logins, - # If the user is connected, sending a POST request to this url removes the connection - # information for this provider from their edX account. - 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), - # We only want to include providers if they are either currently available to be logged - # in with, or if the user is already authenticated with them. - } for state in auth_states if state.provider.display_for_login or state.has_account] - - return context - - -def get_user_orders(user): - """Given a user, get the detail of all the orders from the Ecommerce service. - - Args: - user (User): The user to authenticate as when requesting ecommerce. - - Returns: - list of dict, representing orders returned by the Ecommerce service. - """ - user_orders = [] - commerce_configuration = CommerceConfiguration.current() - user_query = {'username': user.username} - - use_cache = commerce_configuration.is_cache_enabled - cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None - commerce_user_orders = get_api_data( - commerce_configuration, - 'orders', - api_client=get_ecommerce_api_client(user), - base_api_url=get_ecommerce_api_base_url(), - querystring=user_query, - cache_key=cache_key - ) - - for order in commerce_user_orders: - if order['status'].lower() == 'complete': - date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ") - order_data = { - 'number': order['number'], - 'price': order['total_excl_tax'], - 'order_date': strftime_localized(date_placed, 'SHORT_DATE'), - 'receipt_url': EcommerceService().get_receipt_page_url(order['number']), - 'lines': order['lines'], - } - user_orders.append(order_data) - - return user_orders - - -def _get_extended_profile_fields(): - """Retrieve the extended profile fields from site configuration to be shown on the - Account Settings page - - Returns: - A list of dicts. Each dict corresponds to a single field. The keys per field are: - "field_name" : name of the field stored in user_profile.meta - "field_label" : The label of the field. - "field_type" : TextField or ListField - "field_options": a list of tuples for options in the dropdown in case of ListField - """ - - extended_profile_fields = [] - fields_already_showing = ['username', 'name', 'email', 'pref-lang', 'country', 'time_zone', 'level_of_education', - 'gender', 'year_of_birth', 'language_proficiencies', 'social_links'] - - field_labels_map = { - "first_name": _("First Name"), - "last_name": _("Last Name"), - "city": _("City"), - "state": _("State/Province/Region"), - "company": _("Company"), - "title": _("Title"), - "job_title": _("Job Title"), - "mailing_address": _("Mailing address"), - "goals": _("Tell us why you're interested in {platform_name}").format( - platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME) - ), - "profession": _("Profession"), - "specialty": _("Specialty"), - "work_experience": _("Work experience") - } - - extended_profile_field_names = configuration_helpers.get_value('extended_profile_fields', []) - for field_to_exclude in fields_already_showing: - if field_to_exclude in extended_profile_field_names: - extended_profile_field_names.remove(field_to_exclude) - - extended_profile_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', []) - extended_profile_field_option_tuples = {} - for field in extended_profile_field_options.keys(): - field_options = extended_profile_field_options[field] - extended_profile_field_option_tuples[field] = [(option.lower(), option) for option in field_options] - - for field in extended_profile_field_names: - field_dict = { - "field_name": field, - "field_label": field_labels_map.get(field, field), - } - - field_options = extended_profile_field_option_tuples.get(field) - if field_options: - field_dict["field_type"] = "ListField" - field_dict["field_options"] = field_options - else: - field_dict["field_type"] = "TextField" - extended_profile_fields.append(field_dict) - - return extended_profile_fields diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_filters.py b/openedx/core/djangoapps/user_api/accounts/tests/test_filters.py deleted file mode 100644 index 782549aea0..0000000000 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_filters.py +++ /dev/null @@ -1,241 +0,0 @@ -""" -Test that various filters are fired for views in the certificates app. -""" -from django.http import HttpResponse -from django.test import override_settings -from django.urls import reverse -from openedx_filters import PipelineStep -from openedx_filters.learning.filters import AccountSettingsRenderStarted -from rest_framework import status -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase - -from openedx.core.djangolib.testing.utils import skip_unless_lms -from common.djangoapps.student.tests.factories import UserFactory - - -class TestRenderInvalidAccountSettings(PipelineStep): - """ - Utility class used when getting steps for pipeline. - """ - - def run_filter(self, context, template_name): # pylint: disable=arguments-differ - """ - Pipeline step that stops the course about render process. - """ - raise AccountSettingsRenderStarted.RenderInvalidAccountSettings( - "You can't access the account settings page.", - account_settings_template="static_templates/server-error.html", - ) - - -class TestRedirectToPage(PipelineStep): - """ - Utility class used when getting steps for pipeline. - """ - - def run_filter(self, context, template_name): # pylint: disable=arguments-differ - """ - Pipeline step that redirects to dashboard before rendering the account settings page. - - When raising RedirectToPage, this filter uses a redirect_to field handled by - the course about view that redirects to that URL. - """ - raise AccountSettingsRenderStarted.RedirectToPage( - "You can't access this page, redirecting to dashboard.", - redirect_to="/courses", - ) - - -class TestRedirectToDefaultPage(PipelineStep): - """ - Utility class used when getting steps for pipeline. - """ - - def run_filter(self, context, template_name): # pylint: disable=arguments-differ - """ - Pipeline step that redirects to dashboard before rendering the account settings page. - - When raising RedirectToPage, this filter uses a redirect_to field handled by - the course about view that redirects to that URL. - """ - raise AccountSettingsRenderStarted.RedirectToPage( - "You can't access this page, redirecting to dashboard." - ) - - -class TestRenderCustomResponse(PipelineStep): - """ - Utility class used when getting steps for pipeline. - """ - - def run_filter(self, context, template_name): # pylint: disable=arguments-differ - """Pipeline step that returns a custom response when rendering the account settings page.""" - response = HttpResponse("Here's the text of the web page.") - raise AccountSettingsRenderStarted.RenderCustomResponse( - "You can't access this page.", - response=response, - ) - - -class TestAccountSettingsRender(PipelineStep): - """ - Utility class used when getting steps for pipeline. - """ - - def run_filter(self, context, template_name): # pylint: disable=arguments-differ - """Pipeline step that returns a custom response when rendering the account settings page.""" - template_name = 'static_templates/about.html' - return { - "context": context, "template_name": template_name, - } - - -@skip_unless_lms -class TestAccountSettingsFilters(SharedModuleStoreTestCase): - """ - Tests for the Open edX Filters associated with the account settings proccess. - - This class guarantees that the following filters are triggered during the user's account settings rendering: - - - AccountSettingsRenderStarted - """ - def setUp(self): # pylint: disable=arguments-differ - super().setUp() - self.user = UserFactory.create( - username="somestudent", - first_name="Student", - last_name="Person", - email="robot@robot.org", - is_active=True, - password="password", - ) - self.client.login(username=self.user.username, password="password") - self.account_settings_url = '/account/settings' - - @override_settings( - OPEN_EDX_FILTERS_CONFIG={ - "org.openedx.learning.student.settings.render.started.v1": { - "pipeline": [ - "openedx.core.djangoapps.user_api.accounts.tests.test_filters.TestAccountSettingsRender", - ], - "fail_silently": False, - }, - }, - ) - def test_account_settings_render_filter_executed(self): - """ - Test whether the account settings filter is triggered before the user's - account settings page is rendered. - - Expected result: - - AccountSettingsRenderStarted is triggered and executes TestAccountSettingsRender - """ - response = self.client.get(self.account_settings_url) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertContains(response, "This page left intentionally blank. Feel free to add your own content.") - - @override_settings( - OPEN_EDX_FILTERS_CONFIG={ - "org.openedx.learning.student.settings.render.started.v1": { - "pipeline": [ - "openedx.core.djangoapps.user_api.accounts.tests.test_filters.TestRenderInvalidAccountSettings", # pylint: disable=line-too-long - ], - "fail_silently": False, - }, - }, - PLATFORM_NAME="My site", - ) - def test_account_settings_render_alternative(self): - """ - Test whether the account settings filter is triggered before the user's - account settings page is rendered. - - Expected result: - - AccountSettingsRenderStarted is triggered and executes TestRenderInvalidAccountSettings # pylint: disable=line-too-long - """ - response = self.client.get(self.account_settings_url) - - self.assertContains(response, "There has been a 500 error on the My site servers") - - @override_settings( - OPEN_EDX_FILTERS_CONFIG={ - "org.openedx.learning.student.settings.render.started.v1": { - "pipeline": [ - "openedx.core.djangoapps.user_api.accounts.tests.test_filters.TestRenderCustomResponse", - ], - "fail_silently": False, - }, - }, - ) - def test_account_settings_render_custom_response(self): - """ - Test whether the account settings filter is triggered before the user's - account settings page is rendered. - - Expected result: - - AccountSettingsRenderStarted is triggered and executes TestRenderCustomResponse - """ - response = self.client.get(self.account_settings_url) - - self.assertEqual(response.content, b"Here's the text of the web page.") - - @override_settings( - OPEN_EDX_FILTERS_CONFIG={ - "org.openedx.learning.student.settings.render.started.v1": { - "pipeline": [ - "openedx.core.djangoapps.user_api.accounts.tests.test_filters.TestRedirectToPage", - ], - "fail_silently": False, - }, - }, - ) - def test_account_settings_redirect_to_page(self): - """ - Test whether the account settings filter is triggered before the user's - account settings page is rendered. - - Expected result: - - AccountSettingsRenderStarted is triggered and executes TestRedirectToPage - """ - response = self.client.get(self.account_settings_url) - - self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertEqual('/courses', response.url) - - @override_settings( - OPEN_EDX_FILTERS_CONFIG={ - "org.openedx.learning.student.settings.render.started.v1": { - "pipeline": [ - "openedx.core.djangoapps.user_api.accounts.tests.test_filters.TestRedirectToDefaultPage", - ], - "fail_silently": False, - }, - }, - ) - def test_account_settings_redirect_default(self): - """ - Test whether the account settings filter is triggered before the user's - account settings page is rendered. - - Expected result: - - AccountSettingsRenderStarted is triggered and executes TestRedirectToDefaultPage - """ - response = self.client.get(self.account_settings_url) - - self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertEqual(f"{reverse('dashboard')}", response.url) - - @override_settings(OPEN_EDX_FILTERS_CONFIG={}) - def test_account_settings_render_without_filter_config(self): - """ - Test whether the course about filter is triggered before the course about - render without affecting its execution flow. - - Expected result: - - AccountSettingsRenderStarted executes a noop (empty pipeline). Without any - modification comparing it with the effects of TestAccountSettingsRender. - - The view response is HTTP_200_OK. - """ - response = self.client.get(self.account_settings_url) - - self.assertNotContains(response, "This page left intentionally blank. Feel free to add your own content.") diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_settings_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_settings_views.py deleted file mode 100644 index badee6e875..0000000000 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_settings_views.py +++ /dev/null @@ -1,287 +0,0 @@ -""" Tests for views related to account settings. """ - - -from unittest import mock -from django.conf import settings -from django.contrib import messages -from django.contrib.messages.middleware import MessageMiddleware -from django.http import HttpRequest -from django.test import TestCase -from django.test.utils import override_settings -from django.urls import reverse -from requests import exceptions - -from edx_toggles.toggles.testutils import override_waffle_flag -from lms.djangoapps.commerce.models import CommerceConfiguration -from lms.djangoapps.commerce.tests import factories -from lms.djangoapps.commerce.tests.mocks import mock_get_orders -from openedx.core.djangoapps.dark_lang.models import DarkLangConfig -from openedx.core.djangoapps.lang_pref.tests.test_api import EN, LT_LT -from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin -from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory -from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin -from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration -from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context, get_user_orders -from openedx.core.djangoapps.user_api.accounts.toggles import REDIRECT_TO_ACCOUNT_MICROFRONTEND -from openedx.core.djangoapps.user_api.tests.factories import UserPreferenceFactory -from openedx.core.djangolib.testing.utils import skip_unless_lms -from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields -from common.djangoapps.student.tests.factories import UserFactory -from common.djangoapps.third_party_auth.tests.testutil import ThirdPartyAuthTestMixin - - -@skip_unless_lms -class AccountSettingsViewTest(ThirdPartyAuthTestMixin, SiteMixin, ProgramsApiConfigMixin, TestCase): - """ Tests for the account settings view. """ - - USERNAME = 'student' - PASSWORD = 'password' - FIELDS = [ - 'country', - 'gender', - 'language', - 'level_of_education', - 'password', - 'year_of_birth', - 'preferred_language', - 'time_zone', - ] - - @mock.patch("django.conf.settings.MESSAGE_STORAGE", 'django.contrib.messages.storage.cookie.CookieStorage') - def setUp(self): # pylint: disable=arguments-differ - super().setUp() - self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) - CommerceConfiguration.objects.create(cache_ttl=10, enabled=True) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - - self.request = HttpRequest() - self.request.user = self.user - - # For these tests, two third party auth providers are enabled by default: - self.configure_google_provider(enabled=True, visible=True) - self.configure_facebook_provider(enabled=True, visible=True) - - # Python-social saves auth failure notifcations in Django messages. - # See pipeline.get_duplicate_provider() for details. - self.request.COOKIES = {} - MessageMiddleware(get_response=lambda request: None).process_request(self.request) - messages.error(self.request, 'Facebook is already in use.', extra_tags='Auth facebook') - - @mock.patch('openedx.features.enterprise_support.api.enterprise_customer_for_request') - def test_context(self, mock_enterprise_customer_for_request): - self.request.site = SiteFactory.create() - UserPreferenceFactory(user=self.user, key='pref-lang', value='lt-lt') - DarkLangConfig( - released_languages='en', - changed_by=self.user, - enabled=True, - beta_languages='lt-lt', - enable_beta_languages=True - ).save() - mock_enterprise_customer_for_request.return_value = {} - - with override_settings(LANGUAGES=[EN, LT_LT], LANGUAGE_CODE='en'): - context = account_settings_context(self.request) - - user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username}) - assert context['user_accounts_api_url'] == user_accounts_api_url - - user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username}) - assert context['user_preferences_api_url'] == user_preferences_api_url - - for attribute in self.FIELDS: - assert attribute in context['fields'] - - assert context['user_accounts_api_url'] == reverse('accounts_api', kwargs={'username': self.user.username}) - assert context['user_preferences_api_url'] ==\ - reverse('preferences_api', kwargs={'username': self.user.username}) - - assert context['duplicate_provider'] == 'facebook' - assert context['auth']['providers'][0]['name'] == 'Facebook' - assert context['auth']['providers'][1]['name'] == 'Google' - - assert context['sync_learner_profile_data'] is False - assert context['edx_support_url'] == settings.SUPPORT_SITE_LINK - assert context['enterprise_name'] is None - assert context['enterprise_readonly_account_fields'] ==\ - {'fields': list(get_enterprise_readonly_account_fields(self.user))} - - expected_beta_language = {'code': 'lt-lt', 'name': settings.LANGUAGE_DICT.get('lt-lt')} - assert context['beta_language'] == expected_beta_language - - @with_site_configuration( - configuration={ - 'extended_profile_fields': ['work_experience'] - } - ) - def test_context_extended_profile(self): - """ - Test that if the field is available in extended_profile configuration then the field - will be sent in response. - """ - context = account_settings_context(self.request) - extended_pofile_field = context['extended_profile_fields'][0] - assert extended_pofile_field['field_name'] == 'work_experience' - assert extended_pofile_field['field_label'] == 'Work experience' - - @mock.patch('openedx.core.djangoapps.user_api.accounts.settings_views.enterprise_customer_for_request') - @mock.patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get') - def test_context_for_enterprise_learner( - self, mock_get_auth_provider, mock_enterprise_customer_for_request - ): - dummy_enterprise_customer = { - 'uuid': 'real-ent-uuid', - 'name': 'Dummy Enterprise', - 'identity_provider': 'saml-ubc' - } - mock_enterprise_customer_for_request.return_value = dummy_enterprise_customer - self.request.site = SiteFactory.create() - mock_get_auth_provider.return_value.sync_learner_profile_data = True - context = account_settings_context(self.request) - - user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username}) - assert context['user_accounts_api_url'] == user_accounts_api_url - - user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username}) - assert context['user_preferences_api_url'] == user_preferences_api_url - - for attribute in self.FIELDS: - assert attribute in context['fields'] - - assert context['user_accounts_api_url'] == reverse('accounts_api', kwargs={'username': self.user.username}) - assert context['user_preferences_api_url'] ==\ - reverse('preferences_api', kwargs={'username': self.user.username}) - - assert context['duplicate_provider'] == 'facebook' - assert context['auth']['providers'][0]['name'] == 'Facebook' - assert context['auth']['providers'][1]['name'] == 'Google' - - assert context['sync_learner_profile_data'] == mock_get_auth_provider.return_value.sync_learner_profile_data - assert context['edx_support_url'] == settings.SUPPORT_SITE_LINK - assert context['enterprise_name'] == dummy_enterprise_customer['name'] - assert context['enterprise_readonly_account_fields'] ==\ - {'fields': list(get_enterprise_readonly_account_fields(self.user))} - - def test_view(self): - """ - Test that all fields are visible - """ - view_path = reverse('account_settings') - response = self.client.get(path=view_path) - - for attribute in self.FIELDS: - self.assertContains(response, attribute) - - def test_header_with_programs_listing_enabled(self): - """ - Verify that tabs header will be shown while program listing is enabled. - """ - self.create_programs_config() - view_path = reverse('account_settings') - response = self.client.get(path=view_path) - - self.assertContains(response, 'global-header') - - def test_header_with_programs_listing_disabled(self): - """ - Verify that nav header will be shown while program listing is disabled. - """ - self.create_programs_config(enabled=False) - view_path = reverse('account_settings') - response = self.client.get(path=view_path) - - self.assertContains(response, 'global-header') - - def test_commerce_order_detail(self): - """ - Verify that get_user_orders returns the correct order data. - """ - with mock_get_orders(): - order_detail = get_user_orders(self.user) - - for i, order in enumerate(mock_get_orders.default_response['results']): - expected = { - 'number': order['number'], - 'price': order['total_excl_tax'], - 'order_date': 'Jan 01, 2016', - 'receipt_url': '/checkout/receipt/?order_number=' + order['number'], - 'lines': order['lines'], - } - assert order_detail[i] == expected - - def test_commerce_order_detail_exception(self): - with mock_get_orders(exception=exceptions.HTTPError): - order_detail = get_user_orders(self.user) - - assert not order_detail - - def test_incomplete_order_detail(self): - response = { - 'results': [ - factories.OrderFactory( - status='Incomplete', - lines=[ - factories.OrderLineFactory( - product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory()]) - ) - ] - ) - ] - } - with mock_get_orders(response=response): - order_detail = get_user_orders(self.user) - - assert not order_detail - - def test_order_history_with_no_product(self): - response = { - 'results': [ - factories.OrderFactory( - lines=[ - factories.OrderLineFactory( - product=None - ), - factories.OrderLineFactory( - product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory( - name='certificate_type', - value='verified' - )]) - ) - ] - ) - ] - } - with mock_get_orders(response=response): - order_detail = get_user_orders(self.user) - - assert len(order_detail) == 1 - - def test_redirect_view(self): - old_url_path = reverse('account_settings') - with override_waffle_flag(REDIRECT_TO_ACCOUNT_MICROFRONTEND, active=True): - # Test with waffle flag active and none site setting, redirects to microfrontend - response = self.client.get(path=old_url_path) - self.assertRedirects(response, settings.ACCOUNT_MICROFRONTEND_URL, fetch_redirect_response=False) - - # Test with waffle flag disabled and site setting disabled, does not redirect - response = self.client.get(path=old_url_path) - for attribute in self.FIELDS: - self.assertContains(response, attribute) - - # Test with site setting disabled, does not redirect - site_domain = 'othersite.example.com' - site = self.set_up_site(site_domain, { - 'SITE_NAME': site_domain, - 'ENABLE_ACCOUNT_MICROFRONTEND': False - }) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - response = self.client.get(path=old_url_path) - for attribute in self.FIELDS: - self.assertContains(response, attribute) - - # Test with site setting enabled, redirects to microfrontend - site.configuration.site_values['ENABLE_ACCOUNT_MICROFRONTEND'] = True - site.configuration.save() - site.__class__.objects.clear_cache() - response = self.client.get(path=old_url_path) - self.assertRedirects(response, settings.ACCOUNT_MICROFRONTEND_URL, fetch_redirect_response=False) diff --git a/openedx/core/djangoapps/user_api/accounts/toggles.py b/openedx/core/djangoapps/user_api/accounts/toggles.py deleted file mode 100644 index 80de4fa756..0000000000 --- a/openedx/core/djangoapps/user_api/accounts/toggles.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Toggles for accounts related code. -""" - -from edx_toggles.toggles import WaffleFlag - -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers - -# .. toggle_name: order_history.redirect_to_microfrontend -# .. toggle_implementation: WaffleFlag -# .. toggle_default: False -# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the order history page. -# .. toggle_use_cases: temporary, open_edx -# .. toggle_creation_date: 2019-04-11 -# .. toggle_target_removal_date: 2020-12-31 -# .. toggle_warning: Also set settings.ORDER_HISTORY_MICROFRONTEND_URL and site's -# ENABLE_ORDER_HISTORY_MICROFRONTEND. -# .. toggle_tickets: DEPR-17 -REDIRECT_TO_ORDER_HISTORY_MICROFRONTEND = WaffleFlag('order_history.redirect_to_microfrontend', __name__) - - -def should_redirect_to_order_history_microfrontend(): - return ( - configuration_helpers.get_value('ENABLE_ORDER_HISTORY_MICROFRONTEND') and - REDIRECT_TO_ORDER_HISTORY_MICROFRONTEND.is_enabled() - ) - - -# .. toggle_name: account.redirect_to_microfrontend -# .. toggle_implementation: WaffleFlag -# .. toggle_default: False -# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the account page. -# Its action can be overridden using site's ENABLE_ACCOUNT_MICROFRONTEND setting. -# .. toggle_use_cases: temporary, open_edx -# .. toggle_creation_date: 2019-04-30 -# .. toggle_target_removal_date: 2021-12-31 -# .. toggle_warning: Also set settings.ACCOUNT_MICROFRONTEND_URL. -# .. toggle_tickets: DEPR-17 -REDIRECT_TO_ACCOUNT_MICROFRONTEND = WaffleFlag('account.redirect_to_microfrontend', __name__) - - -def should_redirect_to_account_microfrontend(): - return configuration_helpers.get_value('ENABLE_ACCOUNT_MICROFRONTEND', - REDIRECT_TO_ACCOUNT_MICROFRONTEND.is_enabled()) diff --git a/openedx/core/djangoapps/user_api/legacy_urls.py b/openedx/core/djangoapps/user_api/legacy_urls.py index b3f707f64b..ad02f7f19c 100644 --- a/openedx/core/djangoapps/user_api/legacy_urls.py +++ b/openedx/core/djangoapps/user_api/legacy_urls.py @@ -5,7 +5,6 @@ from django.urls import path, re_path, include from rest_framework import routers from . import views as user_api_views -from .accounts.settings_views import account_settings from .models import UserPreference USER_API_ROUTER = routers.DefaultRouter() @@ -13,7 +12,6 @@ USER_API_ROUTER.register(r'users', user_api_views.UserViewSet) USER_API_ROUTER.register(r'user_prefs', user_api_views.UserPreferenceViewSet) urlpatterns = [ - path('account/settings', account_settings, name='account_settings'), path('user_api/v1/', include(USER_API_ROUTER.urls)), re_path( fr'^user_api/v1/preferences/(?P{UserPreference.KEY_REGEX})/users/$', diff --git a/openedx/core/djangoapps/user_authn/cookies.py b/openedx/core/djangoapps/user_authn/cookies.py index 24f929698f..036baf2125 100644 --- a/openedx/core/djangoapps/user_authn/cookies.py +++ b/openedx/core/djangoapps/user_authn/cookies.py @@ -6,6 +6,7 @@ Utility functions for setting "logged in" cookies used by subdomains. import json import logging import time +from urllib.parse import urljoin from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user @@ -244,8 +245,8 @@ def _get_user_info_cookie_data(request, user): # External sites will need to have fallback mechanisms to handle this case # (most likely just hiding the links). try: - header_urls['account_settings'] = reverse('account_settings') - header_urls['learner_profile'] = reverse('learner_profile', kwargs={'username': user.username}) + header_urls['account_settings'] = settings.ACCOUNT_MICROFRONTEND_URL + header_urls['learner_profile'] = urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{user.username}') except NoReverseMatch: pass diff --git a/openedx/core/djangoapps/user_authn/tests/test_cookies.py b/openedx/core/djangoapps/user_authn/tests/test_cookies.py index 8a7841b3b9..d4d9a59fc9 100644 --- a/openedx/core/djangoapps/user_authn/tests/test_cookies.py +++ b/openedx/core/djangoapps/user_authn/tests/test_cookies.py @@ -4,6 +4,7 @@ from datetime import date import json from unittest.mock import MagicMock, patch +from urllib.parse import urljoin from django.conf import settings from django.http import HttpResponse from django.test import RequestFactory, TestCase @@ -57,8 +58,8 @@ class CookieTests(TestCase): def _get_expected_header_urls(self): expected_header_urls = { 'logout': reverse('logout'), - 'account_settings': reverse('account_settings'), - 'learner_profile': reverse('learner_profile', kwargs={'username': self.user.username}), + 'account_settings': settings.ACCOUNT_MICROFRONTEND_URL, + 'learner_profile': urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{self.user.username}'), } block_url = retrieve_last_sitewide_block_completed(self.user) if block_url: diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py index aa34a076d4..38649bb93a 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py @@ -496,7 +496,7 @@ class LoginTest(SiteMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin): # Check that the URLs are absolute for url in user_info["header_urls"].values(): - assert 'http://testserver/' in url + assert 'http://' in url def test_logout_deletes_mktg_cookies(self): response, _ = self._login_response(self.user_email, self.password) diff --git a/openedx/features/learner_profile/README.rst b/openedx/features/learner_profile/README.rst deleted file mode 100644 index 0dce8e10cc..0000000000 --- a/openedx/features/learner_profile/README.rst +++ /dev/null @@ -1,8 +0,0 @@ -Learner Profile ---------------- - -This directory contains a Django application that provides a view to render -a profile for any Open edX learner. See `Exploring Your Dashboard and Profile`_ -for more details. - -.. _Exploring Your Dashboard and Profile: https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/SFD_dashboard_profile_SectionHead.html?highlight=profile diff --git a/openedx/features/learner_profile/__init__.py b/openedx/features/learner_profile/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html b/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html deleted file mode 100644 index 61c139210a..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html +++ /dev/null @@ -1,40 +0,0 @@ -
-
-
- - -
-
-

- - - - - Loading - -

-
- -
diff --git a/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js b/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js deleted file mode 100644 index d4f81ae7f4..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js +++ /dev/null @@ -1,219 +0,0 @@ -(function(define) { - 'use strict'; - - define([ - 'gettext', - 'jquery', - 'underscore', - 'backbone', - 'logger', - 'edx-ui-toolkit/js/utils/string-utils', - 'edx-ui-toolkit/js/pagination/paging-collection', - 'js/student_account/models/user_account_model', - 'js/student_account/models/user_preferences_model', - 'js/views/fields', - 'learner_profile/js/views/learner_profile_fields', - 'learner_profile/js/views/learner_profile_view', - 'js/student_account/views/account_settings_fields', - 'js/views/message_banner', - 'string_utils' - ], function(gettext, $, _, Backbone, Logger, StringUtils, PagingCollection, AccountSettingsModel, - AccountPreferencesModel, FieldsView, LearnerProfileFieldsView, LearnerProfileView, - AccountSettingsFieldViews, MessageBannerView) { - return function(options) { - var $learnerProfileElement = $('.wrapper-profile'); - - var accountSettingsModel = new AccountSettingsModel( - _.extend( - options.account_settings_data, - { - default_public_account_fields: options.default_public_account_fields, - parental_consent_age_limit: options.parental_consent_age_limit, - enable_coppa_compliance: options.enable_coppa_compliance - } - ), - {parse: true} - ); - var AccountPreferencesModelWithDefaults = AccountPreferencesModel.extend({ - defaults: { - account_privacy: options.default_visibility - } - }); - var accountPreferencesModel = new AccountPreferencesModelWithDefaults(options.preferences_data); - - var editable = options.own_profile ? 'toggle' : 'never'; - - var messageView = new MessageBannerView({ - el: $('.message-banner') - }); - - var accountPrivacyFieldView, - profileImageFieldView, - usernameFieldView, - nameFieldView, - sectionOneFieldViews, - sectionTwoFieldViews, - learnerProfileView, - getProfileVisibility, - showLearnerProfileView; - - accountSettingsModel.url = options.accounts_api_url; - accountPreferencesModel.url = options.preferences_api_url; - - accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({ - model: accountPreferencesModel, - required: true, - editable: 'always', - showMessages: false, - title: gettext('Profile Visibility:'), - valueAttribute: 'account_privacy', - options: [ - ['private', gettext('Limited Profile')], - ['all_users', gettext('Full Profile')] - ], - helpMessage: '', - accountSettingsPageUrl: options.account_settings_page_url, - persistChanges: true - }); - - profileImageFieldView = new LearnerProfileFieldsView.ProfileImageFieldView({ - model: accountSettingsModel, - valueAttribute: 'profile_image', - editable: editable === 'toggle', - messageView: messageView, - imageMaxBytes: options.profile_image_max_bytes, - imageMinBytes: options.profile_image_min_bytes, - imageUploadUrl: options.profile_image_upload_url, - imageRemoveUrl: options.profile_image_remove_url - }); - - usernameFieldView = new FieldsView.ReadonlyFieldView({ - model: accountSettingsModel, - screenReaderTitle: gettext('Username'), - valueAttribute: 'username', - helpMessage: '' - }); - - nameFieldView = new FieldsView.ReadonlyFieldView({ - model: accountSettingsModel, - screenReaderTitle: gettext('Full Name'), - valueAttribute: 'name', - helpMessage: '' - }); - - sectionOneFieldViews = [ - new LearnerProfileFieldsView.SocialLinkIconsView({ - model: accountSettingsModel, - socialPlatforms: options.social_platforms, - ownProfile: options.own_profile - }), - - new FieldsView.DateFieldView({ - title: gettext('Joined'), - titleVisible: true, - model: accountSettingsModel, - screenReaderTitle: gettext('Joined Date'), - valueAttribute: 'date_joined', - helpMessage: '', - userLanguage: accountSettingsModel.get('language'), - userTimezone: accountPreferencesModel.get('time_zone'), - dateFormat: 'MMMM YYYY' // not localized, but hopefully ok. - }), - - new FieldsView.DropdownFieldView({ - title: gettext('Location'), - titleVisible: true, - model: accountSettingsModel, - screenReaderTitle: gettext('Country'), - required: true, - editable: editable, - showMessages: false, - placeholderValue: gettext('Add Country'), - valueAttribute: 'country', - options: options.country_options, - helpMessage: '', - persistChanges: true - }), - - new AccountSettingsFieldViews.LanguageProficienciesFieldView({ - title: gettext('Language'), - titleVisible: true, - model: accountSettingsModel, - screenReaderTitle: gettext('Preferred Language'), - required: false, - editable: editable, - showMessages: false, - placeholderValue: gettext('Add language'), - valueAttribute: 'language_proficiencies', - options: options.language_options, - helpMessage: '', - persistChanges: true - }) - ]; - - sectionTwoFieldViews = [ - new FieldsView.TextareaFieldView({ - model: accountSettingsModel, - editable: editable, - showMessages: false, - title: gettext('About me'), - // eslint-disable-next-line max-len - placeholderValue: gettext("Tell other learners a little about yourself: where you live, what your interests are, why you're taking courses, or what you hope to learn."), - valueAttribute: 'bio', - helpMessage: '', - persistChanges: true, - messagePosition: 'header', - maxCharacters: 300 - }) - ]; - - learnerProfileView = new LearnerProfileView({ - el: $learnerProfileElement, - ownProfile: options.own_profile, - has_preferences_access: options.has_preferences_access, - accountSettingsModel: accountSettingsModel, - preferencesModel: accountPreferencesModel, - accountPrivacyFieldView: accountPrivacyFieldView, - profileImageFieldView: profileImageFieldView, - usernameFieldView: usernameFieldView, - nameFieldView: nameFieldView, - sectionOneFieldViews: sectionOneFieldViews, - sectionTwoFieldViews: sectionTwoFieldViews, - platformName: options.platform_name - }); - - getProfileVisibility = function() { - if (options.has_preferences_access) { - return accountPreferencesModel.get('account_privacy'); - } else { - return accountSettingsModel.get('profile_is_public') ? 'all_users' : 'private'; - } - }; - - showLearnerProfileView = function() { - // Record that the profile page was viewed - Logger.log('edx.user.settings.viewed', { - page: 'profile', - visibility: getProfileVisibility(), - user_id: options.profile_user_id - }); - - // Render the view for the first time - learnerProfileView.render(); - }; - - if (options.has_preferences_access) { - if (accountSettingsModel.get('requires_parental_consent')) { - accountPreferencesModel.set('account_privacy', 'private'); - } - } - showLearnerProfileView(); - - return { - accountSettingsModel: accountSettingsModel, - accountPreferencesModel: accountPreferencesModel, - learnerProfileView: learnerProfileView, - }; - }; - }); -}).call(this, define || RequireJS.define); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js deleted file mode 100644 index 0019c2fc13..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -define( - [ - 'backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/spec/student_account/helpers', - 'learner_profile/js/spec_helpers/helpers', - 'js/views/fields', - 'js/student_account/models/user_account_model', - 'js/student_account/models/user_preferences_model', - 'learner_profile/js/views/learner_profile_view', - 'learner_profile/js/views/learner_profile_fields', - 'learner_profile/js/learner_profile_factory', - 'js/views/message_banner' - ], - function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, - UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) { - 'use strict'; - - describe('edx.user.LearnerProfileFactory', function() { - var createProfilePage; - - beforeEach(function() { - loadFixtures('learner_profile/fixtures/learner_profile.html'); - }); - - afterEach(function() { - Backbone.history.stop(); - }); - - createProfilePage = function(ownProfile, options) { - return new LearnerProfilePage({ - accounts_api_url: Helpers.USER_ACCOUNTS_API_URL, - preferences_api_url: Helpers.USER_PREFERENCES_API_URL, - own_profile: ownProfile, - account_settings_page_url: Helpers.USER_ACCOUNTS_API_URL, - country_options: Helpers.FIELD_OPTIONS, - language_options: Helpers.FIELD_OPTIONS, - has_preferences_access: true, - profile_image_max_bytes: Helpers.IMAGE_MAX_BYTES, - profile_image_min_bytes: Helpers.IMAGE_MIN_BYTES, - profile_image_upload_url: Helpers.IMAGE_UPLOAD_API_URL, - profile_image_remove_url: Helpers.IMAGE_REMOVE_API_URL, - default_visibility: 'all_users', - platform_name: 'edX', - find_courses_url: '/courses/', - account_settings_data: Helpers.createAccountSettingsData(options), - preferences_data: Helpers.createUserPreferencesData() - }); - }; - - it('renders the full profile for a user', function() { - var context, - learnerProfileView; - AjaxHelpers.requests(this); - context = createProfilePage(true); - learnerProfileView = context.learnerProfileView; - - // sets the profile for full view. - context.accountPreferencesModel.set({account_privacy: 'all_users'}); - LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, false); - }); - - it("renders the limited profile for undefined 'year_of_birth'", function() { - var context = createProfilePage(true, {year_of_birth: '', requires_parental_consent: true}), - learnerProfileView = context.learnerProfileView; - - LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView); - }); - - it('renders the limited profile for under 13 users', function() { - var context = createProfilePage( - true, - {year_of_birth: new Date().getFullYear() - 10, requires_parental_consent: true} - ); - var learnerProfileView = context.learnerProfileView; - LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView); - }); - }); - }); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js deleted file mode 100644 index 49b3dbc630..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js +++ /dev/null @@ -1,381 +0,0 @@ -define( - [ - 'backbone', - 'jquery', - 'underscore', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/spec/student_account/helpers', - 'js/student_account/models/user_account_model', - 'learner_profile/js/views/learner_profile_fields', - 'js/views/message_banner' - ], - function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields, - MessageBannerView) { - 'use strict'; - - describe('edx.user.LearnerProfileFields', function() { - var MOCK_YEAR_OF_BIRTH = 1989; - var MOCK_IMAGE_MAX_BYTES = 64; - var MOCK_IMAGE_MIN_BYTES = 16; - - var createImageView = function(options) { - var yearOfBirth = _.isUndefined(options.yearOfBirth) ? MOCK_YEAR_OF_BIRTH : options.yearOfBirth; - var imageMaxBytes = _.isUndefined(options.imageMaxBytes) ? MOCK_IMAGE_MAX_BYTES : options.imageMaxBytes; - var imageMinBytes = _.isUndefined(options.imageMinBytes) ? MOCK_IMAGE_MIN_BYTES : options.imageMinBytes; - var messageView; - - var imageData = { - image_url_large: '/media/profile-images/default.jpg', - has_image: !!options.hasImage - }; - - var accountSettingsModel = new UserAccountModel(); - accountSettingsModel.set({profile_image: imageData}); - accountSettingsModel.set({year_of_birth: yearOfBirth}); - accountSettingsModel.set({requires_parental_consent: !!_.isEmpty(yearOfBirth)}); - - accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL; - - messageView = new MessageBannerView({ - el: $('.message-banner') - }); - - return new LearnerProfileFields.ProfileImageFieldView({ - model: accountSettingsModel, - valueAttribute: 'profile_image', - editable: options.ownProfile, - messageView: messageView, - imageMaxBytes: imageMaxBytes, - imageMinBytes: imageMinBytes, - imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL, - imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL - }); - }; - - var createSocialLinksView = function(ownProfile, socialPlatformLinks) { - var accountSettingsModel = new UserAccountModel(); - accountSettingsModel.set({social_platforms: socialPlatformLinks}); - - return new LearnerProfileFields.SocialLinkIconsView({ - model: accountSettingsModel, - socialPlatforms: ['twitter', 'facebook', 'linkedin'], - ownProfile: ownProfile - }); - }; - - var createFakeImageFile = function(size) { - var fileFakeData = 'i63ljc6giwoskyb9x5sw0169bdcmcxr3cdz8boqv0lik971972cmd6yknvcxr5sw0nvc169bdcmcxsdf'; - return new Blob( - [fileFakeData.substr(0, size)], - {type: 'image/jpg'} - ); - }; - - var initializeUploader = function(view) { - view.$('.upload-button-input').fileupload({ - url: Helpers.IMAGE_UPLOAD_API_URL, - type: 'POST', - add: view.fileSelected, - done: view.imageChangeSucceeded, - fail: view.imageChangeFailed - }); - }; - - beforeEach(function() { - loadFixtures('learner_profile/fixtures/learner_profile.html'); - TemplateHelpers.installTemplate('templates/fields/field_image'); - TemplateHelpers.installTemplate('templates/fields/message_banner'); - TemplateHelpers.installTemplate('learner_profile/templates/social_icons'); - }); - - afterEach(function() { - // image_field.js's window.onBeforeUnload breaks Karma in Chrome, clean it up after each test - $(window).off('beforeunload'); - }); - - describe('ProfileImageFieldView', function() { - var verifyImageUploadButtonMessage = function(view, inProgress) { - var iconName = inProgress ? 'fa-spinner' : 'fa-camera'; - var message = inProgress ? view.titleUploading : view.uploadButtonTitle(); - expect(view.$('.upload-button-icon span').attr('class')).toContain(iconName); - expect(view.$('.upload-button-title').text().trim()).toBe(message); - }; - - var verifyImageRemoveButtonMessage = function(view, inProgress) { - var iconName = inProgress ? 'fa-spinner' : 'fa-remove'; - var message = inProgress ? view.titleRemoving : view.removeButtonTitle(); - expect(view.$('.remove-button-icon span').attr('class')).toContain(iconName); - expect(view.$('.remove-button-title').text().trim()).toBe(message); - }; - - it('can upload profile image', function() { - var requests = AjaxHelpers.requests(this); - var imageName = 'profile_image.jpg'; - var imageView = createImageView({ownProfile: true, hasImage: false}); - var data; - imageView.render(); - - initializeUploader(imageView); - - // Remove button should not be present for default image - expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy(); - - // For default image, image title should be `Upload an image` - verifyImageUploadButtonMessage(imageView, false); - - // Add image to upload queue. Validate the image size and send POST request to upload image - imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]}); - - // Verify image upload progress message - verifyImageUploadButtonMessage(imageView, true); - - // Verify if POST request received for image upload - AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData()); - - // Send 204 NO CONTENT to confirm the image upload success - AjaxHelpers.respondWithNoContent(requests); - - // Upon successful image upload, account settings model will be fetched to - // get the url for newly uploaded image, So we need to send the response for that GET - data = { - profile_image: { - image_url_large: '/media/profile-images/' + imageName, - has_image: true - } - }; - AjaxHelpers.respondWithJson(requests, data); - - // Verify uploaded image name - expect(imageView.$('.image-frame').attr('src')).toContain(imageName); - - // Remove button should be present after successful image upload - expect(imageView.$('.u-field-remove-button').css('display') !== 'none').toBeTruthy(); - - // After image upload, image title should be `Change image` - verifyImageUploadButtonMessage(imageView, false); - }); - - it('can remove profile image', function() { - var requests = AjaxHelpers.requests(this); - var imageView = createImageView({ownProfile: true, hasImage: false}); - var data; - imageView.render(); - - // Verify image remove title - verifyImageRemoveButtonMessage(imageView, false); - - imageView.$('.u-field-remove-button').click(); - - // Verify image remove progress message - verifyImageRemoveButtonMessage(imageView, true); - - // Verify if POST request received for image remove - AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_REMOVE_API_URL, null); - - // Send 204 NO CONTENT to confirm the image removal success - AjaxHelpers.respondWithNoContent(requests); - - // Upon successful image removal, account settings model will be fetched to get default image url - // So we need to send the response for that GET - data = { - profile_image: { - image_url_large: '/media/profile-images/default.jpg', - has_image: false - } - }; - AjaxHelpers.respondWithJson(requests, data); - - // Remove button should not be present for default image - expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy(); - }); - - it("can't remove default profile image", function() { - var imageView = createImageView({ownProfile: true, hasImage: false}); - imageView.render(); - - spyOn(imageView, 'clickedRemoveButton'); - - // Remove button should not be present for default image - expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy(); - - imageView.$('.u-field-remove-button').click(); - - // Remove button click handler should not be called - expect(imageView.clickedRemoveButton).not.toHaveBeenCalled(); - }); - - it("can't upload image having size greater than max size", function() { - var imageView = createImageView({ownProfile: true, hasImage: false}); - imageView.render(); - - initializeUploader(imageView); - - // Add image to upload queue, this will validate the image size - imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(70)]}); - - // Verify error message - expect($('.message-banner').text().trim()) - .toBe('The file must be smaller than 64 bytes in size.'); - }); - - it("can't upload image having size less than min size", function() { - var imageView = createImageView({ownProfile: true, hasImage: false}); - imageView.render(); - - initializeUploader(imageView); - - // Add image to upload queue, this will validate the image size - imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(10)]}); - - // Verify error message - expect($('.message-banner').text().trim()).toBe('The file must be at least 16 bytes in size.'); - }); - - it("can't upload and remove image if parental consent required", function() { - var imageView = createImageView({ownProfile: true, hasImage: false, yearOfBirth: ''}); - imageView.render(); - - spyOn(imageView, 'clickedUploadButton'); - spyOn(imageView, 'clickedRemoveButton'); - - expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy(); - expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy(); - - imageView.$('.u-field-upload-button').click(); - imageView.$('.u-field-remove-button').click(); - - expect(imageView.clickedUploadButton).not.toHaveBeenCalled(); - expect(imageView.clickedRemoveButton).not.toHaveBeenCalled(); - }); - - it("can't upload and remove image on others profile", function() { - var imageView = createImageView({ownProfile: false}); - imageView.render(); - - spyOn(imageView, 'clickedUploadButton'); - spyOn(imageView, 'clickedRemoveButton'); - - expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy(); - expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy(); - - imageView.$('.u-field-upload-button').click(); - imageView.$('.u-field-remove-button').click(); - - expect(imageView.clickedUploadButton).not.toHaveBeenCalled(); - expect(imageView.clickedRemoveButton).not.toHaveBeenCalled(); - }); - - it('shows message if we try to navigate away during image upload/remove', function() { - var imageView = createImageView({ownProfile: true, hasImage: false}); - spyOn(imageView, 'onBeforeUnload'); - imageView.render(); - - initializeUploader(imageView); - - // Add image to upload queue, this will validate image size and send POST request to upload image - imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]}); - - // Verify image upload progress message - verifyImageUploadButtonMessage(imageView, true); - - window.onbeforeunload = null; - $(window).trigger('beforeunload'); - expect(imageView.onBeforeUnload).toHaveBeenCalled(); - }); - - it('shows error message for HTTP 500', function() { - var requests = AjaxHelpers.requests(this); - var imageView = createImageView({ownProfile: true, hasImage: false}); - imageView.render(); - - initializeUploader(imageView); - - // Add image to upload queue. Validate the image size and send POST request to upload image - imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]}); - - // Verify image upload progress message - verifyImageUploadButtonMessage(imageView, true); - - // Verify if POST request received for image upload - AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData()); - - // Send HTTP 500 - AjaxHelpers.respondWithError(requests); - - expect($('.message-banner').text().trim()).toBe(imageView.errorMessage); - }); - }); - - describe('SocialLinkIconsView', function() { - var socialPlatformLinks, - socialLinkData, - socialLinksView, - socialPlatform, - $icon; - - it('icons are visible and links to social profile if added in account settings', function() { - socialPlatformLinks = { - twitter: { - platform: 'twitter', - social_link: 'https://www.twitter.com/edX' - }, - facebook: { - platform: 'facebook', - social_link: 'https://www.facebook.com/edX' - }, - linkedin: { - platform: 'linkedin', - social_link: '' - } - }; - - socialLinksView = createSocialLinksView(true, socialPlatformLinks); - - // Icons should be present and contain links if defined - for (var i = 0; i < Object.keys(socialPlatformLinks); i++) { // eslint-disable-line vars-on-top - socialPlatform = Object.keys(socialPlatformLinks)[i]; - socialLinkData = socialPlatformLinks[socialPlatform]; - if (socialLinkData.social_link) { - // Icons with a social_link value should be displayed with a surrounding link - $icon = socialLinksView.$('span.fa-' + socialPlatform + '-square'); - expect($icon).toExist(); - expect($icon.parent().is('a')); - } else { - // Icons without a social_link value should be displayed without a surrounding link - $icon = socialLinksView.$('span.fa-' + socialPlatform + '-square'); - expect($icon).toExist(); - expect(!$icon.parent().is('a')); - } - } - }); - - it('icons are not visible on a profile with no links', function() { - socialPlatformLinks = { - twitter: { - platform: 'twitter', - social_link: '' - }, - facebook: { - platform: 'facebook', - social_link: '' - }, - linkedin: { - platform: 'linkedin', - social_link: '' - } - }; - - socialLinksView = createSocialLinksView(false, socialPlatformLinks); - - // Icons should not be present if not defined on another user's profile - for (var i = 0; i < Object.keys(socialPlatformLinks); i++) { // eslint-disable-line vars-on-top - socialPlatform = Object.keys(socialPlatformLinks)[i]; - socialLinkData = socialPlatformLinks[socialPlatform]; - $icon = socialLinksView.$('span.fa-' + socialPlatform + '-square'); - expect($icon).toBe(null); - } - }); - }); - }); - }); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js deleted file mode 100644 index 21d2dd0f5d..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js +++ /dev/null @@ -1,218 +0,0 @@ -/* eslint-disable vars-on-top */ -define( - [ - 'gettext', - 'backbone', - 'jquery', - 'underscore', - 'edx-ui-toolkit/js/pagination/paging-collection', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/spec/student_account/helpers', - 'learner_profile/js/spec_helpers/helpers', - 'js/views/fields', - 'js/student_account/models/user_account_model', - 'js/student_account/models/user_preferences_model', - 'learner_profile/js/views/learner_profile_fields', - 'learner_profile/js/views/learner_profile_view', - 'js/student_account/views/account_settings_fields', - 'js/views/message_banner' - ], - function(gettext, Backbone, $, _, PagingCollection, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, - FieldViews, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView, - AccountSettingsFieldViews, MessageBannerView) { - 'use strict'; - - describe('edx.user.LearnerProfileView', function() { - var createLearnerProfileView = function(ownProfile, accountPrivacy, profileIsPublic) { - var accountSettingsModel = new UserAccountModel(); - accountSettingsModel.set(Helpers.createAccountSettingsData()); - accountSettingsModel.set({profile_is_public: profileIsPublic}); - accountSettingsModel.set({profile_image: Helpers.PROFILE_IMAGE}); - - var accountPreferencesModel = new AccountPreferencesModel(); - accountPreferencesModel.set({account_privacy: accountPrivacy}); - - accountPreferencesModel.url = Helpers.USER_PREFERENCES_API_URL; - - var editable = ownProfile ? 'toggle' : 'never'; - - var accountPrivacyFieldView = new LearnerProfileFields.AccountPrivacyFieldView({ - model: accountPreferencesModel, - required: true, - editable: 'always', - showMessages: false, - title: 'edX learners can see my:', - valueAttribute: 'account_privacy', - options: [ - ['all_users', 'Full Profile'], - ['private', 'Limited Profile'] - ], - helpMessage: '', - accountSettingsPageUrl: '/account/settings/' - }); - - var messageView = new MessageBannerView({ - el: $('.message-banner') - }); - - var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({ - model: accountSettingsModel, - valueAttribute: 'profile_image', - editable: editable, - messageView: messageView, - imageMaxBytes: Helpers.IMAGE_MAX_BYTES, - imageMinBytes: Helpers.IMAGE_MIN_BYTES, - imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL, - imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL - }); - - var usernameFieldView = new FieldViews.ReadonlyFieldView({ - model: accountSettingsModel, - valueAttribute: 'username', - helpMessage: '' - }); - - var nameFieldView = new FieldViews.ReadonlyFieldView({ - model: accountSettingsModel, - valueAttribute: 'name', - helpMessage: '' - }); - - var sectionOneFieldViews = [ - new LearnerProfileFields.SocialLinkIconsView({ - model: accountSettingsModel, - socialPlatforms: Helpers.SOCIAL_PLATFORMS, - ownProfile: true - }), - - new FieldViews.DropdownFieldView({ - title: gettext('Location'), - model: accountSettingsModel, - required: false, - editable: editable, - showMessages: false, - placeholderValue: '', - valueAttribute: 'country', - options: Helpers.FIELD_OPTIONS, - helpMessage: '' - }), - - new AccountSettingsFieldViews.LanguageProficienciesFieldView({ - title: gettext('Language'), - model: accountSettingsModel, - required: false, - editable: editable, - showMessages: false, - placeholderValue: 'Add language', - valueAttribute: 'language_proficiencies', - options: Helpers.FIELD_OPTIONS, - helpMessage: '' - }), - - new FieldViews.DateFieldView({ - model: accountSettingsModel, - valueAttribute: 'date_joined', - helpMessage: '' - }) - ]; - - var sectionTwoFieldViews = [ - new FieldViews.TextareaFieldView({ - model: accountSettingsModel, - editable: editable, - showMessages: false, - title: 'About me', - placeholderValue: 'Tell other edX learners a little about yourself: where you live, ' - + "what your interests are, why you're taking courses on edX, or what you hope to learn.", - valueAttribute: 'bio', - helpMessage: '', - messagePosition: 'header' - }) - ]; - - return new LearnerProfileView( - { - el: $('.wrapper-profile'), - ownProfile: ownProfile, - hasPreferencesAccess: true, - accountSettingsModel: accountSettingsModel, - preferencesModel: accountPreferencesModel, - accountPrivacyFieldView: accountPrivacyFieldView, - usernameFieldView: usernameFieldView, - nameFieldView: nameFieldView, - profileImageFieldView: profileImageFieldView, - sectionOneFieldViews: sectionOneFieldViews, - sectionTwoFieldViews: sectionTwoFieldViews, - }); - }; - - beforeEach(function() { - loadFixtures('learner_profile/fixtures/learner_profile.html'); - }); - - afterEach(function() { - Backbone.history.stop(); - }); - - it('shows loading error correctly', function() { - var learnerProfileView = createLearnerProfileView(false, 'all_users'); - - Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true); - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - - learnerProfileView.render(); - learnerProfileView.showLoadingError(); - - Helpers.expectLoadingErrorIsVisible(learnerProfileView, true); - }); - - it('renders all fields as expected for self with full access', function() { - var learnerProfileView = createLearnerProfileView(true, 'all_users', true); - - Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true); - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - - learnerProfileView.render(); - - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView); - }); - - it('renders all fields as expected for self with limited access', function() { - var learnerProfileView = createLearnerProfileView(true, 'private', false); - - Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true); - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - - learnerProfileView.render(); - - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView); - }); - - it('renders the fields as expected for others with full access', function() { - var learnerProfileView = createLearnerProfileView(false, 'all_users', true); - - Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true); - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - - learnerProfileView.render(); - - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, true); - }); - - it('renders the fields as expected for others with limited access', function() { - var learnerProfileView = createLearnerProfileView(false, 'private', false); - - Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true); - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - - learnerProfileView.render(); - - Helpers.expectLoadingErrorIsVisible(learnerProfileView, false); - LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true); - }); - }); - }); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/section_two_tab_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/section_two_tab_spec.js deleted file mode 100644 index d0e22d670b..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/section_two_tab_spec.js +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable vars-on-top */ -define( - [ - 'backbone', 'jquery', 'underscore', - 'js/spec/student_account/helpers', - 'learner_profile/js/views/section_two_tab', - 'js/views/fields', - 'js/student_account/models/user_account_model' - ], - function(Backbone, $, _, Helpers, SectionTwoTabView, FieldViews, UserAccountModel) { - 'use strict'; - - describe('edx.user.SectionTwoTab', function() { - var createSectionTwoView = function(ownProfile, profileIsPublic) { - var accountSettingsModel = new UserAccountModel(); - accountSettingsModel.set(Helpers.createAccountSettingsData()); - accountSettingsModel.set({profile_is_public: profileIsPublic}); - accountSettingsModel.set({profile_image: Helpers.PROFILE_IMAGE}); - - var editable = ownProfile ? 'toggle' : 'never'; - - var sectionTwoFieldViews = [ - new FieldViews.TextareaFieldView({ - model: accountSettingsModel, - editable: editable, - showMessages: false, - title: 'About me', - placeholderValue: 'Tell other edX learners a little about yourself: where you live, ' - + "what your interests are, why you're taking courses on edX, or what you hope to learn.", - valueAttribute: 'bio', - helpMessage: '', - messagePosition: 'header' - }) - ]; - - return new SectionTwoTabView({ - viewList: sectionTwoFieldViews, - showFullProfile: function() { - return profileIsPublic; - }, - ownProfile: ownProfile - }); - }; - - it('full profile displayed for public profile', function() { - var view = createSectionTwoView(false, true); - view.render(); - var bio = view.$el.find('.u-field-bio'); - expect(bio.length).toBe(1); - }); - - it('profile field parts are actually rendered for public profile', function() { - var view = createSectionTwoView(false, true); - _.each(view.options.viewList, function(fieldView) { - spyOn(fieldView, 'render').and.callThrough(); - }); - view.render(); - _.each(view.options.viewList, function(fieldView) { - expect(fieldView.render).toHaveBeenCalled(); - }); - }); - - var testPrivateProfile = function(ownProfile, messageString) { - var view = createSectionTwoView(ownProfile, false); - view.render(); - var bio = view.$el.find('.u-field-bio'); - expect(bio.length).toBe(0); - var msg = view.$el.find('span.profile-private-message'); - expect(msg.length).toBe(1); - expect(_.count(msg.html(), messageString)).toBeTruthy(); - }; - - it('no profile when profile is private for other people', function() { - testPrivateProfile(false, 'This learner is currently sharing a limited profile'); - }); - - it('no profile when profile is private for the user herself', function() { - testPrivateProfile(true, 'You are currently sharing a limited profile'); - }); - - var testProfilePrivatePartsDoNotRender = function(ownProfile) { - var view = createSectionTwoView(ownProfile, false); - _.each(view.options.viewList, function(fieldView) { - spyOn(fieldView, 'render'); - }); - view.render(); - _.each(view.options.viewList, function(fieldView) { - expect(fieldView.render).not.toHaveBeenCalled(); - }); - }; - - it('profile field parts are not rendered for private profile for owner', function() { - testProfilePrivatePartsDoNotRender(true); - }); - - it('profile field parts are not rendered for private profile for other people', function() { - testProfilePrivatePartsDoNotRender(false); - }); - - it('does not allow fields to be edited when visiting a profile for other people', function() { - var view = createSectionTwoView(false, true); - var bio = view.options.viewList[0]; - expect(bio.editable).toBe('never'); - }); - - it("allows fields to be edited when visiting one's own profile", function() { - var view = createSectionTwoView(true, true); - var bio = view.options.viewList[0]; - expect(bio.editable).toBe('toggle'); - }); - }); - } -); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js deleted file mode 100644 index e1369284a4..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js +++ /dev/null @@ -1,133 +0,0 @@ -define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'], function(_, URI, AjaxHelpers) { - 'use strict'; - - var expectProfileElementContainsField = function(element, view) { - var titleElement, fieldTitle; - var $element = $(element); - - // Avoid testing for elements without titles - titleElement = $element.find('.u-field-title'); - if (titleElement.length === 0) { - return; - } - - fieldTitle = titleElement.text().trim(); - if (!_.isUndefined(view.options.title) && !_.isUndefined(fieldTitle)) { - expect(fieldTitle).toBe(view.options.title); - } - - if ('fieldValue' in view || 'imageUrl' in view) { - if ('imageUrl' in view) { - expect($($element.find('.image-frame')[0]).attr('src')).toBe(view.imageUrl()); - } else if (view.fieldType === 'date') { - expect(view.fieldValue()).toBe(view.timezoneFormattedDate()); - } else if (view.fieldValue()) { - expect(view.fieldValue()).toBe(view.modelValue()); - } else if ('optionForValue' in view) { - expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe( - view.displayValue(view.modelValue()) - ); - } else { - expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(view.modelValue()); - } - } else { - throw new Error('Unexpected field type: ' + view.fieldType); - } - }; - - var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) { - var $accountPrivacyElement = $('.wrapper-profile-field-account-privacy'); - var $privacyFieldElement = $($accountPrivacyElement).find('.u-field'); - - if (othersProfile) { - expect($privacyFieldElement.length).toBe(0); - } else { - expect($privacyFieldElement.length).toBe(1); - expectProfileElementContainsField($privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView); - } - }; - - var expectSectionOneTobeRendered = function(learnerProfileView) { - var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')) - .find('.u-field, .social-links'); - - expect(sectionOneFieldElements.length).toBe(7); - expectProfileElementContainsField(sectionOneFieldElements[0], learnerProfileView.options.profileImageFieldView); - expectProfileElementContainsField(sectionOneFieldElements[1], learnerProfileView.options.usernameFieldView); - expectProfileElementContainsField(sectionOneFieldElements[2], learnerProfileView.options.nameFieldView); - - _.each(_.rest(sectionOneFieldElements, 3), function(sectionFieldElement, fieldIndex) { - expectProfileElementContainsField( - sectionFieldElement, - learnerProfileView.options.sectionOneFieldViews[fieldIndex] - ); - }); - }; - - var expectSectionTwoTobeRendered = function(learnerProfileView) { - var $sectionTwoElement = $('.wrapper-profile-section-two'); - var $sectionTwoFieldElements = $($sectionTwoElement).find('.u-field'); - - expect($sectionTwoFieldElements.length).toBe(learnerProfileView.options.sectionTwoFieldViews.length); - - _.each($sectionTwoFieldElements, function(sectionFieldElement, fieldIndex) { - expectProfileElementContainsField( - sectionFieldElement, - learnerProfileView.options.sectionTwoFieldViews[fieldIndex] - ); - }); - }; - - var expectProfileSectionsAndFieldsToBeRendered = function(learnerProfileView, othersProfile) { - expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile); - expectSectionOneTobeRendered(learnerProfileView); - expectSectionTwoTobeRendered(learnerProfileView); - }; - - var expectLimitedProfileSectionsAndFieldsToBeRendered = function(learnerProfileView, othersProfile) { - var sectionOneFieldElements = $('.wrapper-profile-section-one').find('.u-field'); - - expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile); - - expect(sectionOneFieldElements.length).toBe(2); - expectProfileElementContainsField( - sectionOneFieldElements[0], - learnerProfileView.options.profileImageFieldView - ); - expectProfileElementContainsField( - sectionOneFieldElements[1], - learnerProfileView.options.usernameFieldView - ); - - if (othersProfile) { - expect($('.profile-private-message').text()) - .toBe('This learner is currently sharing a limited profile.'); - } else { - expect($('.profile-private-message').text()).toBe('You are currently sharing a limited profile.'); - } - }; - - var expectProfileSectionsNotToBeRendered = function() { - expect($('.wrapper-profile-field-account-privacy').length).toBe(0); - expect($('.wrapper-profile-section-one').length).toBe(0); - expect($('.wrapper-profile-section-two').length).toBe(0); - }; - - var expectTabbedViewToBeUndefined = function(requests, tabbedViewView) { - // Unrelated initial request, no badge request - expect(requests.length).toBe(1); - expect(tabbedViewView).toBe(undefined); - }; - - var expectTabbedViewToBeShown = function(tabbedViewView) { - expect(tabbedViewView.$el.find('.page-content-nav').is(':visible')).toBe(true); - }; - - return { - expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered, - expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered, - expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered, - expectTabbedViewToBeUndefined: expectTabbedViewToBeUndefined, - expectTabbedViewToBeShown: expectTabbedViewToBeShown - }; -}); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js deleted file mode 100644 index 807e3e4c5b..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js +++ /dev/null @@ -1,169 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -(function(define) { - 'use strict'; - - define([ - 'gettext', - 'jquery', - 'underscore', - 'backbone', - 'edx-ui-toolkit/js/utils/string-utils', - 'edx-ui-toolkit/js/utils/html-utils', - 'js/views/fields', - 'js/views/image_field', - 'text!learner_profile/templates/social_icons.underscore', - 'backbone-super' - ], function(gettext, $, _, Backbone, StringUtils, HtmlUtils, FieldViews, ImageFieldView, socialIconsTemplate) { - var LearnerProfileFieldViews = {}; - - LearnerProfileFieldViews.AccountPrivacyFieldView = FieldViews.DropdownFieldView.extend({ - - events: { - 'click button.btn-change-privacy': 'finishEditing', - 'change select': 'showSaveButton' - }, - - render: function() { - this._super(); - this.showNotificationMessage(); - this.updateFieldValue(); - return this; - }, - - showNotificationMessage: function() { - var accountSettingsLink = HtmlUtils.joinHtml( - HtmlUtils.interpolateHtml( - HtmlUtils.HTML(''), {settings_url: this.options.accountSettingsPageUrl} - ), - gettext('Account Settings page.'), - HtmlUtils.HTML('') - ); - if (this.profileIsPrivate) { - this._super( - HtmlUtils.interpolateHtml( - gettext('You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}'), // eslint-disable-line max-len - {account_settings_page_link: accountSettingsLink} - ) - ); - } else if (this.requiresParentalConsent) { - this._super( - HtmlUtils.interpolateHtml( - gettext('You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}'), // eslint-disable-line max-len - {account_settings_page_link: accountSettingsLink} - ) - ); - } else { - this._super(''); - } - }, - - updateFieldValue: function() { - if (!this.isAboveMinimumAge) { - this.$('.u-field-value select').val('private'); - this.disableField(true); - } - }, - - showSaveButton: function() { - $('.btn-change-privacy').removeClass('hidden'); - } - }); - - LearnerProfileFieldViews.ProfileImageFieldView = ImageFieldView.extend({ - - screenReaderTitle: gettext('Profile Image'), - - imageUrl: function() { - return this.model.profileImageUrl(); - }, - - imageAltText: function() { - return StringUtils.interpolate( - gettext('Profile image for {username}'), - {username: this.model.get('username')} - ); - }, - - imageChangeSucceeded: function() { - var view = this; - // Update model to get the latest urls of profile image. - this.model.fetch().done(function() { - view.setCurrentStatus(''); - view.render(); - view.$('.u-field-upload-button').focus(); - }).fail(function() { - view.setCurrentStatus(''); - view.showErrorMessage(view.errorMessage); - }); - }, - - imageChangeFailed: function(e, data) { - this.setCurrentStatus(''); - this.showImageChangeFailedMessage(data.jqXHR.status, data.jqXHR.responseText); - }, - - showImageChangeFailedMessage: function(status, responseText) { - var errors; - if (_.contains([400, 404], status)) { - try { - errors = JSON.parse(responseText); - this.showErrorMessage(errors.user_message); - } catch (error) { - this.showErrorMessage(this.errorMessage); - } - } else { - this.showErrorMessage(this.errorMessage); - } - }, - - showErrorMessage: function(message) { - this.options.messageView.showMessage(message); - }, - - isEditingAllowed: function() { - return this.model.isAboveMinimumAge(); - }, - - isShowingPlaceholder: function() { - return !this.model.hasProfileImage(); - }, - - clickedRemoveButton: function(e, data) { - this.options.messageView.hideMessage(); - this._super(e, data); - }, - - fileSelected: function(e, data) { - this.options.messageView.hideMessage(); - this._super(e, data); - } - }); - - LearnerProfileFieldViews.SocialLinkIconsView = Backbone.View.extend({ - - initialize: function(options) { - this.options = _.extend({}, options); - }, - - render: function() { - var socialLinks = {}; - for (var platformName in this.options.socialPlatforms) { // eslint-disable-line no-restricted-syntax, guard-for-in, vars-on-top, max-len - socialLinks[platformName] = null; - for (var link in this.model.get('social_links')) { // eslint-disable-line no-restricted-syntax, vars-on-top, max-len - if (platformName === this.model.get('social_links')[link].platform) { - socialLinks[platformName] = this.model.get('social_links')[link].social_link; - } - } - } - - HtmlUtils.setHtml(this.$el, HtmlUtils.template(socialIconsTemplate)({ - socialLinks: socialLinks, - ownProfile: this.options.ownProfile - })); - return this; - } - }); - - return LearnerProfileFieldViews; - }); -}).call(this, define || RequireJS.define); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js deleted file mode 100644 index 74c0e6b819..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js +++ /dev/null @@ -1,150 +0,0 @@ -(function(define) { - 'use strict'; - - define( - [ - 'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils', - 'common/js/components/views/tabbed_view', - 'learner_profile/js/views/section_two_tab' - ], - function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab) { - var LearnerProfileView = Backbone.View.extend({ - - initialize: function(options) { - var Router; - this.options = _.extend({}, options); - _.bindAll(this, 'showFullProfile', 'render', 'renderFields', 'showLoadingError'); - this.listenTo(this.options.preferencesModel, 'change:account_privacy', this.render); - Router = Backbone.Router.extend({ - routes: {':about_me': 'loadTab', ':accomplishments': 'loadTab'} - }); - - this.router = new Router(); - this.firstRender = true; - }, - - showFullProfile: function() { - var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge(); - if (this.options.ownProfile) { - return isAboveMinimumAge - && this.options.preferencesModel.get('account_privacy') === 'all_users'; - } else { - return this.options.accountSettingsModel.get('profile_is_public'); - } - }, - - setActiveTab: function(tab) { - // This tab may not actually exist. - if (this.tabbedView.getTabMeta(tab).tab) { - this.tabbedView.setActiveTab(tab); - } - }, - - render: function() { - var tabs, - $tabbedViewElement, - $wrapperProfileBioElement = this.$el.find('.wrapper-profile-bio'), - self = this; - - this.sectionTwoView = new SectionTwoTab({ - viewList: this.options.sectionTwoFieldViews, - showFullProfile: this.showFullProfile, - ownProfile: this.options.ownProfile - }); - - this.renderFields(); - - // Reveal the profile and hide the loading indicator - $('.ui-loading-indicator').addClass('is-hidden'); - $('.wrapper-profile-section-container-one').removeClass('is-hidden'); - $('.wrapper-profile-section-container-two').removeClass('is-hidden'); - - - if (this.showFullProfile()) { - tabs = [ - {view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'} - ]; - - this.tabbedView = new TabbedView({ - tabs: tabs, - router: this.router, - viewLabel: gettext('Profile') - }); - - $tabbedViewElement = this.tabbedView.render().el; - HtmlUtils.setHtml( - $wrapperProfileBioElement, - HtmlUtils.HTML($tabbedViewElement) - ); - - if (this.firstRender) { - this.router.on('route:loadTab', _.bind(this.setActiveTab, this)); - Backbone.history.start(); - this.firstRender = false; - // Load from history. - this.router.navigate((Backbone.history.getFragment() || 'about_me'), {trigger: true}); - } else { - // Restart the router so the tab will be brought up anew. - Backbone.history.stop(); - Backbone.history.start(); - } - } else { - if (this.isCoppaCompliant()) { - // xss-lint: disable=javascript-jquery-html - $wrapperProfileBioElement.html(this.sectionTwoView.render().el); - } - } - return this; - }, - - isCoppaCompliant: function() { - var enableCoppaCompliance = this.options.accountSettingsModel.get('enable_coppa_compliance'), - isAboveAge = this.options.accountSettingsModel.isAboveMinimumAge(); - return !enableCoppaCompliance || (enableCoppaCompliance && isAboveAge); - }, - - renderFields: function() { - var view = this, - fieldView, - imageView, - settings; - - if (this.options.ownProfile && this.isCoppaCompliant()) { - fieldView = this.options.accountPrivacyFieldView; - settings = this.options.accountSettingsModel; - fieldView.profileIsPrivate = !settings.get('year_of_birth'); - fieldView.requiresParentalConsent = settings.get('requires_parental_consent'); - fieldView.isAboveMinimumAge = settings.isAboveMinimumAge(); - fieldView.undelegateEvents(); - this.$('.wrapper-profile-field-account-privacy').prepend(fieldView.render().el); - fieldView.delegateEvents(); - } - - // Clear existing content in user profile card - this.$('.profile-section-one-fields').html(''); - - // Do not show name when in limited mode or no name has been set - if (this.showFullProfile() && this.options.accountSettingsModel.get('name')) { - this.$('.profile-section-one-fields').append(this.options.nameFieldView.render().el); - } - this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el); - - imageView = this.options.profileImageFieldView; - this.$('.profile-image-field').append(imageView.render().el); - - if (this.showFullProfile()) { - _.each(this.options.sectionOneFieldViews, function(childFieldView) { - view.$('.profile-section-one-fields').append(childFieldView.render().el); - }); - } - }, - - showLoadingError: function() { - this.$('.ui-loading-indicator').addClass('is-hidden'); - this.$('.ui-loading-error').removeClass('is-hidden'); - } - }); - - return LearnerProfileView; - }); -}).call(this, define || RequireJS.define); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/section_two_tab.js b/openedx/features/learner_profile/static/learner_profile/js/views/section_two_tab.js deleted file mode 100644 index 23064c9e6b..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/section_two_tab.js +++ /dev/null @@ -1,33 +0,0 @@ -(function(define) { - 'use strict'; - - define( - [ - 'gettext', 'jquery', 'underscore', 'backbone', 'text!learner_profile/templates/section_two.underscore', - 'edx-ui-toolkit/js/utils/html-utils' - ], - function(gettext, $, _, Backbone, sectionTwoTemplate, HtmlUtils) { - var SectionTwoTab = Backbone.View.extend({ - attributes: { - class: 'wrapper-profile-section-two' - }, - template: _.template(sectionTwoTemplate), - initialize: function(options) { - this.options = _.extend({}, options); - }, - render: function() { - var self = this; - var showFullProfile = this.options.showFullProfile(); - this.$el.html(HtmlUtils.HTML(this.template({ownProfile: self.options.ownProfile, showFullProfile: showFullProfile})).toString()); // eslint-disable-line max-len - if (showFullProfile) { - _.each(this.options.viewList, function(fieldView) { - self.$el.find('.field-container').append(fieldView.render().el); - }); - } - return this; - } - }); - - return SectionTwoTab; - }); -}).call(this, define || RequireJS.define); diff --git a/openedx/features/learner_profile/static/learner_profile/templates/section_two.underscore b/openedx/features/learner_profile/static/learner_profile/templates/section_two.underscore deleted file mode 100644 index 0c7d11cd8b..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/section_two.underscore +++ /dev/null @@ -1,10 +0,0 @@ -
-
- <% if (!showFullProfile) { %> - <% if(ownProfile) { %> - <%- gettext("You are currently sharing a limited profile.") %> - <% } else { %> - <%- gettext("This learner is currently sharing a limited profile.") %> - <% } %> - <% } %> -
\ No newline at end of file diff --git a/openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore b/openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore deleted file mode 100644 index 52b864cfb6..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/openedx/features/learner_profile/static/learner_profile/templates/third_party_auth.html b/openedx/features/learner_profile/static/learner_profile/templates/third_party_auth.html deleted file mode 100644 index 07e14bc48a..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/third_party_auth.html +++ /dev/null @@ -1,47 +0,0 @@ -<%page expression_filter="h"/> -<%! -from django.utils.translation import gettext as _ -from common.djangoapps.third_party_auth import pipeline -%> - - diff --git a/openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html b/openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html deleted file mode 100644 index 09e6ce36b9..0000000000 --- a/openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html +++ /dev/null @@ -1,69 +0,0 @@ -## mako - -<%page expression_filter="h"/> - -<%namespace name='static' file='/static_content.html'/> - -<%! -from django.utils.translation import gettext as _ -from openedx.core.djangolib.markup import HTML, Text -%> - -
- % if course_certificates or own_profile: -

Course Certificates

- % if course_certificates: - % for certificate in course_certificates: - <% - course = certificate['course'] - - completion_date_message_html = Text(_('Completed {completion_date_html}')).format( - completion_date_html=HTML( - '' - ).format( - completion_date=certificate['created'], - user_timezone=user_timezone, - user_language=user_language, - ), - ) - %> -
- -
-
${course.display_org_with_default}
-
${course.display_name_with_default}
-

${completion_date_message_html}

-
-
- % endfor - % elif own_profile: -
-

${_("You haven't earned any certificates yet.")}

- % if settings.FEATURES.get('COURSES_ARE_BROWSABLE'): -

- - - ${_('Explore New Courses')} - -

- % endif -
- % endif - % endif -
- -<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> - DateUtilFactory.transform('.localized-datetime'); - diff --git a/openedx/features/learner_profile/templates/learner_profile/learner_profile.html b/openedx/features/learner_profile/templates/learner_profile/learner_profile.html deleted file mode 100644 index 6de4744e66..0000000000 --- a/openedx/features/learner_profile/templates/learner_profile/learner_profile.html +++ /dev/null @@ -1,79 +0,0 @@ -## mako - -<%page expression_filter="h"/> -<%inherit file="/main.html" /> -<%def name="online_help_token()"><% return "profile" %> -<%namespace name='static' file='/static_content.html'/> - -<%! -import json -from django.urls import reverse -from django.utils.translation import gettext as _ -from openedx.core.djangolib.js_utils import dump_js_escaped_json -from openedx.core.djangolib.markup import HTML -%> - -<%block name="pagetitle">${_("Learner Profile")} - -<%block name="bodyclass">view-profile - -<%block name="headextra"> -<%static:css group='style-course'/> - - -
-
-
-
- - % if own_profile: -
-

${_("My Profile")}

-
- ${_('Build out your profile to personalize your identity on {platform_name}.').format( - platform_name=platform_name, - )} -
-
- % endif - -
-
-
- -<%block name="js_extra"> -<%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory"> - var options = ${data | n, dump_js_escaped_json}; - LearnerProfileFactory(options); - - diff --git a/openedx/features/learner_profile/tests/__init__.py b/openedx/features/learner_profile/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/features/learner_profile/tests/views/__init__.py b/openedx/features/learner_profile/tests/views/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/features/learner_profile/tests/views/test_learner_profile.py b/openedx/features/learner_profile/tests/views/test_learner_profile.py deleted file mode 100644 index c4c8352000..0000000000 --- a/openedx/features/learner_profile/tests/views/test_learner_profile.py +++ /dev/null @@ -1,281 +0,0 @@ -""" Tests for student profile views. """ - - -import datetime -from unittest import mock - -import ddt -from django.conf import settings -from django.test.client import RequestFactory -from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag -from opaque_keys.edx.locator import CourseLocator - -from common.djangoapps.course_modes.models import CourseMode -from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory -from common.djangoapps.util.testing import UrlResetMixin -from lms.djangoapps.certificates.data import CertificateStatuses -from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory -from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin -from openedx.features.learner_profile.toggles import REDIRECT_TO_PROFILE_MICROFRONTEND -from openedx.features.learner_profile.views.learner_profile import learner_profile_context -from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order - - -@ddt.ddt -class LearnerProfileViewTest(SiteMixin, UrlResetMixin, ModuleStoreTestCase): - """ Tests for the student profile view. """ - - USERNAME = "username" - OTHER_USERNAME = "other_user" - PASSWORD = "password" - DOWNLOAD_URL = "http://www.example.com/certificate.pdf" - CONTEXT_DATA = [ - 'default_public_account_fields', - 'accounts_api_url', - 'preferences_api_url', - 'account_settings_page_url', - 'has_preferences_access', - 'own_profile', - 'country_options', - 'language_options', - 'account_settings_data', - 'preferences_data', - ] - - def setUp(self): - super().setUp() - self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) - self.other_user = UserFactory.create(username=self.OTHER_USERNAME, password=self.PASSWORD) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - self.course = CourseFactory.create( - start=datetime.datetime(2013, 9, 16, 7, 17, 28), - end=datetime.datetime.now(), - certificate_available_date=datetime.datetime.now(), - ) - - def test_context(self): - """ - Verify learner profile page context data. - """ - request = RequestFactory().get('/url') - request.user = self.user - - context = learner_profile_context(request, self.USERNAME, self.user.is_staff) - - assert context['data']['default_public_account_fields'] == \ - settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'] - - assert context['data']['accounts_api_url'] == \ - reverse('accounts_api', kwargs={'username': self.user.username}) - - assert context['data']['preferences_api_url'] == \ - reverse('preferences_api', kwargs={'username': self.user.username}) - - assert context['data']['profile_image_upload_url'] == \ - reverse('profile_image_upload', kwargs={'username': self.user.username}) - - assert context['data']['profile_image_remove_url'] == \ - reverse('profile_image_remove', kwargs={'username': self.user.username}) - - assert context['data']['profile_image_max_bytes'] == settings.PROFILE_IMAGE_MAX_BYTES - - assert context['data']['profile_image_min_bytes'] == settings.PROFILE_IMAGE_MIN_BYTES - - assert context['data']['account_settings_page_url'] == reverse('account_settings') - - for attribute in self.CONTEXT_DATA: - assert attribute in context['data'] - - def test_view(self): - """ - Verify learner profile page view. - """ - profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME}) - response = self.client.get(path=profile_path) - - for attribute in self.CONTEXT_DATA: - self.assertContains(response, attribute) - - def test_redirect_view(self): - with override_waffle_flag(REDIRECT_TO_PROFILE_MICROFRONTEND, active=True): - profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME}) - - # Test with waffle flag active and site setting disabled, does not redirect - response = self.client.get(path=profile_path) - for attribute in self.CONTEXT_DATA: - self.assertContains(response, attribute) - - # Test with waffle flag active and site setting enabled, redirects to microfrontend - site_domain = 'othersite.example.com' - self.set_up_site(site_domain, { - 'SITE_NAME': site_domain, - 'ENABLE_PROFILE_MICROFRONTEND': True - }) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - response = self.client.get(path=profile_path) - profile_url = settings.PROFILE_MICROFRONTEND_URL - self.assertRedirects(response, profile_url + self.USERNAME, fetch_redirect_response=False) - - def test_records_link(self): - profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME}) - response = self.client.get(path=profile_path) - self.assertContains(response, f'') - - def test_undefined_profile_page(self): - """ - Verify that a 404 is returned for a non-existent profile page. - """ - profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"}) - response = self.client.get(path=profile_path) - assert 404 == response.status_code - - def _create_certificate(self, course_key=None, enrollment_mode=CourseMode.HONOR, status='downloadable'): - """Simulate that the user has a generated certificate. """ - CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, mode=enrollment_mode) - return GeneratedCertificateFactory( - user=self.user, - course_id=course_key or self.course.id, - mode=enrollment_mode, - download_url=self.DOWNLOAD_URL, - status=status, - ) - - @ddt.data(CourseMode.HONOR, CourseMode.PROFESSIONAL, CourseMode.VERIFIED) - def test_certificate_visibility(self, cert_mode): - """ - Verify that certificates are displayed with the correct card mode. - """ - # Add new certificate - cert = self._create_certificate(enrollment_mode=cert_mode) - cert.save() - - response = self.client.get(f'/u/{self.user.username}') - - self.assertContains(response, f'card certificate-card mode-{cert_mode}') - - @ddt.data( - ['downloadable', True], - ['notpassing', False], - ) - @ddt.unpack - def test_certificate_status_visibility(self, status, is_passed_status): - """ - Verify that certificates are only displayed for passing status. - """ - # Add new certificate - cert = self._create_certificate(status=status) - cert.save() - - # Ensure that this test is actually using both passing and non-passing certs. - assert CertificateStatuses.is_passing_status(cert.status) == is_passed_status - - response = self.client.get(f'/u/{self.user.username}') - - if is_passed_status: - self.assertContains(response, f'card certificate-card mode-{cert.mode}') - else: - self.assertNotContains(response, f'card certificate-card mode-{cert.mode}') - - def test_certificate_for_missing_course(self): - """ - Verify that a certificate is not shown for a missing course. - """ - # Add new certificate - cert = self._create_certificate(course_key=CourseLocator.from_string('course-v1:edX+INVALID+1')) - cert.save() - - response = self.client.get(f'/u/{self.user.username}') - - self.assertNotContains(response, f'card certificate-card mode-{cert.mode}') - - @ddt.data(True, False) - def test_no_certificate_visibility(self, own_profile): - """ - Verify that the 'You haven't earned any certificates yet.' well appears on the user's - own profile when they do not have certificates and does not appear when viewing - another user that does not have any certificates. - """ - profile_username = self.user.username if own_profile else self.other_user.username - response = self.client.get(f'/u/{profile_username}') - - if own_profile: - self.assertContains(response, 'You haven't earned any certificates yet.') - else: - self.assertNotContains(response, 'You haven't earned any certificates yet.') - - @ddt.data(True, False) - def test_explore_courses_visibility(self, courses_browsable): - with mock.patch.dict('django.conf.settings.FEATURES', {'COURSES_ARE_BROWSABLE': courses_browsable}): - response = self.client.get(f'/u/{self.user.username}') - if courses_browsable: - self.assertContains(response, 'Explore New Courses') - else: - self.assertNotContains(response, 'Explore New Courses') - - def test_certificate_for_visibility_for_not_viewable_course(self): - """ - Verify that a certificate is not shown if certificate are not viewable to users. - """ - # add new course with certificate_available_date is future date. - course = CourseFactory.create( - certificate_available_date=datetime.datetime.now() + datetime.timedelta(days=5), - certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE - ) - - cert = self._create_certificate(course_key=course.id) - cert.save() - - response = self.client.get(f'/u/{self.user.username}') - - self.assertNotContains(response, f'card certificate-card mode-{cert.mode}') - - def test_certificates_visible_only_for_staff_and_profile_user(self): - """ - Verify that certificates data are passed to template only in case of staff user - and profile user. - """ - request = RequestFactory().get('/url') - request.user = self.user - profile_username = self.other_user.username - user_is_staff = True - context = learner_profile_context(request, profile_username, user_is_staff) - - assert 'achievements_fragment' in context - - user_is_staff = False - context = learner_profile_context(request, profile_username, user_is_staff) - assert 'achievements_fragment' not in context - - profile_username = self.user.username - context = learner_profile_context(request, profile_username, user_is_staff) - assert 'achievements_fragment' in context - - @mock.patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) - def test_certificate_visibility_with_no_cert_config(self): - """ - Verify that certificates are not displayed until there is an active - certificate configuration. - """ - # Add new certificate - cert = self._create_certificate(enrollment_mode=CourseMode.VERIFIED) - cert.download_url = '' - cert.save() - - response = self.client.get(f'/u/{self.user.username}') - self.assertNotContains( - response, f'card certificate-card mode-{CourseMode.VERIFIED}' - ) - - course_overview = CourseOverview.get_from_id(self.course.id) - course_overview.has_any_active_web_certificate = True - course_overview.save() - - response = self.client.get(f'/u/{self.user.username}') - self.assertContains( - response, f'card certificate-card mode-{CourseMode.VERIFIED}' - ) diff --git a/openedx/features/learner_profile/toggles.py b/openedx/features/learner_profile/toggles.py deleted file mode 100644 index 08378b6e90..0000000000 --- a/openedx/features/learner_profile/toggles.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Toggles for Learner Profile page. -""" - - -from edx_toggles.toggles import WaffleFlag -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers - -# Namespace for learner profile waffle flags. -WAFFLE_FLAG_NAMESPACE = 'learner_profile' - -# Waffle flag to redirect to another learner profile experience. -# .. toggle_name: learner_profile.redirect_to_microfrontend -# .. toggle_implementation: WaffleFlag -# .. toggle_default: False -# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the profile page. -# .. toggle_use_cases: temporary, open_edx -# .. toggle_creation_date: 2019-02-19 -# .. toggle_target_removal_date: 2020-12-31 -# .. toggle_warning: Also set settings.PROFILE_MICROFRONTEND_URL and site's ENABLE_PROFILE_MICROFRONTEND. -# .. toggle_tickets: DEPR-17 -REDIRECT_TO_PROFILE_MICROFRONTEND = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.redirect_to_microfrontend', __name__) - - -def should_redirect_to_profile_microfrontend(): - return ( - configuration_helpers.get_value('ENABLE_PROFILE_MICROFRONTEND') and - REDIRECT_TO_PROFILE_MICROFRONTEND.is_enabled() - ) diff --git a/openedx/features/learner_profile/urls.py b/openedx/features/learner_profile/urls.py deleted file mode 100644 index 0f02076568..0000000000 --- a/openedx/features/learner_profile/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Defines URLs for the learner profile. -""" - - -from django.conf import settings -from django.urls import path, re_path - -from openedx.features.learner_profile.views.learner_profile import learner_profile - -from .views.learner_achievements import LearnerAchievementsFragmentView - -urlpatterns = [ - re_path( - r'^{username_pattern}$'.format( - username_pattern=settings.USERNAME_PATTERN, - ), - learner_profile, - name='learner_profile', - ), - path('achievements', LearnerAchievementsFragmentView.as_view(), - name='openedx.learner_profile.learner_achievements_fragment_view', - ), -] diff --git a/openedx/features/learner_profile/views/__init__.py b/openedx/features/learner_profile/views/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/features/learner_profile/views/learner_achievements.py b/openedx/features/learner_profile/views/learner_achievements.py deleted file mode 100644 index 6a7a07e339..0000000000 --- a/openedx/features/learner_profile/views/learner_achievements.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Views to render a learner's achievements. -""" - - -from django.template.loader import render_to_string -from web_fragments.fragment import Fragment - -from lms.djangoapps.certificates import api as certificate_api -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.plugin_api.views import EdxFragmentView - - -class LearnerAchievementsFragmentView(EdxFragmentView): - """ - A fragment to render a learner's achievements. - """ - - def render_to_fragment(self, request, username=None, own_profile=False, **kwargs): # lint-amnesty, pylint: disable=arguments-differ - """ - Renders the current learner's achievements. - """ - course_certificates = self._get_ordered_certificates_for_user(request, username) - context = { - 'course_certificates': course_certificates, - 'own_profile': own_profile, - 'disable_courseware_js': True, - } - if course_certificates or own_profile: - html = render_to_string('learner_profile/learner-achievements-fragment.html', context) - return Fragment(html) - else: - return None - - def _get_ordered_certificates_for_user(self, request, username): - """ - Returns a user's certificates sorted by course name. - """ - course_certificates = certificate_api.get_certificates_for_user(username) - passing_certificates = [] - for course_certificate in course_certificates: - if course_certificate.get('is_passing', False): - course_key = course_certificate['course_key'] - try: - course_overview = CourseOverview.get_from_id(course_key) - course_certificate['course'] = course_overview - if certificate_api.certificates_viewable_for_course(course_overview): - # add certificate into passing certificate list only if it's a PDF certificate - # or there is an active certificate configuration. - if course_certificate['is_pdf_certificate'] or course_overview.has_any_active_web_certificate: - passing_certificates.append(course_certificate) - except CourseOverview.DoesNotExist: - # This is unlikely to fail as the course should exist. - # Ideally the cert should have all the information that - # it needs. This might be solved by the Credentials API. - pass - passing_certificates.sort(key=lambda certificate: certificate['course'].display_name_with_default) - return passing_certificates diff --git a/openedx/features/learner_profile/views/learner_profile.py b/openedx/features/learner_profile/views/learner_profile.py deleted file mode 100644 index 6a3a251fde..0000000000 --- a/openedx/features/learner_profile/views/learner_profile.py +++ /dev/null @@ -1,128 +0,0 @@ -""" Views for a student's profile information. """ - - -from django.conf import settings -from django.contrib.auth.decorators import login_required -from django.contrib.staticfiles.storage import staticfiles_storage -from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404 -from django.shortcuts import redirect, render -from django.urls import reverse -from django.views.decorators.http import require_http_methods -from django_countries import countries - -from common.djangoapps.edxmako.shortcuts import marketing_link -from openedx.core.djangoapps.credentials.utils import get_credentials_records_url -from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.core.djangoapps.user_api.accounts.api import get_account_settings -from openedx.core.djangoapps.user_api.errors import UserNotAuthorized, UserNotFound -from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences -from openedx.features.learner_profile.toggles import should_redirect_to_profile_microfrontend -from openedx.features.learner_profile.views.learner_achievements import LearnerAchievementsFragmentView -from common.djangoapps.student.models import User - - -@login_required -@require_http_methods(['GET']) -def learner_profile(request, username): - """Render the profile page for the specified username. - - Args: - request (HttpRequest) - username (str): username of user whose profile is requested. - - Returns: - HttpResponse: 200 if the page was sent successfully - HttpResponse: 302 if not logged in (redirect to login page) - HttpResponse: 405 if using an unsupported HTTP method - Raises: - Http404: 404 if the specified user is not authorized or does not exist - - Example usage: - GET /account/profile - """ - if should_redirect_to_profile_microfrontend(): - profile_microfrontend_url = f"{settings.PROFILE_MICROFRONTEND_URL}{username}" - if request.GET: - profile_microfrontend_url += f'?{request.GET.urlencode()}' - return redirect(profile_microfrontend_url) - - try: - context = learner_profile_context(request, username, request.user.is_staff) - return render( - request=request, - template_name='learner_profile/learner_profile.html', - context=context - ) - except (UserNotAuthorized, UserNotFound, ObjectDoesNotExist): - raise Http404 # lint-amnesty, pylint: disable=raise-missing-from - - -def learner_profile_context(request, profile_username, user_is_staff): - """Context for the learner profile page. - - Args: - logged_in_user (object): Logged In user. - profile_username (str): username of user whose profile is requested. - user_is_staff (bool): Logged In user has staff access. - build_absolute_uri_func (): - - Returns: - dict - - Raises: - ObjectDoesNotExist: the specified profile_username does not exist. - """ - profile_user = User.objects.get(username=profile_username) - logged_in_user = request.user - - own_profile = (logged_in_user.username == profile_username) - - account_settings_data = get_account_settings(request, [profile_username])[0] - - preferences_data = get_user_preferences(profile_user, profile_username) - - context = { - 'own_profile': own_profile, - 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), - 'data': { - 'profile_user_id': profile_user.id, - 'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'], - 'default_visibility': settings.ACCOUNT_VISIBILITY_CONFIGURATION['default_visibility'], - 'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}), - 'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}), - 'preferences_data': preferences_data, - 'account_settings_data': account_settings_data, - 'profile_image_upload_url': reverse('profile_image_upload', kwargs={'username': profile_username}), - 'profile_image_remove_url': reverse('profile_image_remove', kwargs={'username': profile_username}), - 'profile_image_max_bytes': settings.PROFILE_IMAGE_MAX_BYTES, - 'profile_image_min_bytes': settings.PROFILE_IMAGE_MIN_BYTES, - 'account_settings_page_url': reverse('account_settings'), - 'has_preferences_access': (logged_in_user.username == profile_username or user_is_staff), - 'own_profile': own_profile, - 'country_options': list(countries), - 'find_courses_url': marketing_link('COURSES'), - 'language_options': settings.ALL_LANGUAGES, - 'backpack_ui_img': staticfiles_storage.url('certificates/images/backpack-ui.png'), - 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), - 'social_platforms': settings.SOCIAL_PLATFORMS, - 'enable_coppa_compliance': settings.ENABLE_COPPA_COMPLIANCE, - 'parental_consent_age_limit': settings.PARENTAL_CONSENT_AGE_LIMIT - }, - 'show_program_listing': ProgramsApiConfig.is_enabled(), - 'show_dashboard_tabs': True, - 'disable_courseware_js': True, - 'nav_hidden': True, - 'records_url': get_credentials_records_url(), - } - - if own_profile or user_is_staff: - achievements_fragment = LearnerAchievementsFragmentView().render_to_fragment( - request, - username=profile_user.username, - own_profile=own_profile, - ) - context['achievements_fragment'] = achievements_fragment - - return context diff --git a/webpack-config/file-lists.js b/webpack-config/file-lists.js index 7167a6f5dd..d9e818f912 100644 --- a/webpack-config/file-lists.js +++ b/webpack-config/file-lists.js @@ -79,9 +79,6 @@ module.exports = { path.resolve(__dirname, '../lms/static/js/learner_dashboard/views/program_header_view.js'), path.resolve(__dirname, '../lms/static/js/learner_dashboard/views/sidebar_view.js'), path.resolve(__dirname, '../lms/static/js/learner_dashboard/views/upgrade_message_view.js'), - path.resolve(__dirname, '../lms/static/js/student_account/views/account_section_view.js'), - path.resolve(__dirname, '../lms/static/js/student_account/views/account_settings_fields.js'), - path.resolve(__dirname, '../lms/static/js/student_account/views/account_settings_view.js'), path.resolve(__dirname, '../lms/static/js/student_account/views/FormView.js'), path.resolve(__dirname, '../lms/static/js/student_account/views/LoginView.js'), path.resolve(__dirname, '../lms/static/js/student_account/views/RegisterView.js'),