392 lines
14 KiB
Python
392 lines
14 KiB
Python
""" Views for a student's account information. """
|
|
|
|
import logging
|
|
import json
|
|
from ipware.ip import get_ip
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import (
|
|
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
|
)
|
|
from django.shortcuts import redirect
|
|
from django.http import HttpRequest
|
|
from django_countries import countries
|
|
from django.core.urlresolvers import reverse, resolve
|
|
from django.utils.translation import ugettext as _
|
|
from django_future.csrf import ensure_csrf_cookie
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from lang_pref.api import released_languages
|
|
from opaque_keys import InvalidKeyError
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from edxmako.shortcuts import render_to_response
|
|
from microsite_configuration import microsite
|
|
|
|
from embargo import api as embargo_api
|
|
from external_auth.login_and_register import (
|
|
login as external_auth_login,
|
|
register as external_auth_register
|
|
)
|
|
from student.models import UserProfile
|
|
from student.views import (
|
|
signin_user as old_login_view,
|
|
register_user as old_register_view
|
|
)
|
|
from student_account.helpers import auth_pipeline_urls
|
|
import third_party_auth
|
|
from third_party_auth import pipeline
|
|
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
|
|
|
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
|
|
from openedx.core.djangoapps.user_api.errors import UserNotFound
|
|
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
|
|
|
from student_account.helpers import auth_pipeline_urls
|
|
|
|
|
|
AUDIT_LOG = logging.getLogger("audit")
|
|
|
|
|
|
@require_http_methods(['GET'])
|
|
@ensure_csrf_cookie
|
|
def login_and_registration_form(request, initial_mode="login"):
|
|
"""Render the combined login/registration form, defaulting to login
|
|
|
|
This relies on the JS to asynchronously load the actual form from
|
|
the user_api.
|
|
|
|
Keyword Args:
|
|
initial_mode (string): Either "login" or "register".
|
|
|
|
"""
|
|
# If we're already logged in, redirect to the dashboard
|
|
if request.user.is_authenticated():
|
|
return redirect(reverse('dashboard'))
|
|
|
|
# Retrieve the form descriptions from the user API
|
|
form_descriptions = _get_form_descriptions(request)
|
|
|
|
# If this is a microsite, revert to the old login/registration pages.
|
|
# We need to do this for now to support existing themes.
|
|
if microsite.is_request_in_microsite():
|
|
if initial_mode == "login":
|
|
return old_login_view(request)
|
|
elif initial_mode == "register":
|
|
return old_register_view(request)
|
|
|
|
# Allow external auth to intercept and handle the request
|
|
ext_auth_response = _external_auth_intercept(request, initial_mode)
|
|
if ext_auth_response is not None:
|
|
return ext_auth_response
|
|
|
|
# Otherwise, render the combined login/registration page
|
|
context = {
|
|
'disable_courseware_js': True,
|
|
'initial_mode': initial_mode,
|
|
'third_party_auth': json.dumps(_third_party_auth_context(request)),
|
|
'platform_name': settings.PLATFORM_NAME,
|
|
'responsive': True,
|
|
|
|
# Include form descriptions retrieved from the user API.
|
|
# We could have the JS client make these requests directly,
|
|
# but we include them in the initial page load to avoid
|
|
# the additional round-trip to the server.
|
|
'login_form_desc': form_descriptions['login'],
|
|
'registration_form_desc': form_descriptions['registration'],
|
|
'password_reset_form_desc': form_descriptions['password_reset'],
|
|
|
|
# We need to pass these parameters so that the header's
|
|
# "Sign In" button preserves the querystring params.
|
|
'enrollment_action': request.GET.get('enrollment_action'),
|
|
'course_id': request.GET.get('course_id'),
|
|
'course_mode': request.GET.get('course_mode'),
|
|
}
|
|
|
|
return render_to_response('student_account/login_and_register.html', context)
|
|
|
|
|
|
@require_http_methods(['POST'])
|
|
def password_change_request_handler(request):
|
|
"""Handle password change requests originating from the account page.
|
|
|
|
Uses the Account API to email the user a link to the password reset page.
|
|
|
|
Note:
|
|
The next step in the password reset process (confirmation) is currently handled
|
|
by student.views.password_reset_confirm_wrapper, a custom wrapper around Django's
|
|
password reset confirmation view.
|
|
|
|
Args:
|
|
request (HttpRequest)
|
|
|
|
Returns:
|
|
HttpResponse: 200 if the email was sent successfully
|
|
HttpResponse: 400 if there is no 'email' POST parameter, or if no user with
|
|
the provided email exists
|
|
HttpResponse: 403 if the client has been rate limited
|
|
HttpResponse: 405 if using an unsupported HTTP method
|
|
|
|
Example usage:
|
|
|
|
POST /account/password
|
|
|
|
"""
|
|
limiter = BadRequestRateLimiter()
|
|
if limiter.is_rate_limit_exceeded(request):
|
|
AUDIT_LOG.warning("Password reset rate limit exceeded")
|
|
return HttpResponseForbidden()
|
|
|
|
user = request.user
|
|
# Prefer logged-in user's email
|
|
email = user.email if user.is_authenticated() else request.POST.get('email')
|
|
|
|
if email:
|
|
try:
|
|
request_password_change(email, request.get_host(), request.is_secure())
|
|
except UserNotFound:
|
|
AUDIT_LOG.info("Invalid password reset attempt")
|
|
# Increment the rate limit counter
|
|
limiter.tick_bad_request_counter(request)
|
|
|
|
return HttpResponseBadRequest(_("No user with the provided email address exists."))
|
|
|
|
return HttpResponse(status=200)
|
|
else:
|
|
return HttpResponseBadRequest(_("No email address provided."))
|
|
|
|
|
|
def _third_party_auth_context(request):
|
|
"""Context for third party auth providers and the currently running pipeline.
|
|
|
|
Arguments:
|
|
request (HttpRequest): The request, used to determine if a pipeline
|
|
is currently running.
|
|
|
|
Returns:
|
|
dict
|
|
|
|
"""
|
|
context = {
|
|
"currentProvider": None,
|
|
"providers": []
|
|
}
|
|
|
|
course_id = request.GET.get("course_id")
|
|
email_opt_in = request.GET.get('email_opt_in')
|
|
redirect_to = request.GET.get("next")
|
|
|
|
# Check if the user is trying to enroll in a course
|
|
# that they don't have access to based on country
|
|
# access rules.
|
|
#
|
|
# If so, set the redirect URL to the blocked page.
|
|
# We need to set it here, rather than redirecting
|
|
# from within the pipeline, because a redirect
|
|
# from the pipeline can prevent users
|
|
# from completing the authentication process.
|
|
#
|
|
# Note that we can't check the user's country
|
|
# profile at this point, since the user hasn't
|
|
# authenticated. If the user ends up being blocked
|
|
# by their country preference, we let them enroll;
|
|
# they'll still be blocked when they try to access
|
|
# the courseware.
|
|
if course_id:
|
|
try:
|
|
course_key = CourseKey.from_string(course_id)
|
|
redirect_url = embargo_api.redirect_if_blocked(
|
|
course_key,
|
|
ip_address=get_ip(request),
|
|
url=request.path
|
|
)
|
|
if redirect_url:
|
|
redirect_to = embargo_api.message_url_path(course_key, "enrollment")
|
|
except InvalidKeyError:
|
|
pass
|
|
|
|
login_urls = auth_pipeline_urls(
|
|
third_party_auth.pipeline.AUTH_ENTRY_LOGIN,
|
|
course_id=course_id,
|
|
email_opt_in=email_opt_in,
|
|
redirect_url=redirect_to
|
|
)
|
|
register_urls = auth_pipeline_urls(
|
|
third_party_auth.pipeline.AUTH_ENTRY_REGISTER,
|
|
course_id=course_id,
|
|
email_opt_in=email_opt_in,
|
|
redirect_url=redirect_to
|
|
)
|
|
|
|
if third_party_auth.is_enabled():
|
|
context["providers"] = [
|
|
{
|
|
"name": enabled.NAME,
|
|
"iconClass": enabled.ICON_CLASS,
|
|
"loginUrl": login_urls[enabled.NAME],
|
|
"registerUrl": register_urls[enabled.NAME]
|
|
}
|
|
for enabled in third_party_auth.provider.Registry.enabled()
|
|
]
|
|
|
|
running_pipeline = third_party_auth.pipeline.get(request)
|
|
if running_pipeline is not None:
|
|
current_provider = third_party_auth.provider.Registry.get_by_backend_name(
|
|
running_pipeline.get('backend')
|
|
)
|
|
context["currentProvider"] = current_provider.NAME
|
|
|
|
return context
|
|
|
|
|
|
def _get_form_descriptions(request):
|
|
"""Retrieve form descriptions from the user API.
|
|
|
|
Arguments:
|
|
request (HttpRequest): The original request, used to retrieve session info.
|
|
|
|
Returns:
|
|
dict: Keys are 'login', 'registration', and 'password_reset';
|
|
values are the JSON-serialized form descriptions.
|
|
|
|
"""
|
|
return {
|
|
'login': _local_server_get('/user_api/v1/account/login_session/', request.session),
|
|
'registration': _local_server_get('/user_api/v1/account/registration/', request.session),
|
|
'password_reset': _local_server_get('/user_api/v1/account/password_reset/', request.session)
|
|
}
|
|
|
|
|
|
def _local_server_get(url, session):
|
|
"""Simulate a server-server GET request for an in-process API.
|
|
|
|
Arguments:
|
|
url (str): The URL of the request (excluding the protocol and domain)
|
|
session (SessionStore): The session of the original request,
|
|
used to get past the CSRF checks.
|
|
|
|
Returns:
|
|
str: The content of the response
|
|
|
|
"""
|
|
# Since the user API is currently run in-process,
|
|
# we simulate the server-server API call by constructing
|
|
# our own request object. We don't need to include much
|
|
# information in the request except for the session
|
|
# (to get past through CSRF validation)
|
|
request = HttpRequest()
|
|
request.method = "GET"
|
|
request.session = session
|
|
|
|
# Call the Django view function, simulating
|
|
# the server-server API call
|
|
view, args, kwargs = resolve(url)
|
|
response = view(request, *args, **kwargs)
|
|
|
|
# Return the content of the response
|
|
return response.content
|
|
|
|
|
|
def _external_auth_intercept(request, mode):
|
|
"""Allow external auth to intercept a login/registration request.
|
|
|
|
Arguments:
|
|
request (Request): The original request.
|
|
mode (str): Either "login" or "register"
|
|
|
|
Returns:
|
|
Response or None
|
|
|
|
"""
|
|
if mode == "login":
|
|
return external_auth_login(request)
|
|
elif mode == "register":
|
|
return external_auth_register(request)
|
|
|
|
|
|
@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
|
|
|
|
"""
|
|
return render_to_response('student_account/account_settings.html', account_settings_context(request))
|
|
|
|
|
|
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 = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS]
|
|
|
|
context = {
|
|
'auth': {},
|
|
'duplicate_provider': None,
|
|
'fields': {
|
|
'country': {
|
|
'options': list(countries),
|
|
}, 'gender': {
|
|
'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # 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], # pylint: disable=translation-of-non-string
|
|
}, 'password': {
|
|
'url': reverse('password_reset'),
|
|
}, 'year_of_birth': {
|
|
'options': year_of_birth_options,
|
|
}, 'preferred_language': {
|
|
'options': settings.ALL_LANGUAGES,
|
|
}
|
|
},
|
|
'platform_name': settings.PLATFORM_NAME,
|
|
'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}),
|
|
'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}),
|
|
}
|
|
|
|
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'] = [{
|
|
'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.
|
|
'connect_url': pipeline.get_login_url(
|
|
state.provider.NAME,
|
|
pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS,
|
|
# The url the user should be directed to after the auth process has completed.
|
|
redirect_url=reverse('account_settings'),
|
|
),
|
|
# 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.NAME),
|
|
} for state in auth_states]
|
|
|
|
return context
|