Commit e89afa93 by Will Daly

WIP: add login and registration end-points to the user API.

parent 80613361
...@@ -65,9 +65,15 @@ def profile_info(username): ...@@ -65,9 +65,15 @@ def profile_info(username):
return None return None
profile_dict = { profile_dict = {
u'username': profile.user.username, "username": profile.user.username,
u'email': profile.user.email, "email": profile.user.email,
u'full_name': profile.name, "full_name": profile.name,
"level_of_education": profile.level_of_education,
"mailing_address": profile.mailing_address,
"year_of_birth": profile.year_of_birth,
"goals": profile.goals,
"city": profile.city,
"country": profile.country,
} }
return profile_dict return profile_dict
......
...@@ -4,6 +4,8 @@ This is NOT part of the public API. ...@@ -4,6 +4,8 @@ This is NOT part of the public API.
""" """
from functools import wraps from functools import wraps
import logging import logging
import json
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
...@@ -54,3 +56,242 @@ def intercept_errors(api_error, ignore_errors=[]): ...@@ -54,3 +56,242 @@ def intercept_errors(api_error, ignore_errors=[]):
raise api_error(msg) raise api_error(msg)
return _wrapped return _wrapped
return _decorator return _decorator
class InvalidFieldError(Exception):
"""The provided field definition is not valid. """
class FormDescription(object):
"""Generate a JSON representation of a form. """
ALLOWED_TYPES = ["text", "select", "textarea"]
ALLOWED_RESTRICTIONS = {
"text": ["min_length", "max_length"],
}
def __init__(self, method, submit_url):
"""Configure how the form should be submitted.
Args:
method (unicode): The HTTP method used to submit the form.
submit_url (unicode): The URL where the form should be submitted.
"""
self.method = method
self.submit_url = submit_url
self.fields = []
def add_field(
self, name, label=u"", field_type=u"text", default=u"",
placeholder=u"", instructions=u"", required=True, restrictions=None,
options=None
):
"""Add a field to the form description.
Args:
name (unicode): The name of the field, which is the key for the value
to send back to the server.
Keyword Arguments:
label (unicode): The label for the field (e.g. "E-mail" or "Username")
field_type (unicode): The type of the field. See `ALLOWED_TYPES` for
acceptable values.
default (unicode): The default value for the field.
placeholder (unicode): Placeholder text in the field
(e.g. "user@example.com" for an email field)
instructions (unicode): Short instructions for using the field
(e.g. "This is the email address you used when you registered.")
required (boolean): Whether the field is required or optional.
restrictions (dict): Validation restrictions for the field.
See `ALLOWED_RESTRICTIONS` for acceptable values.
options (list): For "select" fields, a list of tuples
(value, display_name) representing the options available to
the user. `value` is the value of the field to send to the server,
and `display_name` is the name to display to the user.
If the field type is "select", you *must* provide this kwarg.
Raises:
InvalidFieldError
"""
if field_type not in self.ALLOWED_TYPES:
msg = u"Field type '{field_type}' is not a valid type. Allowed types are: {allowed}.".format(
field_type=field_type,
allowed=", ".join(self.ALLOWED_TYPES)
)
raise InvalidFieldError(msg)
field_dict = {
"label": label,
"name": name,
"type": field_type,
"default": default,
"placeholder": placeholder,
"instructions": instructions,
"required": required,
"restrictions": {}
}
if field_type == "select":
if options is not None:
field_dict["options"] = [
{"value": option_value, "name": option_name}
for option_value, option_name in options
]
else:
raise InvalidFieldError("You must provide options for a select field.")
if restrictions is not None:
allowed_restrictions = self.ALLOWED_RESTRICTIONS.get(field_type, [])
for key, val in restrictions.iteritems():
if key in allowed_restrictions:
field_dict["restrictions"][key] = val
else:
msg = "Restriction '{restriction}' is not allowed for field type '{field_type}'".format(
restriction=key,
field_type=field_type
)
raise InvalidFieldError(msg)
self.fields.append(field_dict)
def to_json(self):
"""Create a JSON representation of the form description.
Here's an example of the output:
{
"method": "post",
"submit_url": "/submit",
"fields": [
{
"name": "cheese_or_wine",
"label": "Cheese or Wine?",
"default": "cheese",
"type": "select",
"required": True,
"placeholder": "",
"instructions": "",
"options": [
{"value": "cheese", "name": "Cheese"},
{"value": "wine", "name": "Wine"}
]
"restrictions": {},
},
{
"name": "comments",
"label": "comments",
"default": "",
"type": "text",
"required": False,
"placeholder": "Any comments?",
"instructions": "Please enter additional comments here."
"restrictions": {
"max_length": 200
}
},
...
]
}
If the field is NOT a "select" type, then the "options"
key will be omitted.
Returns:
unicode
"""
return json.dumps({
"method": self.method,
"submit_url": self.submit_url,
"fields": self.fields
})
def shim_student_view(view_func, check_logged_in=False):
"""Create a "shim" view for a view function from the student Django app.
Specifically, we need to:
* Strip out enrollment params, since the client for the new registration/login
page will communicate with the enrollment API to update enrollments.
* Return responses with HTTP status codes indicating success/failure
(instead of always using status 200, but setting "success" to False in
the JSON-serialized content of the response)
* Use status code 302 for redirects instead of
"redirect_url" in the JSON-serialized content of the response.
* Use status code 403 to indicate a login failure.
The shim will preserve any cookies set by the view.
Arguments:
view_func (function): The view function from the student Django app.
Keyword Args:
check_logged_in (boolean): If true, check whether the user successfully
authenticated and if not set the status to 403.
Returns:
function
"""
@wraps(view_func)
def _inner(request):
# Strip out enrollment action stuff, since we're handling that elsewhere
if "enrollment_action" in request.POST:
del request.POST["enrollment_action"]
if "course_id" in request.POST:
del request.POST["course_id"]
# Actually call the function!
# TODO ^^
response = view_func(request)
# Most responses from this view are a JSON dict
# TODO -- explain this more
try:
response_dict = json.loads(response.content)
msg = response_dict.get("value", u"")
redirect_url = response_dict.get("redirect_url")
except (ValueError, TypeError):
msg = response.content
redirect_url = None
# If the user could not be authenticated
if check_logged_in and not request.user.is_authenticated():
response.status_code = 403
response.content = msg
# Handle redirects
# TODO -- explain why this is safe
elif redirect_url is not None:
response.status_code = 302
response.content = redirect_url
# Handle errors
elif response.status_code != 200 or not response_dict.get("success", False):
# TODO -- explain this
if response.status_code == 200:
response.status_code = 400
response.content = msg
# Otherwise, return the response
else:
response.content = msg
# Return the response.
# IMPORTANT: this NEEDS to preserve session variables / cookies!
return response
return _inner
...@@ -10,6 +10,8 @@ user_api_router.register(r'user_prefs', user_api_views.UserPreferenceViewSet) ...@@ -10,6 +10,8 @@ user_api_router.register(r'user_prefs', user_api_views.UserPreferenceViewSet)
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^v1/', include(user_api_router.urls)), url(r'^v1/', include(user_api_router.urls)),
url(r'^v1/account/login_session/$', user_api_views.LoginSessionView.as_view(), name="user_api_login_session"),
url(r'^v1/account/registration/$', user_api_views.RegistrationView.as_view(), name="user_api_registration"),
url( url(
r'^v1/preferences/(?P<pref_key>{})/users/$'.format(UserPreference.KEY_REGEX), r'^v1/preferences/(?P<pref_key>{})/users/$'.format(UserPreference.KEY_REGEX),
user_api_views.PreferenceUsersListView.as_view() user_api_views.PreferenceUsersListView.as_view()
......
"""TODO"""
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseBadRequest
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework import authentication from rest_framework import authentication
from rest_framework import filters from rest_framework import filters
from rest_framework import generics from rest_framework import generics
from rest_framework import permissions from rest_framework import permissions
from rest_framework import status
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.response import Response from django_countries.countries import COUNTRIES
from user_api.serializers import UserSerializer, UserPreferenceSerializer from user_api.serializers import UserSerializer, UserPreferenceSerializer
from user_api.models import UserPreference from user_api.models import UserPreference, UserProfile
from django_comment_common.models import Role from django_comment_common.models import Role
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from user_api.api import account as account_api, profile as profile_api
from user_api.helpers import FormDescription, shim_student_view
class ApiKeyHeaderPermission(permissions.BasePermission): class ApiKeyHeaderPermission(permissions.BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
...@@ -31,6 +41,251 @@ class ApiKeyHeaderPermission(permissions.BasePermission): ...@@ -31,6 +41,251 @@ class ApiKeyHeaderPermission(permissions.BasePermission):
) )
class LoginSessionView(APIView):
"""TODO"""
def get(self, request):
"""Render a form for allowing a user to log in.
TODO
"""
form_desc = FormDescription("post", reverse("user_api_login_session"))
form_desc.add_field(
"email",
label=_(u"E-mail"),
placeholder=_(u"example: username@domain.com"),
instructions=_(
u"This is the e-mail address you used to register with {platform}"
).format(platform=settings.PLATFORM_NAME),
restrictions={
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH,
}
)
form_desc.add_field(
"password",
label=_(u"Password"),
restrictions={
"min_length": account_api.PASSWORD_MIN_LENGTH,
"max_length": account_api.PASSWORD_MAX_LENGTH,
}
)
return HttpResponse(form_desc.to_json(), content_type="application/json")
@method_decorator(ensure_csrf_cookie)
def post(self, request):
"""Authenticate a user and log them in.
TODO
"""
# Validate the parameters
# If either param is missing, it's a malformed request
email = request.POST.get("email")
password = request.POST.get("password")
if email is None or password is None:
return HttpResponseBadRequest()
return self._login_shim(request)
def _login_shim(self, request):
# Initially, this should be a shim to student views,
# since it will be too much work to re-implement everything there.
# Eventually, we'll want to pull out that functionality into this Django app.
from student.views import login_user
return shim_student_view(login_user, check_logged_in=True)(request)
class RegistrationView(APIView):
"""TODO"""
DEFAULT_FIELDS = ["email", "name", "username", "password"]
EXTRA_FIELDS = [
"city", "country", "level_of_education", "gender",
"year_of_birth", "mailing_address", "goals",
]
def __init__(self, *args, **kwargs):
super(RegistrationView, self).__init__(*args, **kwargs)
self.field_handlers = {}
for field_name in (self.DEFAULT_FIELDS + self.EXTRA_FIELDS):
handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name))
self.field_handlers[field_name] = handler
def get(self, request):
"""Render a form for allowing the user to register.
TODO
"""
form_desc = FormDescription("post", reverse("user_api_registration"))
# Default fields are always required
for field_name in self.DEFAULT_FIELDS:
self.field_handlers[field_name](form_desc, required=True)
# Extra fields from configuration may be required, optional, or hidden
# TODO -- explain error handling here
for field_name in self.EXTRA_FIELDS:
field_setting = settings.REGISTRATION_EXTRA_FIELDS.get(field_name)
handler = self.field_handlers[field_name]
if field_setting in ["required", "optional"]:
handler(form_desc, required=(field_setting == "required"))
elif field_setting != "hidden":
# TODO -- warning here
pass
return HttpResponse(form_desc.to_json(), content_type="application/json")
def post(self, request):
"""Create the user's account.
TODO
"""
# Backwards compat:
# TODO -- explain this
request.POST["honor_code"] = "true"
request.POST["terms_of_service"] = "true"
# Initially, this should be a shim to student views.
# Eventually, we'll want to pull that functionality into this API.
from student.views import create_account
return shim_student_view(create_account)(request)
def _add_email_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field(
"email",
label=_(u"E-mail"),
placeholder=_(u"example: username@domain.com"),
instructions=_(
u"This is the e-mail address you used to register with {platform}"
).format(platform=settings.PLATFORM_NAME),
restrictions={
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH,
},
required=required
)
def _add_name_field(self, form_desc, required=True):
"""TODO"""
form_desc.add_field(
"name",
label=_(u"Full Name"),
instructions=_(u"Needed for any certificates you may earn"),
restrictions={
"max_length": profile_api.FULL_NAME_MAX_LENGTH,
},
required=required
)
def _add_username_field(self, form_desc, required=True):
"""TODO"""
form_desc.add_field(
"username",
label=_(u"Public Username"),
instructions=_(u"Will be shown in any discussions or forums you participate in (cannot be changed)"),
restrictions={
"min_length": account_api.USERNAME_MIN_LENGTH,
"max_length": account_api.USERNAME_MAX_LENGTH,
},
required=required
)
def _add_password_field(self, form_desc, required=True):
"""TODO"""
form_desc.add_field(
"password",
label=_(u"Password"),
restrictions={
"min_length": account_api.PASSWORD_MIN_LENGTH,
"max_length": account_api.PASSWORD_MAX_LENGTH,
},
required=required
)
def _add_level_of_education_field(self, form_desc, required=True):
""" TODO """
form_desc.add_field(
"level_of_education",
label=_("Highest Level of Education Completed"),
field_type="select",
options=self._options_with_default(UserProfile.LEVEL_OF_EDUCATION_CHOICES),
required=required
)
def _add_gender_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field(
"gender",
label=_("Gender"),
field_type="select",
options=self._options_with_default(UserProfile.GENDER_CHOICES),
required=required
)
def _add_year_of_birth_field(self, form_desc, required=True):
"""TODO """
options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS]
form_desc.add_field(
"year_of_birth",
label=_("Year of Birth"),
field_type="select",
options=self._options_with_default(options),
required=required
)
def _add_mailing_address_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field(
"mailing_address",
label=_("Mailing Address"),
field_type="textarea",
required=required
)
def _add_goals_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field(
"goals",
label=_("Please share with us your reasons for registering with edX"),
field_type="textarea",
required=required
)
def _add_city_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field(
"city",
label=_("City"),
required=required
)
def _add_country_field(self, form_desc, required=True):
"""TODO """
options = [
(country_code, unicode(country_name))
for country_code, country_name in COUNTRIES
]
form_desc.add_field(
"country",
label=_("Country"),
field_type="select",
options=self._options_with_default(options),
required=required
)
def _options_with_default(self, options):
"""TODO """
return (
[("", "--")] + list(options)
)
class UserViewSet(viewsets.ReadOnlyModelViewSet): class UserViewSet(viewsets.ReadOnlyModelViewSet):
authentication_classes = (authentication.SessionAuthentication,) authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (ApiKeyHeaderPermission,) permission_classes = (ApiKeyHeaderPermission,)
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import re import re
from urllib import urlencode from urllib import urlencode
import json
from mock import patch from mock import patch
import ddt import ddt
from django.test import TestCase from django.test import TestCase
...@@ -60,6 +61,37 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): ...@@ -60,6 +61,37 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
response = self.client.get(reverse('account_index')) response = self.client.get(reverse('account_index'))
self.assertContains(response, "Student Account") self.assertContains(response, "Student Account")
@ddt.data(
("login", "login"),
("register", "register"),
)
@ddt.unpack
def test_login_and_registration_form(self, url_name, initial_mode):
response = self.client.get(reverse(url_name))
expected_data = u"data-initial-mode=\"{mode}\"".format(mode=initial_mode)
self.assertContains(response, expected_data)
@ddt.data("login", "register")
def test_login_and_registration_third_party_auth_urls(self, url_name):
response = self.client.get(reverse(url_name))
# This relies on the THIRD_PARTY_AUTH configuration in the test settings
expected_data = u"data-third-party-auth-providers=\"{providers}\"".format(
providers=json.dumps([
{
u'icon_class': u'icon-facebook',
u'login_url': u'/auth/login/facebook/?auth_entry=login',
u'name': u'Facebook'
},
{
u'icon_class': u'icon-google-plus',
u'login_url': u'/auth/login/google-oauth2/?auth_entry=login',
u'name': u'Google'
}
])
)
self.assertContains(response, expected_data)
def test_change_email(self): def test_change_email(self):
response = self._change_email(self.NEW_EMAIL, self.PASSWORD) response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
......
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.conf import settings
urlpatterns = patterns( urlpatterns = patterns(
'student_account.views', 'student_account.views',
url(r'^$', 'index', name='account_index'), url(r'^login/$', 'login_and_registration_form', {'initial_mode': 'login'}, name='login'),
url(r'^email$', 'email_change_request_handler', name='email_change_request'), url(r'^register/$', 'login_and_registration_form', {'initial_mode': 'register'}, name='register'),
url(r'^email/confirmation/(?P<key>[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'),
) )
if settings.FEATURES.get('ENABLE_NEW_DASHBOARD'):
urlpatterns += patterns(
'student_account.views',
url(r'^$', 'index', name='account_index'),
url(r'^email$', 'email_change_request_handler', name='email_change_request'),
url(r'^email/confirmation/(?P<key>[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'),
)
\ No newline at end of file
""" Views for a student's account information. """ """ Views for a student's account information. """
import json
from django.conf import settings from django.conf import settings
from django.http import ( from django.http import (
QueryDict, HttpResponse, HttpResponse, HttpResponseBadRequest, HttpResponseServerError
HttpResponseBadRequest, HttpResponseServerError
) )
from django.core.mail import send_mail from django.core.mail import send_mail
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
import third_party_auth
from microsite_configuration import microsite from microsite_configuration import microsite
from user_api.api import account as account_api from user_api.api import account as account_api
...@@ -41,6 +42,38 @@ def index(request): ...@@ -41,6 +42,38 @@ def index(request):
) )
@require_http_methods(['GET'])
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 "registration".
"""
context = {
'disable_courseware_js': True,
'initial_mode': initial_mode,
'third_party_auth_providers': json.dumps([])
}
if microsite.get_value("ENABLE_THIRD_PARTY_AUTH", settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH")):
context["third_party_auth_providers"] = json.dumps([
{
"name": enabled.NAME,
"icon_class": enabled.ICON_CLASS,
"login_url": third_party_auth.pipeline.get_login_url(
enabled.NAME, third_party_auth.pipeline.AUTH_ENTRY_LOGIN
),
}
for enabled in third_party_auth.provider.Registry.enabled()
])
return render_to_response('student_account/login_and_register.html', context)
@login_required @login_required
@require_http_methods(['POST']) @require_http_methods(['POST'])
@ensure_csrf_cookie @ensure_csrf_cookie
......
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.conf import settings
urlpatterns = patterns(
'student_profile.views', urlpatterns = []
url(r'^$', 'index', name='profile_index'),
url(r'^preferences$', 'preference_handler', name='preference_handler'),
url(r'^preferences/languages$', 'language_info', name='language_info'), if settings.FEATURES.get('ENABLE_NEW_DASHBOARD'):
) urlpatterns = patterns(
'student_profile.views',
url(r'^$', 'index', name='profile_index'),
url(r'^preferences$', 'preference_handler', name='preference_handler'),
url(r'^preferences/languages$', 'language_info', name='language_info'),
)
...@@ -203,6 +203,17 @@ simplefilter('ignore') # Change to "default" to see the first instance of each ...@@ -203,6 +203,17 @@ simplefilter('ignore') # Change to "default" to see the first instance of each
######### Third-party auth ########## ######### Third-party auth ##########
FEATURES['ENABLE_THIRD_PARTY_AUTH'] = True FEATURES['ENABLE_THIRD_PARTY_AUTH'] = True
THIRD_PARTY_AUTH = {
"Google": {
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test",
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test",
},
"Facebook": {
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test",
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test",
},
}
################################## OPENID ##################################### ################################## OPENID #####################################
FEATURES['AUTH_USE_OPENID'] = True FEATURES['AUTH_USE_OPENID'] = True
FEATURES['AUTH_USE_OPENID_PROVIDER'] = True FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
......
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='/static_content.html'/>
<%inherit file="../main.html" />
<%block name="pagetitle">${_("Login and Register")}</%block>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<%static:js group='student_account'/>
</%block>
<%block name="header_extras">
% for template_name in ["account"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="student_account/${template_name}.underscore" />
</script>
% endfor
</%block>
<h1>Login and Registration!</h1>
<p>This is a placeholder for the combined login and registration form</p>
## TODO: Use JavaScript to populate this div with
## the actual registration/login forms (loaded asynchronously from the user API)
## The URLS for the forms are:
## - GET /user_api/v1/registration/
## - GET /user_api/v1/login_session/
##
## You can post back to those URLs with JSON-serialized
## data from the form fields in order to complete the registration
## or login.
##
## Also TODO: we need to figure out how to enroll students in
## a course if they got here from a course about page.
##
## third_party_auth_providers is a JSON-serialized list of
## dictionaries of the form:
## {
## "name": "Facebook",
## "icon_class": "facebook-icon",
## "login_url": "http://api.facebook.com/auth"
## }
##
## Note that this list may be empty.
##
<div id="login-and-registration-container"
data-initial-mode="${initial_mode}"
data-third-party-auth-providers="${third_party_auth_providers}"
/>
...@@ -375,6 +375,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -375,6 +375,10 @@ if settings.COURSEWARE_ENABLED:
# LTI endpoints listing # LTI endpoints listing
url(r'^courses/{}/lti_rest_endpoints/'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/lti_rest_endpoints/'.format(settings.COURSE_ID_PATTERN),
'courseware.views.get_course_lti_endpoints', name='lti_rest_endpoints'), 'courseware.views.get_course_lti_endpoints', name='lti_rest_endpoints'),
# Student account and profile
url(r'^account/', include('student_account.urls')),
url(r'^profile/', include('student_profile.urls')),
) )
# allow course staff to change to student view of courseware # allow course staff to change to student view of courseware
...@@ -537,12 +541,6 @@ if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): ...@@ -537,12 +541,6 @@ if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
url(r'', include('third_party_auth.urls')), url(r'', include('third_party_auth.urls')),
) )
# If enabled, expose the URLs for the new dashboard, account, and profile pages
if settings.FEATURES.get('ENABLE_NEW_DASHBOARD'):
urlpatterns += (
url(r'^profile/', include('student_profile.urls')),
url(r'^account/', include('student_account.urls')),
)
urlpatterns = patterns(*urlpatterns) urlpatterns = patterns(*urlpatterns)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment