WIP: add login and registration end-points to the user API.
This commit is contained in:
@@ -65,9 +65,15 @@ def profile_info(username):
|
||||
return None
|
||||
|
||||
profile_dict = {
|
||||
u'username': profile.user.username,
|
||||
u'email': profile.user.email,
|
||||
u'full_name': profile.name,
|
||||
"username": profile.user.username,
|
||||
"email": profile.user.email,
|
||||
"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
|
||||
|
||||
@@ -4,6 +4,8 @@ This is NOT part of the public API.
|
||||
"""
|
||||
from functools import wraps
|
||||
import logging
|
||||
import json
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,3 +56,242 @@ def intercept_errors(api_error, ignore_errors=[]):
|
||||
raise api_error(msg)
|
||||
return _wrapped
|
||||
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
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import base64
|
||||
"""Tests for the user API at the HTTP request level. """
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
import datetime
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from unittest import SkipTest
|
||||
from user_api.models import UserPreference
|
||||
import ddt
|
||||
from pytz import UTC
|
||||
from django_countries.countries import COUNTRIES
|
||||
|
||||
from user_api.api import account as account_api, profile as profile_api
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from user_api.tests.factories import UserPreferenceFactory
|
||||
from django_comment_common import models
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
@@ -532,3 +542,554 @@ class PreferenceUsersListViewTest(UserApiTestCase):
|
||||
self.assertUserIsValid(user)
|
||||
all_user_uris = [user["url"] for user in first_page_users + second_page_users]
|
||||
self.assertEqual(len(set(all_user_uris)), 2)
|
||||
|
||||
|
||||
class LoginSessionViewTest(ApiTestCase):
|
||||
"""Tests for the login end-points of the user API. """
|
||||
|
||||
USERNAME = "bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "password"
|
||||
|
||||
def setUp(self):
|
||||
super(LoginSessionViewTest, self).setUp()
|
||||
self.url = reverse("user_api_login_session")
|
||||
|
||||
def test_allowed_methods(self):
|
||||
self.assertAllowedMethods(self.url, ["GET", "POST", "HEAD", "OPTIONS"])
|
||||
|
||||
def test_put_not_allowed(self):
|
||||
response = self.client.put(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_delete_not_allowed(self):
|
||||
response = self.client.delete(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_patch_not_allowed(self):
|
||||
raise SkipTest("Django 1.4's test client does not support patch")
|
||||
|
||||
def test_login_form(self):
|
||||
# Retrieve the login form
|
||||
response = self.client.get(self.url, content_type="application/json")
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that the form description matches what we expect
|
||||
form_desc = json.loads(response.content)
|
||||
self.assertEqual(form_desc["method"], "post")
|
||||
self.assertEqual(form_desc["submit_url"], self.url)
|
||||
self.assertEqual(form_desc["fields"], [
|
||||
{
|
||||
u"name": u"email",
|
||||
u"default": u"",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"E-mail",
|
||||
u"placeholder": u"example: username@domain.com",
|
||||
u"instructions": u"This is the e-mail address you used to register with edX",
|
||||
u"restrictions": {
|
||||
u"min_length": 3,
|
||||
u"max_length": 254
|
||||
},
|
||||
},
|
||||
{
|
||||
u"name": u"password",
|
||||
u"default": u"",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"Password",
|
||||
u"placeholder": u"",
|
||||
u"instructions": u"",
|
||||
u"restrictions": {
|
||||
u"min_length": 2,
|
||||
u"max_length": 75
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
def test_login(self):
|
||||
# Create a test user
|
||||
UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
|
||||
# Login
|
||||
response = self.client.post(self.url, {
|
||||
"email": self.EMAIL,
|
||||
"password": self.PASSWORD,
|
||||
})
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that we logged in successfully by accessing
|
||||
# a page that requires authentication.
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
self.assertHttpOK(response)
|
||||
|
||||
def test_invalid_credentials(self):
|
||||
# Create a test user
|
||||
UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
|
||||
# Invalid password
|
||||
response = self.client.post(self.url, {
|
||||
"email": self.EMAIL,
|
||||
"password": "invalid"
|
||||
})
|
||||
self.assertHttpForbidden(response)
|
||||
|
||||
# Invalid email address
|
||||
response = self.client.post(self.url, {
|
||||
"email": "invalid@example.com",
|
||||
"password": self.PASSWORD,
|
||||
})
|
||||
self.assertHttpForbidden(response)
|
||||
|
||||
def test_missing_login_params(self):
|
||||
# Create a test user
|
||||
UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
|
||||
# Missing password
|
||||
response = self.client.post(self.url, {
|
||||
"email": self.EMAIL,
|
||||
})
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
# Missing email
|
||||
response = self.client.post(self.url, {
|
||||
"password": self.PASSWORD,
|
||||
})
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
# Missing both email and password
|
||||
response = self.client.post(self.url, {})
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RegistrationViewTest(ApiTestCase):
|
||||
"""Tests for the registration end-points of the User API. """
|
||||
|
||||
USERNAME = "bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "password"
|
||||
NAME = "Bob Smith"
|
||||
EDUCATION = "m"
|
||||
YEAR_OF_BIRTH = "1998"
|
||||
ADDRESS = "123 Fake Street"
|
||||
CITY = "Springfield"
|
||||
COUNTRY = "us"
|
||||
GOALS = "Learn all the things!"
|
||||
|
||||
def setUp(self):
|
||||
super(RegistrationViewTest, self).setUp()
|
||||
self.url = reverse("user_api_registration")
|
||||
|
||||
def test_allowed_methods(self):
|
||||
self.assertAllowedMethods(self.url, ["GET", "POST", "HEAD", "OPTIONS"])
|
||||
|
||||
def test_put_not_allowed(self):
|
||||
response = self.client.put(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_delete_not_allowed(self):
|
||||
response = self.client.delete(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_patch_not_allowed(self):
|
||||
raise SkipTest("Django 1.4's test client does not support patch")
|
||||
|
||||
def test_register_form_default_fields(self):
|
||||
no_extra_fields_setting = {}
|
||||
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"email",
|
||||
u"default": u"",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"E-mail",
|
||||
u"placeholder": u"example: username@domain.com",
|
||||
u"instructions": u"This is the e-mail address you used to register with edX",
|
||||
u"restrictions": {
|
||||
u"min_length": 3,
|
||||
u"max_length": 254
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"name",
|
||||
u"default": u"",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"Full Name",
|
||||
u"placeholder": u"",
|
||||
u"instructions": u"Needed for any certificates you may earn",
|
||||
u"restrictions": {
|
||||
"max_length": 255,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"username",
|
||||
u"default": u"",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"Public Username",
|
||||
u"placeholder": u"",
|
||||
u"instructions": u"Will be shown in any discussions or forums you participate in (cannot be changed)",
|
||||
u"restrictions": {
|
||||
u"min_length": 2,
|
||||
u"max_length": 30,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"password",
|
||||
u"default": u"",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"Password",
|
||||
u"placeholder": u"",
|
||||
u"instructions": u"",
|
||||
u"restrictions": {
|
||||
u"min_length": 2,
|
||||
u"max_length": 75
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def test_register_form_level_of_education(self):
|
||||
self._assert_reg_field(
|
||||
{"level_of_education": "optional"},
|
||||
{
|
||||
"name": "level_of_education",
|
||||
"default": "",
|
||||
"type": "select",
|
||||
"required": False,
|
||||
"label": "Highest Level of Education Completed",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"options": [
|
||||
{"value": "", "name": "--"},
|
||||
{"value": "p", "name": "Doctorate"},
|
||||
{"value": "m", "name": "Master's or professional degree"},
|
||||
{"value": "b", "name": "Bachelor's degree"},
|
||||
{"value": "a", "name": "Associate's degree"},
|
||||
{"value": "hs", "name": "Secondary/high school"},
|
||||
{"value": "jhs", "name": "Junior secondary/junior high/middle school"},
|
||||
{"value": "el", "name": "Elementary/primary school"},
|
||||
{"value": "none", "name": "None"},
|
||||
{"value": "other", "name": "Other"},
|
||||
],
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
def test_register_form_gender(self):
|
||||
self._assert_reg_field(
|
||||
{"gender": "optional"},
|
||||
{
|
||||
"name": "gender",
|
||||
"default": "",
|
||||
"type": "select",
|
||||
"required": False,
|
||||
"label": "Gender",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"options": [
|
||||
{"value": "", "name": "--"},
|
||||
{"value": "m", "name": "Male"},
|
||||
{"value": "f", "name": "Female"},
|
||||
{"value": "o", "name": "Other"},
|
||||
],
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
def test_register_form_year_of_birth(self):
|
||||
this_year = datetime.datetime.now(UTC).year
|
||||
year_options = (
|
||||
[{"value": "", "name": "--"}] + [
|
||||
{"value": unicode(year), "name": unicode(year)}
|
||||
for year in range(this_year, this_year - 120, -1)
|
||||
]
|
||||
)
|
||||
self._assert_reg_field(
|
||||
{"year_of_birth": "optional"},
|
||||
{
|
||||
"name": "year_of_birth",
|
||||
"default": "",
|
||||
"type": "select",
|
||||
"required": False,
|
||||
"label": "Year of Birth",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"options": year_options,
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
def test_registration_form_mailing_address(self):
|
||||
self._assert_reg_field(
|
||||
{"mailing_address": "optional"},
|
||||
{
|
||||
"name": "mailing_address",
|
||||
"default": "",
|
||||
"type": "textarea",
|
||||
"required": False,
|
||||
"label": "Mailing Address",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
def test_registration_form_goals(self):
|
||||
self._assert_reg_field(
|
||||
{"goals": "optional"},
|
||||
{
|
||||
"name": "goals",
|
||||
"default": "",
|
||||
"type": "textarea",
|
||||
"required": False,
|
||||
"label": "Please share with us your reasons for registering with edX",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
def test_registration_form_city(self):
|
||||
self._assert_reg_field(
|
||||
{"city": "optional"},
|
||||
{
|
||||
"name": "city",
|
||||
"default": "",
|
||||
"type": "text",
|
||||
"required": False,
|
||||
"label": "City",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
def test_registration_form_country(self):
|
||||
country_options = (
|
||||
[{"name": "--", "value": ""}] +
|
||||
[
|
||||
{"value": country_code, "name": unicode(country_name)}
|
||||
for country_code, country_name in COUNTRIES
|
||||
]
|
||||
)
|
||||
self._assert_reg_field(
|
||||
{"country": "required"},
|
||||
{
|
||||
"label": "Country",
|
||||
"name": "country",
|
||||
"default": "",
|
||||
"type": "select",
|
||||
"required": True,
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"options": country_options,
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
@override_settings(REGISTRATION_EXTRA_FIELDS={
|
||||
"level_of_education": "optional",
|
||||
"gender": "optional",
|
||||
"year_of_birth": "optional",
|
||||
"mailing_address": "optional",
|
||||
"goals": "optional",
|
||||
"city": "optional",
|
||||
"country": "required",
|
||||
})
|
||||
def test_field_order(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that all fields render in the correct order
|
||||
form_desc = json.loads(response.content)
|
||||
field_names = [field["name"] for field in form_desc["fields"]]
|
||||
self.assertEqual(field_names, [
|
||||
"email",
|
||||
"name",
|
||||
"username",
|
||||
"password",
|
||||
"city",
|
||||
"country",
|
||||
"level_of_education",
|
||||
"gender",
|
||||
"year_of_birth",
|
||||
"mailing_address",
|
||||
"goals",
|
||||
])
|
||||
|
||||
def test_register(self):
|
||||
# Create a new registration
|
||||
response = self.client.post(self.url, {
|
||||
"email": self.EMAIL,
|
||||
"name": self.NAME,
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
})
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that the user exists
|
||||
self.assertEqual(
|
||||
account_api.account_info(self.USERNAME),
|
||||
{
|
||||
"username": self.USERNAME,
|
||||
"email": self.EMAIL,
|
||||
"is_active": False
|
||||
}
|
||||
)
|
||||
|
||||
# Verify that the user's full name is set
|
||||
profile_info = profile_api.profile_info(self.USERNAME)
|
||||
self.assertEqual(profile_info["full_name"], self.NAME)
|
||||
|
||||
# Verify that we've been logged in
|
||||
# by trying to access a page that requires authentication
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
self.assertHttpOK(response)
|
||||
|
||||
@override_settings(REGISTRATION_EXTRA_FIELDS={
|
||||
"level_of_education": "optional",
|
||||
"gender": "optional",
|
||||
"year_of_birth": "optional",
|
||||
"mailing_address": "optional",
|
||||
"goals": "optional",
|
||||
"city": "optional",
|
||||
"country": "required",
|
||||
})
|
||||
def test_register_with_profile_info(self):
|
||||
# Register, providing lots of demographic info
|
||||
response = self.client.post(self.url, {
|
||||
"email": self.EMAIL,
|
||||
"name": self.NAME,
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
"level_of_education": self.EDUCATION,
|
||||
"mailing_address": self.ADDRESS,
|
||||
"year_of_birth": self.YEAR_OF_BIRTH,
|
||||
"goals": self.GOALS,
|
||||
"city": self.CITY,
|
||||
"country": self.COUNTRY
|
||||
})
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify the profile information
|
||||
profile_info = profile_api.profile_info(self.USERNAME)
|
||||
self.assertEqual(profile_info["level_of_education"], self.EDUCATION)
|
||||
self.assertEqual(profile_info["mailing_address"], self.ADDRESS)
|
||||
self.assertEqual(profile_info["year_of_birth"], int(self.YEAR_OF_BIRTH))
|
||||
self.assertEqual(profile_info["goals"], self.GOALS)
|
||||
self.assertEqual(profile_info["city"], self.CITY)
|
||||
self.assertEqual(profile_info["country"], self.COUNTRY)
|
||||
|
||||
def test_activation_email(self):
|
||||
# Register, which should trigger an activation email
|
||||
response = self.client.post(self.url, {
|
||||
"email": self.EMAIL,
|
||||
"name": self.NAME,
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
})
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that the activation email was sent
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
sent_email = mail.outbox[0]
|
||||
self.assertEqual(sent_email.to, [self.EMAIL])
|
||||
self.assertEqual(sent_email.subject, "Activate Your edX Account")
|
||||
self.assertIn("activate your account", sent_email.body)
|
||||
|
||||
@ddt.data(
|
||||
{"email": ""},
|
||||
{"email": "invalid"},
|
||||
{"name": ""},
|
||||
{"username": ""},
|
||||
{"username": "a"},
|
||||
{"password": ""},
|
||||
)
|
||||
def test_register_invalid_input(self, invalid_fields):
|
||||
# Initially, the field values are all valid
|
||||
data = {
|
||||
"email": self.EMAIL,
|
||||
"name": self.NAME,
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
}
|
||||
|
||||
# Override the valid fields, making the input invalid
|
||||
data.update(invalid_fields)
|
||||
|
||||
# Attempt to create the account, expecting an error response
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
|
||||
@override_settings(REGISTRATION_EXTRA_FIELDS={"country": "required"})
|
||||
@ddt.data("email", "name", "username", "password", "country")
|
||||
def test_register_missing_required_field(self, missing_field):
|
||||
data = {
|
||||
"email": self.EMAIL,
|
||||
"name": self.NAME,
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
"country": self.COUNTRY,
|
||||
}
|
||||
|
||||
del data[missing_field]
|
||||
|
||||
# Send a request missing a field
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
def test_register_already_authenticated(self):
|
||||
data = {
|
||||
"email": self.EMAIL,
|
||||
"name": self.NAME,
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
}
|
||||
|
||||
# Register once, which will also log us in
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Try to register again
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
def _assert_reg_field(self, extra_fields_setting, expected_field):
|
||||
"""Retrieve the registration form description from the server and
|
||||
verify that it contains the expected field.
|
||||
|
||||
Args:
|
||||
extra_fields_setting (dict): Override the Django setting controlling
|
||||
which extra fields are displayed in the form.
|
||||
|
||||
expected_field (dict): The field definition we expect to find in the form.
|
||||
|
||||
Raises:
|
||||
AssertionError
|
||||
|
||||
"""
|
||||
# Retrieve the registration form description
|
||||
with override_settings(REGISTRATION_EXTRA_FIELDS=extra_fields_setting):
|
||||
response = self.client.get(self.url)
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that the form description matches what we'd expect
|
||||
form_desc = json.loads(response.content)
|
||||
self.assertIn(expected_field, form_desc["fields"])
|
||||
|
||||
@@ -10,6 +10,8 @@ user_api_router.register(r'user_prefs', user_api_views.UserPreferenceViewSet)
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
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(
|
||||
r'^v1/preferences/(?P<pref_key>{})/users/$'.format(UserPreference.KEY_REGEX),
|
||||
user_api_views.PreferenceUsersListView.as_view()
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
"""TODO"""
|
||||
|
||||
from django.conf import settings
|
||||
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 filters
|
||||
from rest_framework import generics
|
||||
from rest_framework import permissions
|
||||
from rest_framework import status
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.views import APIView
|
||||
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.models import UserPreference
|
||||
from user_api.models import UserPreference, UserProfile
|
||||
from django_comment_common.models import Role
|
||||
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):
|
||||
def has_permission(self, request, view):
|
||||
@@ -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):
|
||||
authentication_classes = (authentication.SessionAuthentication,)
|
||||
permission_classes = (ApiKeyHeaderPermission,)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import re
|
||||
from urllib import urlencode
|
||||
import json
|
||||
from mock import patch
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
@@ -60,6 +61,37 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
response = self.client.get(reverse('account_index'))
|
||||
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):
|
||||
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
from django.conf.urls import patterns, url
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
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'),
|
||||
url(r'^login/$', 'login_and_registration_form', {'initial_mode': 'login'}, name='login'),
|
||||
url(r'^register/$', 'login_and_registration_form', {'initial_mode': 'register'}, name='register'),
|
||||
)
|
||||
|
||||
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'),
|
||||
)
|
||||
@@ -1,15 +1,16 @@
|
||||
""" Views for a student's account information. """
|
||||
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.http import (
|
||||
QueryDict, HttpResponse,
|
||||
HttpResponseBadRequest, HttpResponseServerError
|
||||
HttpResponse, HttpResponseBadRequest, HttpResponseServerError
|
||||
)
|
||||
from django.core.mail import send_mail
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
import third_party_auth
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from user_api.api import account as account_api
|
||||
@@ -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
|
||||
@require_http_methods(['POST'])
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from django.conf.urls import patterns, url
|
||||
from django.conf import settings
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
|
||||
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
|
||||
######### Third-party auth ##########
|
||||
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 #####################################
|
||||
FEATURES['AUTH_USE_OPENID'] = True
|
||||
FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
|
||||
|
||||
52
lms/templates/student_account/login_and_register.html
Normal file
52
lms/templates/student_account/login_and_register.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<%! 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}"
|
||||
/>
|
||||
10
lms/urls.py
10
lms/urls.py
@@ -375,6 +375,10 @@ if settings.COURSEWARE_ENABLED:
|
||||
# LTI endpoints listing
|
||||
url(r'^courses/{}/lti_rest_endpoints/'.format(settings.COURSE_ID_PATTERN),
|
||||
'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
|
||||
@@ -537,12 +541,6 @@ if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user