diff --git a/openedx/core/djangoapps/user_api/serializers.py b/openedx/core/djangoapps/user_api/serializers.py index 19a320b..d744c84 100644 --- a/openedx/core/djangoapps/user_api/serializers.py +++ b/openedx/core/djangoapps/user_api/serializers.py @@ -2,8 +2,10 @@ Django REST Framework serializers for the User API application """ from django.contrib.auth.models import User +from django.utils.timezone import now from rest_framework import serializers +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from student.models import UserProfile from .models import UserPreference @@ -90,3 +92,20 @@ class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abst """ time_zone = serializers.CharField() description = serializers.CharField() + + +class SoftwareSecurePhotoVerificationSerializer(serializers.ModelSerializer): + """ + Serializer that generates a representation of a user's photo verification status. + """ + is_verified = serializers.SerializerMethodField() + + def get_is_verified(self, obj): + """ + Return a boolean indicating if a the user is verified. + """ + return obj.status == 'approved' and obj.expiration_datetime > now() + + class Meta(object): + fields = ('status', 'expiration_datetime', 'is_verified') + model = SoftwareSecurePhotoVerification diff --git a/openedx/core/djangoapps/user_api/urls.py b/openedx/core/djangoapps/user_api/urls.py index bb76490..8aa44ad 100644 --- a/openedx/core/djangoapps/user_api/urls.py +++ b/openedx/core/djangoapps/user_api/urls.py @@ -8,6 +8,7 @@ from django.conf.urls import patterns, url from ..profile_images.views import ProfileImageView from .accounts.views import AccountViewSet from .preferences.views import PreferencesView, PreferencesDetailView +from .verification_api.views import PhotoVerificationStatusView ACCOUNT_LIST = AccountViewSet.as_view({ @@ -26,16 +27,21 @@ urlpatterns = patterns( url( r'^v1/accounts/{}/image$'.format(settings.USERNAME_PATTERN), ProfileImageView.as_view(), - name="accounts_profile_image_api" + name='accounts_profile_image_api' + ), + url( + r'^v1/accounts/{}/verification_status/$'.format(settings.USERNAME_PATTERN), + PhotoVerificationStatusView.as_view(), + name='verification_status' ), url( r'^v1/preferences/{}$'.format(settings.USERNAME_PATTERN), PreferencesView.as_view(), - name="preferences_api" + name='preferences_api' ), url( r'^v1/preferences/{}/(?P<preference_key>[a-zA-Z0-9_]+)$'.format(settings.USERNAME_PATTERN), PreferencesDetailView.as_view(), - name="preferences_detail_api" + name='preferences_detail_api' ), ) diff --git a/openedx/core/djangoapps/user_api/verification_api/__init__.py b/openedx/core/djangoapps/user_api/verification_api/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/openedx/core/djangoapps/user_api/verification_api/__init__.py diff --git a/openedx/core/djangoapps/user_api/verification_api/tests/__init__.py b/openedx/core/djangoapps/user_api/verification_api/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/openedx/core/djangoapps/user_api/verification_api/tests/__init__.py diff --git a/openedx/core/djangoapps/user_api/verification_api/tests/test_views.py b/openedx/core/djangoapps/user_api/verification_api/tests/test_views.py new file mode 100644 index 0000000..34ff38e --- /dev/null +++ b/openedx/core/djangoapps/user_api/verification_api/tests/test_views.py @@ -0,0 +1,92 @@ +""" Tests for API endpoints. """ +from __future__ import unicode_literals + +import datetime +import freezegun +import json + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.test import TestCase +from django.test.utils import override_settings + +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification +from student.tests.factories import UserFactory + +FROZEN_TIME = '2015-01-01' +VERIFY_STUDENT = {'DAYS_GOOD_FOR': 365} + + +@freezegun.freeze_time(FROZEN_TIME) +@override_settings(VERIFY_STUDENT=VERIFY_STUDENT) +class PhotoVerificationStatusViewTests(TestCase): + """ Tests for the PhotoVerificationStatusView endpoint. """ + CREATED_AT = datetime.datetime.strptime(FROZEN_TIME, '%Y-%m-%d') + PASSWORD = 'test' + + def setUp(self): + super(PhotoVerificationStatusViewTests, self).setUp() + self.user = UserFactory.create(password=self.PASSWORD) + self.staff = UserFactory.create(is_staff=True, password=self.PASSWORD) + self.verification = SoftwareSecurePhotoVerification.objects.create(user=self.user, status='submitted') + self.path = reverse('verification_status', kwargs={'username': self.user.username}) + self.client.login(username=self.staff.username, password=self.PASSWORD) + + def assert_path_not_found(self, path): + """ Assert the path returns HTTP 404. """ + response = self.client.get(path) + self.assertEqual(response.status_code, 404) + + def assert_verification_returned(self, verified=False): + """ Assert the path returns HTTP 200 and returns appropriately-serialized data. """ + response = self.client.get(self.path) + self.assertEqual(response.status_code, 200) + expected_expires = self.CREATED_AT + datetime.timedelta(settings.VERIFY_STUDENT['DAYS_GOOD_FOR']) + + expected = { + 'status': self.verification.status, + 'expiration_datetime': '{}Z'.format(expected_expires.isoformat()), + 'is_verified': verified + } + self.assertEqual(json.loads(response.content), expected) + + def test_non_existent_user(self): + """ The endpoint should return HTTP 404 if the user does not exist. """ + path = reverse('verification_status', kwargs={'username': 'abc123'}) + self.assert_path_not_found(path) + + def test_no_verifications(self): + """ The endpoint should return HTTP 404 if the user has no verifications. """ + user = UserFactory.create() + path = reverse('verification_status', kwargs={'username': user.username}) + self.assert_path_not_found(path) + + def test_authentication_required(self): + """ The endpoint should return HTTP 403 if the user is not authenticated. """ + self.client.logout() + response = self.client.get(self.path) + self.assertEqual(response.status_code, 401) + + def test_staff_user(self): + """ The endpoint should be accessible to staff users. """ + self.client.login(username=self.staff.username, password=self.PASSWORD) + self.assert_verification_returned() + + def test_owner(self): + """ The endpoint should be accessible to the user who submitted the verification request. """ + self.client.login(username=self.user.username, password=self.user.password) + self.assert_verification_returned() + + def test_non_owner_or_staff_user(self): + """ The endpoint should NOT be accessible if the request is not made by the submitter or staff user. """ + user = UserFactory.create() + self.client.login(username=user.username, password=self.PASSWORD) + response = self.client.get(self.path) + self.assertEqual(response.status_code, 403) + + def test_approved_verification(self): + """ The endpoint should return that the user is verified if the user's verification is accepted. """ + self.verification.status = 'approved' + self.verification.save() + self.client.login(username=self.user.username, password=self.user.password) + self.assert_verification_returned(verified=True) diff --git a/openedx/core/djangoapps/user_api/verification_api/views.py b/openedx/core/djangoapps/user_api/verification_api/views.py new file mode 100644 index 0000000..5244915 --- /dev/null +++ b/openedx/core/djangoapps/user_api/verification_api/views.py @@ -0,0 +1,28 @@ +""" Verification API v1 views. """ +from django.http import Http404 +from edx_rest_framework_extensions.authentication import JwtAuthentication +from rest_framework.authentication import SessionAuthentication +from rest_framework.generics import RetrieveAPIView +from rest_framework_oauth.authentication import OAuth2Authentication + +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification +from openedx.core.djangoapps.user_api.serializers import SoftwareSecurePhotoVerificationSerializer +from openedx.core.lib.api.permissions import IsStaffOrOwner + + +class PhotoVerificationStatusView(RetrieveAPIView): + """ PhotoVerificationStatus detail endpoint. """ + authentication_classes = (JwtAuthentication, OAuth2Authentication, SessionAuthentication,) + permission_classes = (IsStaffOrOwner,) + serializer_class = SoftwareSecurePhotoVerificationSerializer + + def get_object(self): + username = self.kwargs['username'] + verifications = SoftwareSecurePhotoVerification.objects.filter(user__username=username).order_by('-updated_at') + + if len(verifications) > 0: + verification = verifications[0] + self.check_object_permissions(self.request, verification) + return verification + + raise Http404