feat: post handler for agreements api (#33488)

This commit is contained in:
Erica Nwoga
2023-12-11 13:58:35 -05:00
committed by GitHub
parent 26299929e2
commit a74f510f71
8 changed files with 201 additions and 6 deletions

View File

@@ -493,6 +493,16 @@ FEATURES = {
# .. 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

View File

@@ -970,6 +970,16 @@ FEATURES = {
# .. 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

View File

@@ -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'),
),
]

View File

@@ -24,7 +24,7 @@ class IntegritySignature(TimeStampedModel):
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 LTIPIITool(models.Model):
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 LTIPIISignature(models.Model):
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.
"""

View File

@@ -3,7 +3,7 @@ Serializers for the Agreements app
"""
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')

View File

@@ -10,12 +10,14 @@ from django.urls import reverse
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 @@ class IntegritySignatureViewTests(APITestCase, ModuleStoreTestCase):
)
)
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)

View File

@@ -5,10 +5,13 @@ URLs for the Agreements API
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'),
]

View File

@@ -15,9 +15,10 @@ from common.djangoapps.student import auth
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 @@ class IntegritySignatureView(AuthenticatedAPIView):
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)