Skip to content

Commit

Permalink
feat: post handler for agreements api (#33488)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericanwoga authored Dec 11, 2023
1 parent 2629992 commit a74f510
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 6 deletions.
10 changes: 10 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
),
]
6 changes: 3 additions & 3 deletions openedx/core/djangoapps/agreements/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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.
"""
Expand All @@ -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.
"""
Expand Down
15 changes: 14 additions & 1 deletion openedx/core/djangoapps/agreements/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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')
71 changes: 71 additions & 0 deletions openedx/core/djangoapps/agreements/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
5 changes: 4 additions & 1 deletion openedx/core/djangoapps/agreements/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]
45 changes: 44 additions & 1 deletion openedx/core/djangoapps/agreements/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

0 comments on commit a74f510

Please sign in to comment.