From a74f510f71c19142aeb380facdc2cdfecf16c38a Mon Sep 17 00:00:00 2001 From: Erica Nwoga <66533300+chimsara@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:58:35 -0500 Subject: [PATCH] feat: post handler for agreements api (#33488) --- cms/envs/common.py | 10 +++ lms/envs/common.py | 10 +++ .../migrations/0005_timestampedmodels.py | 45 ++++++++++++ openedx/core/djangoapps/agreements/models.py | 6 +- .../core/djangoapps/agreements/serializers.py | 15 +++- .../djangoapps/agreements/tests/test_views.py | 71 +++++++++++++++++++ openedx/core/djangoapps/agreements/urls.py | 5 +- openedx/core/djangoapps/agreements/views.py | 45 +++++++++++- 8 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 openedx/core/djangoapps/agreements/migrations/0005_timestampedmodels.py diff --git a/cms/envs/common.py b/cms/envs/common.py index e4d23241bf4a..8e538e8f56e7 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -493,6 +493,16 @@ # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MST-1348' 'ENABLE_INTEGRITY_SIGNATURE': False, + # .. toggle_name: FEATURES['ENABLE_LTI_PII_ACKNOWLEDGEMENT'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Enables the lti pii acknowledgement feature for a course + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2023-10 + # .. toggle_target_removal_date: None + # .. toggle_tickets: 'https://2u-internal.atlassian.net/browse/MST-2055' + 'ENABLE_LTI_PII_ACKNOWLEDGEMENT': False, + # .. toggle_name: MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW # .. toggle_implementation: DjangoSetting # .. toggle_default: False diff --git a/lms/envs/common.py b/lms/envs/common.py index d328174a712d..a481acd33423 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -970,6 +970,16 @@ # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MST-1348' 'ENABLE_INTEGRITY_SIGNATURE': False, + # .. toggle_name: FEATURES['ENABLE_LTI_PII_ACKNOWLEDGEMENT'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Enables the lti pii acknowledgement feature for a course + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2023-10 + # .. toggle_target_removal_date: None + # .. toggle_tickets: 'https://2u-internal.atlassian.net/browse/MST-2055' + 'ENABLE_LTI_PII_ACKNOWLEDGEMENT': False, + # .. toggle_name: FEATURES['ENABLE_NEW_BULK_EMAIL_EXPERIENCE'] # .. toggle_implementation: DjangoSetting # .. toggle_default: False diff --git a/openedx/core/djangoapps/agreements/migrations/0005_timestampedmodels.py b/openedx/core/djangoapps/agreements/migrations/0005_timestampedmodels.py new file mode 100644 index 000000000000..53eb07fc3daa --- /dev/null +++ b/openedx/core/djangoapps/agreements/migrations/0005_timestampedmodels.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.22 on 2023-10-25 14:58 + +from django.db import migrations +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('agreements', '0004_proctoringpiisignature'), + ] + + operations = [ + migrations.AddField( + model_name='ltipiisignature', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + ), + migrations.AddField( + model_name='ltipiisignature', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + ), + migrations.AddField( + model_name='ltipiitool', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + ), + migrations.AddField( + model_name='ltipiitool', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + ), + migrations.AddField( + model_name='proctoringpiisignature', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + ), + migrations.AddField( + model_name='proctoringpiisignature', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + ), + ] diff --git a/openedx/core/djangoapps/agreements/models.py b/openedx/core/djangoapps/agreements/models.py index 7a74d9d28600..461d7936c4bb 100644 --- a/openedx/core/djangoapps/agreements/models.py +++ b/openedx/core/djangoapps/agreements/models.py @@ -24,7 +24,7 @@ class Meta: unique_together = ('user', 'course_key') -class LTIPIITool(models.Model): +class LTIPIITool(TimeStampedModel): """ This model stores the relationship between a course and the LTI tools in the course that share PII. """ @@ -36,7 +36,7 @@ class Meta: app_label = 'agreements' -class LTIPIISignature(models.Model): +class LTIPIISignature(TimeStampedModel): """ This model stores a user's acknowledgement to share PII via LTI tools in a particular course. """ @@ -54,7 +54,7 @@ class Meta: app_label = 'agreements' -class ProctoringPIISignature(models.Model): +class ProctoringPIISignature(TimeStampedModel): """ This model stores a user's acknowledgment to share PII via proctoring in a particular course. """ diff --git a/openedx/core/djangoapps/agreements/serializers.py b/openedx/core/djangoapps/agreements/serializers.py index 5e58301a3895..11e9d57f4054 100644 --- a/openedx/core/djangoapps/agreements/serializers.py +++ b/openedx/core/djangoapps/agreements/serializers.py @@ -3,7 +3,7 @@ """ from rest_framework import serializers -from openedx.core.djangoapps.agreements.models import IntegritySignature +from openedx.core.djangoapps.agreements.models import IntegritySignature, LTIPIISignature from openedx.core.lib.api.serializers import CourseKeyField @@ -18,3 +18,16 @@ class IntegritySignatureSerializer(serializers.ModelSerializer): class Meta: model = IntegritySignature() fields = ('username', 'course_id', 'created_at') + + +class LTIPIISignatureSerializer(serializers.ModelSerializer): + """ + Serializer for LTIPIISignature model + """ + username = serializers.CharField(source='user.username') + course_id = CourseKeyField(source='course_key') + created_at = serializers.DateTimeField(source='created') + + class Meta: + model = LTIPIISignature + fields = ('username', 'course_id', 'lti_tools', 'created_at') diff --git a/openedx/core/djangoapps/agreements/tests/test_views.py b/openedx/core/djangoapps/agreements/tests/test_views.py index 7056f7bf5ace..4c52e5853f05 100644 --- a/openedx/core/djangoapps/agreements/tests/test_views.py +++ b/openedx/core/djangoapps/agreements/tests/test_views.py @@ -10,12 +10,14 @@ from rest_framework.test import APITestCase from rest_framework import status from freezegun import freeze_time +import json 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, + get_lti_pii_signature ) from openedx.core.djangolib.testing.utils import skip_unless_lms from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order @@ -218,3 +220,72 @@ def test_post_integrity_signature_no_waffle_flag(self): ) ) self._assert_response(response, status.HTTP_404_NOT_FOUND) + + +@skip_unless_lms +@patch.dict(settings.FEATURES, {'ENABLE_LTI_PII_ACKNOWLEDGEMENT': True}) +class LTIPIISignatureSignatureViewTests(APITestCase, ModuleStoreTestCase): + """ + Tests for the LTI PII Signature View + """ + USERNAME = "Bob" + PASSWORD = "edx" + + OTHER_USERNAME = "Jane" + + STAFF_USERNAME = "Alice" + + def setUp(self): + super().setUp() + + self.course = CourseFactory.create() + + self.user = UserFactory.create( + username=self.USERNAME, + password=self.PASSWORD, + ) + self.other_user = UserFactory.create( + username=self.OTHER_USERNAME, + password=self.PASSWORD, + ) + self.lti_tools = json.dumps({"first_lti_tool": "This is the first tool", + "second_lti_tool": "This is the second tool"}) + + self.client.login(username=self.USERNAME, password=self.PASSWORD) + self.course_id = str(self.course.id) + self.time_created = datetime.now() + + def _assert_response(self, response, expected_response, user=None, course_id=None): + """ + Assert response is correct for the given information + """ + assert response.status_code == expected_response + if user and course_id: + data = response.data + assert data['username'] == user.username + assert data['course_id'] == course_id + + @patch.dict(settings.FEATURES, {'ENABLE_LTI_PII_ACKNOWLEDGEMENT': False}) + def test_enabled_lti_pii_signature(self): + response = self.client.post( + reverse( + 'lti_pii_signature', + kwargs={'course_id': self.course_id}, + ) + ) + self._assert_response(response, status.HTTP_404_NOT_FOUND) + + def test_post_lti_pii_signature_invalid_serializer(self): + response = self.client.post(reverse('lti_pii_signature', kwargs={'course_id': self.course_id}), + {"username": self.user.username, "course_id": self.course_id, + "lti_tools": self.lti_tools, "created_at": "0000-00-00"}) + self._assert_response(response, status.HTTP_500_INTERNAL_SERVER_ERROR, self.user, self.course_id) + + def test_post_lti_pii_signature(self): + response = self.client.post(reverse('lti_pii_signature', kwargs={'course_id': self.course_id}), + {"username": self.user.username, "course_id": self.course_id, + "lti_tools": self.lti_tools, "created_at": self.time_created}) + self._assert_response(response, status.HTTP_200_OK, self.user, self.course_id) + signature = get_lti_pii_signature(self.user.username, self.course_id) + self.assertEqual(signature.user.username, self.user.username) + self.assertEqual(signature.lti_tools, self.lti_tools) diff --git a/openedx/core/djangoapps/agreements/urls.py b/openedx/core/djangoapps/agreements/urls.py index 1b52d1af92fb..d9d009d65ac1 100644 --- a/openedx/core/djangoapps/agreements/urls.py +++ b/openedx/core/djangoapps/agreements/urls.py @@ -5,10 +5,13 @@ from django.conf import settings from django.urls import re_path -from .views import IntegritySignatureView +from .views import IntegritySignatureView, LTIPIISignatureView urlpatterns = [ re_path(r'^integrity_signature/{course_id}$'.format( course_id=settings.COURSE_ID_PATTERN ), IntegritySignatureView.as_view(), name='integrity_signature'), + re_path(r'^lti_pii_signature/{course_id}$'.format( + course_id=settings.COURSE_ID_PATTERN + ), LTIPIISignatureView.as_view(), name='lti_pii_signature'), ] diff --git a/openedx/core/djangoapps/agreements/views.py b/openedx/core/djangoapps/agreements/views.py index 26eb6c4489e8..82de8caabf00 100644 --- a/openedx/core/djangoapps/agreements/views.py +++ b/openedx/core/djangoapps/agreements/views.py @@ -15,9 +15,10 @@ from common.djangoapps.student.roles import CourseStaffRole from openedx.core.djangoapps.agreements.api import ( create_integrity_signature, + create_lti_pii_signature, get_integrity_signature, ) -from openedx.core.djangoapps.agreements.serializers import IntegritySignatureSerializer +from openedx.core.djangoapps.agreements.serializers import IntegritySignatureSerializer, LTIPIISignatureSerializer def is_user_course_or_global_staff(user, course_id): @@ -119,3 +120,45 @@ def post(self, request, course_id): signature = create_integrity_signature(username, course_id) serializer = IntegritySignatureSerializer(signature) return Response(serializer.data) + + +class LTIPIISignatureView(AuthenticatedAPIView): + """ + Endpoint for a LTI PII Signature + /lti_pii_signature/{course_id} + + HTTP POST + * If an LTI PII signature does not exist for the user + course, creates one and + returns it. If one does exist, returns the existing signature. + """ + + def post(self, request, course_id): + """ + Create an LTI PII signature for the requesting user and course. If a signature + already exists, returns the existing signature instead of creating a new one. + + /api/agreements/v1/lti_pii_signature/{course_id} + + Example response: + { + username: "janedoe", + course_id: "org.2/course_2/Run_2", + created_at: "2021-04-23T18:25:43.511Z" + } + """ + if not settings.FEATURES.get('ENABLE_LTI_PII_ACKNOWLEDGEMENT'): + return Response( + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = LTIPIISignatureSerializer(data=request.data) + statusStr = "" + if serializer.is_valid(): + username = request.user.username + lti_tools = request.data.get("lti_tools") + signature = create_lti_pii_signature(username, course_id, lti_tools) + serializer = LTIPIISignatureSerializer(signature) + statusStr = status.HTTP_200_OK + else: + statusStr = status.HTTP_500_INTERNAL_SERVER_ERROR + return Response(data=serializer.data, status=statusStr)