From 8be1c7c18b043fffc570483cc65207839820f41c Mon Sep 17 00:00:00 2001 From: Bianca Severino Date: Wed, 12 May 2021 10:15:53 -0400 Subject: [PATCH] feat: POST endpoint for integrity signature --- .../core/djangoapps/agreements/serializers.py | 20 ++++++ .../djangoapps/agreements/tests/test_views.py | 65 +++++++++++++++++-- openedx/core/djangoapps/agreements/views.py | 51 +++++++++++---- 3 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 openedx/core/djangoapps/agreements/serializers.py diff --git a/openedx/core/djangoapps/agreements/serializers.py b/openedx/core/djangoapps/agreements/serializers.py new file mode 100644 index 0000000000..5e58301a38 --- /dev/null +++ b/openedx/core/djangoapps/agreements/serializers.py @@ -0,0 +1,20 @@ +""" +Serializers for the Agreements app +""" +from rest_framework import serializers + +from openedx.core.djangoapps.agreements.models import IntegritySignature +from openedx.core.lib.api.serializers import CourseKeyField + + +class IntegritySignatureSerializer(serializers.ModelSerializer): + """ + Serializer for the IntegritySignature model + """ + username = serializers.CharField(source='user.username') + course_id = CourseKeyField(source='course_key') + created_at = serializers.DateTimeField(source='created') + + class Meta: + model = IntegritySignature() + fields = ('username', 'course_id', 'created_at') diff --git a/openedx/core/djangoapps/agreements/tests/test_views.py b/openedx/core/djangoapps/agreements/tests/test_views.py index 86507a8478..dc2e976060 100644 --- a/openedx/core/djangoapps/agreements/tests/test_views.py +++ b/openedx/core/djangoapps/agreements/tests/test_views.py @@ -1,20 +1,26 @@ """ Tests for agreements views """ + +from datetime import datetime, timedelta + +from django.urls import reverse from rest_framework.test import APITestCase from rest_framework import status -from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag +from freezegun import freeze_time from common.djangoapps.student.tests.factories import UserFactory, AdminFactory from common.djangoapps.student.roles import CourseStaffRole +from openedx.core.djangoapps.agreements.api import ( + create_integrity_signature, + get_integrity_signatures_for_course, +) +from openedx.core.djangoapps.agreements.toggles import ENABLE_INTEGRITY_SIGNATURE from openedx.core.djangolib.testing.utils import skip_unless_lms from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -from ..api import create_integrity_signature -from ..toggles import ENABLE_INTEGRITY_SIGNATURE - @skip_unless_lms @override_waffle_flag(ENABLE_INTEGRITY_SIGNATURE, active=True) @@ -161,3 +167,54 @@ class IntegritySignatureViewTests(APITestCase, ModuleStoreTestCase): ) ) self._assert_response(response, status.HTTP_404_NOT_FOUND) + + def test_post_integrity_signature(self): + response = self.client.post( + reverse('integrity_signature', kwargs={'course_id': self.course_id}) + ) + self._assert_response(response, status.HTTP_200_OK, self.user, self.course_id) + + # Check that the course has a signature created + signatures = get_integrity_signatures_for_course(self.course_id) + self.assertEqual(len(signatures), 1) + self.assertEqual(signatures[0].user.username, self.USERNAME) + + def test_post_duplicate_integrity_signature(self): + # Create a signature + original_response = self.client.post( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + + '?username={}'.format(self.other_user.username) + ) + + # Attempt to create a new signature in the future + with freeze_time(datetime.now() + timedelta(days=1)): + new_response = self.client.post( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + ) + + # The created_at field in the response should equal the original time created + self.assertEqual( + original_response.data['created_at'], + new_response.data['created_at'], + ) + + # The course should not have a second signature + signatures = get_integrity_signatures_for_course(self.course_id) + self.assertEqual(len(signatures), 1) + self.assertEqual(signatures[0].user.username, self.USERNAME) + + @override_waffle_flag(ENABLE_INTEGRITY_SIGNATURE, active=False) + def test_post_integrity_signature_no_waffle_flag(self): + response = self.client.post( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + ) + self._assert_response(response, status.HTTP_404_NOT_FOUND) diff --git a/openedx/core/djangoapps/agreements/views.py b/openedx/core/djangoapps/agreements/views.py index afb5057883..89e088db31 100644 --- a/openedx/core/djangoapps/agreements/views.py +++ b/openedx/core/djangoapps/agreements/views.py @@ -1,4 +1,6 @@ -"""Views served by the agreements app. """ +""" +Views served by the Agreements app +""" from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from rest_framework import status @@ -10,9 +12,12 @@ from opaque_keys.edx.keys import CourseKey from common.djangoapps.student import auth from common.djangoapps.student.roles import CourseStaffRole - -from .api import get_integrity_signature -from .toggles import is_integrity_signature_enabled +from openedx.core.djangoapps.agreements.api import ( + create_integrity_signature, + get_integrity_signature, +) +from openedx.core.djangoapps.agreements.serializers import IntegritySignatureSerializer +from openedx.core.djangoapps.agreements.toggles import is_integrity_signature_enabled def is_user_course_or_global_staff(user, course_id): @@ -26,7 +31,7 @@ def is_user_course_or_global_staff(user, course_id): class AuthenticatedAPIView(APIView): """ - Authenticated API View. + Authenticated API View. """ authentication_classes = (SessionAuthentication, JwtAuthentication) permission_classes = (IsAuthenticated,) @@ -44,6 +49,10 @@ class IntegritySignatureView(AuthenticatedAPIView): ** Scenarios ** ?username=xyz returns an existing signed integrity agreement for the given user and course + + HTTP POST + * If an integrity signature does not exist for the user + course, creates one and + returns it. If one does exist, returns the existing signature. """ def get(self, request, course_id): @@ -85,12 +94,30 @@ class IntegritySignatureView(AuthenticatedAPIView): status=status.HTTP_404_NOT_FOUND, ) - created_at = str(signature.created) + serializer = IntegritySignatureSerializer(signature) + return Response(serializer.data) - data = { - 'username': username, - 'course_id': course_id, - 'created_at': created_at, - } + def post(self, request, course_id): + """ + Create an integrity signature for the requesting user and course. If a signature + already exists, returns the existing signature instead of creating a new one. - return Response(data) + /api/agreements/v1/integrity_signature/{course_id} + + Example response: + { + username: "janedoe", + course_id: "org.2/course_2/Run_2", + created_at: "2021-04-23T18:25:43.511Z" + } + """ + # check that waffle flag is enabled + if not is_integrity_signature_enabled(): + return Response( + status=status.HTTP_404_NOT_FOUND, + ) + + username = request.user.username + signature = create_integrity_signature(username, course_id) + serializer = IntegritySignatureSerializer(signature) + return Response(serializer.data)