Commit 7ef9ec83 by Will Daly

Added better docstrings and comments

parent e89afa93
...@@ -5,6 +5,7 @@ This is NOT part of the public API. ...@@ -5,6 +5,7 @@ This is NOT part of the public API.
from functools import wraps from functools import wraps
import logging import logging
import json import json
from django.http import HttpResponseBadRequest
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
...@@ -58,6 +59,34 @@ def intercept_errors(api_error, ignore_errors=[]): ...@@ -58,6 +59,34 @@ def intercept_errors(api_error, ignore_errors=[]):
return _decorator return _decorator
def require_post_params(required_params):
"""
View decorator that ensures the required POST params are
present. If not, returns an HTTP response with status 400.
Args:
required_params (list): The required parameter keys.
Returns:
HttpResponse
"""
def _decorator(func):
@wraps(func)
def _wrapped(*args, **kwargs):
request = args[0]
missing_params = set(required_params) - set(request.POST.keys())
if len(missing_params) > 0:
msg = u"Missing POST parameters: {missing}".format(
missing=", ".join(missing_params)
)
return HttpResponseBadRequest(msg)
else:
return func(request)
return _wrapped
return _decorator
class InvalidFieldError(Exception): class InvalidFieldError(Exception):
"""The provided field definition is not valid. """ """The provided field definition is not valid. """
...@@ -244,22 +273,30 @@ def shim_student_view(view_func, check_logged_in=False): ...@@ -244,22 +273,30 @@ def shim_student_view(view_func, check_logged_in=False):
function function
""" """
@wraps(view_func) @wraps(view_func)
def _inner(request): def _inner(request):
# Strip out enrollment action stuff, since we're handling that elsewhere # The login and registration handlers in student view try to change
# the user's enrollment status if these parameters are present.
# Since we want the JavaScript client to communicate directly with
# the enrollment API, we want to prevent the student views from
# updating enrollments.
if "enrollment_action" in request.POST: if "enrollment_action" in request.POST:
del request.POST["enrollment_action"] del request.POST["enrollment_action"]
if "course_id" in request.POST: if "course_id" in request.POST:
del request.POST["course_id"] del request.POST["course_id"]
# Actually call the function! # Call the original view to generate a response.
# TODO ^^ # We can safely modify the status code or content
# of the response, but to be safe we won't mess
# with the headers.
response = view_func(request) response = view_func(request)
# Most responses from this view are a JSON dict # Most responses from this view are JSON-encoded
# TODO -- explain this more # dictionaries with keys "success", "value", and
# (sometimes) "redirect_url".
# We want to communicate some of this information
# using HTTP status codes instead.
try: try:
response_dict = json.loads(response.content) response_dict = json.loads(response.content)
msg = response_dict.get("value", u"") msg = response_dict.get("value", u"")
...@@ -268,30 +305,35 @@ def shim_student_view(view_func, check_logged_in=False): ...@@ -268,30 +305,35 @@ def shim_student_view(view_func, check_logged_in=False):
msg = response.content msg = response.content
redirect_url = None redirect_url = None
# If the user could not be authenticated # If the user is not authenticated, and we expect them to be
# send a status 403.
if check_logged_in and not request.user.is_authenticated(): if check_logged_in and not request.user.is_authenticated():
response.status_code = 403 response.status_code = 403
response.content = msg response.content = msg
# Handle redirects # If the view wants to redirect us, send a status 302
# TODO -- explain why this is safe
elif redirect_url is not None: elif redirect_url is not None:
response.status_code = 302 response.status_code = 302
response.content = redirect_url response.content = redirect_url
# Handle errors # If an error condition occurs, send a status 400
elif response.status_code != 200 or not response_dict.get("success", False): elif response.status_code != 200 or not response_dict.get("success", False):
# TODO -- explain this # The student views tend to send status 200 even when an error occurs
# If the JSON-serialized content has a value "success" set to False,
# then we know an error occurred.
if response.status_code == 200: if response.status_code == 200:
response.status_code = 400 response.status_code = 400
response.content = msg response.content = msg
# Otherwise, return the response # If the response is successful, then return the content
# of the response directly rather than including it
# in a JSON-serialized dictionary.
else: else:
response.content = msg response.content = msg
# Return the response. # Return the response, preserving the original headers.
# IMPORTANT: this NEEDS to preserve session variables / cookies! # This is really important, since the student views set cookies
# that are used elsewhere in the system (such as the marketing site).
return response return response
return _inner return _inner
"""TODO""" """HTTP end-points for the User API. """
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.http import HttpResponse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
...@@ -21,7 +22,7 @@ from django_comment_common.models import Role ...@@ -21,7 +22,7 @@ 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.api import account as account_api, profile as profile_api
from user_api.helpers import FormDescription, shim_student_view from user_api.helpers import FormDescription, shim_student_view, require_post_params
class ApiKeyHeaderPermission(permissions.BasePermission): class ApiKeyHeaderPermission(permissions.BasePermission):
...@@ -42,12 +43,24 @@ class ApiKeyHeaderPermission(permissions.BasePermission): ...@@ -42,12 +43,24 @@ class ApiKeyHeaderPermission(permissions.BasePermission):
class LoginSessionView(APIView): class LoginSessionView(APIView):
"""TODO""" """HTTP end-points for logging in users. """
def get(self, request): def get(self, request):
"""Render a form for allowing a user to log in. """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
TODO
""" """
form_desc = FormDescription("post", reverse("user_api_login_session")) form_desc = FormDescription("post", reverse("user_api_login_session"))
...@@ -76,32 +89,38 @@ class LoginSessionView(APIView): ...@@ -76,32 +89,38 @@ class LoginSessionView(APIView):
return HttpResponse(form_desc.to_json(), content_type="application/json") return HttpResponse(form_desc.to_json(), content_type="application/json")
@method_decorator(ensure_csrf_cookie) @method_decorator(ensure_csrf_cookie)
@method_decorator(require_post_params(["email", "password"]))
def post(self, request): def post(self, request):
"""Authenticate a user and log them in. """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
TODO
""" """
# Validate the parameters # For the initial implementation, shim the existing login view
# If either param is missing, it's a malformed request # from the student Django app.
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 from student.views import login_user
return shim_student_view(login_user, check_logged_in=True)(request) return shim_student_view(login_user, check_logged_in=True)(request)
class RegistrationView(APIView): class RegistrationView(APIView):
"""TODO""" """HTTP end-points for creating a new user. """
DEFAULT_FIELDS = ["email", "name", "username", "password"] DEFAULT_FIELDS = ["email", "name", "username", "password"]
EXTRA_FIELDS = [ EXTRA_FIELDS = [
"city", "country", "level_of_education", "gender", "city", "country", "level_of_education", "gender",
"year_of_birth", "mailing_address", "goals", "year_of_birth", "mailing_address", "goals",
...@@ -110,15 +129,32 @@ class RegistrationView(APIView): ...@@ -110,15 +129,32 @@ class RegistrationView(APIView):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RegistrationView, self).__init__(*args, **kwargs) super(RegistrationView, self).__init__(*args, **kwargs)
# Map field names to the instance method used to add the field to the form
self.field_handlers = {} self.field_handlers = {}
for field_name in (self.DEFAULT_FIELDS + self.EXTRA_FIELDS): for field_name in (self.DEFAULT_FIELDS + self.EXTRA_FIELDS):
handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name)) handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name))
self.field_handlers[field_name] = handler self.field_handlers[field_name] = handler
def get(self, request): def get(self, request):
"""Render a form for allowing the user to register. """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
TODO
""" """
form_desc = FormDescription("post", reverse("user_api_registration")) form_desc = FormDescription("post", reverse("user_api_registration"))
...@@ -126,37 +162,48 @@ class RegistrationView(APIView): ...@@ -126,37 +162,48 @@ class RegistrationView(APIView):
for field_name in self.DEFAULT_FIELDS: for field_name in self.DEFAULT_FIELDS:
self.field_handlers[field_name](form_desc, required=True) self.field_handlers[field_name](form_desc, required=True)
# Extra fields from configuration may be required, optional, or hidden # Extra fields configured in Django settings
# TODO -- explain error handling here # may be required, optional, or hidden
for field_name in self.EXTRA_FIELDS: for field_name in self.EXTRA_FIELDS:
field_setting = settings.REGISTRATION_EXTRA_FIELDS.get(field_name) field_setting = settings.REGISTRATION_EXTRA_FIELDS.get(field_name, "hidden")
handler = self.field_handlers[field_name] handler = self.field_handlers[field_name]
if field_setting in ["required", "optional"]: if field_setting in ["required", "optional"]:
handler(form_desc, required=(field_setting == "required")) handler(form_desc, required=(field_setting == "required"))
elif field_setting != "hidden": elif field_setting != "hidden":
# TODO -- warning here msg = u"Setting REGISTRATION_EXTRA_FIELDS values must be either required, optional, or hidden."
pass raise ImproperlyConfigured(msg)
return HttpResponse(form_desc.to_json(), content_type="application/json") return HttpResponse(form_desc.to_json(), content_type="application/json")
@method_decorator(ensure_csrf_cookie)
def post(self, request): def post(self, request):
"""Create the user's account. """Create the user's account.
TODO Arguments:
request (HTTPRequest)
Returns:
HttpResponse: 200 on success
HttpResponse: 400 if the request is not valid.
HttpResponse: 302 if redirecting to another page.
""" """
# Backwards compat: # Backwards compatability
# TODO -- explain this # We used to validate that the users had checked
# "honor code" and "terms of service"
# on the registration form. Now we rely on the client
# to display this to users and validate that they
# agree before making the request to this service.
request.POST["honor_code"] = "true" request.POST["honor_code"] = "true"
request.POST["terms_of_service"] = "true" request.POST["terms_of_service"] = "true"
# Initially, this should be a shim to student views. # For the initial implementation, shim the existing login view
# Eventually, we'll want to pull that functionality into this API. # from the student Django app.
from student.views import create_account from student.views import create_account
return shim_student_view(create_account)(request) return shim_student_view(create_account)(request)
def _add_email_field(self, form_desc, required=True): def _add_email_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field( form_desc.add_field(
"email", "email",
label=_(u"E-mail"), label=_(u"E-mail"),
...@@ -172,7 +219,6 @@ class RegistrationView(APIView): ...@@ -172,7 +219,6 @@ class RegistrationView(APIView):
) )
def _add_name_field(self, form_desc, required=True): def _add_name_field(self, form_desc, required=True):
"""TODO"""
form_desc.add_field( form_desc.add_field(
"name", "name",
label=_(u"Full Name"), label=_(u"Full Name"),
...@@ -184,7 +230,6 @@ class RegistrationView(APIView): ...@@ -184,7 +230,6 @@ class RegistrationView(APIView):
) )
def _add_username_field(self, form_desc, required=True): def _add_username_field(self, form_desc, required=True):
"""TODO"""
form_desc.add_field( form_desc.add_field(
"username", "username",
label=_(u"Public Username"), label=_(u"Public Username"),
...@@ -197,7 +242,6 @@ class RegistrationView(APIView): ...@@ -197,7 +242,6 @@ class RegistrationView(APIView):
) )
def _add_password_field(self, form_desc, required=True): def _add_password_field(self, form_desc, required=True):
"""TODO"""
form_desc.add_field( form_desc.add_field(
"password", "password",
label=_(u"Password"), label=_(u"Password"),
...@@ -209,7 +253,6 @@ class RegistrationView(APIView): ...@@ -209,7 +253,6 @@ class RegistrationView(APIView):
) )
def _add_level_of_education_field(self, form_desc, required=True): def _add_level_of_education_field(self, form_desc, required=True):
""" TODO """
form_desc.add_field( form_desc.add_field(
"level_of_education", "level_of_education",
label=_("Highest Level of Education Completed"), label=_("Highest Level of Education Completed"),
...@@ -219,7 +262,6 @@ class RegistrationView(APIView): ...@@ -219,7 +262,6 @@ class RegistrationView(APIView):
) )
def _add_gender_field(self, form_desc, required=True): def _add_gender_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field( form_desc.add_field(
"gender", "gender",
label=_("Gender"), label=_("Gender"),
...@@ -229,7 +271,6 @@ class RegistrationView(APIView): ...@@ -229,7 +271,6 @@ class RegistrationView(APIView):
) )
def _add_year_of_birth_field(self, form_desc, required=True): def _add_year_of_birth_field(self, form_desc, required=True):
"""TODO """
options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS]
form_desc.add_field( form_desc.add_field(
"year_of_birth", "year_of_birth",
...@@ -240,7 +281,6 @@ class RegistrationView(APIView): ...@@ -240,7 +281,6 @@ class RegistrationView(APIView):
) )
def _add_mailing_address_field(self, form_desc, required=True): def _add_mailing_address_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field( form_desc.add_field(
"mailing_address", "mailing_address",
label=_("Mailing Address"), label=_("Mailing Address"),
...@@ -249,7 +289,6 @@ class RegistrationView(APIView): ...@@ -249,7 +289,6 @@ class RegistrationView(APIView):
) )
def _add_goals_field(self, form_desc, required=True): def _add_goals_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field( form_desc.add_field(
"goals", "goals",
label=_("Please share with us your reasons for registering with edX"), label=_("Please share with us your reasons for registering with edX"),
...@@ -258,7 +297,6 @@ class RegistrationView(APIView): ...@@ -258,7 +297,6 @@ class RegistrationView(APIView):
) )
def _add_city_field(self, form_desc, required=True): def _add_city_field(self, form_desc, required=True):
"""TODO """
form_desc.add_field( form_desc.add_field(
"city", "city",
label=_("City"), label=_("City"),
...@@ -266,7 +304,6 @@ class RegistrationView(APIView): ...@@ -266,7 +304,6 @@ class RegistrationView(APIView):
) )
def _add_country_field(self, form_desc, required=True): def _add_country_field(self, form_desc, required=True):
"""TODO """
options = [ options = [
(country_code, unicode(country_name)) (country_code, unicode(country_name))
for country_code, country_name in COUNTRIES for country_code, country_name in COUNTRIES
...@@ -280,7 +317,7 @@ class RegistrationView(APIView): ...@@ -280,7 +317,7 @@ class RegistrationView(APIView):
) )
def _options_with_default(self, options): def _options_with_default(self, options):
"""TODO """ """Include a default option as the first option. """
return ( return (
[("", "--")] + list(options) [("", "--")] + list(options)
) )
......
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