465 lines
16 KiB
Python
465 lines
16 KiB
Python
"""HTTP end-points for the User API. """
|
|
import copy
|
|
import json
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import User
|
|
from django.http import HttpResponse
|
|
from django.core.urlresolvers import reverse
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
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 viewsets
|
|
from rest_framework.views import APIView
|
|
from rest_framework.exceptions import ParseError
|
|
from django_countries import countries
|
|
from user_api.serializers import UserSerializer, UserPreferenceSerializer
|
|
from user_api.models import UserPreference, UserProfile
|
|
from django_comment_common.models import Role
|
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
|
from edxmako.shortcuts import marketing_link
|
|
|
|
from user_api.api import account as account_api, profile as profile_api
|
|
from user_api.helpers import FormDescription, shim_student_view, require_post_params
|
|
|
|
|
|
class ApiKeyHeaderPermission(permissions.BasePermission):
|
|
def has_permission(self, request, view):
|
|
"""
|
|
Check for permissions by matching the configured API key and header
|
|
|
|
If settings.DEBUG is True and settings.EDX_API_KEY is not set or None,
|
|
then allow the request. Otherwise, allow the request if and only if
|
|
settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is
|
|
present in the request and matches the setting.
|
|
"""
|
|
api_key = getattr(settings, "EDX_API_KEY", None)
|
|
return (
|
|
(settings.DEBUG and api_key is None) or
|
|
(api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key)
|
|
)
|
|
|
|
|
|
class LoginSessionView(APIView):
|
|
"""HTTP end-points for logging in users. """
|
|
|
|
def get(self, request):
|
|
"""Return a description of the login form.
|
|
|
|
This decouples clients from the API definition:
|
|
if the API decides to modify the form, clients won't need
|
|
to be updated.
|
|
|
|
See `user_api.helpers.FormDescription` for examples
|
|
of the JSON-encoded form description.
|
|
|
|
Arguments:
|
|
request (HttpRequest)
|
|
|
|
Returns:
|
|
HttpResponse
|
|
|
|
"""
|
|
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"),
|
|
field_type="password",
|
|
restrictions={
|
|
"min_length": account_api.PASSWORD_MIN_LENGTH,
|
|
"max_length": account_api.PASSWORD_MAX_LENGTH,
|
|
}
|
|
)
|
|
|
|
form_desc.add_field(
|
|
"remember",
|
|
field_type="checkbox",
|
|
label=_("Remember me"),
|
|
default=False,
|
|
required=False,
|
|
)
|
|
|
|
return HttpResponse(form_desc.to_json(), content_type="application/json")
|
|
|
|
@method_decorator(ensure_csrf_cookie)
|
|
@method_decorator(require_post_params(["email", "password"]))
|
|
def post(self, request):
|
|
"""Log in a user.
|
|
|
|
Arguments:
|
|
request (HttpRequest)
|
|
|
|
Returns:
|
|
HttpResponse: 200 on success
|
|
HttpResponse: 400 if the request is not valid.
|
|
HttpResponse: 403 if authentication failed.
|
|
HttpResponse: 302 if redirecting to another page.
|
|
|
|
Example Usage:
|
|
|
|
POST /user_api/v1/login_session
|
|
with POST params `email` and `password`
|
|
|
|
200 OK
|
|
|
|
"""
|
|
# For the initial implementation, shim the existing login view
|
|
# from the student Django app.
|
|
from student.views import login_user
|
|
return shim_student_view(login_user, check_logged_in=True)(request)
|
|
|
|
|
|
class RegistrationView(APIView):
|
|
"""HTTP end-points for creating a new user. """
|
|
|
|
DEFAULT_FIELDS = ["email", "name", "username", "password"]
|
|
|
|
EXTRA_FIELDS = [
|
|
"city", "country", "level_of_education", "gender",
|
|
"year_of_birth", "mailing_address", "goals",
|
|
"honor_code", "terms_of_service",
|
|
]
|
|
|
|
def _is_field_visible(self, field_name):
|
|
"""Check whether a field is visible based on Django settings. """
|
|
return self._extra_fields_setting.get(field_name) in ["required", "optional"]
|
|
|
|
def _is_field_required(self, field_name):
|
|
"""Check whether a field is required based on Django settings. """
|
|
return self._extra_fields_setting.get(field_name) == "required"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(RegistrationView, self).__init__(*args, **kwargs)
|
|
|
|
# Backwards compatibility: Honor code is required by default, unless
|
|
# explicitly set to "optional" in Django settings.
|
|
self._extra_fields_setting = copy.deepcopy(settings.REGISTRATION_EXTRA_FIELDS)
|
|
self._extra_fields_setting["honor_code"] = self._extra_fields_setting.get("honor_code", "required")
|
|
|
|
# Check that the setting is configured correctly
|
|
for field_name in self.EXTRA_FIELDS:
|
|
if self._extra_fields_setting.get(field_name, "hidden") not in ["required", "optional", "hidden"]:
|
|
msg = u"Setting REGISTRATION_EXTRA_FIELDS values must be either required, optional, or hidden."
|
|
raise ImproperlyConfigured(msg)
|
|
|
|
# Map field names to the instance method used to add the field to the form
|
|
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):
|
|
"""Return a description of the registration form.
|
|
|
|
This decouples clients from the API definition:
|
|
if the API decides to modify the form, clients won't need
|
|
to be updated.
|
|
|
|
This is especially important for the registration form,
|
|
since different edx-platform installations might
|
|
collect different demographic information.
|
|
|
|
See `user_api.helpers.FormDescription` for examples
|
|
of the JSON-encoded form description.
|
|
|
|
Arguments:
|
|
request (HttpRequest)
|
|
|
|
Returns:
|
|
HttpResponse
|
|
|
|
"""
|
|
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 configured in Django settings
|
|
# may be required, optional, or hidden
|
|
for field_name in self.EXTRA_FIELDS:
|
|
if self._is_field_visible(field_name):
|
|
self.field_handlers[field_name](
|
|
form_desc,
|
|
required=self._is_field_required(field_name)
|
|
)
|
|
|
|
return HttpResponse(form_desc.to_json(), content_type="application/json")
|
|
|
|
@method_decorator(ensure_csrf_cookie)
|
|
@method_decorator(require_post_params(DEFAULT_FIELDS))
|
|
def post(self, request):
|
|
"""Create the user's account.
|
|
|
|
Arguments:
|
|
request (HTTPRequest)
|
|
|
|
Returns:
|
|
HttpResponse: 200 on success
|
|
HttpResponse: 400 if the request is not valid.
|
|
HttpResponse: 302 if redirecting to another page.
|
|
|
|
"""
|
|
# Handle duplicate username/email
|
|
conflicts = account_api.check_account_exists(
|
|
username=request.POST.get('username'),
|
|
email=request.POST.get('email')
|
|
)
|
|
if conflicts:
|
|
return HttpResponse(
|
|
status=409,
|
|
content=json.dumps(conflicts),
|
|
content_type="application/json"
|
|
)
|
|
|
|
# For the initial implementation, shim the existing login view
|
|
# from the student Django app.
|
|
from student.views import create_account
|
|
return shim_student_view(create_account)(request)
|
|
|
|
def _add_email_field(self, form_desc, required=True):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
form_desc.add_field(
|
|
"mailing_address",
|
|
label=_("Mailing Address"),
|
|
field_type="textarea",
|
|
required=required
|
|
)
|
|
|
|
def _add_goals_field(self, form_desc, required=True):
|
|
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):
|
|
form_desc.add_field(
|
|
"city",
|
|
label=_("City"),
|
|
required=required
|
|
)
|
|
|
|
def _add_country_field(self, form_desc, required=True):
|
|
sorted_countries = sorted(
|
|
countries.countries, key=lambda(__, name): unicode(name)
|
|
)
|
|
options = [
|
|
(country_code, unicode(country_name))
|
|
for country_code, country_name in sorted_countries
|
|
]
|
|
form_desc.add_field(
|
|
"country",
|
|
label=_("Country"),
|
|
field_type="select",
|
|
options=self._options_with_default(options),
|
|
required=required
|
|
)
|
|
|
|
def _add_honor_code_field(self, form_desc, required=True):
|
|
# Separate terms of service and honor code checkboxes
|
|
if self._is_field_visible("terms_of_service"):
|
|
terms_text = _(u"Honor Code")
|
|
|
|
# Combine terms of service and honor code checkboxes
|
|
else:
|
|
# Translators: This is a legal document users must agree to in order to register a new account.
|
|
terms_text = _(u"Terms of Service and Honor Code")
|
|
|
|
# Translators: "Terms of service" is a legal document users must agree to in order to register a new account.
|
|
label = _(
|
|
u"I agree to the {terms_of_service}"
|
|
).format(
|
|
terms_of_service=u"<a href=\"{url}\">{terms_text}</a>".format(
|
|
url=marketing_link("HONOR"),
|
|
terms_text=terms_text
|
|
)
|
|
)
|
|
|
|
form_desc.add_field(
|
|
"honor_code",
|
|
label=label,
|
|
field_type="checkbox",
|
|
default=False,
|
|
required=required,
|
|
)
|
|
|
|
def _add_terms_of_service_field(self, form_desc, required=True):
|
|
# Translators: This is a legal document users must agree to in order to register a new account.
|
|
terms_text = _(u"Terms of Service")
|
|
|
|
# Translators: "Terms of service" is a legal document users must agree to in order to register a new account.
|
|
label = _(
|
|
u"I agree to the {terms_of_service}"
|
|
).format(
|
|
terms_of_service=u"<a href=\"{url}\">{terms_text}</a>".format(
|
|
url=marketing_link("TOS"),
|
|
terms_text=terms_text
|
|
)
|
|
)
|
|
|
|
form_desc.add_field(
|
|
"terms_of_service",
|
|
label=label,
|
|
field_type="checkbox",
|
|
default=False,
|
|
required=required,
|
|
)
|
|
|
|
def _options_with_default(self, options):
|
|
"""Include a default option as the first option. """
|
|
return (
|
|
[("", "--")] + list(options)
|
|
)
|
|
|
|
|
|
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
|
authentication_classes = (authentication.SessionAuthentication,)
|
|
permission_classes = (ApiKeyHeaderPermission,)
|
|
queryset = User.objects.all().prefetch_related("preferences")
|
|
serializer_class = UserSerializer
|
|
paginate_by = 10
|
|
paginate_by_param = "page_size"
|
|
|
|
|
|
class ForumRoleUsersListView(generics.ListAPIView):
|
|
"""
|
|
Forum roles are represented by a list of user dicts
|
|
"""
|
|
authentication_classes = (authentication.SessionAuthentication,)
|
|
permission_classes = (ApiKeyHeaderPermission,)
|
|
serializer_class = UserSerializer
|
|
paginate_by = 10
|
|
paginate_by_param = "page_size"
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Return a list of users with the specified role/course pair
|
|
"""
|
|
name = self.kwargs['name']
|
|
course_id_string = self.request.QUERY_PARAMS.get('course_id')
|
|
if not course_id_string:
|
|
raise ParseError('course_id must be specified')
|
|
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id_string)
|
|
role = Role.objects.get_or_create(course_id=course_id, name=name)[0]
|
|
users = role.users.all()
|
|
return users
|
|
|
|
|
|
class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
|
|
authentication_classes = (authentication.SessionAuthentication,)
|
|
permission_classes = (ApiKeyHeaderPermission,)
|
|
queryset = UserPreference.objects.all()
|
|
filter_backends = (filters.DjangoFilterBackend,)
|
|
filter_fields = ("key", "user")
|
|
serializer_class = UserPreferenceSerializer
|
|
paginate_by = 10
|
|
paginate_by_param = "page_size"
|
|
|
|
|
|
class PreferenceUsersListView(generics.ListAPIView):
|
|
authentication_classes = (authentication.SessionAuthentication,)
|
|
permission_classes = (ApiKeyHeaderPermission,)
|
|
serializer_class = UserSerializer
|
|
paginate_by = 10
|
|
paginate_by_param = "page_size"
|
|
|
|
def get_queryset(self):
|
|
return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences")
|