Files
edx-platform/common/djangoapps/student/views/management.py
jawad khan 5025a3ffd9 feat!: remove "Text Me the App" page from web app because of security concern
Someone at edx was able to quickly send a few texts to himself using the tool.
Each text came from a different number and there was no message about the ability to unsubscribe or stop.
This could potentially be used to spam users as well as potentially result in charges to edX for high volume.

page url https://courses.edx.org/text-me-the-app
LEARNER-8286
2021-03-30 23:38:24 +05:00

865 lines
34 KiB
Python

""" # lint-amnesty, pylint: disable=cyclic-import
Student Views
"""
import datetime
import logging
import uuid
from collections import namedtuple
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser, User # lint-amnesty, pylint: disable=imported-auth-user
from django.contrib.sites.models import Site
from django.core.validators import ValidationError, validate_email
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import Signal, receiver # lint-amnesty, pylint: disable=unused-import
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import redirect
from django.template.context_processors import csrf
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie # lint-amnesty, pylint: disable=unused-import
from django.views.decorators.http import require_GET, require_http_methods, require_POST # lint-amnesty, pylint: disable=unused-import
from edx_ace import ace
from edx_ace.recipient import Recipient
from edx_django_utils import monitoring as monitoring_utils
from eventtracking import tracker
from ipware.ip import get_client_ip
# Note that this lives in LMS, so this dependency should be refactored.
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from common.djangoapps.track import views as track_views
from lms.djangoapps.bulk_email.models import Optout
from common.djangoapps.course_modes.models import CourseMode
from lms.djangoapps.courseware.courses import get_courses, sort_by_announcement, sort_by_start_date
from common.djangoapps.edxmako.shortcuts import marketing_link, render_to_response, render_to_string # lint-amnesty, pylint: disable=unused-import
from common.djangoapps.entitlements.models import CourseEntitlement
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.catalog.utils import get_programs_with_type
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming import helpers as theming_helpers
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.user_authn.toggles import should_redirect_to_authn_microfrontend
from openedx.core.djangolib.markup import HTML, Text
from common.djangoapps.student.email_helpers import generate_activation_email_context
from common.djangoapps.student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info
from common.djangoapps.student.message_types import AccountActivation, EmailChange, EmailChangeConfirmation, RecoveryEmailCreate # lint-amnesty, pylint: disable=line-too-long
from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=unused-import
AccountRecovery,
CourseEnrollment,
PendingEmailChange, # unimport:skip
PendingSecondaryEmailChange,
Registration,
RegistrationCookieConfiguration,
UserAttribute,
UserProfile,
UserSignupSource,
UserStanding,
create_comments_service_user,
email_exists_or_retired
)
from common.djangoapps.student.signals import REFUND_ORDER
from common.djangoapps.student.tasks import send_activation_email
from common.djangoapps.util.db import outer_atomic
from common.djangoapps.util.json_request import JsonResponse
from xmodule.modulestore.django import modulestore
log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
ReverifyInfo = namedtuple(
'ReverifyInfo',
'course_id course_name course_number date status display'
)
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
# Used as the name of the user attribute for tracking affiliate registrations
REGISTRATION_AFFILIATE_ID = 'registration_affiliate_id'
REGISTRATION_UTM_PARAMETERS = {
'utm_source': 'registration_utm_source',
'utm_medium': 'registration_utm_medium',
'utm_campaign': 'registration_utm_campaign',
'utm_term': 'registration_utm_term',
'utm_content': 'registration_utm_content',
}
REGISTRATION_UTM_CREATED_AT = 'registration_utm_created_at'
USER_ACCOUNT_ACTIVATED = 'edx.user.account.activated'
def csrf_token(context):
"""
A csrf token that can be included in a form.
"""
token = context.get('csrf_token', '')
if token == 'NOTPROVIDED':
return ''
return (HTML('<div style="display:none"><input type="hidden"'
' name="csrfmiddlewaretoken" value="{}" /></div>').format(Text(token)))
# NOTE: This view is not linked to directly--it is called from
# branding/views.py:index(), which is cached for anonymous users.
# This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed)
def index(request, extra_context=None, user=AnonymousUser()):
"""
Render the edX main page.
extra_context is used to allow immediate display of certain modal windows, eg signup.
"""
if extra_context is None:
extra_context = {}
courses = get_courses(user)
if configuration_helpers.get_value(
"ENABLE_COURSE_SORTING_BY_START_DATE",
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"],
):
courses = sort_by_start_date(courses)
else:
courses = sort_by_announcement(courses)
context = {'courses': courses}
context['homepage_overlay_html'] = configuration_helpers.get_value('homepage_overlay_html')
# This appears to be an unused context parameter, at least for the master templates...
context['show_partners'] = configuration_helpers.get_value('show_partners', True)
# TO DISPLAY A YOUTUBE WELCOME VIDEO
# 1) Change False to True
context['show_homepage_promo_video'] = configuration_helpers.get_value('show_homepage_promo_video', False)
# Maximum number of courses to display on the homepage.
context['homepage_course_max'] = configuration_helpers.get_value(
'HOMEPAGE_COURSE_MAX', settings.HOMEPAGE_COURSE_MAX
)
# 2) Add your video's YouTube ID (11 chars, eg "123456789xX"), or specify via site configuration
# Note: This value should be moved into a configuration setting and plumbed-through to the
# context via the site configuration workflow, versus living here
youtube_video_id = configuration_helpers.get_value('homepage_promo_video_youtube_id', "your-youtube-id")
context['homepage_promo_video_youtube_id'] = youtube_video_id
# allow for theme override of the courses list
context['courses_list'] = theming_helpers.get_template_path('courses_list.html')
# Insert additional context for use in the template
context.update(extra_context)
# Add marketable programs to the context.
context['programs_list'] = get_programs_with_type(request.site, include_hidden=False)
return render_to_response('index.html', context)
def compose_activation_email(root_url, user, user_registration=None, route_enabled=False, profile_name=''):
"""
Construct all the required params for the activation email
through celery task
"""
if user_registration is None:
user_registration = Registration.objects.get(user=user)
message_context = generate_activation_email_context(user, user_registration)
message_context.update({
'confirm_activation_link': '{root_url}/activate/{activation_key}'.format(
root_url=root_url,
activation_key=message_context['key']
),
'route_enabled': route_enabled,
'routed_user': user.username,
'routed_user_email': user.email,
'routed_profile_name': profile_name,
})
if route_enabled:
dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL']
else:
dest_addr = user.email
msg = AccountActivation().personalize(
recipient=Recipient(user.id, dest_addr),
language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
user_context=message_context,
)
return msg
def compose_and_send_activation_email(user, profile, user_registration=None):
"""
Construct all the required params and send the activation email
through celery task
Arguments:
user: current logged-in user
profile: profile object of the current logged-in user
user_registration: registration of the current logged-in user
"""
route_enabled = settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL')
root_url = configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL)
msg = compose_activation_email(root_url, user, user_registration, route_enabled, profile.name)
send_activation_email.delay(str(msg))
@login_required
def course_run_refund_status(request, course_id):
"""
Get Refundable status for a course.
Arguments:
request: The request object.
course_id (str): The unique identifier for the course.
Returns:
Json response.
"""
try:
course_key = CourseKey.from_string(course_id)
course_enrollment = CourseEnrollment.get_enrollment(request.user, course_key)
except InvalidKeyError:
logging.exception("The course key used to get refund status caused InvalidKeyError during look up.")
return JsonResponse({'course_refundable_status': ''}, status=406)
refundable_status = course_enrollment.refundable()
logging.info(f"Course refund status for course {course_id} is {refundable_status}")
return JsonResponse({'course_refundable_status': refundable_status}, status=200)
def _update_email_opt_in(request, org):
"""
Helper function used to hit the profile API if email opt-in is enabled.
"""
email_opt_in = request.POST.get('email_opt_in')
if email_opt_in is not None:
email_opt_in_boolean = email_opt_in == 'true'
preferences_api.update_email_opt_in(request.user, org, email_opt_in_boolean)
@transaction.non_atomic_requests
@require_POST
@outer_atomic(read_committed=True)
def change_enrollment(request, check_access=True):
"""
Modify the enrollment status for the logged-in user.
TODO: This is lms specific and does not belong in common code.
The request parameter must be a POST request (other methods return 405)
that specifies course_id and enrollment_action parameters. If course_id or
enrollment_action is not specified, if course_id is not valid, if
enrollment_action is something other than "enroll" or "unenroll", if
enrollment_action is "enroll" and enrollment is closed for the course, or
if enrollment_action is "unenroll" and the user is not enrolled in the
course, a 400 error will be returned. If the user is not logged in, 403
will be returned; it is important that only this case return 403 so the
front end can redirect the user to a registration or login page when this
happens. This function should only be called from an AJAX request, so
the error messages in the responses should never actually be user-visible.
Args:
request (`Request`): The Django request object
Keyword Args:
check_access (boolean): If True, we check that an accessible course actually
exists for the given course_key before we enroll the student.
The default is set to False to avoid breaking legacy code or
code with non-standard flows (ex. beta tester invitations), but
for any standard enrollment flow you probably want this to be True.
Returns:
Response
"""
# Get the user
user = request.user
# Ensure the user is authenticated
if not user.is_authenticated:
return HttpResponseForbidden()
# Ensure we received a course_id
action = request.POST.get("enrollment_action")
if 'course_id' not in request.POST:
return HttpResponseBadRequest(_("Course id not specified"))
try:
course_id = CourseKey.from_string(request.POST.get("course_id"))
except InvalidKeyError:
log.warning(
"User %s tried to %s with invalid course id: %s",
user.username,
action,
request.POST.get("course_id"),
)
return HttpResponseBadRequest(_("Invalid course id"))
# Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features
# on a per-course basis.
monitoring_utils.set_custom_attribute('course_id', str(course_id))
if action == "enroll":
# Make sure the course exists
# We don't do this check on unenroll, or a bad course id can't be unenrolled from
if not modulestore().has_course(course_id):
log.warning(
"User %s tried to enroll in non-existent course %s",
user.username,
course_id
)
return HttpResponseBadRequest(_("Course id is invalid"))
# Record the user's email opt-in preference
if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
_update_email_opt_in(request, course_id.org)
available_modes = CourseMode.modes_for_course_dict(course_id)
# Check whether the user is blocked from enrolling in this course
# This can occur if the user's IP is on a global blacklist
# or if the user is enrolling in a country in which the course
# is not available.
redirect_url = embargo_api.redirect_if_blocked(
course_id, user=user, ip_address=get_client_ip(request)[0],
url=request.path
)
if redirect_url:
return HttpResponse(redirect_url)
if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id):
return HttpResponse(reverse('courseware', args=[str(course_id)]))
# Check that auto enrollment is allowed for this course
# (= the course is NOT behind a paywall)
if CourseMode.can_auto_enroll(course_id):
# Enroll the user using the default mode (audit)
# We're assuming that users of the course enrollment table
# will NOT try to look up the course enrollment model
# by its slug. If they do, it's possible (based on the state of the database)
# for no such model to exist, even though we've set the enrollment type
# to "audit".
try:
enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes)
if enroll_mode:
CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode)
except Exception: # pylint: disable=broad-except
return HttpResponseBadRequest(_("Could not enroll"))
# If we have more than one course mode or professional ed is enabled,
# then send the user to the choose your track page.
# (In the case of no-id-professional/professional ed, this will redirect to a page that
# funnels users directly into the verification / payment flow)
if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes):
return HttpResponse(
reverse("course_modes_choose", kwargs={'course_id': str(course_id)})
)
# Otherwise, there is only one mode available (the default)
return HttpResponse()
elif action == "unenroll":
enrollment = CourseEnrollment.get_enrollment(user, course_id)
if not enrollment:
return HttpResponseBadRequest(_("You are not enrolled in this course"))
certificate_info = cert_info(user, enrollment.course_overview)
if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES:
return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course"))
CourseEnrollment.unenroll(user, course_id)
REFUND_ORDER.send(sender=None, course_enrollment=enrollment)
return HttpResponse()
else:
return HttpResponseBadRequest(_("Enrollment action is invalid"))
@require_GET
@login_required
@ensure_csrf_cookie
def manage_user_standing(request):
"""
Renders the view used to manage user standing. Also displays a table
of user accounts that have been disabled and who disabled them.
"""
if not request.user.is_staff:
raise Http404
all_disabled_accounts = UserStanding.objects.filter(
account_status=UserStanding.ACCOUNT_DISABLED
)
all_disabled_users = [standing.user for standing in all_disabled_accounts]
headers = ['username', 'account_changed_by']
rows = []
for user in all_disabled_users:
row = [user.username, user.standing.changed_by]
rows.append(row)
context = {'headers': headers, 'rows': rows}
return render_to_response("manage_user_standing.html", context)
@require_POST
@login_required
@ensure_csrf_cookie
def disable_account_ajax(request):
"""
Ajax call to change user standing. Endpoint of the form
in manage_user_standing.html
"""
if not request.user.is_staff:
raise Http404
username = request.POST.get('username')
context = {}
if username is None or username.strip() == '':
context['message'] = _('Please enter a username')
return JsonResponse(context, status=400)
account_action = request.POST.get('account_action')
if account_action is None:
context['message'] = _('Please choose an option')
return JsonResponse(context, status=400)
username = username.strip()
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
context['message'] = _("User with username {} does not exist").format(username)
return JsonResponse(context, status=400)
else:
user_account, _success = UserStanding.objects.get_or_create(
user=user, defaults={'changed_by': request.user},
)
if account_action == 'disable':
user_account.account_status = UserStanding.ACCOUNT_DISABLED
context['message'] = _("Successfully disabled {}'s account").format(username)
log.info("%s disabled %s's account", request.user, username)
elif account_action == 'reenable':
user_account.account_status = UserStanding.ACCOUNT_ENABLED
context['message'] = _("Successfully reenabled {}'s account").format(username)
log.info("%s reenabled %s's account", request.user, username)
else:
context['message'] = _("Unexpected account status")
return JsonResponse(context, status=400)
user_account.changed_by = request.user
user_account.standing_last_changed_at = datetime.datetime.now(UTC)
user_account.save()
return JsonResponse(context)
@receiver(post_save, sender=User)
def user_signup_handler(sender, **kwargs): # pylint: disable=unused-argument
"""
Handler that saves the user Signup Source when the user is created
"""
if 'created' in kwargs and kwargs['created']:
site = configuration_helpers.get_value('SITE_NAME')
if site:
user_signup_source = UserSignupSource(user=kwargs['instance'], site=site)
user_signup_source.save()
log.info('user {} originated from a white labeled "Microsite"'.format(kwargs['instance'].id))
@ensure_csrf_cookie
def activate_account(request, key):
"""
When link in activation e-mail is clicked
"""
# If request is in Studio call the appropriate view
if theming_helpers.get_project_root_name().lower() == 'cms':
monitoring_utils.set_custom_attribute('student_activate_account', 'cms')
return activate_account_studio(request, key)
# TODO: Use custom attribute to determine if there are any `activate_account` calls for cms in Production.
# If not, the templates wouldn't be needed for cms, but we still need a way to activate for cms tests.
monitoring_utils.set_custom_attribute('student_activate_account', 'lms')
activation_message_type = None
try:
registration = Registration.objects.get(activation_key=key)
except (Registration.DoesNotExist, Registration.MultipleObjectsReturned):
activation_message_type = 'error'
messages.error(
request,
HTML(_(
'{html_start}Your account could not be activated{html_end}'
'Something went wrong, please <a href="{support_url}">contact support</a> to resolve this issue.'
)).format(
support_url=configuration_helpers.get_value(
'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK
) or settings.SUPPORT_SITE_LINK,
html_start=HTML('<p class="message-title">'),
html_end=HTML('</p>'),
),
extra_tags='account-activation aa-icon'
)
else:
if registration.user.is_active:
activation_message_type = 'info'
messages.info(
request,
HTML(_('{html_start}This account has already been activated.{html_end}')).format(
html_start=HTML('<p class="message-title">'),
html_end=HTML('</p>'),
),
extra_tags='account-activation aa-icon',
)
else:
registration.activate()
# Success message for logged in users.
message = _('{html_start}Success{html_end} You have activated your account.')
tracker.emit(
USER_ACCOUNT_ACTIVATED,
{
"user_id": registration.user.id,
"activation_timestamp": registration.activation_timestamp
}
)
if not request.user.is_authenticated:
# Success message for logged out users
message = _(
'{html_start}Success! You have activated your account.{html_end}'
'You will now receive email updates and alerts from us related to'
' the courses you are enrolled in. Sign In to continue.'
)
# Add message for later use.
activation_message_type = 'success'
messages.success(
request,
HTML(message).format(
html_start=HTML('<p class="message-title">'),
html_end=HTML('</p>'),
),
extra_tags='account-activation aa-icon',
)
if should_redirect_to_authn_microfrontend() and not request.user.is_authenticated:
url_path = f'/login?account_activation_status={activation_message_type}'
return redirect(settings.AUTHN_MICROFRONTEND_URL + url_path)
return redirect('dashboard')
@ensure_csrf_cookie
def activate_account_studio(request, key):
"""
When link in activation e-mail is clicked and the link belongs to studio.
"""
try:
registration = Registration.objects.get(activation_key=key)
except (Registration.DoesNotExist, Registration.MultipleObjectsReturned):
return render_to_response(
"registration/activation_invalid.html",
{'csrf': csrf(request)['csrf_token']}
)
else:
user_logged_in = request.user.is_authenticated
already_active = True
if not registration.user.is_active:
registration.activate()
already_active = False
return render_to_response(
"registration/activation_complete.html",
{
'user_logged_in': user_logged_in,
'already_active': already_active
}
)
def validate_new_email(user, new_email):
"""
Given a new email for a user, does some basic verification of the new address If any issues are encountered
with verification a ValueError will be thrown.
"""
try:
validate_email(new_email)
except ValidationError:
raise ValueError(_('Valid e-mail address required.')) # lint-amnesty, pylint: disable=raise-missing-from
if new_email == user.email:
raise ValueError(_('Old email is the same as the new email.'))
def validate_secondary_email(user, new_email):
"""
Enforce valid email addresses.
"""
from openedx.core.djangoapps.user_api.accounts.api import get_email_validation_error, \
get_secondary_email_validation_error
if get_email_validation_error(new_email):
raise ValueError(_('Valid e-mail address required.'))
# Make sure that if there is an active recovery email address, that is not the same as the new one.
if hasattr(user, "account_recovery"):
if user.account_recovery.is_active and new_email == user.account_recovery.secondary_email:
raise ValueError(_('Old email is the same as the new email.'))
# Make sure that secondary email address is not same as user's primary email.
if new_email == user.email:
raise ValueError(_('Cannot be same as your sign in email address.'))
message = get_secondary_email_validation_error(new_email)
if message:
raise ValueError(message)
def do_email_change_request(user, new_email, activation_key=None, secondary_email_change_request=False):
"""
Given a new email for a user, does some basic verification of the new address and sends an activation message
to the new address. If any issues are encountered with verification or sending the message, a ValueError will
be thrown.
"""
# if activation_key is not passing as an argument, generate a random key
if not activation_key:
activation_key = uuid.uuid4().hex
confirm_link = reverse('confirm_email_change', kwargs={'key': activation_key, })
if secondary_email_change_request:
PendingSecondaryEmailChange.objects.update_or_create(
user=user,
defaults={
'new_secondary_email': new_email,
'activation_key': activation_key,
}
)
confirm_link = reverse('activate_secondary_email', kwargs={'key': activation_key})
else:
PendingEmailChange.objects.update_or_create(
user=user,
defaults={
'new_email': new_email,
'activation_key': activation_key,
}
)
use_https = theming_helpers.get_current_request().is_secure()
site = Site.objects.get_current()
message_context = get_base_template_context(site)
message_context.update({
'old_email': user.email,
'new_email': new_email,
'confirm_link': '{protocol}://{site}{link}'.format(
protocol='https' if use_https else 'http',
site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
link=confirm_link,
),
})
if secondary_email_change_request:
msg = RecoveryEmailCreate().personalize(
recipient=Recipient(user.id, new_email),
language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
user_context=message_context,
)
else:
msg = EmailChange().personalize(
recipient=Recipient(user.id, new_email),
language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
user_context=message_context,
)
try:
ace.send(msg)
except Exception:
from_address = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
log.error('Unable to send email activation link to user from "%s"', from_address, exc_info=True)
raise ValueError(_('Unable to send email activation link. Please try again later.')) # lint-amnesty, pylint: disable=raise-missing-from
if not secondary_email_change_request:
# When the email address change is complete, a "edx.user.settings.changed" event will be emitted.
# But because changing the email address is multi-step, we also emit an event here so that we can
# track where the request was initiated.
tracker.emit(
SETTING_CHANGE_INITIATED,
{
"setting": "email",
"old": message_context['old_email'],
"new": message_context['new_email'],
"user_id": user.id,
}
)
@ensure_csrf_cookie
def activate_secondary_email(request, key):
"""
This is called when the activation link is clicked. We activate the secondary email
for the requested user.
"""
try:
pending_secondary_email_change = PendingSecondaryEmailChange.objects.get(activation_key=key)
except PendingSecondaryEmailChange.DoesNotExist:
return render_to_response("invalid_email_key.html", {})
try:
account_recovery = pending_secondary_email_change.user.account_recovery
except AccountRecovery.DoesNotExist:
account_recovery = AccountRecovery(user=pending_secondary_email_change.user)
try:
account_recovery.update_recovery_email(pending_secondary_email_change.new_secondary_email)
except ValidationError:
return render_to_response("secondary_email_change_failed.html", {
'secondary_email': pending_secondary_email_change.new_secondary_email
})
pending_secondary_email_change.delete()
return render_to_response("secondary_email_change_successful.html")
@ensure_csrf_cookie
def confirm_email_change(request, key):
"""
User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update
"""
with transaction.atomic():
try:
pec = PendingEmailChange.objects.get(activation_key=key)
except PendingEmailChange.DoesNotExist:
response = render_to_response("invalid_email_key.html", {})
transaction.set_rollback(True)
return response
user = pec.user
address_context = {
'old_email': user.email,
'new_email': pec.new_email
}
if len(User.objects.filter(email=pec.new_email)) != 0:
response = render_to_response("email_exists.html", {})
transaction.set_rollback(True)
return response
use_https = request.is_secure()
if settings.FEATURES['ENABLE_MKTG_SITE']:
contact_link = marketing_link('CONTACT')
else:
contact_link = '{protocol}://{site}{link}'.format(
protocol='https' if use_https else 'http',
site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
link=reverse('contact'),
)
site = Site.objects.get_current()
message_context = get_base_template_context(site)
message_context.update({
'old_email': user.email,
'new_email': pec.new_email,
'contact_link': contact_link,
'from_address': configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL),
})
msg = EmailChangeConfirmation().personalize(
recipient=Recipient(user.id, user.email),
language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
user_context=message_context,
)
u_prof = UserProfile.objects.get(user=user)
meta = u_prof.get_meta()
if 'old_emails' not in meta:
meta['old_emails'] = []
meta['old_emails'].append([user.email, datetime.datetime.now(UTC).isoformat()])
u_prof.set_meta(meta)
u_prof.save()
# Send it to the old email...
try:
ace.send(msg)
except Exception: # pylint: disable=broad-except
log.warning('Unable to send confirmation email to old address', exc_info=True)
response = render_to_response("email_change_failed.html", {'email': user.email})
transaction.set_rollback(True)
return response
user.email = pec.new_email
user.save()
pec.delete()
# And send it to the new email...
msg.recipient = Recipient(user.id, pec.new_email)
try:
ace.send(msg)
except Exception: # pylint: disable=broad-except
log.warning('Unable to send confirmation email to new address', exc_info=True)
response = render_to_response("email_change_failed.html", {'email': pec.new_email})
transaction.set_rollback(True)
return response
response = render_to_response("email_change_successful.html", address_context)
return response
@require_POST
@login_required
@ensure_csrf_cookie
def change_email_settings(request):
"""
Modify logged-in user's setting for receiving emails from a course.
"""
user = request.user
course_id = request.POST.get("course_id")
course_key = CourseKey.from_string(course_id)
receive_emails = request.POST.get("receive_emails")
if receive_emails:
optout_object = Optout.objects.filter(user=user, course_id=course_key)
if optout_object:
optout_object.delete()
log.info(
"User %s (%s) opted in to receive emails from course %s",
user.username,
user.email,
course_id,
)
track_views.server_track(
request,
"change-email-settings",
{"receive_emails": "yes", "course": course_id},
page='dashboard',
)
else:
Optout.objects.get_or_create(user=user, course_id=course_key)
log.info(
"User %s (%s) opted out of receiving emails from course %s",
user.username,
user.email,
course_id,
)
track_views.server_track(
request,
"change-email-settings",
{"receive_emails": "no", "course": course_id},
page='dashboard',
)
return JsonResponse({"success": True})