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:
@@ -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/$',
|
||||
|
||||
Reference in New Issue
Block a user