feat: post handler for agreements api (#33488)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user