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>
This commit is contained in:
190
cms/envs/test.py
190
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"}}
|
||||
}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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("<a href='{account_setting_page}'>").format(
|
||||
account_setting_page=reverse('account_settings'),
|
||||
account_setting_page=settings.ACCOUNT_MICROFRONTEND_URL,
|
||||
),
|
||||
link_end=HTML("</a>")
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
}])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/<provider>.
|
||||
# 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/<provider>
|
||||
# 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/<provider>.
|
||||
# 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/<provider>.
|
||||
# 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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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 '<samlp:AuthnRequest' in xml
|
||||
assert "<samlp:AuthnRequest" in xml
|
||||
|
||||
(msg, action_type, idp_name, response_data, next_url, xml), _kwargs = mock_log.call_args_list[1]
|
||||
assert msg.startswith('SAML login %s')
|
||||
assert action_type == 'response'
|
||||
assert msg.startswith("SAML login %s")
|
||||
assert action_type == "response"
|
||||
assert idp_name == self.PROVIDER_IDP_SLUG
|
||||
self.assertDictContainsSubset({"RelayState": idp_name}, response_data)
|
||||
assert 'SAMLResponse' in response_data
|
||||
assert "SAMLResponse" in response_data
|
||||
assert next_url == expected_next_url
|
||||
assert '<saml2p:Response' in xml
|
||||
assert "<saml2p:Response" in xml
|
||||
else:
|
||||
assert not mock_log.called
|
||||
|
||||
def test_configure_testshib_provider_with_cache_duration(self):
|
||||
""" Enable and configure the TestShib SAML IdP as a third_party_auth provider """
|
||||
"""Enable and configure the TestShib SAML IdP as a third_party_auth provider"""
|
||||
kwargs = {}
|
||||
kwargs.setdefault('name', self.PROVIDER_NAME)
|
||||
kwargs.setdefault('enabled', True)
|
||||
kwargs.setdefault('visible', True)
|
||||
kwargs.setdefault('slug', self.PROVIDER_IDP_SLUG)
|
||||
kwargs.setdefault('entity_id', TESTSHIB_ENTITY_ID)
|
||||
kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL_WITH_CACHE_DURATION)
|
||||
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("name", self.PROVIDER_NAME)
|
||||
kwargs.setdefault("enabled", True)
|
||||
kwargs.setdefault("visible", True)
|
||||
kwargs.setdefault("slug", self.PROVIDER_IDP_SLUG)
|
||||
kwargs.setdefault("entity_id", TESTSHIB_ENTITY_ID)
|
||||
kwargs.setdefault("metadata_source", TESTSHIB_METADATA_URL_WITH_CACHE_DURATION)
|
||||
kwargs.setdefault("icon_class", "fa-university")
|
||||
kwargs.setdefault("attr_email", "urn:oid:1.3.6.1.4.1.5923.1.1.1.6") # eduPersonPrincipalName
|
||||
self.configure_saml_provider(**kwargs)
|
||||
assert httpretty.is_enabled()
|
||||
num_total, num_skipped, num_attempted, num_updated, num_failed, failure_messages = fetch_saml_metadata()
|
||||
@@ -476,70 +481,72 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
super().setUp()
|
||||
|
||||
# Mock the call to the SAP SuccessFactors assertion endpoint
|
||||
SAPSF_ASSERTION_URL = 'http://successfactors.com/oauth/idp'
|
||||
SAPSF_ASSERTION_URL = "http://successfactors.com/oauth/idp"
|
||||
|
||||
def assertion_callback(_request, _uri, headers):
|
||||
"""
|
||||
Return a fake assertion after checking that the input is what we expect.
|
||||
"""
|
||||
assert b'private_key=fake_private_key_here' in _request.body
|
||||
assert b'user_id=myself' in _request.body
|
||||
assert b'token_url=http%3A%2F%2Fsuccessfactors.com%2Foauth%2Ftoken' in _request.body
|
||||
assert b'client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB' in _request.body
|
||||
return (200, headers, 'fake_saml_assertion')
|
||||
assert b"private_key=fake_private_key_here" in _request.body
|
||||
assert b"user_id=myself" in _request.body
|
||||
assert b"token_url=http%3A%2F%2Fsuccessfactors.com%2Foauth%2Ftoken" in _request.body
|
||||
assert b"client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB" in _request.body
|
||||
return (200, headers, "fake_saml_assertion")
|
||||
|
||||
httpretty.register_uri(httpretty.POST, SAPSF_ASSERTION_URL, content_type='text/plain', body=assertion_callback)
|
||||
httpretty.register_uri(httpretty.POST, SAPSF_ASSERTION_URL, content_type="text/plain", body=assertion_callback)
|
||||
|
||||
SAPSF_BAD_ASSERTION_URL = 'http://successfactors.com/oauth-fake/idp'
|
||||
SAPSF_BAD_ASSERTION_URL = "http://successfactors.com/oauth-fake/idp"
|
||||
|
||||
def bad_callback(_request, _uri, headers):
|
||||
"""
|
||||
Return a 404 error when someone tries to call the URL.
|
||||
"""
|
||||
return (404, headers, 'NOT AN ASSERTION')
|
||||
return (404, headers, "NOT AN ASSERTION")
|
||||
|
||||
httpretty.register_uri(httpretty.POST, SAPSF_BAD_ASSERTION_URL, content_type='text/plain', body=bad_callback)
|
||||
httpretty.register_uri(httpretty.POST, SAPSF_BAD_ASSERTION_URL, content_type="text/plain", body=bad_callback)
|
||||
|
||||
# Mock the call to the SAP SuccessFactors token endpoint
|
||||
SAPSF_TOKEN_URL = 'http://successfactors.com/oauth/token'
|
||||
SAPSF_TOKEN_URL = "http://successfactors.com/oauth/token"
|
||||
|
||||
def token_callback(_request, _uri, headers):
|
||||
"""
|
||||
Return a fake assertion after checking that the input is what we expect.
|
||||
"""
|
||||
assert b'assertion=fake_saml_assertion' in _request.body
|
||||
assert b'company_id=NCC1701D' in _request.body
|
||||
assert b'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer' in _request.body
|
||||
assert b'client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB' in _request.body
|
||||
assert b"assertion=fake_saml_assertion" in _request.body
|
||||
assert b"company_id=NCC1701D" in _request.body
|
||||
assert b"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer" in _request.body
|
||||
assert b"client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB" in _request.body
|
||||
return (200, headers, '{"access_token": "faketoken"}')
|
||||
|
||||
httpretty.register_uri(httpretty.POST, SAPSF_TOKEN_URL, content_type='application/json', body=token_callback)
|
||||
httpretty.register_uri(httpretty.POST, SAPSF_TOKEN_URL, content_type="application/json", body=token_callback)
|
||||
|
||||
# Mock the call to the SAP SuccessFactors OData user endpoint
|
||||
ODATA_USER_URL = (
|
||||
'http://api.successfactors.com/odata/v2/User(userId=\'myself\')'
|
||||
'?$select=firstName,lastName,defaultFullName,email'
|
||||
"http://api.successfactors.com/odata/v2/User(userId='myself')"
|
||||
"?$select=firstName,lastName,defaultFullName,email"
|
||||
)
|
||||
|
||||
def user_callback(request, _uri, headers):
|
||||
auth_header = request.headers.get('Authorization')
|
||||
assert auth_header == 'Bearer faketoken'
|
||||
auth_header = request.headers.get("Authorization")
|
||||
assert auth_header == "Bearer faketoken"
|
||||
return (
|
||||
200,
|
||||
headers,
|
||||
json.dumps({
|
||||
'd': {
|
||||
'username': 'jsmith',
|
||||
'firstName': 'John',
|
||||
'lastName': 'Smith',
|
||||
'defaultFullName': 'John Smith',
|
||||
'email': 'john@smith.com',
|
||||
'country': 'United States',
|
||||
json.dumps(
|
||||
{
|
||||
"d": {
|
||||
"username": "jsmith",
|
||||
"firstName": "John",
|
||||
"lastName": "Smith",
|
||||
"defaultFullName": "John Smith",
|
||||
"email": "john@smith.com",
|
||||
"country": "United States",
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
httpretty.register_uri(httpretty.GET, ODATA_USER_URL, content_type='application/json', body=user_callback)
|
||||
httpretty.register_uri(httpretty.GET, ODATA_USER_URL, content_type="application/json", body=user_callback)
|
||||
|
||||
def _mock_odata_api_for_error(self, odata_api_root_url, username):
|
||||
"""
|
||||
@@ -550,17 +557,17 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
"""
|
||||
Return a 500 error when someone tries to call the URL.
|
||||
"""
|
||||
headers['CorrelationId'] = 'aefd38b7-c92c-445a-8c7a-487a3f0c7a9d'
|
||||
headers['RequestNo'] = '[787177]' # This is the format SAPSF returns for the transaction request number
|
||||
return 500, headers, 'Failure!'
|
||||
headers["CorrelationId"] = "aefd38b7-c92c-445a-8c7a-487a3f0c7a9d"
|
||||
headers["RequestNo"] = "[787177]" # This is the format SAPSF returns for the transaction request number
|
||||
return 500, headers, "Failure!"
|
||||
|
||||
fields = ','.join(SapSuccessFactorsIdentityProvider.default_field_mapping.copy())
|
||||
url = '{root_url}User(userId=\'{user_id}\')?$select={fields}'.format(
|
||||
fields = ",".join(SapSuccessFactorsIdentityProvider.default_field_mapping.copy())
|
||||
url = "{root_url}User(userId='{user_id}')?$select={fields}".format(
|
||||
root_url=odata_api_root_url,
|
||||
user_id=username,
|
||||
fields=fields,
|
||||
)
|
||||
httpretty.register_uri(httpretty.GET, url, body=callback, content_type='application/json')
|
||||
httpretty.register_uri(httpretty.GET, url, body=callback, content_type="application/json")
|
||||
return url
|
||||
|
||||
def test_register_insufficient_sapsf_metadata(self):
|
||||
@@ -569,7 +576,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
SuccessFactors API, and test that it falls back to the data it receives from the SAML assertion.
|
||||
"""
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings='{"key_i_dont_need":"value_i_also_dont_need"}',
|
||||
)
|
||||
@@ -579,7 +586,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
self.USER_USERNAME = "myself"
|
||||
self._test_register()
|
||||
|
||||
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
|
||||
@patch.dict("django.conf.settings.REGISTRATION_EXTRA_FIELDS", country="optional")
|
||||
def test_register_sapsf_metadata_present(self):
|
||||
"""
|
||||
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
|
||||
@@ -589,19 +596,19 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
what we're looking for, and when an empty override is provided (expected behavior is that
|
||||
existing value maps will be left alone).
|
||||
"""
|
||||
expected_country = 'US'
|
||||
expected_country = "US"
|
||||
provider_settings = {
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
|
||||
'odata_company_id': 'NCC1701D',
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": "http://api.successfactors.com/odata/v2/",
|
||||
"odata_company_id": "NCC1701D",
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps(provider_settings)
|
||||
other_settings=json.dumps(provider_settings),
|
||||
)
|
||||
self._test_register(country=expected_country)
|
||||
|
||||
@@ -616,47 +623,49 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
"""
|
||||
# Mock the call to the SAP SuccessFactors OData user endpoint
|
||||
ODATA_USER_URL = (
|
||||
'http://api.successfactors.com/odata/v2/User(userId=\'myself\')'
|
||||
'?$select=firstName,country,lastName,defaultFullName,email'
|
||||
"http://api.successfactors.com/odata/v2/User(userId='myself')"
|
||||
"?$select=firstName,country,lastName,defaultFullName,email"
|
||||
)
|
||||
|
||||
def user_callback(request, _uri, headers):
|
||||
auth_header = request.headers.get('Authorization')
|
||||
assert auth_header == 'Bearer faketoken'
|
||||
auth_header = request.headers.get("Authorization")
|
||||
assert auth_header == "Bearer faketoken"
|
||||
return (
|
||||
200,
|
||||
headers,
|
||||
json.dumps({
|
||||
'd': {
|
||||
'username': 'jsmith',
|
||||
'firstName': 'John',
|
||||
'lastName': 'Smith',
|
||||
'defaultFullName': 'John Smith',
|
||||
'country': 'United States'
|
||||
json.dumps(
|
||||
{
|
||||
"d": {
|
||||
"username": "jsmith",
|
||||
"firstName": "John",
|
||||
"lastName": "Smith",
|
||||
"defaultFullName": "John Smith",
|
||||
"country": "United States",
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
httpretty.register_uri(httpretty.GET, ODATA_USER_URL, content_type='application/json', body=user_callback)
|
||||
httpretty.register_uri(httpretty.GET, ODATA_USER_URL, content_type="application/json", body=user_callback)
|
||||
|
||||
provider_settings = {
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
|
||||
'odata_company_id': 'NCC1701D',
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": "http://api.successfactors.com/odata/v2/",
|
||||
"odata_company_id": "NCC1701D",
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps(provider_settings),
|
||||
default_email='default@testshib.org'
|
||||
default_email="default@testshib.org",
|
||||
)
|
||||
self.USER_EMAIL = 'default@testshib.org'
|
||||
self.USER_EMAIL = "default@testshib.org"
|
||||
self._test_register()
|
||||
|
||||
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
|
||||
@patch.dict("django.conf.settings.REGISTRATION_EXTRA_FIELDS", country="optional")
|
||||
def test_register_sapsf_metadata_present_override_relevant_value(self):
|
||||
"""
|
||||
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
|
||||
@@ -666,26 +675,26 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
what we're looking for, and when an empty override is provided (expected behavior is that
|
||||
existing value maps will be left alone).
|
||||
"""
|
||||
value_map = {'country': {'United States': 'NZ'}}
|
||||
expected_country = 'NZ'
|
||||
value_map = {"country": {"United States": "NZ"}}
|
||||
expected_country = "NZ"
|
||||
provider_settings = {
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
|
||||
'odata_company_id': 'NCC1701D',
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": "http://api.successfactors.com/odata/v2/",
|
||||
"odata_company_id": "NCC1701D",
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
if value_map:
|
||||
provider_settings['sapsf_value_mappings'] = value_map
|
||||
provider_settings["sapsf_value_mappings"] = value_map
|
||||
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps(provider_settings)
|
||||
other_settings=json.dumps(provider_settings),
|
||||
)
|
||||
self._test_register(country=expected_country)
|
||||
|
||||
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
|
||||
@patch.dict("django.conf.settings.REGISTRATION_EXTRA_FIELDS", country="optional")
|
||||
def test_register_sapsf_metadata_present_override_other_value(self):
|
||||
"""
|
||||
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
|
||||
@@ -695,26 +704,26 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
what we're looking for, and when an empty override is provided (expected behavior is that
|
||||
existing value maps will be left alone).
|
||||
"""
|
||||
value_map = {'country': {'Australia': 'blahfake'}}
|
||||
expected_country = 'US'
|
||||
value_map = {"country": {"Australia": "blahfake"}}
|
||||
expected_country = "US"
|
||||
provider_settings = {
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
|
||||
'odata_company_id': 'NCC1701D',
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": "http://api.successfactors.com/odata/v2/",
|
||||
"odata_company_id": "NCC1701D",
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
if value_map:
|
||||
provider_settings['sapsf_value_mappings'] = value_map
|
||||
provider_settings["sapsf_value_mappings"] = value_map
|
||||
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps(provider_settings)
|
||||
other_settings=json.dumps(provider_settings),
|
||||
)
|
||||
self._test_register(country=expected_country)
|
||||
|
||||
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
|
||||
@patch.dict("django.conf.settings.REGISTRATION_EXTRA_FIELDS", country="optional")
|
||||
def test_register_sapsf_metadata_present_empty_value_override(self):
|
||||
"""
|
||||
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
|
||||
@@ -725,22 +734,22 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
existing value maps will be left alone).
|
||||
"""
|
||||
|
||||
value_map = {'country': {}}
|
||||
expected_country = 'US'
|
||||
value_map = {"country": {}}
|
||||
expected_country = "US"
|
||||
provider_settings = {
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
|
||||
'odata_company_id': 'NCC1701D',
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": "http://api.successfactors.com/odata/v2/",
|
||||
"odata_company_id": "NCC1701D",
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
if value_map:
|
||||
provider_settings['sapsf_value_mappings'] = value_map
|
||||
provider_settings["sapsf_value_mappings"] = value_map
|
||||
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps(provider_settings)
|
||||
other_settings=json.dumps(provider_settings),
|
||||
)
|
||||
self._test_register(country=expected_country)
|
||||
|
||||
@@ -750,15 +759,17 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
metadata from the SAML assertion.
|
||||
"""
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps({
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth-fake/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
|
||||
'odata_company_id': 'NCC1701D',
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
})
|
||||
other_settings=json.dumps(
|
||||
{
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth-fake/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": "http://api.successfactors.com/odata/v2/",
|
||||
"odata_company_id": "NCC1701D",
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
),
|
||||
)
|
||||
# Because we're getting details from the assertion, fall back to the initial set of details.
|
||||
self.USER_EMAIL = "myself@testshib.org"
|
||||
@@ -776,39 +787,41 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
|
||||
self.USER_NAME = "Me Myself And I"
|
||||
self.USER_USERNAME = "myself"
|
||||
|
||||
odata_company_id = 'NCC1701D'
|
||||
odata_api_root_url = 'http://api.successfactors.com/odata/v2/'
|
||||
odata_company_id = "NCC1701D"
|
||||
odata_api_root_url = "http://api.successfactors.com/odata/v2/"
|
||||
mocked_odata_api_url = self._mock_odata_api_for_error(odata_api_root_url, self.USER_USERNAME)
|
||||
self._configure_testshib_provider(
|
||||
identity_provider_type='sap_success_factors',
|
||||
identity_provider_type="sap_success_factors",
|
||||
metadata_source=TESTSHIB_METADATA_URL,
|
||||
other_settings=json.dumps({
|
||||
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
|
||||
'sapsf_private_key': 'fake_private_key_here',
|
||||
'odata_api_root_url': odata_api_root_url,
|
||||
'odata_company_id': odata_company_id,
|
||||
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
|
||||
})
|
||||
other_settings=json.dumps(
|
||||
{
|
||||
"sapsf_oauth_root_url": "http://successfactors.com/oauth/",
|
||||
"sapsf_private_key": "fake_private_key_here",
|
||||
"odata_api_root_url": odata_api_root_url,
|
||||
"odata_company_id": odata_company_id,
|
||||
"odata_client_id": "TatVotSEiCMteSNWtSOnLanCtBGwNhGB",
|
||||
}
|
||||
),
|
||||
)
|
||||
with LogCapture(level=logging.WARNING) as log_capture:
|
||||
self._test_register()
|
||||
logging_messages = str([log_msg.getMessage() for log_msg in log_capture.records]).replace('\\', '')
|
||||
logging_messages = str([log_msg.getMessage() for log_msg in log_capture.records]).replace("\\", "")
|
||||
assert odata_company_id in logging_messages
|
||||
assert mocked_odata_api_url in logging_messages
|
||||
assert self.USER_USERNAME in logging_messages
|
||||
assert 'SAPSuccessFactors' in logging_messages
|
||||
assert 'Error message' in logging_messages
|
||||
assert 'System message' in logging_messages
|
||||
assert 'Headers' in logging_messages
|
||||
assert "SAPSuccessFactors" in logging_messages
|
||||
assert "Error message" in logging_messages
|
||||
assert "System message" in logging_messages
|
||||
assert "Headers" in logging_messages
|
||||
|
||||
@skip('Test not necessary for this subclass')
|
||||
@skip("Test not necessary for this subclass")
|
||||
def test_get_saml_idp_class_with_fake_identifier(self):
|
||||
pass
|
||||
|
||||
@skip('Test not necessary for this subclass')
|
||||
@skip("Test not necessary for this subclass")
|
||||
def test_login(self):
|
||||
pass
|
||||
|
||||
@skip('Test not necessary for this subclass')
|
||||
@skip("Test not necessary for this subclass")
|
||||
def test_register(self):
|
||||
pass
|
||||
|
||||
@@ -53,3 +53,42 @@ ignore_dirs:
|
||||
third_party:
|
||||
- wiki
|
||||
- edx_proctoring_proctortrack
|
||||
|
||||
|
||||
# How should .po files be segmented? See i18n/segment.py for details. Strings
|
||||
# that are only found in a particular segment are segregated into that .po file
|
||||
# so that translators can focus on separate parts of the product.
|
||||
#
|
||||
# We segregate Studio so we can provide new languages for LMS without having to
|
||||
# also translate the Studio strings. LMS needs the strings from lms/* and
|
||||
# common/*, so those will stay in the main .po file.
|
||||
segment:
|
||||
django-partial.po: # This .po file..
|
||||
django-studio.po: # produces this .po file..
|
||||
- cms/* # by segregating strings from these files.
|
||||
# Anything that doesn't match a pattern stays in the original file.
|
||||
djangojs-partial.po:
|
||||
djangojs-studio.po:
|
||||
- cms/*
|
||||
mako.po:
|
||||
mako-studio.po:
|
||||
- cms/*
|
||||
underscore.po:
|
||||
underscore-studio.po:
|
||||
- cms/*
|
||||
|
||||
# How should the generate step merge files?
|
||||
generate_merge:
|
||||
django.po:
|
||||
- django-partial.po
|
||||
- django-studio.po
|
||||
- mako.po
|
||||
- mako-studio.po
|
||||
- wiki.po
|
||||
- edx_proctoring_proctortrack.po
|
||||
djangojs.po:
|
||||
- djangojs-partial.po
|
||||
- djangojs-studio.po
|
||||
- djangojs-account-settings-view.po
|
||||
- underscore.po
|
||||
- underscore-studio.po
|
||||
|
||||
@@ -111,7 +111,7 @@ def _get_course_email_context(course):
|
||||
'course_url': course_url,
|
||||
'course_image_url': image_url,
|
||||
'course_end_date': course_end_date,
|
||||
'account_settings_url': '{}{}'.format(lms_root_url, reverse('account_settings')),
|
||||
'account_settings_url': settings.ACCOUNT_MICROFRONTEND_URL,
|
||||
'email_settings_url': '{}{}'.format(lms_root_url, reverse('dashboard')),
|
||||
'logo_url': get_logo_url_for_email(),
|
||||
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
|
||||
@@ -743,7 +743,7 @@ class TestCourseEmailContext(SharedModuleStoreTestCase):
|
||||
assert email_context['course_image_url'] == \
|
||||
f'{scheme}://edx.org/asset-v1:{course_id_fragment}+type@asset+block@images_course_image.jpg'
|
||||
assert email_context['email_settings_url'] == f'{scheme}://edx.org/dashboard'
|
||||
assert email_context['account_settings_url'] == f'{scheme}://edx.org/account/settings'
|
||||
assert email_context['account_settings_url'] == settings.ACCOUNT_MICROFRONTEND_URL
|
||||
|
||||
@override_settings(LMS_ROOT_URL="http://edx.org")
|
||||
def test_insecure_email_context(self):
|
||||
|
||||
@@ -2201,7 +2201,7 @@ def financial_assistance_form(request, course_id=None):
|
||||
'header_text': _get_fa_header(FINANCIAL_ASSISTANCE_HEADER),
|
||||
'course_id': course_id,
|
||||
'dashboard_url': reverse('dashboard'),
|
||||
'account_settings_url': reverse('account_settings'),
|
||||
'account_settings_url': settings.ACCOUNT_MICROFRONTEND_URL,
|
||||
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
'user_details': {
|
||||
'email': user.email,
|
||||
|
||||
@@ -4,6 +4,7 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -629,7 +630,7 @@ def create_user_profile_context(request, course_key, user_id):
|
||||
'page': query_params['page'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
'sort_preference': user.default_sort_key,
|
||||
'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}),
|
||||
'learner_profile_page_url': urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{django_user.username}'),
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
@@ -3307,7 +3307,6 @@ INSTALLED_APPS = [
|
||||
'openedx.features.course_bookmarks',
|
||||
'openedx.features.course_experience',
|
||||
'openedx.features.enterprise_support.apps.EnterpriseSupportConfig',
|
||||
'openedx.features.learner_profile',
|
||||
'openedx.features.course_duration_limits',
|
||||
'openedx.features.content_type_gating',
|
||||
'openedx.features.discounts',
|
||||
|
||||
@@ -385,6 +385,7 @@ EDXNOTES_CLIENT_NAME = 'edx_notes_api-backend-service'
|
||||
############## Settings for Microfrontends #########################
|
||||
LEARNING_MICROFRONTEND_URL = 'http://localhost:2000'
|
||||
ACCOUNT_MICROFRONTEND_URL = 'http://localhost:1997'
|
||||
PROFILE_MICROFRONTEND_URL = 'http://localhost:1995'
|
||||
COMMUNICATIONS_MICROFRONTEND_URL = 'http://localhost:1984'
|
||||
AUTHN_MICROFRONTEND_URL = 'http://localhost:1999'
|
||||
AUTHN_MICROFRONTEND_DOMAIN = 'localhost:1999'
|
||||
|
||||
@@ -564,7 +564,7 @@ PDF_RECEIPT_BILLING_ADDRESS = 'add your own billing address here with appropriat
|
||||
PDF_RECEIPT_TERMS_AND_CONDITIONS = 'add your own terms and conditions'
|
||||
PDF_RECEIPT_TAX_ID_LABEL = 'Tax ID'
|
||||
|
||||
PROFILE_MICROFRONTEND_URL = "http://profile-mfe/abc/"
|
||||
PROFILE_MICROFRONTEND_URL = "http://profile-mfe"
|
||||
ORDER_HISTORY_MICROFRONTEND_URL = "http://order-history-mfe/"
|
||||
ACCOUNT_MICROFRONTEND_URL = "http://account-mfe"
|
||||
AUTHN_MICROFRONTEND_URL = "http://authn-mfe"
|
||||
|
||||
@@ -1,334 +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',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/spec/student_account/account_settings_fields_helpers',
|
||||
'js/student_account/views/account_settings_factory',
|
||||
'js/student_account/views/account_settings_view'
|
||||
],
|
||||
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers, Helpers,
|
||||
AccountSettingsFieldViewSpecHelpers, AccountSettingsPage) {
|
||||
'use strict';
|
||||
|
||||
describe('edx.user.AccountSettingsFactory', function() {
|
||||
var createAccountSettingsPage = 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
|
||||
);
|
||||
return context.accountSettingsView;
|
||||
};
|
||||
|
||||
var requests;
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures('<div class="wrapper-account-settings"></div>');
|
||||
});
|
||||
|
||||
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('<div class="wrapper-account-settings"></div>');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 <a href="https://support.edx.org/">edX Support</a> 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('<div class="wrapper-account-settings"></div>');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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(
|
||||
'<a href="{edx_support_url}">', {
|
||||
edx_support_url: edxSupportUrl
|
||||
}
|
||||
)
|
||||
),
|
||||
link_end: HtmlUtils.HTML('</a>')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -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(
|
||||
'<a href="{passwordResetSupportUrl}">', {
|
||||
passwordResetSupportUrl: this.options.passwordResetSupportUrl
|
||||
}
|
||||
)
|
||||
),
|
||||
anchorEnd: HtmlUtils.HTML('</a>')
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}),
|
||||
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);
|
||||
@@ -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);
|
||||
@@ -1 +0,0 @@
|
||||
../../openedx/features/learner_profile/static/learner_profile
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%! from django.utils.translation import gettext as _ %>
|
||||
<div class="wrapper-msg urgency-high">
|
||||
<div class="msg">
|
||||
<div class="msg-content">
|
||||
<h2 class="sr">${_("Could Not Link Accounts")}</h2>
|
||||
<div class="copy">
|
||||
## 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.
|
||||
<p>${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name=duplicate_provider, platform_name=platform_name)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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
|
||||
%>
|
||||
|
||||
<div class="nav-item hidden-mobile">
|
||||
@@ -44,8 +44,8 @@ should_show_order_history = should_redirect_to_order_history_microfrontend() and
|
||||
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL}/${enterprise_customer_portal.get('slug')}" role="menuitem">${_("Dashboard")}</a></div>
|
||||
% endif
|
||||
|
||||
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('learner_profile', kwargs={'username': username})}" role="menuitem">${_("Profile")}</a></div>
|
||||
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('account_settings')}" role="menuitem">${_("Account")}</a></div>
|
||||
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{user.username}')}" role="menuitem">${_("Profile")}</a></div>
|
||||
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ACCOUNT_MICROFRONTEND_URL}" role="menuitem">${_("Account")}</a></div>
|
||||
% if should_show_order_history:
|
||||
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ORDER_HISTORY_MICROFRONTEND_URL}" role="menuitem">${_("Order History")}</a></div>
|
||||
% endif
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%!
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from webpack_loader.templatetags.webpack_loader import render_bundle
|
||||
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
|
||||
%>
|
||||
|
||||
<%inherit file="/main.html" />
|
||||
<%def name="online_help_token()"><% return "learneraccountsettings" %></%def>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="pagetitle">${_("Account Settings")}</%block>
|
||||
|
||||
% if duplicate_provider:
|
||||
<section>
|
||||
<%include file='/dashboard/_dashboard_third_party_error.html' />
|
||||
</section>
|
||||
% endif
|
||||
|
||||
<div class="wrapper-account-settings"></div>
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course'/>
|
||||
<link type="text/css" rel="stylesheet" href="${STATIC_URL}paragon/static/paragon.min.css">
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="js/student_account/views/account_settings_factory" class_name="AccountSettingsFactory">
|
||||
var fieldsData = ${ fields | n, dump_js_escaped_json },
|
||||
ordersHistoryData = ${ order_history | n, dump_js_escaped_json },
|
||||
authData = ${ auth | n, dump_js_escaped_json },
|
||||
platformName = '${ static.get_platform_name() | n, js_escaped_string }',
|
||||
contactEmail = '${ static.get_contact_email_address() | n, js_escaped_string }',
|
||||
allowEmailChange = ${ bool(settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']) | n, dump_js_escaped_json },
|
||||
socialPlatforms = ${ settings.SOCIAL_PLATFORMS | n, dump_js_escaped_json },
|
||||
|
||||
syncLearnerProfileData = ${ bool(sync_learner_profile_data) | n, dump_js_escaped_json },
|
||||
enterpriseName = '${ enterprise_name | n, js_escaped_string }',
|
||||
enterpriseReadonlyAccountFields = ${ enterprise_readonly_account_fields | n, dump_js_escaped_json },
|
||||
edxSupportUrl = '${ edx_support_url | n, js_escaped_string }',
|
||||
extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json },
|
||||
displayAccountDeletion = ${ enable_account_deletion | n, dump_js_escaped_json};
|
||||
isSecondaryEmailFeatureEnabled = ${ bool(is_secondary_email_feature_enabled()) | n, dump_js_escaped_json },
|
||||
enableCoppaCompliance = ${ bool(enable_coppa_compliance) | n, dump_js_escaped_json },
|
||||
|
||||
AccountSettingsFactory(
|
||||
fieldsData,
|
||||
${ disable_order_history_tab | n, dump_js_escaped_json },
|
||||
ordersHistoryData,
|
||||
authData,
|
||||
'${ password_reset_support_link | n, js_escaped_string }',
|
||||
'${ user_accounts_api_url | n, js_escaped_string }',
|
||||
'${ user_preferences_api_url | n, js_escaped_string }',
|
||||
${ user.id | n, dump_js_escaped_json },
|
||||
platformName,
|
||||
contactEmail,
|
||||
allowEmailChange,
|
||||
enableCoppaCompliance,
|
||||
socialPlatforms,
|
||||
|
||||
syncLearnerProfileData,
|
||||
enterpriseName,
|
||||
enterpriseReadonlyAccountFields,
|
||||
edxSupportUrl,
|
||||
extendedProfileFields,
|
||||
displayAccountDeletion,
|
||||
isSecondaryEmailFeatureEnabled,
|
||||
${ beta_language | n, dump_js_escaped_json },
|
||||
);
|
||||
</%static:require_module>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.auth = ${ auth | n, dump_js_escaped_json };
|
||||
window.isActive = ${ user.is_active | n, dump_js_escaped_json };
|
||||
window.additionalSiteSpecificDeletionText = "${ static.get_value('SITE_SPECIFIC_DELETION_TEXT', _(' and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School')) | n, js_escaped_string }";
|
||||
window.mktgRootLink = "${ static.marketing_link('ROOT') | n, js_escaped_string }";
|
||||
window.platformName = "${ platform_name | n, js_escaped_string }";
|
||||
window.siteName = "${ static.get_value('SITE_NAME', settings.SITE_NAME) | n, js_escaped_string }";
|
||||
window.mktgEmailOptIn = ${ settings.MARKETING_EMAILS_OPT_IN | n, dump_js_escaped_json };;
|
||||
|
||||
</script>
|
||||
<%static:webpack entry="StudentAccountDeletionInitializer">
|
||||
</%static:webpack>
|
||||
</%block>
|
||||
@@ -1,30 +0,0 @@
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="account-settings-container">
|
||||
<% if (message) { %>
|
||||
<div class="account-settings-message">
|
||||
<div id="beta-language-message" class="alert-message warning" aria-live="assertive" role="alert">
|
||||
<span><%= HtmlUtils.ensureHtml(message) %></span>
|
||||
<div class="alert-actions">
|
||||
<button class="btn-alert-primary" data-old-lang-code="<%- oldLangCode %>"><%- gettext('Switch Language Back') %></button>
|
||||
<a href="<%- helpTranslateLink %>" rel="noopener" target="_blank" class="btn-alert-secondary"><%= HtmlUtils.ensureHtml(helpTranslateText) %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="wrapper-header">
|
||||
<h2 class="header-title"><%- gettext("Account Settings") %></h2>
|
||||
<div class="left list-inline account-nav" role="tablist">
|
||||
<% _.each(accountSettingsTabs, function(tab) { %>
|
||||
<button id="<%- tab.id %>" aria-controls="<%- tab.name %>-tabpanel" tabindex="<%- tab.tabindex %>" aria-selected="<%- tab.selected %>" aria-expanded="<%- tab.expanded %>" data-name="<%- tab.name %>" aria-describedby="header-subtitle-<%- tab.name %>" class="tab account-nav-link <%- tab.class %>" role="tab">
|
||||
<%- tab.label %>
|
||||
</button>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-settings-sections">
|
||||
<% _.each(accountSettingsTabs, function(tab) { %>
|
||||
<div id="<%- tab.name %>-tabpanel" class="account-settings-tabpanels <% if (!tab.class) { %> hidden <% } %>" aria-label="<%- tab.label %>" role="tabpanel"></div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,31 +0,0 @@
|
||||
<h2 class="sr" id="header-subtitle-<%- tabName %>">
|
||||
<%- tabLabel %>
|
||||
</h2>
|
||||
<% _.each(sections, function(section) { %>
|
||||
<div class="section">
|
||||
<h3 class="section-header"><%- gettext(section.title) %></h3>
|
||||
<% if (section.subtitle && _.isUndefined(section.message)) { %>
|
||||
<p class="account-settings-header-subtitle"><%- section.subtitle %></p>
|
||||
<% } %>
|
||||
|
||||
<% if (section.message) { %>
|
||||
<div class="account-settings-section-message">
|
||||
<div class="alert-message <%- section.messageType%>" aria-live="polite">
|
||||
<i class="fa fa-info-circle message-icon <%- section.messageType %>" aria-hidden="true"></i>
|
||||
<span><%= HtmlUtils.ensureHtml(section.message) %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (section.domHookId) { %>
|
||||
<div id="<%- section.domHookId %>"></div>
|
||||
<% } %>
|
||||
|
||||
<div class="account-settings-section-body <%- tabName %>-section-body">
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy"><%- gettext("An error occurred. Please reload the page.") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
@@ -28,7 +28,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
|
||||
<a data-hj-suppress class="dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${username}</a>
|
||||
<ul role="menu" class="dropdown-menu dropdown-menu-right" id="${_("Usermenu")}" aria-labelledby="dropdownMenuLink" tabindex="-1">
|
||||
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
|
||||
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('account_settings')}">${_("Account")}</a></li>
|
||||
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${settings.ACCOUNT_MICROFRONTEND_URL}">${_("Account")}</a></li>
|
||||
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('logout')}">${_("Sign Out")}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
|
||||
</div>
|
||||
<ul role="menu" class="nav flex-column align-items-center">
|
||||
<li role="presentation" class="nav-item nav-item-open-collapsed-only collapse"><a role="menuitem" href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
|
||||
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${reverse('account_settings')}">${_("Account")}</a></li>
|
||||
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${settings.ACCOUNT_MICROFRONTEND_URL}">${_("Account")}</a></li>
|
||||
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${reverse('logout')}">${_("Sign Out")}</a></li>
|
||||
</ul>
|
||||
% else:
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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 <em>My site</em> 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.")
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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<pref_key>{UserPreference.KEY_REGEX})/users/$',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -1,40 +0,0 @@
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
<div class="wrapper-profile">
|
||||
<div class="profile profile-other">
|
||||
<div class="wrapper-profile-field-account-privacy"></div>
|
||||
<div class="wrapper-profile-sections account-settings-container">
|
||||
<div class="wrapper-profile-section-container-one">
|
||||
<div class="wrapper-profile-section-one">
|
||||
<div class="profile-image-field">
|
||||
</div>
|
||||
<div class="profile-section-one-fields">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy">An error occurred. Try loading the page again.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper-profile-section-container-two">
|
||||
<div class="wrapper-profile-bio">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-loading-indicator">
|
||||
<p>
|
||||
<span class="spin">
|
||||
<span class="icon fa fa-refresh" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="copy">
|
||||
Loading
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy">
|
||||
An error occurred. Please reload the page.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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('<a href="{settings_url}">'), {settings_url: this.options.accountSettingsPageUrl}
|
||||
),
|
||||
gettext('Account Settings page.'),
|
||||
HtmlUtils.HTML('</a>')
|
||||
);
|
||||
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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="profile-section-two-fields">
|
||||
<div class="field-container"></div>
|
||||
<% if (!showFullProfile) { %>
|
||||
<% if(ownProfile) { %>
|
||||
<span class="profile-private-message"><%- gettext("You are currently sharing a limited profile.") %></span>
|
||||
<% } else { %>
|
||||
<span class="profile-private-message"><%- gettext("This learner is currently sharing a limited profile.") %></span>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="social-links">
|
||||
<% for (var platform in socialLinks) { %>
|
||||
<% if (socialLinks[platform]) { %>
|
||||
<a rel="noopener" target="_blank" href= <%-socialLinks[platform]%>>
|
||||
<span class="icon fa fa-<%-platform%>-square" data-platform=<%-platform%> aria-hidden="true"></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
from django.utils.translation import gettext as _
|
||||
from common.djangoapps.third_party_auth import pipeline
|
||||
%>
|
||||
|
||||
<li class="controls--account">
|
||||
<span class="title">
|
||||
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
|
||||
${_("Connected Accounts")}
|
||||
</span>
|
||||
<span class="data">
|
||||
<span class="third-party-auth">
|
||||
% for state in provider_user_states:
|
||||
<div class="auth-provider">
|
||||
<div class="status">
|
||||
% if state.has_account:
|
||||
<span class="icon fa fa-link" aria-hidden="true"></span> <span class="copy">${_('Linked')}</span>
|
||||
% else:
|
||||
<span class="icon fa fa-unlink" aria-hidden="true"></span><span class="copy">${_('Not Linked')}</span>
|
||||
% endif
|
||||
</div>
|
||||
<span class="provider">${state.provider.name}</span>
|
||||
<span class="control">
|
||||
<form
|
||||
action="${pipeline.get_disconnect_url(state.provider.provider_id, state.association_id)}"
|
||||
method="post"
|
||||
name="${state.get_unlink_form_name()}">
|
||||
% if state.has_account:
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
|
||||
<button type="button" onclick="document.${state.get_unlink_form_name()}.submit()">
|
||||
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
|
||||
${_("Unlink")}
|
||||
</button>
|
||||
% elif state.provider.display_for_login:
|
||||
<a href="${pipeline.get_login_url(state.provider.provider_id, pipeline.AUTH_ENTRY_PROFILE)}">
|
||||
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
|
||||
${_("Link")}
|
||||
</a>
|
||||
% endif
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
% endfor
|
||||
</span>
|
||||
</li>
|
||||
@@ -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
|
||||
%>
|
||||
|
||||
<div class="learner-achievements">
|
||||
% if course_certificates or own_profile:
|
||||
<h3 class="u-field-title">Course Certificates</h3>
|
||||
% 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(
|
||||
'<span'
|
||||
' class="localized-datetime start-date"'
|
||||
' data-datetime="{completion_date}"'
|
||||
' data-format="shortDate"'
|
||||
' data-timezone="{user_timezone}"'
|
||||
' data-language="{user_language}"'
|
||||
'></span>'
|
||||
).format(
|
||||
completion_date=certificate['created'],
|
||||
user_timezone=user_timezone,
|
||||
user_language=user_language,
|
||||
),
|
||||
)
|
||||
%>
|
||||
<div class="card certificate-card mode-${certificate['type']}">
|
||||
<div class="card-logo">
|
||||
<h4 class="sr-only">
|
||||
${_('{course_mode} certificate').format(
|
||||
course_mode=certificate['type'],
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-supertitle">${course.display_org_with_default}</div>
|
||||
<div class="card-title">${course.display_name_with_default}</div>
|
||||
<p class="card-text">${completion_date_message_html}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
% elif own_profile:
|
||||
<div class="learner-message">
|
||||
<h4 class="message-header">${_("You haven't earned any certificates yet.")}</h4>
|
||||
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
|
||||
<p class="message-actions">
|
||||
<a class="btn btn-brand" href="${marketing_link('COURSES')}">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
${_('Explore New Courses')}
|
||||
</a>
|
||||
</p>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform('.localized-datetime');
|
||||
</%static:require_module_async>
|
||||
@@ -1,79 +0,0 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="/main.html" />
|
||||
<%def name="online_help_token()"><% return "profile" %></%def>
|
||||
<%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>
|
||||
|
||||
<%block name="bodyclass">view-profile</%block>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course'/>
|
||||
</%block>
|
||||
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="wrapper-profile">
|
||||
<div class="profile ${'profile-self' if own_profile else 'profile-other'}">
|
||||
<div class="wrapper-profile-field-account-privacy">
|
||||
% if own_profile and records_url:
|
||||
<div class="wrapper-profile-records">
|
||||
<a href="${records_url}">
|
||||
<button class="btn profile-records-button">${_("View My Records")}</button>
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% if own_profile:
|
||||
<div class="profile-header">
|
||||
<h2 class="header">${_("My Profile")}</h2>
|
||||
<div class="subheader">
|
||||
${_('Build out your profile to personalize your identity on {platform_name}.').format(
|
||||
platform_name=platform_name,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div class="wrapper-profile-sections account-settings-container">
|
||||
<div class="ui-loading-indicator">
|
||||
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
|
||||
</div>
|
||||
<div class="wrapper-profile-section-container-one is-hidden">
|
||||
<div class="wrapper-profile-section-one">
|
||||
<div class="profile-image-field">
|
||||
</div>
|
||||
<div class="profile-section-one-fields">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy">${_("An error occurred. Try loading the page again.")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper-profile-section-container-two is-hidden">
|
||||
<div class="wrapper-profile-bio"></div>
|
||||
% if achievements_fragment:
|
||||
${HTML(achievements_fragment.body_html())}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<%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);
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
@@ -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'<a href="{CREDENTIALS_PUBLIC_SERVICE_URL}/records/">')
|
||||
|
||||
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}'
|
||||
)
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user